Add ServerBearerTokenAuthenticationConverter
Issue: gh-5605
This commit is contained in:
parent
4f417f01a7
commit
2056b3440f
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue