1
0
mirror of synced 2026-08-04 17:27:13 +00:00

Compare commits

..

7 Commits

Author SHA1 Message Date
Sumit Kumar Das 2c4db99229 Differentiate Forwarded and X-Forwarded headers in proxy docs
The proxy server section pointed at RFC 7239 and then told the reader to
configure the application server for the X-Forwarded headers, conflating
the standard Forwarded header with the non-standard X-Forwarded-* set.

Describe the two kinds of headers separately, note that most proxies send
X-Forwarded-* while Spring Framework and servers such as Reactor Netty and
Jetty understand both, and state that the edge proxy has to drop or
overwrite untrusted values for both kinds rather than only one.

Closes gh-19461

Signed-off-by: Sumit Kumar Das <skdas5405@gmail.com>
2026-08-03 17:52:32 -06:00
Josh Cummings 0f6f453ea0 Increase Default NimbusJwtDecoder Timeouts to 30 Seconds
NimbusJwtDecoder's default RestOperations now respects the JDK's
sun.net.client.defaultConnectTimeout/defaultReadTimeout system properties,
falling back to 30 seconds instead of the previous 500 milliseconds,
matching JwtDecoderProviderConfigurationUtils's existing behavior.

Also documents this default and the RestOperations override in the
reference guide and migration guide (the reference guide's existing
"Configuring Timeouts" section already claimed 30 seconds -- it's been
inaccurate since the 500ms default shipped and is now correct again), and
documents providing a custom JwtDecoderFactory<ClientRegistration> for
OAuth2 Login's ID Token decoding.

Issue gh-19474

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-08-03 15:51:41 -06:00
Josh Cummings 1eef373ca0 Improve Error Message for Invalid JSR-250 Usage
This commit adds an IllegalStateException guard when spring-security-access
is missing and the application is using @EnableGlobalMethodSecurity with
jsr250Enabled.

Issue gh-19441

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-07-31 16:01:21 -06:00
jyx-07 7a03cd5a55 Fail fast when spring-security-access is missing
The "Move Core Access API" refactoring (gh-17847) relocated
MethodSecurityMetadataSourceAdvisor and MethodSecurityInterceptor
from spring-security-core, a mandatory dependency of
spring-security-config, into the new spring-security-access module,
which spring-security-config only depends on optionally.

GlobalMethodSecuritySelector (backing the deprecated
@EnableGlobalMethodSecurity) and ReactiveMethodSecuritySelector
(backing @EnableReactiveMethodSecurity(useAuthorizationManager =
false)) still unconditionally import configuration that constructs
those classes: MethodSecurityMetadataSourceAdvisorRegistrar in proxy
mode, GlobalMethodSecurityConfiguration in both proxy and aspectj
mode, and ReactiveMethodSecurityConfiguration for the legacy reactive
path. Applications that use any of these deprecated configuration
paths without explicitly adding spring-security-access now fail at
startup with a confusing NoClassDefFoundError deep inside Spring's
configuration-processing machinery, instead of an actionable message.

@EnableMethodSecurity and @EnableReactiveMethodSecurity's default
(AuthorizationManager-based) mode, the non-deprecated replacements,
never reference these classes and are unaffected either way.

Add a ClassUtils.isPresent check to both selectors so that, whenever
a legacy configuration path that needs it is chosen (proxy mode,
aspectj mode, or the legacy reactive interceptor), a missing
spring-security-access dependency now fails fast with a clear
IllegalStateException that names the missing dependency and points
to the supported alternative, rather than a NoClassDefFoundError.

This preserves gh-17847's footprint-reduction intent: the check only
runs for the deprecated legacy annotations, so the majority of
applications using @EnableMethodSecurity see no change in behavior or
dependencies. @EnableGlobalMethodSecurity remains deprecated; this
change adds no new investment in it beyond giving existing users of
it a clear diagnostic instead of a confusing crash.

Closes gh-19441

