Add BearerTokenAuthenticationConverter

BearerTokenAuthenticationConverter is introduced to solve the
problem of not being able to change AuthenticationDetailsSource.
BearerTokenAuthenticationFilter delegates to
BearerTokenAuthenticationConverter the task of creating
BearerTokenAuthenticationToken and setting AuthenticationDetailsSource.
BearerTokenAuthenticationConverter is customizable and the customized
converter can be used in BearerTokenAuthenticationFilter.

Closes gh-8840
This commit is contained in:
Jeongjin Kim 2020-08-19 16:40:04 +09:00 committed by Josh Cummings
parent efb394d3b2
commit 31f310fd22
No known key found for this signature in database
GPG Key ID: 49EF60DD7FF83443
6 changed files with 333 additions and 16 deletions

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.
@ -39,6 +39,7 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
import org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenAuthenticationProvider;
@ -51,6 +52,7 @@ import org.springframework.security.oauth2.server.resource.web.DefaultBearerToke
import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.util.matcher.AndRequestMatcher;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
import org.springframework.security.web.util.matcher.NegatedRequestMatcher;
@ -78,6 +80,8 @@ import org.springframework.web.accept.HeaderContentNegotiationStrategy;
* authentication failures are handled
* <li>{@link #bearerTokenResolver(BearerTokenResolver)} - customizes how to resolve a
* bearer token from the request</li>
* <li>{@link #bearerTokenAuthenticationConverter(AuthenticationConverter)}</li> -
* customizes how to convert a bear token authentication from the request
* <li>{@link #jwt(Customizer)} - enables Jwt-encoded bearer token support</li>
* <li>{@link #opaqueToken(Customizer)} - enables opaque bearer token support</li>
* </ul>
@ -159,6 +163,8 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
private BearerTokenRequestMatcher requestMatcher = new BearerTokenRequestMatcher();
private AuthenticationConverter authenticationConverter;
public OAuth2ResourceServerConfigurer(ApplicationContext context) {
Assert.notNull(context, "context cannot be null");
this.context = context;
@ -189,6 +195,13 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
return this;
}
public OAuth2ResourceServerConfigurer<H> bearerTokenAuthenticationConverter(
AuthenticationConverter authenticationConverter) {
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
this.authenticationConverter = authenticationConverter;
return this;
}
public JwtConfigurer jwt() {
if (this.jwtConfigurer == null) {
this.jwtConfigurer = new JwtConfigurer(this.context);
@ -252,8 +265,11 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
AuthenticationManager authenticationManager = getAuthenticationManager(http);
resolver = (request) -> authenticationManager;
}
this.authenticationConverter = getBearerTokenAuthenticationConverter();
BearerTokenAuthenticationFilter filter = new BearerTokenAuthenticationFilter(resolver);
filter.setBearerTokenResolver(bearerTokenResolver);
filter.setAuthenticationConverter(this.authenticationConverter);
filter.setAuthenticationEntryPoint(this.authenticationEntryPoint);
filter = postProcess(filter);
http.addFilter(filter);
@ -347,6 +363,20 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
return this.bearerTokenResolver;
}
AuthenticationConverter getBearerTokenAuthenticationConverter() {
if (this.authenticationConverter == null) {
if (this.context.getBeanNamesForType(BearerTokenAuthenticationConverter.class).length > 0) {
this.authenticationConverter = this.context.getBean(BearerTokenAuthenticationConverter.class);
}
else {
BearerTokenAuthenticationConverter converter = new BearerTokenAuthenticationConverter();
converter.setBearerTokenResolver(getBearerTokenResolver());
this.authenticationConverter = converter;
}
}
return this.authenticationConverter;
}
public class JwtConfigurer {
private final ApplicationContext context;

View File

@ -33,6 +33,7 @@ import java.util.Map;
import java.util.stream.Collectors;
import javax.annotation.PreDestroy;
import javax.servlet.http.HttpServletRequest;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
@ -108,7 +109,9 @@ import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.TestJwts;
import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication;
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtIssuerAuthenticationManagerResolver;
@ -720,6 +723,72 @@ public class OAuth2ResourceServerConfigurerTests {
assertThat(oauth2.getBearerTokenResolver()).isInstanceOf(DefaultBearerTokenResolver.class);
}
@Test
public void getBearerTokenAuthenticationConverterWhenDuplicateConverterBeansAndAnotherOnTheDslThenTheDslOneIsUsed() {
BearerTokenAuthenticationConverter converterBean = new BearerTokenAuthenticationConverter();
BearerTokenAuthenticationConverter converter = new BearerTokenAuthenticationConverter();
GenericWebApplicationContext context = new GenericWebApplicationContext();
context.registerBean("converterOne", BearerTokenAuthenticationConverter.class, () -> converterBean);
context.registerBean("converterTwo", BearerTokenAuthenticationConverter.class, () -> converterBean);
this.spring.context(context).autowire();
OAuth2ResourceServerConfigurer oauth2 = new OAuth2ResourceServerConfigurer(context);
oauth2.bearerTokenAuthenticationConverter(converter);
assertThat(oauth2.getBearerTokenAuthenticationConverter()).isEqualTo(converter);
}
@Test
public void getBearerTokenAuthenticationConverterWhenDuplicateConverterBeansThenWiringException() {
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> this.spring
.register(MultipleBearerTokenAuthenticationConverterBeansConfig.class, JwtDecoderConfig.class)
.autowire()).withRootCauseInstanceOf(NoUniqueBeanDefinitionException.class);
}
@Test
public void getBearerTokenAuthenticationConverterWhenConverterBeanAndAnotherOnTheDslThenTheDslOneIsUsed() {
BearerTokenAuthenticationConverter converter = new BearerTokenAuthenticationConverter();
BearerTokenAuthenticationConverter converterBean = new BearerTokenAuthenticationConverter();
GenericWebApplicationContext context = new GenericWebApplicationContext();
context.registerBean(BearerTokenAuthenticationConverter.class, () -> converterBean);
this.spring.context(context).autowire();
OAuth2ResourceServerConfigurer oauth2 = new OAuth2ResourceServerConfigurer(context);
oauth2.bearerTokenAuthenticationConverter(converter);
assertThat(oauth2.getBearerTokenAuthenticationConverter()).isEqualTo(converter);
}
@Test
public void getBearerTokenAuthenticationConverterWhenNoConverterSpecifiedThenTheDefaultIsUsed() {
ApplicationContext context = this.spring.context(new GenericWebApplicationContext()).getContext();
OAuth2ResourceServerConfigurer oauth2 = new OAuth2ResourceServerConfigurer(context);
assertThat(oauth2.getBearerTokenAuthenticationConverter())
.isInstanceOf(BearerTokenAuthenticationConverter.class);
}
@Test
public void getBearerTokenAuthenticationConverterWhenConverterBeanRegisteredThenBeanIsUsed() {
BearerTokenAuthenticationConverter converterBean = new BearerTokenAuthenticationConverter();
GenericWebApplicationContext context = new GenericWebApplicationContext();
context.registerBean(BearerTokenAuthenticationConverter.class, () -> converterBean);
this.spring.context(context).autowire();
OAuth2ResourceServerConfigurer oauth2 = new OAuth2ResourceServerConfigurer(context);
assertThat(oauth2.getBearerTokenAuthenticationConverter()).isEqualTo(converterBean);
}
@Test
public void getBearerTokenAuthenticationConverterWhenOnlyResolverBeanRegisteredThenUseTheResolver() {
HttpServletRequest servletRequest = mock(HttpServletRequest.class);
BearerTokenResolver resolverBean = (request) -> "bearer customToken";
GenericWebApplicationContext context = new GenericWebApplicationContext();
context.registerBean(BearerTokenResolver.class, () -> resolverBean);
this.spring.context(context).autowire();
OAuth2ResourceServerConfigurer oauth2 = new OAuth2ResourceServerConfigurer(context);
BearerTokenAuthenticationToken bearerTokenAuthenticationToken = (BearerTokenAuthenticationToken) oauth2
.getBearerTokenAuthenticationConverter().convert(servletRequest);
String token = bearerTokenAuthenticationToken.getToken();
assertThat(token).isEqualTo("bearer customToken");
}
@Test
public void requestWhenCustomJwtDecoderWiredOnDslThenUsed() throws Exception {
this.spring.register(CustomJwtDecoderOnDsl.class, BasicController.class).autowire();
@ -1871,6 +1940,32 @@ public class OAuth2ResourceServerConfigurerTests {
}
@EnableWebSecurity
static class MultipleBearerTokenAuthenticationConverterBeansConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.oauth2ResourceServer()
.jwt();
// @formatter:on
}
@Bean
BearerTokenAuthenticationConverter converterOne() {
BearerTokenAuthenticationConverter converter = new BearerTokenAuthenticationConverter();
return converter;
}
@Bean
BearerTokenAuthenticationConverter converterTwo() {
BearerTokenAuthenticationConverter converter = new BearerTokenAuthenticationConverter();
return converter;
}
}
@EnableWebSecurity
static class CustomJwtDecoderOnDsl extends WebSecurityConfigurerAdapter {

View File

@ -0,0 +1,81 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://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.authentication;
import javax.servlet.http.HttpServletRequest;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.Assert;
/**
* Converts from a HttpServletRequest to {@link BearerTokenAuthenticationToken} that can
* be authenticated. Null authentication possible if there was no Authorization header
* with Bearer Token.
*
* @author Jeongjin Kim
* @since 5.5
*/
public final class BearerTokenAuthenticationConverter implements AuthenticationConverter {
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
private BearerTokenResolver bearerTokenResolver;
public BearerTokenAuthenticationConverter() {
this.bearerTokenResolver = new DefaultBearerTokenResolver();
}
@Override
public BearerTokenAuthenticationToken convert(HttpServletRequest request) {
String token = this.bearerTokenResolver.resolve(request);
if (token == null) {
return null;
}
BearerTokenAuthenticationToken authenticationRequest = new BearerTokenAuthenticationToken(token);
authenticationRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
return authenticationRequest;
}
/**
* Set the {@link BearerTokenResolver} to use. Defaults to
* {@link DefaultBearerTokenResolver}.
* @param bearerTokenResolver the {@code BearerTokenResolver} to use
*/
public void setBearerTokenResolver(BearerTokenResolver bearerTokenResolver) {
Assert.notNull(bearerTokenResolver, "bearerTokenResolver cannot be null");
this.bearerTokenResolver = bearerTokenResolver;
}
/**
* Set the {@link AuthenticationDetailsSource} to use. Defaults to
* {@link WebAuthenticationDetailsSource}.
* @param authenticationDetailsSource the {@code AuthenticationDetailsSource} to use
*/
public void setAuthenticationDetailsSource(
AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
Assert.notNull(authenticationDetailsSource, "authenticationDetailsSource cannot be null");
this.authenticationDetailsSource = authenticationDetailsSource;
}
}

View File

@ -24,7 +24,6 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.core.log.LogMessage;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationManagerResolver;
import org.springframework.security.authentication.AuthenticationServiceException;
@ -32,12 +31,12 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.Assert;
import org.springframework.web.filter.OncePerRequestFilter;
@ -61,10 +60,6 @@ public final class BearerTokenAuthenticationFilter extends OncePerRequestFilter
private final AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver;
private final AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
private BearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver();
private AuthenticationEntryPoint authenticationEntryPoint = new BearerTokenAuthenticationEntryPoint();
private AuthenticationFailureHandler authenticationFailureHandler = (request, response, exception) -> {
@ -74,6 +69,8 @@ public final class BearerTokenAuthenticationFilter extends OncePerRequestFilter
this.authenticationEntryPoint.commence(request, response, exception);
};
private AuthenticationConverter authenticationConverter = new BearerTokenAuthenticationConverter();
/**
* Construct a {@code BearerTokenAuthenticationFilter} using the provided parameter(s)
* @param authenticationManagerResolver
@ -106,22 +103,21 @@ public final class BearerTokenAuthenticationFilter extends OncePerRequestFilter
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String token;
Authentication authenticationRequest;
try {
token = this.bearerTokenResolver.resolve(request);
authenticationRequest = this.authenticationConverter.convert(request);
}
catch (OAuth2AuthenticationException invalid) {
catch (AuthenticationException invalid) {
this.logger.trace("Sending to authentication entry point since failed to resolve bearer token", invalid);
this.authenticationEntryPoint.commence(request, response, invalid);
return;
}
if (token == null) {
if (authenticationRequest == null) {
this.logger.trace("Did not process request since did not find bearer token");
filterChain.doFilter(request, response);
return;
}
BearerTokenAuthenticationToken authenticationRequest = new BearerTokenAuthenticationToken(token);
authenticationRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
try {
AuthenticationManager authenticationManager = this.authenticationManagerResolver.resolve(request);
Authentication authenticationResult = authenticationManager.authenticate(authenticationRequest);
@ -144,10 +140,17 @@ public final class BearerTokenAuthenticationFilter extends OncePerRequestFilter
* Set the {@link BearerTokenResolver} to use. Defaults to
* {@link DefaultBearerTokenResolver}.
* @param bearerTokenResolver the {@code BearerTokenResolver} to use
* @deprecated Instead, use {@link BearerTokenAuthenticationConverter} explicitly
* @see BearerTokenAuthenticationConverter
*/
@Deprecated
public void setBearerTokenResolver(BearerTokenResolver bearerTokenResolver) {
Assert.notNull(bearerTokenResolver, "bearerTokenResolver cannot be null");
this.bearerTokenResolver = bearerTokenResolver;
Assert.isTrue(this.authenticationConverter instanceof BearerTokenAuthenticationConverter,
"bearerTokenResolver and authenticationConverter cannot both be customized in this filter. "
+ "Since you've customized the authenticationConverter, "
+ "please consider configuring the bearerTokenResolver there.");
((BearerTokenAuthenticationConverter) this.authenticationConverter).setBearerTokenResolver(bearerTokenResolver);
}
/**
@ -171,4 +174,15 @@ public final class BearerTokenAuthenticationFilter extends OncePerRequestFilter
this.authenticationFailureHandler = authenticationFailureHandler;
}
/**
* Set the {@link AuthenticationConverter} to use. Defaults to
* {@link BearerTokenAuthenticationConverter}.
* @param authenticationConverter the {@code AuthenticationConverter} to use
* @since 5.5
*/
public void setAuthenticationConverter(AuthenticationConverter authenticationConverter) {
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
this.authenticationConverter = authenticationConverter;
}
}

View File

@ -0,0 +1,87 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://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.authentication;
import javax.servlet.http.HttpServletRequest;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.http.HttpHeaders;
import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link BearerTokenAuthenticationConverter}
*
* @author Jeongjin Kim
* @since 5.5
*/
@RunWith(MockitoJUnitRunner.class)
public class BearerTokenAuthenticationConverterTests {
private BearerTokenAuthenticationConverter converter;
@Before
public void setup() {
this.converter = new BearerTokenAuthenticationConverter();
}
@Test
public void setBearerTokenResolverWithNullThenThrowsException() {
// @formatter:off
assertThatIllegalArgumentException()
.isThrownBy(() -> this.converter.setBearerTokenResolver(null))
.withMessageContaining("bearerTokenResolver cannot be null");
// @formatter:on
}
@Test
public void setAuthenticationDetailsSourceWithNullThenThrowsException() {
// @formatter:off
assertThatIllegalArgumentException()
.isThrownBy(() -> this.converter.setAuthenticationDetailsSource(null))
.withMessageContaining("authenticationDetailsSource cannot be null");
// @formatter:on
}
@Test
public void convertWhenNoBearerTokenHeaderThenNull() {
HttpServletRequest request = mock(HttpServletRequest.class);
BearerTokenAuthenticationToken convert = this.converter.convert(request);
assertThat(convert).isNull();
}
@Test
public void convertWhenBearerTokenThenBearerTokenAuthenticationToken() {
HttpServletRequest request = mock(HttpServletRequest.class);
given(request.getHeader(HttpHeaders.AUTHORIZATION)).willReturn("Bearer token");
BearerTokenAuthenticationToken token = this.converter.convert(request);
assertThat(token.getToken()).isEqualTo("token");
}
}

View File

@ -187,6 +187,16 @@ public class BearerTokenAuthenticationFilterTests {
// @formatter:on
}
@Test
public void setAuthenticationConverterWhenNullThenThrowsException() {
// @formatter:off
BearerTokenAuthenticationFilter filter = new BearerTokenAuthenticationFilter(this.authenticationManager);
assertThatIllegalArgumentException()
.isThrownBy(() -> filter.setAuthenticationConverter(null))
.withMessageContaining("authenticationConverter cannot be null");
// @formatter:on
}
@Test
public void constructorWhenNullAuthenticationManagerThenThrowsException() {
// @formatter:off