mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-05-30 16:52:13 +00:00
Add support for access token in body parameter as per rfc 6750 Sec. 2.2
Issue gh-15818
This commit is contained in:
parent
03e090c2d7
commit
9674532f4d
@ -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;
|
||||
*/
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
private String token(ServerHttpRequest request) {
|
||||
String authorizationHeaderToken = resolveFromAuthorizationHeader(request.getHeaders());
|
||||
String parameterToken = resolveAccessTokenFromRequest(request);
|
||||
private Mono<String> token(ServerWebExchange exchange) {
|
||||
final ServerHttpRequest request = exchange.getRequest();
|
||||
|
||||
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;
|
||||
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
|
||||
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}
|
||||
|
||||
}
|
||||
|
@ -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 {
|
||||
|
||||
}
|
||||
|
||||
@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…
x
Reference in New Issue
Block a user