Skip to content

Commit 9674532

Browse files
jonah1und1sjohnr
authored andcommitted
Add support for access token in body parameter as per rfc 6750 Sec. 2.2
Issue gh-15818
1 parent 03e090c commit 9674532

File tree

2 files changed

+199
-30
lines changed

2 files changed

+199
-30
lines changed

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

Lines changed: 95 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,20 @@
1616

1717
package org.springframework.security.oauth2.server.resource.web.server.authentication;
1818

19+
import static org.springframework.security.oauth2.server.resource.BearerTokenErrors.invalidRequest;
20+
1921
import java.util.List;
2022
import java.util.regex.Matcher;
2123
import java.util.regex.Pattern;
2224

25+
import reactor.core.publisher.Flux;
2326
import reactor.core.publisher.Mono;
27+
import reactor.util.function.Tuple2;
28+
import reactor.util.function.Tuples;
2429

2530
import org.springframework.http.HttpHeaders;
2631
import org.springframework.http.HttpMethod;
32+
import org.springframework.http.MediaType;
2733
import org.springframework.http.server.reactive.ServerHttpRequest;
2834
import org.springframework.security.core.Authentication;
2935
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
@@ -47,16 +53,20 @@
4753
*/
4854
public class ServerBearerTokenAuthenticationConverter implements ServerAuthenticationConverter {
4955

56+
public static final String ACCESS_TOKEN_NAME = "access_token";
57+
public static final String MULTIPLE_BEARER_TOKENS_ERROR_MSG = "Found multiple bearer tokens in the request";
5058
private static final Pattern authorizationPattern = Pattern.compile("^Bearer (?<token>[a-zA-Z0-9-._~+/]+=*)$",
5159
Pattern.CASE_INSENSITIVE);
5260

5361
private boolean allowUriQueryParameter = false;
5462

63+
private boolean allowFormEncodedBodyParameter = false;
64+
5565
private String bearerTokenHeaderName = HttpHeaders.AUTHORIZATION;
5666

5767
@Override
5868
public Mono<Authentication> convert(ServerWebExchange exchange) {
59-
return Mono.fromCallable(() -> token(exchange.getRequest())).map((token) -> {
69+
return Mono.defer(() -> token(exchange)).map(token -> {
6070
if (token.isEmpty()) {
6171
BearerTokenError error = invalidTokenError();
6272
throw new OAuth2AuthenticationException(error);
@@ -65,43 +75,53 @@ public Mono<Authentication> convert(ServerWebExchange exchange) {
6575
});
6676
}
6777

68-
private String token(ServerHttpRequest request) {
69-
String authorizationHeaderToken = resolveFromAuthorizationHeader(request.getHeaders());
70-
String parameterToken = resolveAccessTokenFromRequest(request);
71-
72-
if (authorizationHeaderToken != null) {
73-
if (parameterToken != null) {
74-
BearerTokenError error = BearerTokenErrors
75-
.invalidRequest("Found multiple bearer tokens in the request");
76-
throw new OAuth2AuthenticationException(error);
77-
}
78-
return authorizationHeaderToken;
79-
}
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);
84-
}
85-
return parameterToken;
78+
private Mono<String> token(ServerWebExchange exchange) {
79+
final ServerHttpRequest request = exchange.getRequest();
80+
81+
return Flux.merge(resolveFromAuthorizationHeader(request.getHeaders()).map(s -> Tuples.of(s, TokenSource.HEADER)),
82+
resolveAccessTokenFromRequest(request).map(s -> Tuples.of(s, TokenSource.QUERY_PARAMETER)),
83+
resolveAccessTokenFromBody(exchange).map(s -> Tuples.of(s, TokenSource.BODY_PARAMETER)))
84+
.collectList()
85+
.mapNotNull(tokenTuples -> {
86+
switch (tokenTuples.size()) {
87+
case 0:
88+
return null;
89+
case 1:
90+
return getTokenIfSupported(tokenTuples.get(0), request);
91+
default:
92+
BearerTokenError error = invalidRequest(MULTIPLE_BEARER_TOKENS_ERROR_MSG);
93+
throw new OAuth2AuthenticationException(error);
94+
}
95+
});
8696
}
8797

88-
private String resolveAccessTokenFromRequest(ServerHttpRequest request) {
89-
if (!isParameterTokenSupportedForRequest(request)) {
90-
return null;
91-
}
92-
List<String> parameterTokens = request.getQueryParams().get("access_token");
98+
private static Mono<String> resolveAccessTokenFromRequest(ServerHttpRequest request) {
99+
List<String> parameterTokens = request.getQueryParams().get(ACCESS_TOKEN_NAME);
93100
if (CollectionUtils.isEmpty(parameterTokens)) {
94-
return null;
101+
return Mono.empty();
95102
}
96103
if (parameterTokens.size() == 1) {
97-
return parameterTokens.get(0);
104+
return Mono.just(parameterTokens.get(0));
98105
}
99106

100-
BearerTokenError error = BearerTokenErrors.invalidRequest("Found multiple bearer tokens in the request");
107+
BearerTokenError error = invalidRequest(MULTIPLE_BEARER_TOKENS_ERROR_MSG);
101108
throw new OAuth2AuthenticationException(error);
102109

103110
}
104111

112+
private String getTokenIfSupported(Tuple2<String, TokenSource> tokenTuple, ServerHttpRequest request) {
113+
switch (tokenTuple.getT2()) {
114+
case HEADER:
115+
return tokenTuple.getT1();
116+
case QUERY_PARAMETER:
117+
return isParameterTokenSupportedForRequest(request) ? tokenTuple.getT1() : null;
118+
case BODY_PARAMETER:
119+
return isBodyParameterTokenSupportedForRequest(request) ? tokenTuple.getT1() : null;
120+
default:
121+
throw new IllegalArgumentException();
122+
}
123+
}
124+
105125
/**
106126
* Set if transport of access token using URI query parameter is supported. Defaults
107127
* to {@code false}.
@@ -127,25 +147,70 @@ public void setBearerTokenHeaderName(String bearerTokenHeaderName) {
127147
this.bearerTokenHeaderName = bearerTokenHeaderName;
128148
}
129149

130-
private String resolveFromAuthorizationHeader(HttpHeaders headers) {
150+
/**
151+
* Set if transport of access token using form-encoded body parameter is supported.
152+
* Defaults to {@code false}.
153+
* @param allowFormEncodedBodyParameter if the form-encoded body parameter is
154+
* supported
155+
* @since 6.5
156+
*/
157+
public void setAllowFormEncodedBodyParameter(boolean allowFormEncodedBodyParameter) {
158+
this.allowFormEncodedBodyParameter = allowFormEncodedBodyParameter;
159+
}
160+
161+
private Mono<String> resolveFromAuthorizationHeader(HttpHeaders headers) {
131162
String authorization = headers.getFirst(this.bearerTokenHeaderName);
132163
if (!StringUtils.startsWithIgnoreCase(authorization, "bearer")) {
133-
return null;
164+
return Mono.empty();
134165
}
135166
Matcher matcher = authorizationPattern.matcher(authorization);
136167
if (!matcher.matches()) {
137168
BearerTokenError error = invalidTokenError();
138169
throw new OAuth2AuthenticationException(error);
139170
}
140-
return matcher.group("token");
171+
return Mono.just(matcher.group("token"));
141172
}
142173

143174
private static BearerTokenError invalidTokenError() {
144175
return BearerTokenErrors.invalidToken("Bearer token is malformed");
145176
}
146177

178+
private Mono<String> resolveAccessTokenFromBody(ServerWebExchange exchange) {
179+
if (!allowFormEncodedBodyParameter) {
180+
return Mono.empty();
181+
}
182+
183+
final ServerHttpRequest request = exchange.getRequest();
184+
185+
if (request.getMethod() == HttpMethod.POST &&
186+
MediaType.APPLICATION_FORM_URLENCODED.equalsTypeAndSubtype(request.getHeaders().getContentType())) {
187+
188+
return exchange.getFormData().mapNotNull(formData -> {
189+
if (formData.isEmpty()) {
190+
return null;
191+
}
192+
final List<String> tokens = formData.get(ACCESS_TOKEN_NAME);
193+
if (tokens == null) {
194+
return null;
195+
}
196+
if (tokens.size() > 1) {
197+
BearerTokenError error = invalidRequest(MULTIPLE_BEARER_TOKENS_ERROR_MSG);
198+
throw new OAuth2AuthenticationException(error);
199+
}
200+
return formData.getFirst(ACCESS_TOKEN_NAME);
201+
});
202+
}
203+
return Mono.empty();
204+
}
205+
206+
private boolean isBodyParameterTokenSupportedForRequest(ServerHttpRequest request) {
207+
return this.allowFormEncodedBodyParameter && HttpMethod.POST == request.getMethod();
208+
}
209+
147210
private boolean isParameterTokenSupportedForRequest(ServerHttpRequest request) {
148211
return this.allowUriQueryParameter && HttpMethod.GET.equals(request.getMethod());
149212
}
150213

214+
private enum TokenSource {HEADER, QUERY_PARAMETER, BODY_PARAMETER}
215+
151216
}

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

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232

3333
import static org.assertj.core.api.Assertions.assertThat;
3434
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
35+
import static org.springframework.http.HttpHeaders.AUTHORIZATION;
36+
import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED;
37+
import static org.springframework.mock.http.server.reactive.MockServerHttpRequest.post;
3538

3639
/**
3740
* @author Rob Winch
@@ -219,6 +222,107 @@ void resolveWhenQueryParameterHasMultipleAccessTokensThenOAuth2AuthenticationExc
219222

220223
}
221224

225+
@Test
226+
void resolveWhenBodyParameterIsPresentThenTokenIsResolved() {
227+
this.converter.setAllowFormEncodedBodyParameter(true);
228+
MockServerHttpRequest request = post("/").contentType(APPLICATION_FORM_URLENCODED)
229+
.body("access_token=" + TEST_TOKEN);
230+
231+
assertThat(convertToToken(request).getToken()).isEqualTo(TEST_TOKEN);
232+
}
233+
234+
235+
@Test
236+
void resolveWhenBodyParameterIsPresentButNotAllowedThenTokenIsNotResolved() {
237+
this.converter.setAllowFormEncodedBodyParameter(false);
238+
MockServerHttpRequest request = post("/").contentType(APPLICATION_FORM_URLENCODED)
239+
.body("access_token=" + TEST_TOKEN);
240+
241+
assertThat(convertToToken(request)).isNull();
242+
}
243+
244+
@Test
245+
void resolveWhenBodyParameterHasMultipleAccessTokensThenOAuth2AuthenticationException() {
246+
this.converter.setAllowFormEncodedBodyParameter(true);
247+
MockServerHttpRequest request = post("/").contentType(APPLICATION_FORM_URLENCODED)
248+
.body("access_token=" + TEST_TOKEN + "&access_token=" + TEST_TOKEN);
249+
250+
assertThatExceptionOfType(OAuth2AuthenticationException.class)
251+
.isThrownBy(() -> convertToToken(request))
252+
.satisfies(ex -> {
253+
BearerTokenError error = (BearerTokenError) ex.getError();
254+
assertThat(error.getDescription()).isEqualTo("Found multiple bearer tokens in the request");
255+
assertThat(error.getErrorCode()).isEqualTo(BearerTokenErrorCodes.INVALID_REQUEST);
256+
assertThat(error.getUri()).isEqualTo("https://tools.ietf.org/html/rfc6750#section-3.1");
257+
assertThat(error.getHttpStatus()).isEqualTo(HttpStatus.BAD_REQUEST);
258+
});
259+
}
260+
261+
@Test
262+
void resolveBodyContainsOtherParameterAsWellThenTokenIsResolved() {
263+
this.converter.setAllowFormEncodedBodyParameter(true);
264+
MockServerHttpRequest request = post("/").contentType(APPLICATION_FORM_URLENCODED)
265+
.body("access_token=" + TEST_TOKEN + "&other_param=value");
266+
267+
assertThat(convertToToken(request).getToken()).isEqualTo(TEST_TOKEN);
268+
}
269+
270+
@Test
271+
void resolveWhenNoBodyParameterThenTokenIsNotResolved() {
272+
this.converter.setAllowFormEncodedBodyParameter(true);
273+
MockServerHttpRequest.BaseBuilder<?> request = post("/").contentType(APPLICATION_FORM_URLENCODED);
274+
275+
assertThat(convertToToken(request)).isNull();
276+
}
277+
278+
@Test
279+
void resolveWhenWrongBodyParameterThenTokenIsNotResolved() {
280+
this.converter.setAllowFormEncodedBodyParameter(true);
281+
MockServerHttpRequest request = post("/").contentType(APPLICATION_FORM_URLENCODED)
282+
.body("other_param=value");
283+
284+
assertThat(convertToToken(request)).isNull();
285+
}
286+
287+
@Test
288+
void resolveWhenValidHeaderIsPresentTogetherWithBodyParameterThenAuthenticationExceptionIsThrown() {
289+
this.converter.setAllowFormEncodedBodyParameter(true);
290+
MockServerHttpRequest request = post("/").header(AUTHORIZATION, "Bearer " + TEST_TOKEN)
291+
.contentType(APPLICATION_FORM_URLENCODED)
292+
.body("access_token=" + TEST_TOKEN);
293+
294+
assertThatExceptionOfType(OAuth2AuthenticationException.class)
295+
.isThrownBy(() -> convertToToken(request))
296+
.withMessageContaining("Found multiple bearer tokens in the request");
297+
}
298+
299+
@Test
300+
void resolveWhenValidQueryParameterIsPresentTogetherWithBodyParameterThenAuthenticationExceptionIsThrown() {
301+
this.converter.setAllowUriQueryParameter(true);
302+
this.converter.setAllowFormEncodedBodyParameter(true);
303+
MockServerHttpRequest request = post("/").queryParam("access_token", TEST_TOKEN)
304+
.contentType(APPLICATION_FORM_URLENCODED)
305+
.body("access_token=" + TEST_TOKEN);
306+
307+
assertThatExceptionOfType(OAuth2AuthenticationException.class)
308+
.isThrownBy(() -> convertToToken(request))
309+
.withMessageContaining("Found multiple bearer tokens in the request");
310+
}
311+
312+
@Test
313+
void resolveWhenValidQueryParameterIsPresentTogetherWithBodyParameterAndValidHeaderThenAuthenticationExceptionIsThrown() {
314+
this.converter.setAllowUriQueryParameter(true);
315+
this.converter.setAllowFormEncodedBodyParameter(true);
316+
MockServerHttpRequest request = post("/").header(AUTHORIZATION, "Bearer " + TEST_TOKEN)
317+
.queryParam("access_token", TEST_TOKEN)
318+
.contentType(APPLICATION_FORM_URLENCODED)
319+
.body("access_token=" + TEST_TOKEN);
320+
321+
assertThatExceptionOfType(OAuth2AuthenticationException.class)
322+
.isThrownBy(() -> convertToToken(request))
323+
.withMessageContaining("Found multiple bearer tokens in the request");
324+
}
325+
222326
// gh-16038
223327
@Test
224328
void resolveWhenRequestContainsTwoAccessTokenQueryParametersAndSupportIsDisabledThenTokenIsNotResolved() {

0 commit comments

Comments
 (0)