feat: Add default bearer token resolver for rfc6750#section-2.1-2.3 (#1869)

pull/1871/head
guqing 2022-04-22 11:04:10 +08:00 committed by GitHub
parent 599ab5618b
commit 673c2068d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 354 additions and 0 deletions

View File

@ -0,0 +1,29 @@
package run.halo.app.identity.authentication.verifier;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
/**
* A strategy for resolving
* <a href="https://tools.ietf.org/html/rfc6750#section-1.2">Bearer Token</a>
* s from the {@link HttpServletRequest}.
*
* @author guqing
* @see
* <a href="https://tools.ietf.org/html/rfc6750#section-2">RFC 6750 Section 2: Authenticated Requests</a>
* @since 2.0.0
*/
@FunctionalInterface
public interface BearerTokenResolver {
/**
* Resolve any
* <a href="https://tools.ietf.org/html/rfc6750#section-1.2">Bearer Token</a>
* value from the request.
*
* @param request the request
* @return the Bearer Token value or {@code null} if none found
* @throws OAuth2AuthenticationException if the found token is invalid
*/
String resolve(HttpServletRequest request);
}

View File

@ -0,0 +1,126 @@
package run.halo.app.identity.authentication.verifier;
import jakarta.servlet.http.HttpServletRequest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.util.StringUtils;
/**
* The default {@link BearerTokenResolver} implementation based on RFC 6750.
*
* @author guqing
* @see
* <a href="https://tools.ietf.org/html/rfc6750#section-2">RFC 6750 Section 2: Authenticated Requests</a>
* @since 2.0.0
*/
public final class DefaultBearerTokenResolver implements BearerTokenResolver {
private static final Pattern authorizationPattern =
Pattern.compile("^Bearer (?<token>[a-zA-Z0-9-._~+/]+=*)$",
Pattern.CASE_INSENSITIVE);
private boolean allowFormEncodedBodyParameter = false;
private boolean allowUriQueryParameter = false;
private String bearerTokenHeaderName = HttpHeaders.AUTHORIZATION;
@Override
public String resolve(final HttpServletRequest request) {
final String authorizationHeaderToken = resolveFromAuthorizationHeader(request);
final String parameterToken = isParameterTokenSupportedForRequest(request)
? resolveFromRequestParameters(request) : null;
if (authorizationHeaderToken != null) {
if (parameterToken != null) {
final BearerTokenError error = BearerTokenErrors
.invalidRequest("Found multiple bearer tokens in the request");
throw new OAuth2AuthenticationException(error);
}
return authorizationHeaderToken;
}
if (parameterToken != null && isParameterTokenEnabledForRequest(request)) {
return parameterToken;
}
return null;
}
/**
* 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
*/
public void setAllowFormEncodedBodyParameter(boolean allowFormEncodedBodyParameter) {
this.allowFormEncodedBodyParameter = allowFormEncodedBodyParameter;
}
/**
* Set if transport of access token using URI query parameter is supported. Defaults
* to {@code false}.
* <p>
* 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;
}
/**
* Set this value to configure what header is checked when resolving a Bearer Token.
* This value is defaulted to {@link HttpHeaders#AUTHORIZATION}.
* <p>
* This allows other headers to be used as the Bearer Token source such as
* {@link HttpHeaders#PROXY_AUTHORIZATION}
*
* @param bearerTokenHeaderName the header to check when retrieving the Bearer Token.
* @since 5.4
*/
public void setBearerTokenHeaderName(String bearerTokenHeaderName) {
this.bearerTokenHeaderName = bearerTokenHeaderName;
}
private String resolveFromAuthorizationHeader(HttpServletRequest request) {
String authorization = request.getHeader(this.bearerTokenHeaderName);
if (!StringUtils.startsWithIgnoreCase(authorization, "bearer")) {
return null;
}
Matcher matcher = authorizationPattern.matcher(authorization);
if (!matcher.matches()) {
BearerTokenError error = BearerTokenErrors.invalidToken("Bearer token is malformed");
throw new OAuth2AuthenticationException(error);
}
return matcher.group("token");
}
private static String resolveFromRequestParameters(HttpServletRequest request) {
String[] values = request.getParameterValues("access_token");
if (values == null || values.length == 0) {
return null;
}
if (values.length == 1) {
return values[0];
}
BearerTokenError error =
BearerTokenErrors.invalidRequest("Found multiple bearer tokens in the request");
throw new OAuth2AuthenticationException(error);
}
private boolean isParameterTokenSupportedForRequest(final HttpServletRequest request) {
return (("POST".equals(request.getMethod())
&& MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(request.getContentType()))
|| "GET".equals(request.getMethod()));
}
private boolean isParameterTokenEnabledForRequest(final HttpServletRequest request) {
return ((this.allowFormEncodedBodyParameter && "POST".equals(request.getMethod())
&& MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(request.getContentType()))
|| (this.allowUriQueryParameter && "GET".equals(request.getMethod())));
}
}

