Browse Source

Add support for access token in body parameter as per rfc 6750 Sec. 2.2

Issue gh-15818
pull/16905/head
Jonah Klöckner 1 year ago committed by Steve Riesenberg
parent
commit
9674532f4d
No known key found for this signature in database
GPG Key ID: 3D0169B18AB8F0A9
  1. 125
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/authentication/ServerBearerTokenAuthenticationConverter.java
  2. 104
      oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/server/authentication/ServerBearerTokenAuthenticationConverterTests.java

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

@ -16,14 +16,20 @@ @@ -16,14 +16,20 @@
package org.springframework.security.oauth2.server.resource.web.server.authentication;
import static org.springframework.security.oauth2.server.resource.BearerTokenErrors.invalidRequest;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuple2;
import reactor.util.function.Tuples;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
@ -47,16 +53,20 @@ import org.springframework.web.server.ServerWebExchange; @@ -47,16 +53,20 @@ import org.springframework.web.server.ServerWebExchange;
*/
public class ServerBearerTokenAuthenticationConverter implements ServerAuthenticationConverter {
public static final String ACCESS_TOKEN_NAME = "access_token";
public static final String MULTIPLE_BEARER_TOKENS_ERROR_MSG = "Found multiple bearer tokens in the request";
private static final Pattern authorizationPattern = Pattern.compile("^Bearer (?<token>[a-zA-Z0-9-._~+/]+=*)$",
Pattern.CASE_INSENSITIVE);
private boolean allowUriQueryParameter = false;
private boolean allowFormEncodedBodyParameter = false;
private String bearerTokenHeaderName = HttpHeaders.AUTHORIZATION;
@Override
public Mono<Authentication> convert(ServerWebExchange exchange) {
return Mono.fromCallable(() -> token(exchange.getRequest())).map((token) -> {
return Mono.defer(() -> token(exchange)).map(token -> {
if (token.isEmpty()) {
BearerTokenError error = invalidTokenError();
throw new OAuth2AuthenticationException(error);
@ -65,43 +75,53 @@ public class ServerBearerTokenAuthenticationConverter implements ServerAuthentic @@ -65,43 +75,53 @@ public class ServerBearerTokenAuthenticationConverter implements ServerAuthentic
});
}
private String token(ServerHttpRequest request) {
String authorizationHeaderToken = resolveFromAuthorizationHeader(request.getHeaders());
String parameterToken = resolveAccessTokenFromRequest(request);
if (authorizationHeaderToken != null) {
if (parameterToken != null) {
BearerTokenError error = BearerTokenErrors
.invalidRequest("Found multiple bearer tokens in the request");
throw new OAuth2AuthenticationException(error);
}
return authorizationHeaderToken;
}
if (parameterToken != null && !StringUtils.hasText(parameterToken)) {
BearerTokenError error = BearerTokenErrors
.invalidRequest("The requested token parameter is an empty string");
throw new OAuth2AuthenticationException(error);
}
return parameterToken;
private Mono<String> token(ServerWebExchange exchange) {
final ServerHttpRequest request = exchange.getRequest();
return Flux.merge(resolveFromAuthorizationHeader(request.getHeaders()).map(s -> Tuples.of(s, TokenSource.HEADER)),
resolveAccessTokenFromRequest(request).map(s -> Tuples.of(s, TokenSource.QUERY_PARAMETER)),
resolveAccessTokenFromBody(exchange).map(s -> Tuples.of(s, TokenSource.BODY_PARAMETER)))
.collectList()
.mapNotNull(tokenTuples -> {
switch (tokenTuples.size()) {
case 0:
return null;
case 1:
return getTokenIfSupported(tokenTuples.get(0), request);
default:
BearerTokenError error = invalidRequest(MULTIPLE_BEARER_TOKENS_ERROR_MSG);
throw new OAuth2AuthenticationException(error);
}
});
}
private String resolveAccessTokenFromRequest(ServerHttpRequest request) {
if (!isParameterTokenSupportedForRequest(request)) {
return null;
}
List<String> parameterTokens = request.getQueryParams().get("access_token");
private static Mono<String> resolveAccessTokenFromRequest(ServerHttpRequest request) {
List<String> parameterTokens = request.getQueryParams().get(ACCESS_TOKEN_NAME);
if (CollectionUtils.isEmpty(parameterTokens)) {
return null;
return Mono.empty();
}
if (parameterTokens.size() == 1) {
return parameterTokens.get(0);
return Mono.just(parameterTokens.get(0));
}
BearerTokenError error = BearerTokenErrors.invalidRequest("Found multiple bearer tokens in the request");
BearerTokenError error = invalidRequest(MULTIPLE_BEARER_TOKENS_ERROR_MSG);
throw new OAuth2AuthenticationException(error);
}
private String getTokenIfSupported(Tuple2<String, TokenSource> tokenTuple, ServerHttpRequest request) {
switch (tokenTuple.getT2()) {
case HEADER:
return tokenTuple.getT1();
case QUERY_PARAMETER:
return isParameterTokenSupportedForRequest(request) ? tokenTuple.getT1() : null;
case BODY_PARAMETER:
return isBodyParameterTokenSupportedForRequest(request) ? tokenTuple.getT1() : null;
default:
throw new IllegalArgumentException();
}
}
/**
* Set if transport of access token using URI query parameter is supported. Defaults
* to {@code false}.
@ -127,25 +147,70 @@ public class ServerBearerTokenAuthenticationConverter implements ServerAuthentic @@ -127,25 +147,70 @@ public class ServerBearerTokenAuthenticationConverter implements ServerAuthentic
this.bearerTokenHeaderName = bearerTokenHeaderName;
}
private String resolveFromAuthorizationHeader(HttpHeaders headers) {
/**
* Set if transport of access token using form-encoded body parameter is supported.
* Defaults to {@code false}.
* @param allowFormEncodedBodyParameter if the form-encoded body parameter is
* supported
* @since 6.5
*/
public void setAllowFormEncodedBodyParameter(boolean allowFormEncodedBodyParameter) {
this.allowFormEncodedBodyParameter = allowFormEncodedBodyParameter;
}
private Mono<String> resolveFromAuthorizationHeader(HttpHeaders headers) {
String authorization = headers.getFirst(this.bearerTokenHeaderName);
if (!StringUtils.startsWithIgnoreCase(authorization, "bearer")) {
return null;
return Mono.empty();
}
Matcher matcher = authorizationPattern.matcher(authorization);
if (!matcher.matches()) {
BearerTokenError error = invalidTokenError();
throw new OAuth2AuthenticationException(error);
}
return matcher.group("token");
return Mono.just(matcher.group("token"));
}
private static BearerTokenError invalidTokenError() {
return BearerTokenErrors.invalidToken("Bearer token is malformed");
}
private Mono<String> resolveAccessTokenFromBody(ServerWebExchange exchange) {
if (!allowFormEncodedBodyParameter) {
return Mono.empty();
}
final ServerHttpRequest request = exchange.getRequest();
if (request.getMethod() == HttpMethod.POST &&
MediaType.APPLICATION_FORM_URLENCODED.equalsTypeAndSubtype(request.getHeaders().getContentType())) {
return exchange.getFormData().mapNotNull(formData -> {
if (formData.isEmpty()) {
return null;
}
final List<String> tokens = formData.get(ACCESS_TOKEN_NAME);
if (tokens == null) {
return null;
}
if (tokens.size() > 1) {
BearerTokenError error = invalidRequest(MULTIPLE_BEARER_TOKENS_ERROR_MSG);
throw new OAuth2AuthenticationException(error);
}
return formData.getFirst(ACCESS_TOKEN_NAME);
});
}
return Mono.empty();
}
private boolean isBodyParameterTokenSupportedForRequest(ServerHttpRequest request) {
return this.allowFormEncodedBodyParameter && HttpMethod.POST == request.getMethod();
}
private boolean isParameterTokenSupportedForRequest(ServerHttpRequest request) {
return this.allowUriQueryParameter && HttpMethod.GET.equals(request.getMethod());
}
private enum TokenSource {HEADER, QUERY_PARAMETER, BODY_PARAMETER}
}

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

@ -32,6 +32,9 @@ import org.springframework.security.oauth2.server.resource.authentication.Bearer @@ -32,6 +32,9 @@ import org.springframework.security.oauth2.server.resource.authentication.Bearer
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.springframework.http.HttpHeaders.AUTHORIZATION;
import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED;
import static org.springframework.mock.http.server.reactive.MockServerHttpRequest.post;
/**
* @author Rob Winch
@ -219,6 +222,107 @@ public class ServerBearerTokenAuthenticationConverterTests { @@ -219,6 +222,107 @@ public class ServerBearerTokenAuthenticationConverterTests {
}
@Test
void resolveWhenBodyParameterIsPresentThenTokenIsResolved() {
this.converter.setAllowFormEncodedBodyParameter(true);
MockServerHttpRequest request = post("/").contentType(APPLICATION_FORM_URLENCODED)
.body("access_token=" + TEST_TOKEN);
assertThat(convertToToken(request).getToken()).isEqualTo(TEST_TOKEN);
}
@Test
void resolveWhenBodyParameterIsPresentButNotAllowedThenTokenIsNotResolved() {
this.converter.setAllowFormEncodedBodyParameter(false);
MockServerHttpRequest request = post("/").contentType(APPLICATION_FORM_URLENCODED)
.body("access_token=" + TEST_TOKEN);
assertThat(convertToToken(request)).isNull();
}
@Test
void resolveWhenBodyParameterHasMultipleAccessTokensThenOAuth2AuthenticationException() {
this.converter.setAllowFormEncodedBodyParameter(true);
MockServerHttpRequest request = post("/").contentType(APPLICATION_FORM_URLENCODED)
.body("access_token=" + TEST_TOKEN + "&access_token=" + TEST_TOKEN);
assertThatExceptionOfType(OAuth2AuthenticationException.class)
.isThrownBy(() -> convertToToken(request))
.satisfies(ex -> {
BearerTokenError error = (BearerTokenError) ex.getError();
assertThat(error.getDescription()).isEqualTo("Found multiple bearer tokens in the request");
assertThat(error.getErrorCode()).isEqualTo(BearerTokenErrorCodes.INVALID_REQUEST);
assertThat(error.getUri()).isEqualTo("https://tools.ietf.org/html/rfc6750#section-3.1");
assertThat(error.getHttpStatus()).isEqualTo(HttpStatus.BAD_REQUEST);
});
}
@Test
void resolveBodyContainsOtherParameterAsWellThenTokenIsResolved() {
this.converter.setAllowFormEncodedBodyParameter(true);
MockServerHttpRequest request = post("/").contentType(APPLICATION_FORM_URLENCODED)
.body("access_token=" + TEST_TOKEN + "&other_param=value");
assertThat(convertToToken(request).getToken()).isEqualTo(TEST_TOKEN);
}
@Test
void resolveWhenNoBodyParameterThenTokenIsNotResolved() {
this.converter.setAllowFormEncodedBodyParameter(true);
MockServerHttpRequest.BaseBuilder<?> request = post("/").contentType(APPLICATION_FORM_URLENCODED);
assertThat(convertToToken(request)).isNull();
}
@Test
void resolveWhenWrongBodyParameterThenTokenIsNotResolved() {
this.converter.setAllowFormEncodedBodyParameter(true);
MockServerHttpRequest request = post("/").contentType(APPLICATION_FORM_URLENCODED)
.body("other_param=value");
assertThat(convertToToken(request)).isNull();
}
@Test
void resolveWhenValidHeaderIsPresentTogetherWithBodyParameterThenAuthenticationExceptionIsThrown() {
this.converter.setAllowFormEncodedBodyParameter(true);
MockServerHttpRequest request = post("/").header(AUTHORIZATION, "Bearer " + TEST_TOKEN)
.contentType(APPLICATION_FORM_URLENCODED)
.body("access_token=" + TEST_TOKEN);
assertThatExceptionOfType(OAuth2AuthenticationException.class)
.isThrownBy(() -> convertToToken(request))
.withMessageContaining("Found multiple bearer tokens in the request");
}
@Test
void resolveWhenValidQueryParameterIsPresentTogetherWithBodyParameterThenAuthenticationExceptionIsThrown() {
this.converter.setAllowUriQueryParameter(true);
this.converter.setAllowFormEncodedBodyParameter(true);
MockServerHttpRequest request = post("/").queryParam("access_token", TEST_TOKEN)
.contentType(APPLICATION_FORM_URLENCODED)
.body("access_token=" + TEST_TOKEN);
assertThatExceptionOfType(OAuth2AuthenticationException.class)
.isThrownBy(() -> convertToToken(request))
.withMessageContaining("Found multiple bearer tokens in the request");
}
@Test
void resolveWhenValidQueryParameterIsPresentTogetherWithBodyParameterAndValidHeaderThenAuthenticationExceptionIsThrown() {
this.converter.setAllowUriQueryParameter(true);
this.converter.setAllowFormEncodedBodyParameter(true);
MockServerHttpRequest request = post("/").header(AUTHORIZATION, "Bearer " + TEST_TOKEN)
.queryParam("access_token", TEST_TOKEN)
.contentType(APPLICATION_FORM_URLENCODED)
.body("access_token=" + TEST_TOKEN);
assertThatExceptionOfType(OAuth2AuthenticationException.class)
.isThrownBy(() -> convertToToken(request))
.withMessageContaining("Found multiple bearer tokens in the request");
}
// gh-16038
@Test
void resolveWhenRequestContainsTwoAccessTokenQueryParametersAndSupportIsDisabledThenTokenIsNotResolved() {

Loading…
Cancel
Save