Signed-off-by: jyx-07 <s25069@gsm.hs.kr>
2026-07-31 16:01:21 -06:00
Andrey Litvitski e4fafce066 Fix OAuth2PushedAuthorizationRequestUri parsing
OAuth2PushedAuthorizationRequestUri uses Base64URL encoding for the state, which can produce a value containing ___, the same sequence used as the delimiter. As a result, indexOf may locate a delimiter within the state instead of the delimiter preceding the expiration timestamp.

Use lastIndexOf to reliably locate the delimiter before the timestamp without changing the existing request URI format.

Closes gh-19444

Signed-off-by: Andrey Litvitski <andrey1010102008@gmail.com>
2026-07-31 13:19:21 -06:00
Josh Cummings 2104c49597 Add tests for OAuth2PushedAuthorizationRequestUri
Adds baseline test coverage for OAuth2PushedAuthorizationRequestUri,
which previously had none: a round-trip via create()/parse() and an
explicit parse() case on a hand-constructed request URI.

See gh-19444.

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-07-31 13:19:21 -06:00
Josh Cummings c5b1f78118 Support Testing Against Snapshot Spring LDAP Versions
Adds an opt-in override to spring-security-dependencies so it can be
built against a specific Spring LDAP version instead of whatever's
declared in the version catalog, matching the existing pattern for
Spring Framework/Reactor/Spring Data/Micrometer. Passing
-PisOverrideVersionCatalog=true -PspringLdapVersion=X takes precedence
over the catalog. Default behavior (no properties passed) is unchanged.

