Add ServerBearerTokenAuthenticationConverter

Issue: gh-5605
This commit is contained in:
Rob Winch 2018-07-06 13:56:19 -05:00
parent 4f417f01a7
commit 2056b3440f
2 changed files with 241 additions and 0 deletions

View File

@ -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 <a href="https://tools.ietf.org/html/rfc6750#section-1.2" target="_blank">Bearer Token</a>s
* from the {@link ServerWebExchange}.
*
* @author Rob Winch
* @since 5.1
* @see <a href="https://tools.ietf.org/html/rfc6750#section-2" target="_blank">RFC 6750 Section 2: Authenticated Requests</a>
*/
public class ServerBearerTokenAuthenticationConverter implements
Function<ServerWebExchange, Mono<Authentication>> {
private static final Pattern authorizationPattern = Pattern.compile("^Bearer (?<token>[a-zA-Z0-9-._~+/]+)=*$");
private boolean allowUriQueryParameter = false;
public Mono<Authentication> 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());
}
}

View File

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