Skip to content

Commit 6803ac4

Browse files
committed
Add support for access token in body parameter as per rfc 6750 Sec. 2.2
1 parent f496e1b commit 6803ac4

File tree

2 files changed

+206
-30
lines changed

2 files changed

+206
-30
lines changed

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

Lines changed: 91 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2024 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.
@@ -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,38 +75,45 @@ 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 && isParameterTokenSupportedForRequest(request)) {
81-
return parameterToken;
82-
}
83-
return null;
78+
private Mono<String> token(ServerWebExchange exchange) {
79+
final var 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 -> switch (tokenTuples.size()) {
86+
case 0 -> null;
87+
case 1 -> getTokenIfSupported(tokenTuples.get(0), request);
88+
default -> {
89+
BearerTokenError error = invalidRequest(MULTIPLE_BEARER_TOKENS_ERROR_MSG);
90+
throw new OAuth2AuthenticationException(error);
91+
}
92+
});
8493
}
8594

86-
private static String resolveAccessTokenFromRequest(ServerHttpRequest request) {
87-
List<String> parameterTokens = request.getQueryParams().get("access_token");
95+
private static Mono<String> resolveAccessTokenFromRequest(ServerHttpRequest request) {
96+
List<String> parameterTokens = request.getQueryParams().get(ACCESS_TOKEN_NAME);
8897
if (CollectionUtils.isEmpty(parameterTokens)) {
89-
return null;
98+
return Mono.empty();
9099
}
91100
if (parameterTokens.size() == 1) {
92-
return parameterTokens.get(0);
101+
return Mono.just(parameterTokens.get(0));
93102
}
94103

95-
BearerTokenError error = BearerTokenErrors.invalidRequest("Found multiple bearer tokens in the request");
104+
BearerTokenError error = invalidRequest(MULTIPLE_BEARER_TOKENS_ERROR_MSG);
96105
throw new OAuth2AuthenticationException(error);
97106

98107
}
99108

109+
private String getTokenIfSupported(Tuple2<String, TokenSource> tokenTuple, ServerHttpRequest request) {
110+
return switch (tokenTuple.getT2()) {
111+
case HEADER -> tokenTuple.getT1();
112+
case QUERY_PARAMETER -> isParameterTokenSupportedForRequest(request) ? tokenTuple.getT1() : null;
113+
case BODY_PARAMETER -> isBodyParameterTokenSupportedForRequest(request) ? tokenTuple.getT1() : null;
114+
};
115+
}
116+
100117
/**
101118
* Set if transport of access token using URI query parameter is supported. Defaults
102119
* to {@code false}.
@@ -122,25 +139,73 @@ public void setBearerTokenHeaderName(String bearerTokenHeaderName) {
122139
this.bearerTokenHeaderName = bearerTokenHeaderName;
123140
}
124141

125-
private String resolveFromAuthorizationHeader(HttpHeaders headers) {
142+
/**
143+
* Set if transport of access token using form-encoded body parameter is supported.
144+
* Defaults to {@code false}.
145+
* @param allowFormEncodedBodyParameter if the form-encoded body parameter is
146+
* supported
147+
*/
148+
public void setAllowFormEncodedBodyParameter(boolean allowFormEncodedBodyParameter) {
149+
this.allowFormEncodedBodyParameter = allowFormEncodedBodyParameter;
150+
}
151+
152+
private Mono<String> resolveFromAuthorizationHeader(HttpHeaders headers) {
126153
String authorization = headers.getFirst(this.bearerTokenHeaderName);
127154
if (!StringUtils.startsWithIgnoreCase(authorization, "bearer")) {
128-
return null;
155+
return Mono.empty();
129156
}
130157
Matcher matcher = authorizationPattern.matcher(authorization);
131158
if (!matcher.matches()) {
132159
BearerTokenError error = invalidTokenError();
133160
throw new OAuth2AuthenticationException(error);
134161
}
135-
return matcher.group("token");
162+
return Mono.just(matcher.group("token"));
136163
}
137164

138165
private static BearerTokenError invalidTokenError() {
139166
return BearerTokenErrors.invalidToken("Bearer token is malformed");
140167
}
141168

169+
private Mono<String> resolveAccessTokenFromBody(ServerWebExchange exchange) {
170+
if (!allowFormEncodedBodyParameter) {
171+
return Mono.empty();
172+
}
173+
174+
final var request = exchange.getRequest();
175+
176+
if (request.getMethod() == HttpMethod.POST &&
177+
MediaType.APPLICATION_FORM_URLENCODED.equalsTypeAndSubtype(request.getHeaders().getContentType())) {
178+
179+
return exchange.getFormData().mapNotNull(formData -> {
180+
if (formData.isEmpty()) {
181+
return null;
182+
}
183+
if (formData.size() > 1) {
184+
var error = invalidRequest("The HTTP request entity-body is not single-part");
185+
throw new OAuth2AuthenticationException(error);
186+
}
187+
final var tokens = formData.get(ACCESS_TOKEN_NAME);
188+
if (tokens == null) {
189+
return null;
190+
}
191+
if (tokens.size() > 1) {
192+
var error = invalidRequest(MULTIPLE_BEARER_TOKENS_ERROR_MSG);
193+
throw new OAuth2AuthenticationException(error);
194+
}
195+
return formData.getFirst(ACCESS_TOKEN_NAME);
196+
});
197+
}
198+
return Mono.empty();
199+
}
200+
201+
private boolean isBodyParameterTokenSupportedForRequest(ServerHttpRequest request) {
202+
return this.allowFormEncodedBodyParameter && HttpMethod.POST == request.getMethod();
203+
}
204+
142205
private boolean isParameterTokenSupportedForRequest(ServerHttpRequest request) {
143206
return this.allowUriQueryParameter && HttpMethod.GET.equals(request.getMethod());
144207
}
145208

209+
private enum TokenSource {HEADER, QUERY_PARAMETER, BODY_PARAMETER}
210+
146211
}

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

Lines changed: 115 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,16 @@
1616

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

19+
import static org.assertj.core.api.Assertions.assertThat;
20+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
21+
import static org.springframework.http.HttpHeaders.AUTHORIZATION;
22+
import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED;
23+
import static org.springframework.mock.http.server.reactive.MockServerHttpRequest.post;
24+
1925
import java.util.Base64;
2026

2127
import org.junit.jupiter.api.BeforeEach;
2228
import org.junit.jupiter.api.Test;
23-
2429
import org.springframework.http.HttpHeaders;
2530
import org.springframework.http.HttpStatus;
2631
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
@@ -30,9 +35,6 @@
3035
import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes;
3136
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken;
3237

33-
import static org.assertj.core.api.Assertions.assertThat;
34-
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
35-
3638
/**
3739
* @author Rob Winch
3840
* @since 5.1
@@ -217,6 +219,115 @@ void resolveWhenQueryParameterHasMultipleAccessTokensThenOAuth2AuthenticationExc
217219

218220
}
219221

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

0 commit comments

Comments
 (0)