Add Support BearerTokenAuthenticationConverter
Closes gh-14750 Signed-off-by: Max Batischev <mblancer@mail.ru>
This commit is contained in:
parent
3f0326d3f1
commit
4967f3feee
|
|
@ -23,6 +23,8 @@ import java.util.Map;
|
|||
import java.util.function.Supplier;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
|
|
@ -37,10 +39,12 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHt
|
|||
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.core.Authentication;
|
||||
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.BearerTokenAuthenticationToken;
|
||||
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;
|
||||
|
|
@ -49,13 +53,14 @@ import org.springframework.security.oauth2.server.resource.introspection.OpaqueT
|
|||
import org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector;
|
||||
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
|
||||
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
|
||||
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
|
||||
import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler;
|
||||
import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationConverter;
|
||||
import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
|
||||
import org.springframework.security.web.access.DelegatingAccessDeniedHandler;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.security.web.csrf.CsrfException;
|
||||
import org.springframework.security.web.util.matcher.AndRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
|
||||
|
|
@ -64,6 +69,7 @@ import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
|||
import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.accept.ContentNegotiationStrategy;
|
||||
import org.springframework.web.accept.HeaderContentNegotiationStrategy;
|
||||
|
||||
|
|
@ -156,7 +162,7 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
|
|||
|
||||
private AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver;
|
||||
|
||||
private BearerTokenResolver bearerTokenResolver;
|
||||
private AuthenticationConverter authenticationConverter;
|
||||
|
||||
private JwtConfigurer jwtConfigurer;
|
||||
|
||||
|
|
@ -194,9 +200,25 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated please use {@link #authenticationConverter} instead
|
||||
*/
|
||||
@Deprecated
|
||||
public OAuth2ResourceServerConfigurer<H> bearerTokenResolver(BearerTokenResolver bearerTokenResolver) {
|
||||
Assert.notNull(bearerTokenResolver, "bearerTokenResolver cannot be null");
|
||||
this.bearerTokenResolver = bearerTokenResolver;
|
||||
this.authenticationConverter = new BearerTokenResolverAuthenticationConverterAdapter(bearerTokenResolver);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthenticationConverter} to use.
|
||||
* @param authenticationConverter the authentication converter
|
||||
* @return the {@link OAuth2ResourceServerConfigurer} for further configuration
|
||||
* @since 6.5
|
||||
*/
|
||||
public OAuth2ResourceServerConfigurer<H> authenticationConverter(AuthenticationConverter authenticationConverter) {
|
||||
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
|
||||
this.authenticationConverter = authenticationConverter;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
@ -271,8 +293,6 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
|
|||
|
||||
@Override
|
||||
public void configure(H http) {
|
||||
BearerTokenResolver bearerTokenResolver = getBearerTokenResolver();
|
||||
this.requestMatcher.setBearerTokenResolver(bearerTokenResolver);
|
||||
AuthenticationManagerResolver resolver = this.authenticationManagerResolver;
|
||||
if (resolver == null) {
|
||||
AuthenticationManager authenticationManager = getAuthenticationManager(http);
|
||||
|
|
@ -280,7 +300,9 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
|
|||
}
|
||||
|
||||
BearerTokenAuthenticationFilter filter = new BearerTokenAuthenticationFilter(resolver);
|
||||
filter.setBearerTokenResolver(bearerTokenResolver);
|
||||
AuthenticationConverter converter = getAuthenticationConverter();
|
||||
this.requestMatcher.setAuthenticationConverter(converter);
|
||||
filter.setAuthenticationConverter(converter);
|
||||
filter.setAuthenticationEntryPoint(this.authenticationEntryPoint);
|
||||
filter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
|
||||
filter = postProcess(filter);
|
||||
|
|
@ -363,16 +385,29 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
|
|||
return http.getSharedObject(AuthenticationManager.class);
|
||||
}
|
||||
|
||||
BearerTokenResolver getBearerTokenResolver() {
|
||||
if (this.bearerTokenResolver == null) {
|
||||
if (this.context.getBeanNamesForType(BearerTokenResolver.class).length > 0) {
|
||||
this.bearerTokenResolver = this.context.getBean(BearerTokenResolver.class);
|
||||
}
|
||||
else {
|
||||
this.bearerTokenResolver = new DefaultBearerTokenResolver();
|
||||
}
|
||||
AuthenticationConverter getAuthenticationConverter() {
|
||||
if (this.authenticationConverter != null) {
|
||||
return this.authenticationConverter;
|
||||
}
|
||||
return this.bearerTokenResolver;
|
||||
if (this.context.getBeanNamesForType(AuthenticationConverter.class).length > 0) {
|
||||
this.authenticationConverter = this.context.getBean(AuthenticationConverter.class);
|
||||
}
|
||||
else if (this.context.getBeanNamesForType(BearerTokenResolver.class).length > 0) {
|
||||
BearerTokenResolver bearerTokenResolver = this.context.getBean(BearerTokenResolver.class);
|
||||
this.authenticationConverter = new BearerTokenResolverAuthenticationConverterAdapter(bearerTokenResolver);
|
||||
}
|
||||
else {
|
||||
this.authenticationConverter = new BearerTokenAuthenticationConverter();
|
||||
}
|
||||
return this.authenticationConverter;
|
||||
}
|
||||
|
||||
BearerTokenResolver getBearerTokenResolver() {
|
||||
AuthenticationConverter authenticationConverter = getAuthenticationConverter();
|
||||
if (authenticationConverter instanceof BearerTokenResolverAuthenticationConverterAdapter bearer) {
|
||||
return bearer.bearerTokenResolver;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public class JwtConfigurer {
|
||||
|
|
@ -560,21 +595,43 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
|
|||
|
||||
private static final class BearerTokenRequestMatcher implements RequestMatcher {
|
||||
|
||||
private BearerTokenResolver bearerTokenResolver;
|
||||
private AuthenticationConverter authenticationConverter;
|
||||
|
||||
@Override
|
||||
public boolean matches(HttpServletRequest request) {
|
||||
try {
|
||||
return this.bearerTokenResolver.resolve(request) != null;
|
||||
return this.authenticationConverter.convert(request) != null;
|
||||
}
|
||||
catch (OAuth2AuthenticationException ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void setBearerTokenResolver(BearerTokenResolver tokenResolver) {
|
||||
Assert.notNull(tokenResolver, "resolver cannot be null");
|
||||
this.bearerTokenResolver = tokenResolver;
|
||||
void setAuthenticationConverter(AuthenticationConverter authenticationConverter) {
|
||||
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
|
||||
this.authenticationConverter = authenticationConverter;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class BearerTokenResolverAuthenticationConverterAdapter implements AuthenticationConverter {
|
||||
|
||||
private final Log logger = LogFactory.getLog(BearerTokenResolverAuthenticationConverterAdapter.class);
|
||||
|
||||
private final BearerTokenResolver bearerTokenResolver;
|
||||
|
||||
BearerTokenResolverAuthenticationConverterAdapter(BearerTokenResolver bearerTokenResolver) {
|
||||
this.bearerTokenResolver = bearerTokenResolver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication convert(HttpServletRequest request) {
|
||||
String token = this.bearerTokenResolver.resolve(request);
|
||||
if (!StringUtils.hasText(token)) {
|
||||
this.logger.trace("Did not process request since did not find bearer token");
|
||||
return null;
|
||||
}
|
||||
return new BearerTokenAuthenticationToken(token);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
|
|
@ -23,6 +23,7 @@ import jakarta.servlet.http.HttpServletRequest;
|
|||
import org.w3c.dom.Element;
|
||||
|
||||
import org.springframework.beans.BeanMetadataElement;
|
||||
import org.springframework.beans.factory.BeanDefinitionStoreException;
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.config.BeanReference;
|
||||
|
|
@ -43,9 +44,10 @@ import org.springframework.security.oauth2.server.resource.authentication.Opaque
|
|||
import org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector;
|
||||
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
|
||||
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
|
||||
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
|
||||
import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler;
|
||||
import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationConverter;
|
||||
import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
|
@ -64,10 +66,14 @@ final class OAuth2ResourceServerBeanDefinitionParser implements BeanDefinitionPa
|
|||
|
||||
static final String BEARER_TOKEN_RESOLVER_REF = "bearer-token-resolver-ref";
|
||||
|
||||
static final String AUTHENTICATION_CONVERTER_REF = "authentication-converter-ref";
|
||||
|
||||
static final String ENTRY_POINT_REF = "entry-point-ref";
|
||||
|
||||
static final String BEARER_TOKEN_RESOLVER = "bearerTokenResolver";
|
||||
|
||||
static final String AUTHENTICATION_CONVERTER = "authenticationConverter";
|
||||
|
||||
static final String AUTHENTICATION_ENTRY_POINT = "authenticationEntryPoint";
|
||||
|
||||
private final BeanReference authenticationManager;
|
||||
|
|
@ -124,11 +130,16 @@ final class OAuth2ResourceServerBeanDefinitionParser implements BeanDefinitionPa
|
|||
pc.getReaderContext().registerWithGeneratedName(opaqueTokenAuthenticationProvider)));
|
||||
}
|
||||
BeanMetadataElement bearerTokenResolver = getBearerTokenResolver(oauth2ResourceServer);
|
||||
BeanDefinitionBuilder requestMatcherBuilder = BeanDefinitionBuilder
|
||||
.rootBeanDefinition(BearerTokenRequestMatcher.class);
|
||||
requestMatcherBuilder.addConstructorArgValue(bearerTokenResolver);
|
||||
BeanDefinition requestMatcher = requestMatcherBuilder.getBeanDefinition();
|
||||
BeanMetadataElement authenticationConverter = getAuthenticationConverter(oauth2ResourceServer);
|
||||
if (bearerTokenResolver != null && authenticationConverter != null) {
|
||||
throw new BeanDefinitionStoreException(
|
||||
"You cannot use bearer-token-ref and authentication-converter-ref in the same oauth2-resource-server element");
|
||||
}
|
||||
if (bearerTokenResolver == null && authenticationConverter == null) {
|
||||
authenticationConverter = new RootBeanDefinition(BearerTokenAuthenticationConverter.class);
|
||||
}
|
||||
BeanMetadataElement authenticationEntryPoint = getEntryPoint(oauth2ResourceServer);
|
||||
BeanDefinition requestMatcher = buildRequestMatcher(bearerTokenResolver, authenticationConverter);
|
||||
this.entryPoints.put(requestMatcher, authenticationEntryPoint);
|
||||
this.deniedHandlers.put(requestMatcher, this.accessDeniedHandler);
|
||||
this.ignoreCsrfRequestMatchers.add(requestMatcher);
|
||||
|
|
@ -136,13 +147,33 @@ final class OAuth2ResourceServerBeanDefinitionParser implements BeanDefinitionPa
|
|||
.rootBeanDefinition(BearerTokenAuthenticationFilter.class);
|
||||
BeanMetadataElement authenticationManagerResolver = getAuthenticationManagerResolver(oauth2ResourceServer);
|
||||
filterBuilder.addConstructorArgValue(authenticationManagerResolver);
|
||||
filterBuilder.addPropertyValue(BEARER_TOKEN_RESOLVER, bearerTokenResolver);
|
||||
filterBuilder.addPropertyValue(AUTHENTICATION_ENTRY_POINT, authenticationEntryPoint);
|
||||
filterBuilder.addPropertyValue("securityContextHolderStrategy",
|
||||
this.authenticationFilterSecurityContextHolderStrategy);
|
||||
|
||||
if (authenticationConverter != null) {
|
||||
filterBuilder.addPropertyValue(AUTHENTICATION_CONVERTER, authenticationConverter);
|
||||
}
|
||||
if (bearerTokenResolver != null) {
|
||||
filterBuilder.addPropertyValue(BEARER_TOKEN_RESOLVER, bearerTokenResolver);
|
||||
}
|
||||
return filterBuilder.getBeanDefinition();
|
||||
}
|
||||
|
||||
private BeanDefinition buildRequestMatcher(BeanMetadataElement bearerTokenResolver,
|
||||
BeanMetadataElement authenticationConverter) {
|
||||
if (bearerTokenResolver != null) {
|
||||
BeanDefinitionBuilder requestMatcherBuilder = BeanDefinitionBuilder
|
||||
.rootBeanDefinition(BearerTokenRequestMatcher.class);
|
||||
requestMatcherBuilder.addConstructorArgValue(bearerTokenResolver);
|
||||
return requestMatcherBuilder.getBeanDefinition();
|
||||
}
|
||||
BeanDefinitionBuilder requestMatcherBuilder = BeanDefinitionBuilder
|
||||
.rootBeanDefinition(BearerTokenAuthenticationRequestMatcher.class);
|
||||
requestMatcherBuilder.addConstructorArgValue(authenticationConverter);
|
||||
return requestMatcherBuilder.getBeanDefinition();
|
||||
}
|
||||
|
||||
void validateConfiguration(Element oauth2ResourceServer, Element jwt, Element opaqueToken, ParserContext pc) {
|
||||
if (!oauth2ResourceServer.hasAttribute(AUTHENTICATION_MANAGER_RESOLVER_REF)) {
|
||||
if (jwt == null && opaqueToken == null) {
|
||||
|
|
@ -178,11 +209,19 @@ final class OAuth2ResourceServerBeanDefinitionParser implements BeanDefinitionPa
|
|||
BeanMetadataElement getBearerTokenResolver(Element element) {
|
||||
String bearerTokenResolverRef = element.getAttribute(BEARER_TOKEN_RESOLVER_REF);
|
||||
if (!StringUtils.hasLength(bearerTokenResolverRef)) {
|
||||
return new RootBeanDefinition(DefaultBearerTokenResolver.class);
|
||||
return null;
|
||||
}
|
||||
return new RuntimeBeanReference(bearerTokenResolverRef);
|
||||
}
|
||||
|
||||
BeanMetadataElement getAuthenticationConverter(Element element) {
|
||||
String authenticationConverterRef = element.getAttribute(AUTHENTICATION_CONVERTER_REF);
|
||||
if (!StringUtils.hasLength(authenticationConverterRef)) {
|
||||
return null;
|
||||
}
|
||||
return new RuntimeBeanReference(authenticationConverterRef);
|
||||
}
|
||||
|
||||
BeanMetadataElement getEntryPoint(Element element) {
|
||||
String entryPointRef = element.getAttribute(ENTRY_POINT_REF);
|
||||
if (!StringUtils.hasLength(entryPointRef)) {
|
||||
|
|
@ -366,4 +405,25 @@ final class OAuth2ResourceServerBeanDefinitionParser implements BeanDefinitionPa
|
|||
|
||||
}
|
||||
|
||||
static final class BearerTokenAuthenticationRequestMatcher implements RequestMatcher {
|
||||
|
||||
private final AuthenticationConverter authenticationConverter;
|
||||
|
||||
BearerTokenAuthenticationRequestMatcher(AuthenticationConverter authenticationConverter) {
|
||||
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
|
||||
this.authenticationConverter = authenticationConverter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(HttpServletRequest request) {
|
||||
try {
|
||||
return this.authenticationConverter.convert(request) != null;
|
||||
}
|
||||
catch (OAuth2AuthenticationException ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -650,6 +650,9 @@ oauth2-resource-server.attlist &=
|
|||
oauth2-resource-server.attlist &=
|
||||
## Reference to a AuthenticationEntryPoint
|
||||
attribute entry-point-ref {xsd:token}?
|
||||
oauth2-resource-server.attlist &=
|
||||
## Reference to a AuthenticationConverter
|
||||
attribute authentication-converter-ref {xsd:token}?
|
||||
|
||||
jwt =
|
||||
## Configures JWT authentication
|
||||
|
|
|
|||
|
|
@ -1999,6 +1999,12 @@
|
|||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="authentication-converter-ref" type="xs:token">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Reference to a AuthenticationConverter
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:attributeGroup>
|
||||
<xs:element name="jwt">
|
||||
<xs:annotation>
|
||||
|
|
|
|||
|
|
@ -127,12 +127,14 @@ import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthen
|
|||
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
|
||||
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
|
||||
import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler;
|
||||
import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationConverter;
|
||||
import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.test.web.servlet.ResultMatcher;
|
||||
|
|
@ -759,13 +761,6 @@ public class OAuth2ResourceServerConfigurerTests {
|
|||
assertThat(oauth2.getBearerTokenResolver()).isEqualTo(resolver);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getBearerTokenResolverWhenNoResolverSpecifiedThenTheDefaultIsUsed() {
|
||||
ApplicationContext context = this.spring.context(new GenericWebApplicationContext()).getContext();
|
||||
OAuth2ResourceServerConfigurer oauth2 = new OAuth2ResourceServerConfigurer(context);
|
||||
assertThat(oauth2.getBearerTokenResolver()).isInstanceOf(DefaultBearerTokenResolver.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenCustomAuthenticationDetailsSourceThenUsed() throws Exception {
|
||||
this.spring.register(CustomAuthenticationDetailsSource.class, JwtDecoderConfig.class, BasicController.class)
|
||||
|
|
@ -1415,6 +1410,47 @@ public class OAuth2ResourceServerConfigurerTests {
|
|||
verify(authenticationConverter).convert(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAuthenticationConverterWhenDuplicateConverterBeansAndAnotherOnTheDslThenTheDslOneIsUsed() {
|
||||
AuthenticationConverter converter = mock(AuthenticationConverter.class);
|
||||
AuthenticationConverter converterBean = mock(AuthenticationConverter.class);
|
||||
GenericWebApplicationContext context = new GenericWebApplicationContext();
|
||||
context.registerBean("converterOne", AuthenticationConverter.class, () -> converterBean);
|
||||
context.registerBean("converterTwo", AuthenticationConverter.class, () -> converterBean);
|
||||
this.spring.context(context).autowire();
|
||||
OAuth2ResourceServerConfigurer oauth2 = new OAuth2ResourceServerConfigurer(context);
|
||||
oauth2.authenticationConverter(converter);
|
||||
assertThat(oauth2.getAuthenticationConverter()).isEqualTo(converter);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAuthenticationConverterWhenConverterBeanAndAnotherOnTheDslThenTheDslOneIsUsed() {
|
||||
AuthenticationConverter converter = mock(AuthenticationConverter.class);
|
||||
AuthenticationConverter converterBean = mock(AuthenticationConverter.class);
|
||||
GenericWebApplicationContext context = new GenericWebApplicationContext();
|
||||
context.registerBean(AuthenticationConverter.class, () -> converterBean);
|
||||
this.spring.context(context).autowire();
|
||||
OAuth2ResourceServerConfigurer oauth2 = new OAuth2ResourceServerConfigurer(context);
|
||||
oauth2.authenticationConverter(converter);
|
||||
assertThat(oauth2.getAuthenticationConverter()).isEqualTo(converter);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAuthenticationConverterWhenDuplicateConverterBeansThenWiringException() {
|
||||
assertThatExceptionOfType(BeanCreationException.class)
|
||||
.isThrownBy(
|
||||
() -> this.spring.register(MultipleAuthenticationConverterBeansConfig.class, JwtDecoderConfig.class)
|
||||
.autowire())
|
||||
.withRootCauseInstanceOf(NoUniqueBeanDefinitionException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAuthenticationConverterWhenNoConverterSpecifiedThenTheDefaultIsUsed() {
|
||||
ApplicationContext context = this.spring.context(new GenericWebApplicationContext()).getContext();
|
||||
OAuth2ResourceServerConfigurer oauth2 = new OAuth2ResourceServerConfigurer(context);
|
||||
assertThat(oauth2.getAuthenticationConverter()).isInstanceOf(BearerTokenAuthenticationConverter.class);
|
||||
}
|
||||
|
||||
private static <T> void registerMockBean(GenericApplicationContext context, String name, Class<T> clazz) {
|
||||
context.registerBean(name, clazz, () -> mock(clazz));
|
||||
}
|
||||
|
|
@ -2516,6 +2552,39 @@ public class OAuth2ResourceServerConfigurerTests {
|
|||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class MultipleAuthenticationConverterBeansConfig {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeRequests()
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.oauth2ResourceServer()
|
||||
.jwt();
|
||||
return http.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Bean
|
||||
AuthenticationConverter authenticationConverterOne() {
|
||||
BearerTokenAuthenticationConverter converter = new BearerTokenAuthenticationConverter();
|
||||
converter.setAllowUriQueryParameter(true);
|
||||
return converter;
|
||||
}
|
||||
|
||||
@Bean
|
||||
AuthenticationConverter authenticationConverterTwo() {
|
||||
BearerTokenAuthenticationConverter converter = new BearerTokenAuthenticationConverter();
|
||||
converter.setAllowUriQueryParameter(true);
|
||||
return converter;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class MultipleIssuersConfig {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
|
|
@ -25,7 +25,6 @@ import java.time.Instant;
|
|||
import java.time.ZoneId;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.stream.Collectors;
|
||||
|
|
@ -50,13 +49,11 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
|||
import org.mockito.Mockito;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import org.springframework.beans.BeanMetadataElement;
|
||||
import org.springframework.beans.factory.BeanDefinitionStoreException;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.BeanReference;
|
||||
import org.springframework.beans.factory.parsing.BeanDefinitionParsingException;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.beans.factory.xml.BeanDefinitionParserDelegate;
|
||||
import org.springframework.beans.factory.xml.ParserContext;
|
||||
import org.springframework.beans.factory.xml.XmlReaderContext;
|
||||
|
|
@ -85,12 +82,14 @@ import org.springframework.security.oauth2.jwt.JwtClaimNames;
|
|||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.TestJwts;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector;
|
||||
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenAuthenticationConverter;
|
||||
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
|
||||
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
|
||||
import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
|
|
@ -462,6 +461,24 @@ public class OAuth2ResourceServerBeanDefinitionParserTests {
|
|||
verify(bearerTokenResolver).resolve(any(HttpServletRequest.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWhenCustomAuthenticationConverterThenUses() throws Exception {
|
||||
this.spring
|
||||
.configLocations(xml("MockAuthenticationConverter"), xml("MockJwtDecoder"), xml("AuthenticationConverter"))
|
||||
.autowire();
|
||||
JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class);
|
||||
given(decoder.decode("token")).willReturn(TestJwts.jwt().build());
|
||||
AuthenticationConverter authenticationConverter = this.spring.getContext()
|
||||
.getBean(AuthenticationConverter.class);
|
||||
given(authenticationConverter.convert(any(HttpServletRequest.class)))
|
||||
.willReturn(new BearerTokenAuthenticationToken("token"));
|
||||
|
||||
this.mvc.perform(get("/")).andExpect(status().isNotFound());
|
||||
|
||||
verify(decoder).decode("token");
|
||||
verify(authenticationConverter).convert(any(HttpServletRequest.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenBearerTokenResolverAllowsRequestBodyThenEitherHeaderOrRequestBodyIsAccepted()
|
||||
throws Exception {
|
||||
|
|
@ -521,14 +538,6 @@ public class OAuth2ResourceServerBeanDefinitionParserTests {
|
|||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getBearerTokenResolverWhenNoResolverSpecifiedThenTheDefaultIsUsed() {
|
||||
OAuth2ResourceServerBeanDefinitionParser oauth2 = new OAuth2ResourceServerBeanDefinitionParser(
|
||||
mock(BeanReference.class), mock(List.class), mock(Map.class), mock(Map.class), mock(List.class),
|
||||
mock(BeanMetadataElement.class));
|
||||
assertThat(oauth2.getBearerTokenResolver(mock(Element.class))).isInstanceOf(RootBeanDefinition.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenCustomJwtDecoderThenUsed() throws Exception {
|
||||
this.spring.configLocations(xml("MockJwtDecoder"), xml("Jwt")).autowire();
|
||||
|
|
@ -545,6 +554,12 @@ public class OAuth2ResourceServerBeanDefinitionParserTests {
|
|||
.isThrownBy(() -> this.spring.configLocations(xml("JwtDecoderAndJwkSetUri")).autowire());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configureWhenAuthenticationConverterAndJwkSetUriThenException() {
|
||||
assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(
|
||||
() -> this.spring.configLocations(xml("AuthenticationConverterAndBearerTokenResolver")).autowire());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenRealmNameConfiguredThenUsesOnUnauthenticated() throws Exception {
|
||||
this.spring.configLocations(xml("MockJwtDecoder"), xml("AuthenticationEntryPoint")).autowire();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Copyright 2002-2020 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.
|
||||
-->
|
||||
|
||||
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://www.springframework.org/schema/security"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd
|
||||
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
|
||||
|
||||
<http>
|
||||
<intercept-url pattern="/**" access="authenticated"/>
|
||||
<oauth2-resource-server authentication-converter-ref="authenticationConverter">
|
||||
<jwt decoder-ref="decoder"/>
|
||||
</oauth2-resource-server>
|
||||
</http>
|
||||
|
||||
<b:import resource="userservice.xml"/>
|
||||
</b:beans>
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Copyright 2002-2020 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.
|
||||
-->
|
||||
|
||||
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://www.springframework.org/schema/security"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd
|
||||
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
|
||||
|
||||
<http>
|
||||
<intercept-url pattern="/**" access="authenticated"/>
|
||||
<oauth2-resource-server authentication-converter-ref="authenticationConverter" bearer-token-resolver-ref="bearerTokenResolver">
|
||||
<jwt decoder-ref="decoder"/>
|
||||
</oauth2-resource-server>
|
||||
</http>
|
||||
|
||||
<b:import resource="userservice.xml"/>
|
||||
</b:beans>
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Copyright 2002-2020 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.
|
||||
-->
|
||||
|
||||
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://www.springframework.org/schema/security"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd
|
||||
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
|
||||
|
||||
<b:bean name="authenticationConverter" class="org.mockito.Mockito" factory-method="mock">
|
||||
<b:constructor-arg value="org.springframework.security.web.authentication.AuthenticationConverter" type="java.lang.Class"/>
|
||||
</b:bean>
|
||||
</b:beans>
|
||||
|
|
@ -1272,6 +1272,10 @@ Reference to a `BearerTokenResolver` which will retrieve the bearer token from t
|
|||
* **entry-point-ref**
|
||||
Reference to a `AuthenticationEntryPoint` which will handle unauthorized requests
|
||||
|
||||
[[nsa-oauth2-resource-server-authentication-converter-ref]]
|
||||
* **authentication-converter-ref**
|
||||
Reference to a `AuthenticationConverter` which convert request to authentication
|
||||
|
||||
[[nsa-jwt]]
|
||||
== <jwt>
|
||||
Represents an OAuth 2.0 Resource Server that will authorize JWTs
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
|
|
@ -29,7 +29,10 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
|||
* @since 5.1
|
||||
* @see <a href="https://tools.ietf.org/html/rfc6750#section-2" target="_blank">RFC 6750
|
||||
* Section 2: Authenticated Requests</a>
|
||||
* @deprecated Use
|
||||
* {@link org.springframework.security.web.authentication.AuthenticationConverter} instead
|
||||
*/
|
||||
@Deprecated
|
||||
@FunctionalInterface
|
||||
public interface BearerTokenResolver {
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
* Copyright 2002-2025 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.web.authentication;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.authentication.AuthenticationDetailsSource;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.server.resource.BearerTokenError;
|
||||
import org.springframework.security.oauth2.server.resource.BearerTokenErrors;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Implementation of {@link AuthenticationConverter}, that converts request to
|
||||
* {@link BearerTokenAuthenticationToken}
|
||||
*
|
||||
* @author Max Batischev
|
||||
* @since 6.5
|
||||
*/
|
||||
public final class BearerTokenAuthenticationConverter implements AuthenticationConverter {
|
||||
|
||||
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
|
||||
|
||||
private static final Pattern authorizationPattern = Pattern.compile("^Bearer (?<token>[a-zA-Z0-9-._~+/]+=*)$",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private static final String ACCESS_TOKEN_PARAMETER_NAME = "access_token";
|
||||
|
||||
private boolean allowFormEncodedBodyParameter = false;
|
||||
|
||||
private boolean allowUriQueryParameter = false;
|
||||
|
||||
private String bearerTokenHeaderName = HttpHeaders.AUTHORIZATION;
|
||||
|
||||
@Override
|
||||
public Authentication convert(HttpServletRequest request) {
|
||||
String token = resolveToken(request);
|
||||
if (StringUtils.hasText(token)) {
|
||||
BearerTokenAuthenticationToken authenticationToken = new BearerTokenAuthenticationToken(token);
|
||||
authenticationToken.setDetails(this.authenticationDetailsSource.buildDetails(request));
|
||||
|
||||
return authenticationToken;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String resolveToken(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;
|
||||
}
|
||||
|
||||
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 boolean isParameterTokenEnabledForRequest(HttpServletRequest request) {
|
||||
return ((this.allowFormEncodedBodyParameter && isFormEncodedRequest(request) && !isGetRequest(request)
|
||||
&& !hasAccessTokenInQueryString(request)) || (this.allowUriQueryParameter && isGetRequest(request)));
|
||||
}
|
||||
|
||||
private static String resolveFromRequestParameters(HttpServletRequest request) {
|
||||
String[] values = request.getParameterValues(ACCESS_TOKEN_PARAMETER_NAME);
|
||||
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 isFormEncodedRequest(request) || isGetRequest(request);
|
||||
}
|
||||
|
||||
private boolean isGetRequest(HttpServletRequest request) {
|
||||
return HttpMethod.GET.name().equals(request.getMethod());
|
||||
}
|
||||
|
||||
private boolean isFormEncodedRequest(HttpServletRequest request) {
|
||||
return MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(request.getContentType());
|
||||
}
|
||||
|
||||
private static boolean hasAccessTokenInQueryString(HttpServletRequest request) {
|
||||
return (request.getQueryString() != null) && request.getQueryString().contains(ACCESS_TOKEN_PARAMETER_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this value to configure what header is checked when resolving a Bearer Token.
|
||||
* This value is defaulted to {@link HttpHeaders#AUTHORIZATION}.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
public void setBearerTokenHeaderName(String bearerTokenHeaderName) {
|
||||
this.bearerTokenHeaderName = bearerTokenHeaderName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -44,6 +44,7 @@ import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthen
|
|||
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
|
||||
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.security.web.authentication.AuthenticationEntryPointFailureHandler;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
|
|
@ -83,12 +84,12 @@ public class BearerTokenAuthenticationFilter extends OncePerRequestFilter {
|
|||
private AuthenticationFailureHandler authenticationFailureHandler = new AuthenticationEntryPointFailureHandler(
|
||||
(request, response, exception) -> this.authenticationEntryPoint.commence(request, response, exception));
|
||||
|
||||
private BearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver();
|
||||
|
||||
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
|
||||
|
||||
private SecurityContextRepository securityContextRepository = new RequestAttributeSecurityContextRepository();
|
||||
|
||||
private AuthenticationConverter authenticationConverter = new BearerTokenAuthenticationConverter();
|
||||
|
||||
/**
|
||||
* Construct a {@code BearerTokenAuthenticationFilter} using the provided parameter(s)
|
||||
* @param authenticationManagerResolver
|
||||
|
|
@ -121,24 +122,22 @@ public 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) {
|
||||
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);
|
||||
|
|
@ -194,7 +193,14 @@ public class BearerTokenAuthenticationFilter extends OncePerRequestFilter {
|
|||
*/
|
||||
public void setBearerTokenResolver(BearerTokenResolver bearerTokenResolver) {
|
||||
Assert.notNull(bearerTokenResolver, "bearerTokenResolver cannot be null");
|
||||
this.bearerTokenResolver = bearerTokenResolver;
|
||||
this.authenticationConverter = (request) -> {
|
||||
String token = bearerTokenResolver.resolve(request);
|
||||
if (!StringUtils.hasText(token)) {
|
||||
this.logger.trace("Did not process request since did not find bearer token");
|
||||
return null;
|
||||
}
|
||||
return new BearerTokenAuthenticationToken(token);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -243,4 +249,15 @@ public class BearerTokenAuthenticationFilter extends OncePerRequestFilter {
|
|||
return StringUtils.hasText(jwkThumbprintClaim);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link AuthenticationConverter} to use. Defaults to
|
||||
* {@link BearerTokenAuthenticationConverter}.
|
||||
* @param authenticationConverter the {@code AuthenticationConverter} to use
|
||||
* @since 6.5
|
||||
*/
|
||||
public void setAuthenticationConverter(AuthenticationConverter authenticationConverter) {
|
||||
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
|
||||
this.authenticationConverter = authenticationConverter;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
* Copyright 2002-2025 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.web.authentication;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.security.authentication.AuthenticationDetailsSource;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link BearerTokenAuthenticationConverter}
|
||||
*
|
||||
* @author Max Batischev
|
||||
*/
|
||||
public class BearerTokenAuthenticationConverterTests {
|
||||
|
||||
private static final String X_AUTH_TOKEN_HEADER = "X-Auth-Token";
|
||||
|
||||
private static final String TEST_X_AUTH_TOKEN = "test-x-auth-token";
|
||||
|
||||
private static final String BEARER_TOKEN = "test_bearer_token";
|
||||
|
||||
private final BearerTokenAuthenticationConverter converter = new BearerTokenAuthenticationConverter();
|
||||
|
||||
@Test
|
||||
public void convertWhenAuthorizationHeaderIsPresentThenTokenIsConverted() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + BEARER_TOKEN);
|
||||
|
||||
Authentication authentication = this.converter.convert(request);
|
||||
|
||||
assertThat(authentication).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertWhenQueryParameterIsPresentThenTokenIsConverted() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setMethod(HttpMethod.GET.name());
|
||||
request.addParameter("access_token", BEARER_TOKEN);
|
||||
|
||||
this.converter.setAllowUriQueryParameter(true);
|
||||
|
||||
Authentication authentication = this.converter.convert(request);
|
||||
assertThat(authentication).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertWhenAuthorizationHeaderNotIsPresentThenTokenIsNotConverted() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
|
||||
Authentication authentication = this.converter.convert(request);
|
||||
|
||||
assertThat(authentication).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertWhenAuthorizationHeaderIsPresentTogetherWithQueryParameterThenAuthenticationExceptionIsThrown() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addParameter("access_token", BEARER_TOKEN);
|
||||
request.setMethod(HttpMethod.GET.name());
|
||||
request.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + BEARER_TOKEN);
|
||||
|
||||
assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(() -> this.converter.convert(request))
|
||||
.withMessageContaining("Found multiple bearer tokens in the request");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertWhenXAuthTokenHeaderIsPresentAndBearerTokenHeaderNameSetThenTokenIsConverted() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader(X_AUTH_TOKEN_HEADER, "Bearer " + TEST_X_AUTH_TOKEN);
|
||||
|
||||
this.converter.setBearerTokenHeaderName(X_AUTH_TOKEN_HEADER);
|
||||
|
||||
Authentication authentication = this.converter.convert(request);
|
||||
assertThat(authentication).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertWhenHeaderWithMissingTokenIsPresentThenAuthenticationExceptionIsThrown() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader(HttpHeaders.AUTHORIZATION, "Bearer ");
|
||||
|
||||
assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(() -> this.converter.convert(request))
|
||||
.withMessageContaining(("Bearer token is malformed"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertWhenHeaderWithInvalidCharactersIsPresentThenAuthenticationExceptionIsThrown() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader(HttpHeaders.AUTHORIZATION, "Bearer an\"invalid\"token");
|
||||
|
||||
assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(() -> this.converter.convert(request))
|
||||
.withMessageContaining(("Bearer token is malformed"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void convertWhenCustomAuthenticationDetailsSourceSetThenTokenIsConverted() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + BEARER_TOKEN);
|
||||
AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = Mockito
|
||||
.mock(AuthenticationDetailsSource.class);
|
||||
this.converter.setAuthenticationDetailsSource(authenticationDetailsSource);
|
||||
|
||||
Authentication authentication = this.converter.convert(request);
|
||||
|
||||
verify(authenticationDetailsSource).buildDetails(any());
|
||||
assertThat(authentication).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertWhenFormParameterIsPresentAndAllowFormEncodedBodyParameterThenConverted() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setMethod(HttpMethod.POST.name());
|
||||
request.setContentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE);
|
||||
request.addParameter("access_token", BEARER_TOKEN);
|
||||
this.converter.setAllowFormEncodedBodyParameter(true);
|
||||
|
||||
assertThat(this.converter.convert(request)).isNotNull();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -293,6 +293,16 @@ public class BearerTokenAuthenticationFilterTests {
|
|||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setConverterWhenNullThenThrowsException() {
|
||||
// @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
|
||||
|
|
|
|||
Loading…
Reference in New Issue