From 2056b3440f85f59398486cfa976dd4eacbc9fd58 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 6 Jul 2018 13:56:19 -0500 Subject: [PATCH] Add ServerBearerTokenAuthenticationConverter Issue: gh-5605 --- ...verBearerTokenAuthenticationConverter.java | 107 ++++++++++++++ ...arerTokenAuthenticationConverterTests.java | 134 ++++++++++++++++++ 2 files changed, 241 insertions(+) create mode 100644 oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/ServerBearerTokenAuthenticationConverter.java create mode 100644 oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/server/ServerBearerTokenAuthenticationConverterTests.java diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/ServerBearerTokenAuthenticationConverter.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/ServerBearerTokenAuthenticationConverter.java new file mode 100644 index 0000000000..1025dfb9c3 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/ServerBearerTokenAuthenticationConverter.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.server.resource.web.server; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; +import org.springframework.security.oauth2.server.resource.BearerTokenError; +import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes; +import org.springframework.util.StringUtils; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A strategy for resolving Bearer Tokens + * from the {@link ServerWebExchange}. + * + * @author Rob Winch + * @since 5.1 + * @see RFC 6750 Section 2: Authenticated Requests + */ +public class ServerBearerTokenAuthenticationConverter implements + Function> { + private static final Pattern authorizationPattern = Pattern.compile("^Bearer (?[a-zA-Z0-9-._~+/]+)=*$"); + + private boolean allowUriQueryParameter = false; + + public Mono apply(ServerWebExchange exchange) { + return Mono.justOrEmpty(this.token(exchange.getRequest())) + .map(BearerTokenAuthenticationToken::new); + } + + private String token(ServerHttpRequest request) { + String authorizationHeaderToken = resolveFromAuthorizationHeader(request.getHeaders()); + String parameterToken = request.getQueryParams().getFirst("access_token"); + if (authorizationHeaderToken != null) { + if (parameterToken != null) { + BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INVALID_REQUEST, + HttpStatus.BAD_REQUEST, + "Found multiple bearer tokens in the request", + "https://tools.ietf.org/html/rfc6750#section-3.1"); + throw new OAuth2AuthenticationException(error); + } + return authorizationHeaderToken; + } + else if (parameterToken != null && isParameterTokenSupportedForRequest(request)) { + return parameterToken; + } + return null; + } + + /** + * Set if transport of access token using URI query parameter is supported. Defaults to {@code false}. + * + * The spec recommends against using this mechanism for sending bearer tokens, and even goes as far as + * stating that it was only included for completeness. + * + * @param allowUriQueryParameter if the URI query parameter is supported + */ + public void setAllowUriQueryParameter(boolean allowUriQueryParameter) { + this.allowUriQueryParameter = allowUriQueryParameter; + } + + private static String resolveFromAuthorizationHeader(HttpHeaders headers) { + String authorization = headers.getFirst(HttpHeaders.AUTHORIZATION); + if (StringUtils.hasText(authorization) && authorization.startsWith("Bearer")) { + Matcher matcher = authorizationPattern.matcher(authorization); + + if ( !matcher.matches() ) { + BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INVALID_TOKEN, + HttpStatus.BAD_REQUEST, + "Bearer token is malformed", + "https://tools.ietf.org/html/rfc6750#section-3.1"); + throw new OAuth2AuthenticationException(error); + } + + return matcher.group("token"); + } + return null; + } + + private boolean isParameterTokenSupportedForRequest(ServerHttpRequest request) { + return this.allowUriQueryParameter && HttpMethod.GET.equals(request.getMethod()); + } +} diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/server/ServerBearerTokenAuthenticationConverterTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/server/ServerBearerTokenAuthenticationConverterTests.java new file mode 100644 index 0000000000..3326d17c9a --- /dev/null +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/server/ServerBearerTokenAuthenticationConverterTests.java @@ -0,0 +1,134 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.server.resource.web.server; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; + +import java.util.Base64; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class ServerBearerTokenAuthenticationConverterTests { + private static final String TEST_TOKEN = "test-token"; + + private ServerBearerTokenAuthenticationConverter converter; + + @Before + public void setup() { + this.converter = new ServerBearerTokenAuthenticationConverter(); + } + + @Test + public void resolveWhenValidHeaderIsPresentThenTokenIsResolved() { + MockServerHttpRequest.BaseBuilder request = MockServerHttpRequest + .get("/") + .header(HttpHeaders.AUTHORIZATION, "Bearer " + TEST_TOKEN); + + assertThat(convertToToken(request).getToken()).isEqualTo(TEST_TOKEN); + } + + @Test + public void resolveWhenNoHeaderIsPresentThenTokenIsNotResolved() { + MockServerHttpRequest.BaseBuilder request = MockServerHttpRequest + .get("/"); + + assertThat(convertToToken(request)).isNull(); + } + + @Test + public void resolveWhenHeaderWithWrongSchemeIsPresentThenTokenIsNotResolved() { + MockServerHttpRequest.BaseBuilder request = MockServerHttpRequest + .get("/") + .header(HttpHeaders.AUTHORIZATION, "Basic " + Base64.getEncoder().encodeToString("test:test".getBytes())); + + assertThat(convertToToken(request)).isNull(); + } + + @Test + public void resolveWhenHeaderWithMissingTokenIsPresentThenAuthenticationExceptionIsThrown() { + MockServerHttpRequest.BaseBuilder request = MockServerHttpRequest + .get("/") + .header(HttpHeaders.AUTHORIZATION, "Bearer "); + + assertThatCode(() -> convertToToken(request)) + .isInstanceOf(OAuth2AuthenticationException.class) + .hasMessageContaining(("Bearer token is malformed")); + } + + @Test + public void resolveWhenHeaderWithInvalidCharactersIsPresentThenAuthenticationExceptionIsThrown() { + MockServerHttpRequest.BaseBuilder request = MockServerHttpRequest + .get("/") + .header(HttpHeaders.AUTHORIZATION, "Bearer an\"invalid\"token"); + + assertThatCode(() -> convertToToken(request)) + .isInstanceOf(OAuth2AuthenticationException.class) + .hasMessageContaining(("Bearer token is malformed")); + } + + @Test + public void resolveWhenValidHeaderIsPresentTogetherWithQueryParameterThenAuthenticationExceptionIsThrown() { + MockServerHttpRequest.BaseBuilder request = MockServerHttpRequest + .get("/") + .queryParam("access_token", TEST_TOKEN) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + TEST_TOKEN); + + assertThatCode(() -> convertToToken(request)) + .isInstanceOf(OAuth2AuthenticationException.class) + .hasMessageContaining("Found multiple bearer tokens in the request"); + } + + @Test + public void resolveWhenQueryParameterIsPresentAndSupportedThenTokenIsResolved() { + this.converter.setAllowUriQueryParameter(true); + + MockServerHttpRequest.BaseBuilder request = MockServerHttpRequest + .get("/") + .queryParam("access_token", TEST_TOKEN); + + assertThat(convertToToken(request).getToken()).isEqualTo(TEST_TOKEN); + } + + @Test + public void resolveWhenQueryParameterIsPresentAndNotSupportedThenTokenIsNotResolved() { + MockServerHttpRequest.BaseBuilder request = MockServerHttpRequest + .get("/") + .queryParam("access_token", TEST_TOKEN); + + assertThat(convertToToken(request)).isNull(); + } + + private BearerTokenAuthenticationToken convertToToken(MockServerHttpRequest.BaseBuilder request) { + return convertToToken(request.build()); + } + + private BearerTokenAuthenticationToken convertToToken(MockServerHttpRequest request) { + MockServerWebExchange exchange = MockServerWebExchange.from(request); + return this.converter.apply(exchange).cast(BearerTokenAuthenticationToken.class).block(); + } +}