Closes gh-19481

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-07-31 12:31:17 -06:00
19 changed files with 402 additions and 19 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
issues: write
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Compute Version
id: compute-version
uses: spring-io/spring-release-actions/compute-version@a1f321783a0769dd2aea4fad6c2ae2f95a52b885 # 0.0.5
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
if: github.repository_owner == 'spring-projects'
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: docs-build
fetch-depth: 1
@@ -19,7 +19,7 @@ jobs:
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up JDK 17
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
+2 -2
View File
@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ github.repository == 'spring-projects/spring-security' }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@c8668747d7c264864c8c7f7026d0d277d14a78dc # v2.0.6
with:
@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ github.repository == 'spring-projects/spring-security' }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@c8668747d7c264864c8c7f7026d0d277d14a78dc # v2.0.6
with:
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 1
- name: Dispatch
@@ -39,6 +39,9 @@ import org.springframework.util.ClassUtils;
@Deprecated
final class GlobalMethodSecuritySelector implements ImportSelector {
private static final boolean isAccessPresent = ClassUtils.isPresent(
"org.springframework.security.access.intercept.aopalliance.MethodSecurityMetadataSourceAdvisor", null);
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
Class<EnableGlobalMethodSecurity> annoType = EnableGlobalMethodSecurity.class;
@@ -59,16 +62,26 @@ final class GlobalMethodSecuritySelector implements ImportSelector {
boolean jsr250Enabled = attributes.getBoolean("jsr250Enabled");
List<String> classNames = new ArrayList<>(4);
if (isProxy) {
assertAccessModulePresent();
classNames.add(MethodSecurityMetadataSourceAdvisorRegistrar.class.getName());
}
classNames.add(autoProxyClassName);
if (!skipMethodSecurityConfiguration) {
assertAccessModulePresent();
classNames.add(GlobalMethodSecurityConfiguration.class.getName());
}
if (jsr250Enabled) {
assertAccessModulePresent();
classNames.add(Jsr250MetadataSourceConfiguration.class.getName());
}
return classNames.toArray(new String[0]);
}
private static void assertAccessModulePresent() {
Assert.state(isAccessPresent,
() -> "@EnableGlobalMethodSecurity requires the spring-security-access dependency on the "
+ "classpath. Please add spring-security-access, or migrate to @EnableMethodSecurity "
+ "which does not require it.");
}
}
@@ -26,6 +26,7 @@ import org.springframework.context.annotation.AutoProxyRegistrar;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
@@ -35,6 +36,8 @@ import org.springframework.util.ClassUtils;
*/
class ReactiveMethodSecuritySelector implements ImportSelector {
private static final String METHOD_SECURITY_METADATA_SOURCE_ADVISOR = "org.springframework.security.access.intercept.aopalliance.MethodSecurityMetadataSourceAdvisor";
private static final boolean isDataPresent = ClassUtils
.isPresent("org.springframework.security.data.aot.hint.AuthorizeReturnObjectDataHintsRegistrar", null);
@@ -56,6 +59,11 @@ class ReactiveMethodSecuritySelector implements ImportSelector {
imports.add(ReactiveAuthorizationManagerMethodSecurityConfiguration.class.getName());
}
else {
Assert.state(
ClassUtils.isPresent(METHOD_SECURITY_METADATA_SOURCE_ADVISOR, ClassUtils.getDefaultClassLoader()),
() -> "@EnableReactiveMethodSecurity(useAuthorizationManager = false) requires the "
+ "spring-security-access dependency on the classpath. Please add spring-security-access, "
+ "or use the default useAuthorizationManager = true which does not require it.");
imports.add(ReactiveMethodSecurityConfiguration.class.getName());
}
if (isDataPresent) {
@@ -0,0 +1,138 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.annotation.method.configuration;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AdviceMode;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.test.support.ClassPathExclusions;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for gh-19441: {@code spring-security-access} moved
* {@link org.springframework.security.access.intercept.aopalliance.MethodSecurityMetadataSourceAdvisor}
* out of {@code spring-security-core} and into the optional
* {@code spring-security-access} module. {@link EnableMethodSecurity} and
* {@link EnableReactiveMethodSecurity}'s default (AuthorizationManager-based) mode never
* needed that class and continue to work without {@code spring-security-access} on the
* classpath, but the deprecated legacy method security annotations do need it and
* previously failed with a confusing {@link NoClassDefFoundError} instead of an
* actionable message.
*/
@ClassPathExclusions("spring-security-access-*.jar")
public class Gh19441Tests {
@Test
public void enableMethodSecurityWhenAccessModuleAbsentThenContextStartsCleanly() {
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
context.register(EnableMethodSecurityConfig.class);
context.refresh();
}
}
@Test
public void enableReactiveMethodSecurityWhenAccessModuleAbsentThenContextStartsCleanly() {
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
context.register(EnableReactiveMethodSecurityConfig.class);
context.refresh();
}
}
@Test
public void enableGlobalMethodSecurityWhenProxyModeAndAccessModuleAbsentThenClearException() {
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
context.register(EnableGlobalMethodSecurityProxyConfig.class);
assertThatExceptionOfType(Exception.class).isThrownBy(context::refresh)
.havingRootCause()
.isInstanceOf(IllegalStateException.class)
.withMessageContaining("spring-security-access");
}
}
@Test
public void enableGlobalMethodSecurityWhenAspectJModeAndAccessModuleAbsentThenClearException() {
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
context.register(EnableGlobalMethodSecurityAspectJConfig.class);
assertThatExceptionOfType(Exception.class).isThrownBy(context::refresh)
.havingRootCause()
.isInstanceOf(IllegalStateException.class)
.withMessageContaining("spring-security-access");
}
}
@Test
public void enableReactiveMethodSecurityWhenUseAuthorizationManagerFalseAndAccessModuleAbsentThenClearException() {
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
context.register(EnableReactiveMethodSecurityLegacyConfig.class);
assertThatExceptionOfType(Exception.class).isThrownBy(context::refresh)
.havingRootCause()
.isInstanceOf(IllegalStateException.class)
.withMessageContaining("spring-security-access");
}
}
@Test
public void enableGlobalMethodSecurityWhenAspectJModeAndJsr250EnabledAndConfigurationSubclassedAndAccessModuleAbsentThenClearException() {
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
context.register(EnableGlobalMethodSecurityAspectJJsr250SubclassedConfig.class);
assertThatExceptionOfType(Exception.class).isThrownBy(context::refresh)
.havingRootCause()
.isInstanceOf(IllegalStateException.class)
.withMessageContaining("spring-security-access");
}
}
@Configuration
@EnableMethodSecurity
static class EnableMethodSecurityConfig {
}
@Configuration
@EnableReactiveMethodSecurity
static class EnableReactiveMethodSecurityConfig {
}
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
static class EnableGlobalMethodSecurityProxyConfig {
}
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, mode = AdviceMode.ASPECTJ)
static class EnableGlobalMethodSecurityAspectJConfig {
}
@Configuration
@EnableReactiveMethodSecurity(useAuthorizationManager = false)
static class EnableReactiveMethodSecurityLegacyConfig {
}
@Configuration
@EnableGlobalMethodSecurity(jsr250Enabled = true, mode = AdviceMode.ASPECTJ)
static class EnableGlobalMethodSecurityAspectJJsr250SubclassedConfig extends GlobalMethodSecurityConfiguration {
}
}
+5 -1
View File
@@ -75,7 +75,11 @@ dependencies {
api libs.org.skyscreamer.jsonassert
api libs.org.slf4j.log4j.over.slf4j
api libs.org.slf4j.slf4j.api
api libs.org.springframework.ldap.spring.ldap.core
if (project.hasProperty("isOverrideVersionCatalog") && project.hasProperty("springLdapVersion")) {
api "org.springframework.ldap:spring-ldap-core:${springLdapVersion}"
} else {
api libs.org.springframework.ldap.spring.ldap.core
}
api libs.org.synchronoss.cloud.nio.multipart.parser
api libs.org.apache.maven.resolver.maven.resolver.connector.basic
api libs.org.apache.maven.resolver.maven.resolver.impl
@@ -23,10 +23,28 @@ When using a proxy server, it is important to ensure that you have configured yo
For example, many applications have a load balancer that responds to request for `\https://example.com/` by forwarding the request to an application server at `\https://192.168.0.107`
Without proper configuration, the application server can not know that the load balancer exists and treats the request as though `\https://192.168.0.107:8080` was requested by the client.
To fix this, you can use https://tools.ietf.org/html/rfc7239[RFC 7239] to specify that a load balancer is being used.
To make the application aware of this, you need to configure your application server to be aware of the X-Forwarded headers.
To fix this, the proxy needs to pass on the details of the original request, and the application needs to be configured to use them.
Two kinds of headers are used for this, and it is important to know which of them applies in your deployment:
* The standard `Forwarded` header, defined by https://tools.ietf.org/html/rfc7239[RFC 7239], which carries the original host, protocol, and client in a single header.
* The non-standard `X-Forwarded-*` headers, such as `X-Forwarded-Host`, `X-Forwarded-Proto`, and `X-Forwarded-For`, which predate RFC 7239.
Most proxies still send the `X-Forwarded-*` headers rather than the standard `Forwarded` header, while Spring Framework and servers such as Reactor Netty and Jetty understand both.
Do not assume that only one of them is in use.
[NOTE]
====
Both kinds of headers are supplied by the client unless a proxy overwrites them, so an application that trusts them without a trusted proxy in front of it can be made to believe a request arrived over a different host, protocol, or client address than it really did.
====
For this reason, the proxy at the edge of your network must be configured to remove or overwrite any forwarded headers that arrive from the outside, for _both_ kinds of headers.
Dropping only the `Forwarded` header while passing through `X-Forwarded-*` (or the reverse) leaves the application open to the same spoofing through the other set.
Only headers added by a proxy you control should reach the application.
Once untrusted values are handled at the edge, the application server can be configured to apply the headers.
For example, Tomcat uses https://tomcat.apache.org/tomcat-10.1-doc/api/org/apache/catalina/valves/RemoteIpValve.html[`RemoteIpValve`] and Jetty uses https://eclipse.dev/jetty/javadoc/jetty-11/org/eclipse/jetty/server/ForwardedRequestCustomizer.html[`ForwardedRequestCustomizer`].
Alternatively, Spring users can use https://docs.spring.io/spring-framework/reference/web/webmvc/filters.html#filters-forwarded-headers[`ForwardedHeaderFilter`] with the Servlet stack or https://docs.spring.io/spring-framework/reference/web/webflux/reactive-spring.html#webflux-forwarded-headers[`ForwardedHeaderTransformer`] with the Reactive stack.
Both handle the `Forwarded` header and the `X-Forwarded-*` headers, and both can be configured to remove the headers instead of applying them, which is useful when the application is not behind a proxy.
Spring Boot users can use the `server.forward-headers-strategy` property to configure the application.
See the https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto.webserver.use-behind-a-proxy-server[Spring Boot documentation] for further details.
@@ -132,4 +132,11 @@ authenticationConverter.setBearerTokenResolver(myBearerTokenResolver)
authenticationConverter.setAuthenticationDetailsSource(myAuthenticationDetailsSource)
val filter = BearerTokenAuthenticationFilter(authenticationManager, authenticationConverter)
----
== `NimbusJwtDecoder`'s Default Connect and Read Timeouts Are Now 30 Seconds
`NimbusJwtDecoder`'s default `RestOperations`, used to fetch a JWK Set when no `RestOperations` is otherwise configured, previously used a 500 millisecond connect and read timeout.
This value was too short for many deployments and is now 30 seconds, aligning with the connect and read timeouts already used elsewhere in the OAuth2 Client and Resource Server support, such as xref:servlet/oauth2/client/index.adoc[`ClientRegistrations`].
If your application relies on the previous, shorter timeout -- for example, expecting a fast failure when the authorization server is unreachable -- you can restore it either by setting the JDK's `sun.net.client.defaultConnectTimeout` and `sun.net.client.defaultReadTimeout` system properties (in milliseconds), or by providing your own `RestOperations`, as described in xref:servlet/oauth2/resource-server/jwt.adoc#oauth2resourceserver-jwt-timeouts[Configuring Timeouts].
======
@@ -944,5 +944,52 @@ For MAC-based algorithms (such as `HS256`, `HS384`, or `HS512`), the `client-sec
If more than one `ClientRegistration` is configured for OpenID Connect 1.0 Authentication, the JWS algorithm resolver may evaluate the provided `ClientRegistration` to determine which algorithm to return.
====
[[oauth2login-advanced-idtoken-decoder-factory]]
== Providing a Custom JwtDecoderFactory
`OidcIdTokenDecoderFactory` is deliberately narrow: it exists to make it easy to support multiple `ClientRegistration` instances, each potentially needing a different `JwtDecoder`, and its configuration surface is limited to what most applications need for that purpose, such as the JWS algorithm resolver shown above.
If your application needs deeper control over how the ID Token's `JwtDecoder` is constructed -- for example, providing your own `RestOperations` -- you can instead provide your own `JwtDecoderFactory<ClientRegistration>` `@Bean`.
Because OAuth2 Login does not use a resource server `JwtDecoder` bean for this purpose, this is the supported way to reach the same level of control that `NimbusJwtDecoder`'s builder already offers, without introducing additional configuration properties on `OidcIdTokenDecoderFactory` itself.
For a single `ClientRegistration`, this can be as simple as:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
JwtDecoderFactory<ClientRegistration> idTokenDecoderFactory(RestOperations rest) {
return (clientRegistration) -> {
String issuerUri = clientRegistration.getProviderDetails().getIssuerUri();
NimbusJwtDecoder decoder = NimbusJwtDecoder.withIssuerLocation(issuerUri).restOperations(rest).build();
decoder.setJwtValidator(JwtValidators.createDefaultWithValidators(new OidcIdTokenValidator(clientRegistration)));
decoder.setClaimTypeConverter(OidcIdTokenDecoderFactory.createDefaultClaimTypeConverter());
return decoder;
};
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun idTokenDecoderFactory(rest: RestOperations): JwtDecoderFactory<ClientRegistration> {
return JwtDecoderFactory { clientRegistration ->
val issuerUri = clientRegistration.providerDetails.issuerUri
val decoder = NimbusJwtDecoder.withIssuerLocation(issuerUri).restOperations(rest).build()
decoder.setJwtValidator(JwtValidators.createDefaultWithValidators(OidcIdTokenValidator(clientRegistration)))
decoder.setClaimTypeConverter(OidcIdTokenDecoderFactory.createDefaultClaimTypeConverter())
decoder
}
}
----
======
If you have multiple `ClientRegistration` instances and want to avoid rebuilding a `JwtDecoder` on every request, cache the decoder per registration, for example by annotating the factory method with `@Cacheable` or by keying a `Map` on `ClientRegistration#getRegistrationId()`.
[[oauth2login-advanced-oidc-logout]]
Then, you can proceed to configure xref:servlet/oauth2/login/logout.adoc[logout]
@@ -1582,6 +1582,7 @@ fun jwtDecoder(): JwtDecoder {
== Configuring Timeouts
By default, Resource Server uses connection and socket timeouts of 30 seconds each for coordinating with the authorization server.
You can override these defaults without changing any code by setting the JDK's `sun.net.client.defaultConnectTimeout` and `sun.net.client.defaultReadTimeout` system properties (in milliseconds).
This may be too short in some scenarios.
Further, it doesn't take into account more sophisticated patterns like back-off and discovery.
@@ -27,6 +27,7 @@ import org.springframework.security.crypto.keygen.StringKeyGenerator;
* Requests.
*
* @author Joe Grandja
* @author Andrey Litvitski
* @since 7.0
*/
final class OAuth2PushedAuthorizationRequestUri {
@@ -60,7 +61,7 @@ final class OAuth2PushedAuthorizationRequestUri {
static OAuth2PushedAuthorizationRequestUri parse(String requestUri) {
int stateStartIndex = REQUEST_URI_PREFIX.length();
int expiresAtStartIndex = requestUri.indexOf(REQUEST_URI_DELIMITER) + REQUEST_URI_DELIMITER.length();
int expiresAtStartIndex = requestUri.lastIndexOf(REQUEST_URI_DELIMITER) + REQUEST_URI_DELIMITER.length();
OAuth2PushedAuthorizationRequestUri pushedAuthorizationRequestUri = new OAuth2PushedAuthorizationRequestUri();
pushedAuthorizationRequestUri.requestUri = requestUri;
pushedAuthorizationRequestUri.state = requestUri.substring(stateStartIndex);
@@ -0,0 +1,71 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.authentication;
import java.time.Instant;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link OAuth2PushedAuthorizationRequestUri}.
*
* @author Josh Cummings
* @author Andrey Litvitski
*/
public class OAuth2PushedAuthorizationRequestUriTests {
@Test
public void parseWhenValidRequestUriThenReturnsExpectedValues() {
String state = "abcXYZ123-abcXYZ123";
long epochMilli = 1700000000000L;
String requestUri = "urn:ietf:params:oauth:request_uri:" + state + "___" + epochMilli;
OAuth2PushedAuthorizationRequestUri parsed = OAuth2PushedAuthorizationRequestUri.parse(requestUri);
assertThat(parsed.getRequestUri()).isEqualTo(requestUri);
assertThat(parsed.getState()).isEqualTo(state + "___" + epochMilli);
assertThat(parsed.getExpiresAt()).isEqualTo(Instant.ofEpochMilli(epochMilli));
}
@Test
public void createWhenParsedThenReturnsEquivalentValues() {
Instant expiresAt = Instant.ofEpochMilli(1700000000000L);
OAuth2PushedAuthorizationRequestUri created = OAuth2PushedAuthorizationRequestUri.create(expiresAt);
OAuth2PushedAuthorizationRequestUri parsed = OAuth2PushedAuthorizationRequestUri.parse(created.getRequestUri());
assertThat(parsed.getRequestUri()).isEqualTo(created.getRequestUri());
assertThat(parsed.getState()).isEqualTo(created.getState());
assertThat(parsed.getExpiresAt()).isEqualTo(created.getExpiresAt());
}
@Test
public void parseWhenStateContainsDelimiterThenParsesSuccessfully() {
String state = "xXMGJTZwzXIFL8i_DFu_EM8IeWC___frCWjpiF2q-xs=";
long epochMillis = 1781670640281L;
String requestUri = "urn:ietf:params:oauth:request_uri:" + state + "___" + epochMillis;
OAuth2PushedAuthorizationRequestUri parsedUri = OAuth2PushedAuthorizationRequestUri.parse(requestUri);
assertThat(parsedUri.getRequestUri()).isEqualTo(requestUri);
assertThat(parsedUri.getState()).isEqualTo(state + "___" + epochMillis);
assertThat(parsedUri.getExpiresAt()).isEqualTo(Instant.ofEpochMilli(epochMillis));
}
}
@@ -66,11 +66,9 @@ final class JwtDecoderProviderConfigurationUtils {
private static final RestTemplate rest = new RestTemplate();
static {
int connectTimeout = Integer.parseInt(System.getProperty("sun.net.client.defaultConnectTimeout", "30000"));
int readTimeout = Integer.parseInt(System.getProperty("sun.net.client.defaultReadTimeout", "30000"));
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setConnectTimeout(connectTimeout);
requestFactory.setReadTimeout(readTimeout);
requestFactory.setConnectTimeout(getConnectTimeout());
requestFactory.setReadTimeout(getReadTimeout());
rest.setRequestFactory(requestFactory);
}
@@ -80,6 +78,27 @@ final class JwtDecoderProviderConfigurationUtils {
private JwtDecoderProviderConfigurationUtils() {
}
/**
* Returns the default HTTP connect timeout, in milliseconds, for fetching
* provider/JWK Set metadata. Honors the JDK's
* {@code sun.net.client.defaultConnectTimeout} system property when set, otherwise
* defaults to 30 seconds.
* @return the default HTTP connect timeout, in milliseconds
*/
static int getConnectTimeout() {
return Integer.parseInt(System.getProperty("sun.net.client.defaultConnectTimeout", "30000"));
}
/**
* Returns the default HTTP read timeout, in milliseconds, for fetching provider/JWK
* Set metadata. Honors the JDK's {@code sun.net.client.defaultReadTimeout} system
* property when set, otherwise defaults to 30 seconds.
* @return the default HTTP read timeout, in milliseconds
*/
static int getReadTimeout() {
return Integer.parseInt(System.getProperty("sun.net.client.defaultReadTimeout", "30000"));
}
static Map<String, Object> getConfigurationForOidcIssuerLocation(String oidcIssuerLocation) {
return getConfiguration(oidcIssuerLocation, rest, oidc(oidcIssuerLocation));
}
@@ -296,7 +296,7 @@ public final class NimbusJwtDecoder implements JwtDecoder {
private final Set<SignatureAlgorithm> signatureAlgorithms = new HashSet<>();
private RestOperations restOperations = new RestTemplateWithNimbusDefaultTimeouts();
private RestOperations restOperations = new RestTemplateWithDefaultTimeouts();
private Cache cache = new NoOpCache("default");
@@ -568,12 +568,12 @@ public final class NimbusJwtDecoder implements JwtDecoder {
* A RestTemplate with timeouts configured to avoid blocking indefinitely when
* fetching JWK Sets while holding the reentrantLock.
*/
private static final class RestTemplateWithNimbusDefaultTimeouts extends RestTemplate {
private static final class RestTemplateWithDefaultTimeouts extends RestTemplate {
private RestTemplateWithNimbusDefaultTimeouts() {
private RestTemplateWithDefaultTimeouts() {
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setConnectTimeout(JWKSourceBuilder.DEFAULT_HTTP_CONNECT_TIMEOUT);
requestFactory.setReadTimeout(JWKSourceBuilder.DEFAULT_HTTP_READ_TIMEOUT);
requestFactory.setConnectTimeout(JwtDecoderProviderConfigurationUtils.getConnectTimeout());
requestFactory.setReadTimeout(JwtDecoderProviderConfigurationUtils.getReadTimeout());
setRequestFactory(requestFactory);
}
@@ -30,6 +30,7 @@ import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jose.util.Base64URL;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.security.oauth2.jose.TestKeys;
@@ -46,6 +47,40 @@ import static org.mockito.BDDMockito.mock;
public class JwtDecoderProviderConfigurationUtilsTests {
@AfterEach
public void cleanup() {
System.clearProperty("sun.net.client.defaultConnectTimeout");
System.clearProperty("sun.net.client.defaultReadTimeout");
}
// gh-19474
@Test
public void getConnectTimeoutWhenPropertyNotSetThenDefaultsToThirtySeconds() {
System.clearProperty("sun.net.client.defaultConnectTimeout");
assertThat(JwtDecoderProviderConfigurationUtils.getConnectTimeout()).isEqualTo(30000);
}
// gh-19474
@Test
public void getConnectTimeoutWhenPropertySetThenUses() {
System.setProperty("sun.net.client.defaultConnectTimeout", "5000");
assertThat(JwtDecoderProviderConfigurationUtils.getConnectTimeout()).isEqualTo(5000);
}
// gh-19474
@Test
public void getReadTimeoutWhenPropertyNotSetThenDefaultsToThirtySeconds() {
System.clearProperty("sun.net.client.defaultReadTimeout");
assertThat(JwtDecoderProviderConfigurationUtils.getReadTimeout()).isEqualTo(30000);
}
// gh-19474
@Test
public void getReadTimeoutWhenPropertySetThenUses() {
System.setProperty("sun.net.client.defaultReadTimeout", "5000");
assertThat(JwtDecoderProviderConfigurationUtils.getReadTimeout()).isEqualTo(5000);
}
@Test
public void getSignatureAlgorithmsWhenJwkSetSpecifiesAlgorithmThenUses() throws Exception {
JWKSource<SecurityContext> jwkSource = mock(JWKSource.class);
@@ -67,14 +67,17 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jose.TestKeys;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -391,6 +394,24 @@ public class NimbusJwtDecoderTests {
// @formatter:on
}
// gh-19474
@Test
public void withJwkSetUriWhenDefaultRestOperationsThenUsesConfiguredTimeouts() {
try {
System.setProperty("sun.net.client.defaultConnectTimeout", "12345");
System.setProperty("sun.net.client.defaultReadTimeout", "23456");
NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder builder = NimbusJwtDecoder.withJwkSetUri(JWK_SET_URI);
RestOperations restOperations = (RestOperations) ReflectionTestUtils.getField(builder, "restOperations");
ClientHttpRequestFactory requestFactory = ((RestTemplate) restOperations).getRequestFactory();
assertThat(ReflectionTestUtils.getField(requestFactory, "connectTimeout")).isEqualTo(12345);
assertThat(ReflectionTestUtils.getField(requestFactory, "readTimeout")).isEqualTo(23456);
}
finally {
System.clearProperty("sun.net.client.defaultConnectTimeout");
System.clearProperty("sun.net.client.defaultReadTimeout");
}
}
@Test
public void cacheWhenNullThenThrowsException() {
NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder builder = NimbusJwtDecoder.withJwkSetUri(JWK_SET_URI);