Conditionally resolve bearer token from request parameters

Before this commit, the DefaultBearerTokenResolver unconditionally
resolved the request parameters to check whether multiple tokens
are present in the request and reject those requests as invalid.

This commit changes this behaviour to resolve the request parameters
only if parameter token is supported for the specific request
according to spec (RFC 6750).

Closes gh-10326
This commit is contained in:
Philipp Neuschwander 2021-10-04 18:43:17 +02:00 committed by Steve Riesenberg
parent 88c64b3b7b
commit 6db58cbf8a
4 changed files with 52 additions and 16 deletions

View File

@ -360,7 +360,7 @@ public class OAuth2ResourceServerConfigurerTests {
this.spring.register(JwkSetUriConfig.class).autowire();
// engage csrf
// @formatter:off
this.mvc.perform(post("/").with(bearerToken("token").asParam()))
this.mvc.perform(post("/").header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE).with(bearerToken("token").asParam()))
.andExpect(status().isForbidden())
.andExpect(header().doesNotExist(HttpHeaders.WWW_AUTHENTICATE));
// @formatter:on
@ -370,7 +370,7 @@ public class OAuth2ResourceServerConfigurerTests {
public void postWhenCsrfDisabledWithBearerTokenAsFormParameterThenIgnoresToken() throws Exception {
this.spring.register(CsrfDisabledConfig.class).autowire();
// @formatter:off
this.mvc.perform(post("/").with(bearerToken("token").asParam()))
this.mvc.perform(post("/").header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE).with(bearerToken("token").asParam()))
.andExpect(status().isUnauthorized())
.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, "Bearer"));
// @formatter:on
@ -536,7 +536,7 @@ public class OAuth2ResourceServerConfigurerTests {
mockRestOperations(jwks("Default"));
String token = this.token("ValidNoScopes");
// @formatter:off
this.mvc.perform(post("/authenticated").with(bearerToken(token)))
this.mvc.perform(post("/authenticated").header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE).with(bearerToken(token)))
.andExpect(status().isOk())
.andExpect(content().string("test-subject"));
// @formatter:on
@ -558,7 +558,7 @@ public class OAuth2ResourceServerConfigurerTests {
mockRestOperations(jwks("Default"));
String token = this.token("Expired");
// @formatter:off
this.mvc.perform(post("/authenticated").with(bearerToken(token)))
this.mvc.perform(post("/authenticated").header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE).with(bearerToken(token)))
.andExpect(status().isUnauthorized())
.andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt"));
// @formatter:on
@ -626,7 +626,7 @@ public class OAuth2ResourceServerConfigurerTests {
this.mvc.perform(get("/authenticated").with(bearerToken(JWT_TOKEN)))
.andExpect(status().isOk())
.andExpect(content().string(JWT_SUBJECT));
this.mvc.perform(post("/authenticated").param("access_token", JWT_TOKEN))
this.mvc.perform(post("/authenticated").header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE).param("access_token", JWT_TOKEN))
.andExpect(status().isOk())
.andExpect(content().string(JWT_SUBJECT));
// @formatter:on
@ -659,6 +659,7 @@ public class OAuth2ResourceServerConfigurerTests {
given(decoder.decode(anyString())).willReturn(JWT);
// @formatter:off
MockHttpServletRequestBuilder request = post("/authenticated")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.param("access_token", JWT_TOKEN)
.with(bearerToken(JWT_TOKEN))
.with(csrf());

View File

@ -261,6 +261,7 @@ public class OAuth2ResourceServerBeanDefinitionParserTests {
public void postWhenBearerTokenAsFormParameterThenIgnoresToken() throws Exception {
this.spring.configLocations(xml("JwkSetUri")).autowire();
this.mvc.perform(post("/") // engage csrf
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.param("access_token", "token")).andExpect(status().isForbidden())
.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, "Bearer")); // different
// from
@ -451,7 +452,7 @@ public class OAuth2ResourceServerBeanDefinitionParserTests {
// @formatter:off
this.mvc.perform(get("/authenticated").header("Authorization", "Bearer token"))
.andExpect(status().isNotFound());
this.mvc.perform(post("/authenticated").param("access_token", "token"))
this.mvc.perform(post("/authenticated").header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE).param("access_token", "token"))
.andExpect(status().isNotFound());
// @formatter:on
}
@ -477,6 +478,7 @@ public class OAuth2ResourceServerBeanDefinitionParserTests {
this.spring.configLocations(xml("MockJwtDecoder"), xml("AllowBearerTokenInBody")).autowire();
// @formatter:off
MockHttpServletRequestBuilder request = post("/authenticated")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.param("access_token", "token")
.header("Authorization", "Bearer token")
.with(csrf());

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 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.
@ -22,6 +22,7 @@ import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.server.resource.BearerTokenError;
import org.springframework.security.oauth2.server.resource.BearerTokenErrors;
@ -47,18 +48,19 @@ public final class DefaultBearerTokenResolver implements BearerTokenResolver {
private String bearerTokenHeaderName = HttpHeaders.AUTHORIZATION;
@Override
public String resolve(HttpServletRequest request) {
String authorizationHeaderToken = resolveFromAuthorizationHeader(request);
String parameterToken = resolveFromRequestParameters(request);
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) {
BearerTokenError error = BearerTokenErrors
final BearerTokenError error = BearerTokenErrors
.invalidRequest("Found multiple bearer tokens in the request");
throw new OAuth2AuthenticationException(error);
}
return authorizationHeaderToken;
}
if (parameterToken != null && isParameterTokenSupportedForRequest(request)) {
if (parameterToken != null && isParameterTokenEnabledForRequest(request)) {
return parameterToken;
}
return null;
@ -124,8 +126,15 @@ public final class DefaultBearerTokenResolver implements BearerTokenResolver {
throw new OAuth2AuthenticationException(error);
}
private boolean isParameterTokenSupportedForRequest(HttpServletRequest request) {
return ((this.allowFormEncodedBodyParameter && "POST".equals(request.getMethod()))
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

@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 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.
@ -126,14 +126,38 @@ public class DefaultBearerTokenResolverTests {
.withMessageContaining("Found multiple bearer tokens in the request");
}
// gh-10326
@Test
public void resolveWhenRequestContainsTwoAccessTokenParametersThenAuthenticationExceptionIsThrown() {
public void resolveWhenRequestContainsTwoAccessTokenQueryParametersThenAuthenticationExceptionIsThrown() {
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");
}
// gh-10326
@Test
public void resolveWhenRequestContainsTwoAccessTokenFormParametersThenAuthenticationExceptionIsThrown() {
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");
}
// gh-10326
@Test
public void resolveWhenParameterIsPresentInMultipartRequestAndFormParameterSupportedThenTokenIsNotResolved() {
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
public void resolveWhenFormParameterIsPresentAndSupportedThenTokenIsResolved() {
this.resolver.setAllowFormEncodedBodyParameter(true);