Просмотр исходного кода

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

Issue gh-15818
Jonah Klöckner 11 месяцев назад
Родитель
Сommit
9674532f4d

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

@@ -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);
-
-		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
 		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 - 0
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
 
 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() {