Skip to content

Commit 03e090c

Browse files
committed
Merge branch '6.4.x'
Closes gh-16902
2 parents 9c073db + db34de5 commit 03e090c

File tree

6 files changed

+133
-74
lines changed

6 files changed

+133
-74
lines changed

Diff for: config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -1560,12 +1560,15 @@ static class JwkSetUriConfig {
15601560
@Bean
15611561
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
15621562
// @formatter:off
1563+
DefaultBearerTokenResolver defaultBearerTokenResolver = new DefaultBearerTokenResolver();
1564+
defaultBearerTokenResolver.setAllowUriQueryParameter(true);
15631565
http
15641566
.authorizeRequests()
15651567
.requestMatchers("/requires-read-scope").access("hasAuthority('SCOPE_message:read')")
15661568
.anyRequest().authenticated()
15671569
.and()
15681570
.oauth2ResourceServer()
1571+
.bearerTokenResolver(defaultBearerTokenResolver)
15691572
.jwt()
15701573
.jwkSetUri(this.jwkSetUri);
15711574
return http.build();

Diff for: config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-JwkSetUri.xml

+6-1
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,15 @@
2525

2626
<c:property-placeholder local-override="true"/>
2727

28+
<b:bean id="bearerTokenResolver"
29+
class="org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver">
30+
<b:property name="allowUriQueryParameter" value="true"/>
31+
</b:bean>
32+
2833
<http>
2934
<intercept-url pattern="/**" access="authenticated"/>
3035
<intercept-url pattern="/requires-read-scope" access="hasAuthority('SCOPE_message:read')"/>
31-
<oauth2-resource-server>
36+
<oauth2-resource-server bearer-token-resolver-ref="bearerTokenResolver">
3237
<jwt jwk-set-uri="${jwk-set-uri:https://idp.example.org}"/>
3338
</oauth2-resource-server>
3439
</http>

Diff for: oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/DefaultBearerTokenResolver.java

+65-60
Original file line numberDiff line numberDiff line change
@@ -52,26 +52,77 @@ public final class DefaultBearerTokenResolver implements BearerTokenResolver {
5252

5353
@Override
5454
public String resolve(final HttpServletRequest request) {
55-
final String authorizationHeaderToken = resolveFromAuthorizationHeader(request);
56-
final String parameterToken = isParameterTokenSupportedForRequest(request)
57-
? resolveFromRequestParameters(request) : null;
58-
if (authorizationHeaderToken != null) {
59-
if (parameterToken != null) {
55+
// @formatter:off
56+
return resolveToken(
57+
resolveFromAuthorizationHeader(request),
58+
resolveAccessTokenFromQueryString(request),
59+
resolveAccessTokenFromBody(request)
60+
);
61+
// @formatter:on
62+
}
63+
64+
private static String resolveToken(String... accessTokens) {
65+
if (accessTokens == null || accessTokens.length == 0) {
66+
return null;
67+
}
68+
69+
String accessToken = null;
70+
for (String token : accessTokens) {
71+
if (accessToken == null) {
72+
accessToken = token;
73+
}
74+
else if (token != null) {
6075
BearerTokenError error = BearerTokenErrors
6176
.invalidRequest("Found multiple bearer tokens in the request");
6277
throw new OAuth2AuthenticationException(error);
6378
}
64-
return authorizationHeaderToken;
6579
}
66-
if (parameterToken != null && isParameterTokenEnabledForRequest(request)) {
67-
if (parameterToken.isBlank()) {
68-
BearerTokenError error = BearerTokenErrors
69-
.invalidRequest("The requested token parameter is an empty string");
70-
throw new OAuth2AuthenticationException(error);
71-
}
72-
return parameterToken;
80+
81+
if (accessToken != null && accessToken.isBlank()) {
82+
BearerTokenError error = BearerTokenErrors
83+
.invalidRequest("The requested token parameter is an empty string");
84+
throw new OAuth2AuthenticationException(error);
85+
}
86+
87+
return accessToken;
88+
}
89+
90+
private String resolveFromAuthorizationHeader(HttpServletRequest request) {
91+
String authorization = request.getHeader(this.bearerTokenHeaderName);
92+
if (!StringUtils.startsWithIgnoreCase(authorization, "bearer")) {
93+
return null;
94+
}
95+
96+
Matcher matcher = authorizationPattern.matcher(authorization);
97+
if (!matcher.matches()) {
98+
BearerTokenError error = BearerTokenErrors.invalidToken("Bearer token is malformed");
99+
throw new OAuth2AuthenticationException(error);
100+
}
101+
102+
return matcher.group("token");
103+
}
104+
105+
private String resolveAccessTokenFromQueryString(HttpServletRequest request) {
106+
if (!this.allowUriQueryParameter || !HttpMethod.GET.name().equals(request.getMethod())) {
107+
return null;
108+
}
109+
110+
return resolveToken(request.getParameterValues(ACCESS_TOKEN_PARAMETER_NAME));
111+
}
112+
113+
private String resolveAccessTokenFromBody(HttpServletRequest request) {
114+
if (!this.allowFormEncodedBodyParameter
115+
|| !MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(request.getContentType())
116+
|| HttpMethod.GET.name().equals(request.getMethod())) {
117+
return null;
118+
}
119+
120+
String queryString = request.getQueryString();
121+
if (queryString != null && queryString.contains(ACCESS_TOKEN_PARAMETER_NAME)) {
122+
return null;
73123
}
74-
return null;
124+
125+
return resolveToken(request.getParameterValues(ACCESS_TOKEN_PARAMETER_NAME));
75126
}
76127

77128
/**
@@ -109,50 +160,4 @@ public void setBearerTokenHeaderName(String bearerTokenHeaderName) {
109160
this.bearerTokenHeaderName = bearerTokenHeaderName;
110161
}
111162

112-
private String resolveFromAuthorizationHeader(HttpServletRequest request) {
113-
String authorization = request.getHeader(this.bearerTokenHeaderName);
114-
if (!StringUtils.startsWithIgnoreCase(authorization, "bearer")) {
115-
return null;
116-
}
117-
Matcher matcher = authorizationPattern.matcher(authorization);
118-
if (!matcher.matches()) {
119-
BearerTokenError error = BearerTokenErrors.invalidToken("Bearer token is malformed");
120-
throw new OAuth2AuthenticationException(error);
121-
}
122-
return matcher.group("token");
123-
}
124-
125-
private static String resolveFromRequestParameters(HttpServletRequest request) {
126-
String[] values = request.getParameterValues(ACCESS_TOKEN_PARAMETER_NAME);
127-
if (values == null || values.length == 0) {
128-
return null;
129-
}
130-
if (values.length == 1) {
131-
return values[0];
132-
}
133-
BearerTokenError error = BearerTokenErrors.invalidRequest("Found multiple bearer tokens in the request");
134-
throw new OAuth2AuthenticationException(error);
135-
}
136-
137-
private boolean isParameterTokenSupportedForRequest(final HttpServletRequest request) {
138-
return isFormEncodedRequest(request) || isGetRequest(request);
139-
}
140-
141-
private static boolean isGetRequest(HttpServletRequest request) {
142-
return HttpMethod.GET.name().equals(request.getMethod());
143-
}
144-
145-
private static boolean isFormEncodedRequest(HttpServletRequest request) {
146-
return MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(request.getContentType());
147-
}
148-
149-
private static boolean hasAccessTokenInQueryString(HttpServletRequest request) {
150-
return (request.getQueryString() != null) && request.getQueryString().contains(ACCESS_TOKEN_PARAMETER_NAME);
151-
}
152-
153-
private boolean isParameterTokenEnabledForRequest(HttpServletRequest request) {
154-
return ((this.allowFormEncodedBodyParameter && isFormEncodedRequest(request) && !isGetRequest(request)
155-
&& !hasAccessTokenInQueryString(request)) || (this.allowUriQueryParameter && isGetRequest(request)));
156-
}
157-
158163
}

Diff for: oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/authentication/ServerBearerTokenAuthenticationConverter.java

+10-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -77,18 +77,18 @@ private String token(ServerHttpRequest request) {
7777
}
7878
return authorizationHeaderToken;
7979
}
80-
if (parameterToken != null && isParameterTokenSupportedForRequest(request)) {
81-
if (!StringUtils.hasText(parameterToken)) {
82-
BearerTokenError error = BearerTokenErrors
83-
.invalidRequest("The requested token parameter is an empty string");
84-
throw new OAuth2AuthenticationException(error);
85-
}
86-
return parameterToken;
80+
if (parameterToken != null && !StringUtils.hasText(parameterToken)) {
81+
BearerTokenError error = BearerTokenErrors
82+
.invalidRequest("The requested token parameter is an empty string");
83+
throw new OAuth2AuthenticationException(error);
8784
}
88-
return null;
85+
return parameterToken;
8986
}
9087

91-
private static String resolveAccessTokenFromRequest(ServerHttpRequest request) {
88+
private String resolveAccessTokenFromRequest(ServerHttpRequest request) {
89+
if (!isParameterTokenSupportedForRequest(request)) {
90+
return null;
91+
}
9292
List<String> parameterTokens = request.getQueryParams().get("access_token");
9393
if (CollectionUtils.isEmpty(parameterTokens)) {
9494
return null;

Diff for: oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/DefaultBearerTokenResolverTests.java

+37-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -110,6 +110,7 @@ public void resolveWhenHeaderWithInvalidCharactersIsPresentThenAuthenticationExc
110110

111111
@Test
112112
public void resolveWhenValidHeaderIsPresentTogetherWithFormParameterThenAuthenticationExceptionIsThrown() {
113+
this.resolver.setAllowFormEncodedBodyParameter(true);
113114
MockHttpServletRequest request = new MockHttpServletRequest();
114115
request.addHeader("Authorization", "Bearer " + TEST_TOKEN);
115116
request.setMethod("POST");
@@ -121,6 +122,7 @@ public void resolveWhenValidHeaderIsPresentTogetherWithFormParameterThenAuthenti
121122

122123
@Test
123124
public void resolveWhenValidHeaderIsPresentTogetherWithQueryParameterThenAuthenticationExceptionIsThrown() {
125+
this.resolver.setAllowUriQueryParameter(true);
124126
MockHttpServletRequest request = new MockHttpServletRequest();
125127
request.addHeader("Authorization", "Bearer " + TEST_TOKEN);
126128
request.setMethod("GET");
@@ -133,6 +135,7 @@ public void resolveWhenValidHeaderIsPresentTogetherWithQueryParameterThenAuthent
133135
// gh-10326
134136
@Test
135137
public void resolveWhenRequestContainsTwoAccessTokenQueryParametersThenAuthenticationExceptionIsThrown() {
138+
this.resolver.setAllowUriQueryParameter(true);
136139
MockHttpServletRequest request = new MockHttpServletRequest();
137140
request.setMethod("GET");
138141
request.addParameter("access_token", "token1", "token2");
@@ -143,6 +146,7 @@ public void resolveWhenRequestContainsTwoAccessTokenQueryParametersThenAuthentic
143146
// gh-10326
144147
@Test
145148
public void resolveWhenRequestContainsTwoAccessTokenFormParametersThenAuthenticationExceptionIsThrown() {
149+
this.resolver.setAllowFormEncodedBodyParameter(true);
146150
MockHttpServletRequest request = new MockHttpServletRequest();
147151
request.setMethod("POST");
148152
request.setContentType("application/x-www-form-urlencoded");
@@ -233,6 +237,19 @@ public void resolveWhenPostAndFormParameterIsSupportedAndQueryParameterIsPresent
233237
assertThat(this.resolver.resolve(request)).isNull();
234238
}
235239

240+
@Test
241+
public void resolveWhenPostAndQueryParameterIsSupportedAndFormParameterIsPresentThenTokenIsNotResolved() {
242+
this.resolver.setAllowUriQueryParameter(true);
243+
244+
MockHttpServletRequest request = new MockHttpServletRequest();
245+
request.setMethod("POST");
246+
request.setContentType("application/x-www-form-urlencoded");
247+
request.setQueryString("access_token=" + TEST_TOKEN);
248+
request.addParameter("access_token", TEST_TOKEN);
249+
250+
assertThat(this.resolver.resolve(request)).isNull();
251+
}
252+
236253
@Test
237254
public void resolveWhenFormParameterIsPresentAndNotSupportedThenTokenIsNotResolved() {
238255
MockHttpServletRequest request = new MockHttpServletRequest();
@@ -261,6 +278,25 @@ public void resolveWhenQueryParameterIsPresentAndNotSupportedThenTokenIsNotResol
261278
assertThat(this.resolver.resolve(request)).isNull();
262279
}
263280

281+
// gh-16038
282+
@Test
283+
public void resolveWhenRequestContainsTwoAccessTokenFormParametersAndSupportIsDisabledThenTokenIsNotResolved() {
284+
MockHttpServletRequest request = new MockHttpServletRequest();
285+
request.setMethod("POST");
286+
request.setContentType("application/x-www-form-urlencoded");
287+
request.addParameter("access_token", "token1", "token2");
288+
assertThat(this.resolver.resolve(request)).isNull();
289+
}
290+
291+
// gh-16038
292+
@Test
293+
public void resolveWhenRequestContainsTwoAccessTokenQueryParametersAndSupportIsDisabledThenTokenIsNotResolved() {
294+
MockHttpServletRequest request = new MockHttpServletRequest();
295+
request.setMethod("GET");
296+
request.addParameter("access_token", "token1", "token2");
297+
assertThat(this.resolver.resolve(request)).isNull();
298+
}
299+
264300
@Test
265301
public void resolveWhenQueryParameterIsPresentAndEmptyStringThenTokenIsNotResolved() {
266302
this.resolver.setAllowUriQueryParameter(true);

Diff for: oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/server/authentication/ServerBearerTokenAuthenticationConverterTests.java

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -157,6 +157,7 @@ public void resolveWhenHeaderWithInvalidCharactersIsPresentAndNotSubscribedThenN
157157
@Test
158158
public void resolveWhenValidHeaderIsPresentTogetherWithQueryParameterThenAuthenticationExceptionIsThrown() {
159159
// @formatter:off
160+
this.converter.setAllowUriQueryParameter(true);
160161
MockServerHttpRequest.BaseBuilder<?> request = MockServerHttpRequest.get("/")
161162
.queryParam("access_token", TEST_TOKEN)
162163
.header(HttpHeaders.AUTHORIZATION, "Bearer " + TEST_TOKEN);
@@ -205,6 +206,7 @@ public void resolveWhenQueryParameterIsPresentAndNotSupportedThenTokenIsNotResol
205206

206207
@Test
207208
void resolveWhenQueryParameterHasMultipleAccessTokensThenOAuth2AuthenticationException() {
209+
this.converter.setAllowUriQueryParameter(true);
208210
MockServerHttpRequest.BaseBuilder<?> request = MockServerHttpRequest.get("/")
209211
.queryParam("access_token", TEST_TOKEN, TEST_TOKEN);
210212
assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(() -> convertToToken(request))
@@ -217,6 +219,14 @@ void resolveWhenQueryParameterHasMultipleAccessTokensThenOAuth2AuthenticationExc
217219

218220
}
219221

222+
// gh-16038
223+
@Test
224+
void resolveWhenRequestContainsTwoAccessTokenQueryParametersAndSupportIsDisabledThenTokenIsNotResolved() {
225+
MockServerHttpRequest.BaseBuilder<?> request = MockServerHttpRequest.get("/")
226+
.queryParam("access_token", TEST_TOKEN, TEST_TOKEN);
227+
assertThat(convertToToken(request)).isNull();
228+
}
229+
220230
private BearerTokenAuthenticationToken convertToToken(MockServerHttpRequest.BaseBuilder<?> request) {
221231
return convertToToken(request.build());
222232
}

0 commit comments

Comments
 (0)