Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 8 commits into from
Closed
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -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;
Expand All @@ -47,16 +53,20 @@
*/
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);
Expand All @@ -65,38 +75,53 @@ public Mono<Authentication> convert(ServerWebExchange exchange) {
});
}

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 && isParameterTokenSupportedForRequest(request)) {
return parameterToken;
}
return null;
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 static String resolveAccessTokenFromRequest(ServerHttpRequest request) {
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}.
Expand All @@ -122,25 +147,70 @@ public void setBearerTokenHeaderName(String bearerTokenHeaderName) {
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}

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@

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
Expand Down Expand Up @@ -217,6 +220,107 @@ void resolveWhenQueryParameterHasMultipleAccessTokensThenOAuth2AuthenticationExc

}

@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");
}

private BearerTokenAuthenticationToken convertToToken(MockServerHttpRequest.BaseBuilder<?> request) {
return convertToToken(request.build());
}
Expand Down