View File

@ -0,0 +1,199 @@
package run.halo.app.authentication.verifyer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import java.util.Base64;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import run.halo.app.identity.authentication.verifier.DefaultBearerTokenResolver;
/**
* Tests for {@link DefaultBearerTokenResolver}.
*
* @author guqing
* @since 2.0.0
*/
public class DefaultBearerTokenResolverTest {
private static final String CUSTOM_HEADER = "custom-header";
private static final String TEST_TOKEN = "test-token";
private DefaultBearerTokenResolver resolver;
@BeforeEach
public void setUp() {
this.resolver = new DefaultBearerTokenResolver();
}
@Test
public void resolveWhenValidHeaderIsPresentThenTokenIsResolved() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.addHeader("Authorization", "Bearer " + TEST_TOKEN);
assertThat(this.resolver.resolve(request)).isEqualTo(TEST_TOKEN);
}
@Test
public void resolveWhenHeaderEndsWithPaddingIndicatorThenTokenIsResolved() {
String token = TEST_TOKEN + "==";
MockHttpServletRequest request = new MockHttpServletRequest();
request.addHeader("Authorization", "Bearer " + token);
assertThat(this.resolver.resolve(request)).isEqualTo(token);
}
@Test
public void resolveWhenCustomDefinedHeaderIsValidAndPresentThenTokenIsResolved() {
this.resolver.setBearerTokenHeaderName(CUSTOM_HEADER);
MockHttpServletRequest request = new MockHttpServletRequest();
request.addHeader(CUSTOM_HEADER, "Bearer " + TEST_TOKEN);
assertThat(this.resolver.resolve(request)).isEqualTo(TEST_TOKEN);
}
@Test
public void resolveWhenLowercaseHeaderIsPresentThenTokenIsResolved() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.addHeader("authorization", "bearer " + TEST_TOKEN);
assertThat(this.resolver.resolve(request)).isEqualTo(TEST_TOKEN);
}
@Test
public void resolveWhenNoHeaderIsPresentThenTokenIsNotResolved() {
MockHttpServletRequest request = new MockHttpServletRequest();
assertThat(this.resolver.resolve(request)).isNull();
}
@Test
public void resolveWhenHeaderWithWrongSchemeIsPresentThenTokenIsNotResolved() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.addHeader("Authorization",
"Basic " + Base64.getEncoder().encodeToString("test:test".getBytes()));
assertThat(this.resolver.resolve(request)).isNull();
}
@Test
@DisplayName("resolveWhenHeaderWithMissingTokenIsPresentThenAuthenticationExceptionIsThrown")
public void resolveThenAuthenticationExceptionIsThrown() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.addHeader("Authorization", "Bearer ");
assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(
() -> this.resolver.resolve(request))
.withMessageContaining(("Bearer token is malformed"));
}
@Test
@DisplayName(
"resolveWhenHeaderWithInvalidCharactersIsPresentThenAuthenticationExceptionIsThrown")
public void resolveThenAuthenticationExceptionIsThrown2() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.addHeader("Authorization", "Bearer an\"invalid\"token");
assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(
() -> this.resolver.resolve(request))
.withMessageContaining(("Bearer token is malformed"));
}
@Test
@DisplayName("resolve when valid header is present together with "
+ "form parameter then AuthenticationException is thrown")
public void resolveThenAuthenticationExceptionIsThrown3() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.addHeader("Authorization", "Bearer " + TEST_TOKEN);
request.setMethod("POST");
request.setContentType("application/x-www-form-urlencoded");
request.addParameter("access_token", TEST_TOKEN);
assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(
() -> this.resolver.resolve(request))
.withMessageContaining("Found multiple bearer tokens in the request");
}
@Test
@DisplayName("resolve when valid header is present together with query"
+ " parameter then AuthenticationException is thrown")
public void resolveThenAuthenticationExceptionIsThrown4() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.addHeader("Authorization", "Bearer " + TEST_TOKEN);
request.setMethod("GET");
request.addParameter("access_token", TEST_TOKEN);
assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(
() -> this.resolver.resolve(request))
.withMessageContaining("Found multiple bearer tokens in the request");
}
@Test
@DisplayName("resolve when request contains two access token query"
+ " parameters then AuthenticationException is thrown")
public void resolveThenAuthenticationExceptionIsThrown5() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setMethod("GET");
request.addParameter("access_token", "token1", "token2");
assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(
() -> this.resolver.resolve(request))
.withMessageContaining("Found multiple bearer tokens in the request");
}
@Test
@DisplayName("resolve when request contains two access token form parameters"
+ " then AuthenticationException is thrown")
public void resolveThenAuthenticationExceptionIsThrown6() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setMethod("POST");
request.setContentType("application/x-www-form-urlencoded");
request.addParameter("access_token", "token1", "token2");
assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(
() -> this.resolver.resolve(request))
.withMessageContaining("Found multiple bearer tokens in the request");
}
@Test
@DisplayName("resolve when parameter is present in multipart request"
+ " and form parameter supported then token is not resolved")
public void resolveThenTokenIsNotResolved() {
this.resolver.setAllowFormEncodedBodyParameter(true);
MockHttpServletRequest request = new MockHttpServletRequest();
request.setMethod("POST");
request.setContentType("multipart/form-data");
request.addParameter("access_token", TEST_TOKEN);
assertThat(this.resolver.resolve(request)).isNull();
}
@Test
@DisplayName("resolveWhenFormParameterIsPresentAndSupportedThenTokenIsResolved")
public void resolveThenTokenIsResolved() {
this.resolver.setAllowFormEncodedBodyParameter(true);
MockHttpServletRequest request = new MockHttpServletRequest();
request.setMethod("POST");
request.setContentType("application/x-www-form-urlencoded");
request.addParameter("access_token", TEST_TOKEN);
assertThat(this.resolver.resolve(request)).isEqualTo(TEST_TOKEN);
}
@Test
@DisplayName("resolveWhenFormParameterIsPresentAndNotSupportedThenTokenIsNotResolved")
public void resolveThenTokenIsNotResolved2() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setMethod("POST");
request.setContentType("application/x-www-form-urlencoded");
request.addParameter("access_token", TEST_TOKEN);
assertThat(this.resolver.resolve(request)).isNull();
}
@Test
@DisplayName("resolveWhenQueryParameterIsPresentAndSupportedThenTokenIsResolved")
public void resolveThenTokenIsResolved2() {
this.resolver.setAllowUriQueryParameter(true);
MockHttpServletRequest request = new MockHttpServletRequest();
request.setMethod("GET");
request.addParameter("access_token", TEST_TOKEN);
assertThat(this.resolver.resolve(request)).isEqualTo(TEST_TOKEN);
}
@Test
@DisplayName("resolveWhenQueryParameterIsPresentAndNotSupportedThenTokenIsNotResolved")
public void resolveThenTokenIsNotResolved3() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setMethod("GET");
request.addParameter("access_token", TEST_TOKEN);
assertThat(this.resolver.resolve(request)).isNull();
}
}