2 changed files with 241 additions and 0 deletions
@ -0,0 +1,107 @@
@@ -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 @@
@@ -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