Merge branch '6.3.x' into 6.4.x

Closes gh-16901
This commit is contained in:
Steve Riesenberg 2025-04-07 10:55:51 -05:00
commit db34de59bc
No known key found for this signature in database
GPG Key ID: 3D0169B18AB8F0A9
6 changed files with 133 additions and 74 deletions

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2022 the original author or authors. * Copyright 2002-2025 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -1560,12 +1560,15 @@ public class OAuth2ResourceServerConfigurerTests {
@Bean @Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception { SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// @formatter:off // @formatter:off
DefaultBearerTokenResolver defaultBearerTokenResolver = new DefaultBearerTokenResolver();
defaultBearerTokenResolver.setAllowUriQueryParameter(true);
http http
.authorizeRequests() .authorizeRequests()
.requestMatchers("/requires-read-scope").access("hasAuthority('SCOPE_message:read')") .requestMatchers("/requires-read-scope").access("hasAuthority('SCOPE_message:read')")
.anyRequest().authenticated() .anyRequest().authenticated()
.and() .and()
.oauth2ResourceServer() .oauth2ResourceServer()
.bearerTokenResolver(defaultBearerTokenResolver)
.jwt() .jwt()
.jwkSetUri(this.jwkSetUri); .jwkSetUri(this.jwkSetUri);
return http.build(); return http.build();

View File

@ -25,10 +25,15 @@
<c:property-placeholder local-override="true"/> <c:property-placeholder local-override="true"/>
<b:bean id="bearerTokenResolver"
class="org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver">
<b:property name="allowUriQueryParameter" value="true"/>
</b:bean>
<http> <http>
<intercept-url pattern="/**" access="authenticated"/> <intercept-url pattern="/**" access="authenticated"/>
<intercept-url pattern="/requires-read-scope" access="hasAuthority('SCOPE_message:read')"/> <intercept-url pattern="/requires-read-scope" access="hasAuthority('SCOPE_message:read')"/>
<oauth2-resource-server> <oauth2-resource-server bearer-token-resolver-ref="bearerTokenResolver">
<jwt jwk-set-uri="${jwk-set-uri:https://idp.example.org}"/> <jwt jwk-set-uri="${jwk-set-uri:https://idp.example.org}"/>
</oauth2-resource-server> </oauth2-resource-server>
</http> </http>

View File

@ -52,26 +52,77 @@ public final class DefaultBearerTokenResolver implements BearerTokenResolver {
@Override @Override
public String resolve(final HttpServletRequest request) { public String resolve(final HttpServletRequest request) {
final String authorizationHeaderToken = resolveFromAuthorizationHeader(request); // @formatter:off
final String parameterToken = isParameterTokenSupportedForRequest(request) return resolveToken(
? resolveFromRequestParameters(request) : null; resolveFromAuthorizationHeader(request),
if (authorizationHeaderToken != null) { resolveAccessTokenFromQueryString(request),
if (parameterToken != null) { resolveAccessTokenFromBody(request)
);
// @formatter:on
}
private static String resolveToken(String... accessTokens) {
if (accessTokens == null || accessTokens.length == 0) {
return null;
}
String accessToken = null;
for (String token : accessTokens) {
if (accessToken == null) {
accessToken = token;
}
else if (token != null) {
BearerTokenError error = BearerTokenErrors BearerTokenError error = BearerTokenErrors
.invalidRequest("Found multiple bearer tokens in the request"); .invalidRequest("Found multiple bearer tokens in the request");
throw new OAuth2AuthenticationException(error); throw new OAuth2AuthenticationException(error);
} }
return authorizationHeaderToken;
} }
if (parameterToken != null && isParameterTokenEnabledForRequest(request)) {
if (!StringUtils.hasText(parameterToken)) { if (accessToken != null && accessToken.isBlank()) {
BearerTokenError error = BearerTokenErrors BearerTokenError error = BearerTokenErrors
.invalidRequest("The requested token parameter is an empty string"); .invalidRequest("The requested token parameter is an empty string");
throw new OAuth2AuthenticationException(error); throw new OAuth2AuthenticationException(error);
}
return parameterToken;
} }
return null;
return accessToken;
}
private String resolveFromAuthorizationHeader(HttpServletRequest request) {
String authorization = request.getHeader(this.bearerTokenHeaderName);
if (!StringUtils.startsWithIgnoreCase(authorization, "bearer")) {
return null;
}
Matcher matcher = authorizationPattern.matcher(authorization);
if (!matcher.matches()) {
BearerTokenError error = BearerTokenErrors.invalidToken("Bearer token is malformed");
throw new OAuth2AuthenticationException(error);
}
return matcher.group("token");
}
private String resolveAccessTokenFromQueryString(HttpServletRequest request) {
if (!this.allowUriQueryParameter || !HttpMethod.GET.name().equals(request.getMethod())) {
return null;
}
return resolveToken(request.getParameterValues(ACCESS_TOKEN_PARAMETER_NAME));
}
private String resolveAccessTokenFromBody(HttpServletRequest request) {
if (!this.allowFormEncodedBodyParameter
|| !MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(request.getContentType())
|| HttpMethod.GET.name().equals(request.getMethod())) {
return null;
}
String queryString = request.getQueryString();
if (queryString != null && queryString.contains(ACCESS_TOKEN_PARAMETER_NAME)) {
return null;
}
return resolveToken(request.getParameterValues(ACCESS_TOKEN_PARAMETER_NAME));
} }
/** /**
@ -109,50 +160,4 @@ public final class DefaultBearerTokenResolver implements BearerTokenResolver {
this.bearerTokenHeaderName = bearerTokenHeaderName; this.bearerTokenHeaderName = bearerTokenHeaderName;
} }
private String resolveFromAuthorizationHeader(HttpServletRequest request) {
String authorization = request.getHeader(this.bearerTokenHeaderName);
if (!StringUtils.startsWithIgnoreCase(authorization, "bearer")) {
return null;
}
Matcher matcher = authorizationPattern.matcher(authorization);
if (!matcher.matches()) {
BearerTokenError error = BearerTokenErrors.invalidToken("Bearer token is malformed");
throw new OAuth2AuthenticationException(error);
}
return matcher.group("token");
}
private static String resolveFromRequestParameters(HttpServletRequest request) {
String[] values = request.getParameterValues(ACCESS_TOKEN_PARAMETER_NAME);
if (values == null || values.length == 0) {
return null;
}
if (values.length == 1) {
return values[0];
}
BearerTokenError error = BearerTokenErrors.invalidRequest("Found multiple bearer tokens in the request");
throw new OAuth2AuthenticationException(error);
}
private boolean isParameterTokenSupportedForRequest(final HttpServletRequest request) {
return isFormEncodedRequest(request) || isGetRequest(request);
}
private static boolean isGetRequest(HttpServletRequest request) {
return HttpMethod.GET.name().equals(request.getMethod());
}
private static boolean isFormEncodedRequest(HttpServletRequest request) {
return MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(request.getContentType());
}
private static boolean hasAccessTokenInQueryString(HttpServletRequest request) {
return (request.getQueryString() != null) && request.getQueryString().contains(ACCESS_TOKEN_PARAMETER_NAME);
}
private boolean isParameterTokenEnabledForRequest(HttpServletRequest request) {
return ((this.allowFormEncodedBodyParameter && isFormEncodedRequest(request) && !isGetRequest(request)
&& !hasAccessTokenInQueryString(request)) || (this.allowUriQueryParameter && isGetRequest(request)));
}
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2024 the original author or authors. * Copyright 2002-2025 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -77,18 +77,18 @@ public class ServerBearerTokenAuthenticationConverter implements ServerAuthentic
} }
return authorizationHeaderToken; return authorizationHeaderToken;
} }
if (parameterToken != null && isParameterTokenSupportedForRequest(request)) { if (parameterToken != null && !StringUtils.hasText(parameterToken)) {
if (!StringUtils.hasText(parameterToken)) { BearerTokenError error = BearerTokenErrors
BearerTokenError error = BearerTokenErrors .invalidRequest("The requested token parameter is an empty string");
.invalidRequest("The requested token parameter is an empty string"); throw new OAuth2AuthenticationException(error);
throw new OAuth2AuthenticationException(error);
}
return parameterToken;
} }
return null; return parameterToken;
} }
private static String resolveAccessTokenFromRequest(ServerHttpRequest request) { private String resolveAccessTokenFromRequest(ServerHttpRequest request) {
if (!isParameterTokenSupportedForRequest(request)) {
return null;
}
List<String> parameterTokens = request.getQueryParams().get("access_token"); List<String> parameterTokens = request.getQueryParams().get("access_token");
if (CollectionUtils.isEmpty(parameterTokens)) { if (CollectionUtils.isEmpty(parameterTokens)) {
return null; return null;

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2024 the original author or authors. * Copyright 2002-2025 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -110,6 +110,7 @@ public class DefaultBearerTokenResolverTests {
@Test @Test
public void resolveWhenValidHeaderIsPresentTogetherWithFormParameterThenAuthenticationExceptionIsThrown() { public void resolveWhenValidHeaderIsPresentTogetherWithFormParameterThenAuthenticationExceptionIsThrown() {
this.resolver.setAllowFormEncodedBodyParameter(true);
MockHttpServletRequest request = new MockHttpServletRequest(); MockHttpServletRequest request = new MockHttpServletRequest();
request.addHeader("Authorization", "Bearer " + TEST_TOKEN); request.addHeader("Authorization", "Bearer " + TEST_TOKEN);
request.setMethod("POST"); request.setMethod("POST");
@ -121,6 +122,7 @@ public class DefaultBearerTokenResolverTests {
@Test @Test
public void resolveWhenValidHeaderIsPresentTogetherWithQueryParameterThenAuthenticationExceptionIsThrown() { public void resolveWhenValidHeaderIsPresentTogetherWithQueryParameterThenAuthenticationExceptionIsThrown() {
this.resolver.setAllowUriQueryParameter(true);
MockHttpServletRequest request = new MockHttpServletRequest(); MockHttpServletRequest request = new MockHttpServletRequest();
request.addHeader("Authorization", "Bearer " + TEST_TOKEN); request.addHeader("Authorization", "Bearer " + TEST_TOKEN);
request.setMethod("GET"); request.setMethod("GET");
@ -133,6 +135,7 @@ public class DefaultBearerTokenResolverTests {
// gh-10326 // gh-10326
@Test @Test
public void resolveWhenRequestContainsTwoAccessTokenQueryParametersThenAuthenticationExceptionIsThrown() { public void resolveWhenRequestContainsTwoAccessTokenQueryParametersThenAuthenticationExceptionIsThrown() {
this.resolver.setAllowUriQueryParameter(true);
MockHttpServletRequest request = new MockHttpServletRequest(); MockHttpServletRequest request = new MockHttpServletRequest();
request.setMethod("GET"); request.setMethod("GET");
request.addParameter("access_token", "token1", "token2"); request.addParameter("access_token", "token1", "token2");
@ -143,6 +146,7 @@ public class DefaultBearerTokenResolverTests {
// gh-10326 // gh-10326
@Test @Test
public void resolveWhenRequestContainsTwoAccessTokenFormParametersThenAuthenticationExceptionIsThrown() { public void resolveWhenRequestContainsTwoAccessTokenFormParametersThenAuthenticationExceptionIsThrown() {
this.resolver.setAllowFormEncodedBodyParameter(true);
MockHttpServletRequest request = new MockHttpServletRequest(); MockHttpServletRequest request = new MockHttpServletRequest();
request.setMethod("POST"); request.setMethod("POST");
request.setContentType("application/x-www-form-urlencoded"); request.setContentType("application/x-www-form-urlencoded");
@ -233,6 +237,19 @@ public class DefaultBearerTokenResolverTests {
assertThat(this.resolver.resolve(request)).isNull(); assertThat(this.resolver.resolve(request)).isNull();
} }
@Test
public void resolveWhenPostAndQueryParameterIsSupportedAndFormParameterIsPresentThenTokenIsNotResolved() {
this.resolver.setAllowUriQueryParameter(true);
MockHttpServletRequest request = new MockHttpServletRequest();
request.setMethod("POST");
request.setContentType("application/x-www-form-urlencoded");
request.setQueryString("access_token=" + TEST_TOKEN);
request.addParameter("access_token", TEST_TOKEN);
assertThat(this.resolver.resolve(request)).isNull();
}
@Test @Test
public void resolveWhenFormParameterIsPresentAndNotSupportedThenTokenIsNotResolved() { public void resolveWhenFormParameterIsPresentAndNotSupportedThenTokenIsNotResolved() {
MockHttpServletRequest request = new MockHttpServletRequest(); MockHttpServletRequest request = new MockHttpServletRequest();
@ -261,6 +278,25 @@ public class DefaultBearerTokenResolverTests {
assertThat(this.resolver.resolve(request)).isNull(); assertThat(this.resolver.resolve(request)).isNull();
} }
// gh-16038
@Test
public void resolveWhenRequestContainsTwoAccessTokenFormParametersAndSupportIsDisabledThenTokenIsNotResolved() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setMethod("POST");
request.setContentType("application/x-www-form-urlencoded");
request.addParameter("access_token", "token1", "token2");
assertThat(this.resolver.resolve(request)).isNull();
}
// gh-16038
@Test
public void resolveWhenRequestContainsTwoAccessTokenQueryParametersAndSupportIsDisabledThenTokenIsNotResolved() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setMethod("GET");
request.addParameter("access_token", "token1", "token2");
assertThat(this.resolver.resolve(request)).isNull();
}
@Test @Test
public void resolveWhenQueryParameterIsPresentAndEmptyStringThenTokenIsNotResolved() { public void resolveWhenQueryParameterIsPresentAndEmptyStringThenTokenIsNotResolved() {
this.resolver.setAllowUriQueryParameter(true); this.resolver.setAllowUriQueryParameter(true);

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2024 the original author or authors. * Copyright 2002-2025 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -157,6 +157,7 @@ public class ServerBearerTokenAuthenticationConverterTests {
@Test @Test
public void resolveWhenValidHeaderIsPresentTogetherWithQueryParameterThenAuthenticationExceptionIsThrown() { public void resolveWhenValidHeaderIsPresentTogetherWithQueryParameterThenAuthenticationExceptionIsThrown() {
// @formatter:off // @formatter:off
this.converter.setAllowUriQueryParameter(true);
MockServerHttpRequest.BaseBuilder<?> request = MockServerHttpRequest.get("/") MockServerHttpRequest.BaseBuilder<?> request = MockServerHttpRequest.get("/")
.queryParam("access_token", TEST_TOKEN) .queryParam("access_token", TEST_TOKEN)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + TEST_TOKEN); .header(HttpHeaders.AUTHORIZATION, "Bearer " + TEST_TOKEN);
@ -205,6 +206,7 @@ public class ServerBearerTokenAuthenticationConverterTests {
@Test @Test
void resolveWhenQueryParameterHasMultipleAccessTokensThenOAuth2AuthenticationException() { void resolveWhenQueryParameterHasMultipleAccessTokensThenOAuth2AuthenticationException() {
this.converter.setAllowUriQueryParameter(true);
MockServerHttpRequest.BaseBuilder<?> request = MockServerHttpRequest.get("/") MockServerHttpRequest.BaseBuilder<?> request = MockServerHttpRequest.get("/")
.queryParam("access_token", TEST_TOKEN, TEST_TOKEN); .queryParam("access_token", TEST_TOKEN, TEST_TOKEN);
assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(() -> convertToToken(request)) assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(() -> convertToToken(request))
@ -217,6 +219,14 @@ public class ServerBearerTokenAuthenticationConverterTests {
} }
// gh-16038
@Test
void resolveWhenRequestContainsTwoAccessTokenQueryParametersAndSupportIsDisabledThenTokenIsNotResolved() {
MockServerHttpRequest.BaseBuilder<?> request = MockServerHttpRequest.get("/")
.queryParam("access_token", TEST_TOKEN, TEST_TOKEN);
assertThat(convertToToken(request)).isNull();
}
private BearerTokenAuthenticationToken convertToToken(MockServerHttpRequest.BaseBuilder<?> request) { private BearerTokenAuthenticationToken convertToToken(MockServerHttpRequest.BaseBuilder<?> request) {
return convertToToken(request.build()); return convertToToken(request.build());
} }