diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java index cbb08a218ee..8f45f3e8679 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java @@ -21,10 +21,12 @@ import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Optional; import java.util.function.Supplier; import javax.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContext; import org.springframework.core.convert.converter.Converter; import org.springframework.http.MediaType; @@ -46,6 +48,7 @@ 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; +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.introspection.SpringOpaqueTokenIntrospector; import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint; @@ -107,8 +110,8 @@ * * *

- * When using {@link #opaqueToken(Customizer)}, supply an introspection endpoint and its - * authentication configuration + * When using {@link #opaqueToken(Customizer)}, supply an introspection endpoint with its + * client credentials and an OpaqueTokenAuthenticationConverter *

* *

Security Filters

@@ -138,6 +141,7 @@ * * @author Josh Cummings * @author Evgeniy Cheban + * @author Jerome Wacongne <ch4mp@c4-soft.com> * @since 5.1 * @see BearerTokenAuthenticationFilter * @see JwtAuthenticationProvider @@ -456,6 +460,8 @@ public class OpaqueTokenConfigurer { private Supplier introspector; + private Supplier authenticationConverter; + OpaqueTokenConfigurer(ApplicationContext context) { this.context = context; } @@ -490,6 +496,13 @@ public OpaqueTokenConfigurer introspector(OpaqueTokenIntrospector introspector) return this; } + public OpaqueTokenConfigurer authenticationConverter( + OpaqueTokenAuthenticationConverter authenticationConverter) { + Assert.notNull(authenticationConverter, "authenticationConverter cannot be null"); + this.authenticationConverter = () -> authenticationConverter; + return this; + } + OpaqueTokenIntrospector getIntrospector() { if (this.introspector != null) { return this.introspector.get(); @@ -497,12 +510,27 @@ OpaqueTokenIntrospector getIntrospector() { return this.context.getBean(OpaqueTokenIntrospector.class); } + Optional getAuthenticationConverter() { + if (this.authenticationConverter != null) { + return Optional.of(this.authenticationConverter.get()); + } + try { + return Optional.of(this.context.getBean(OpaqueTokenAuthenticationConverter.class)); + } + catch (NoSuchBeanDefinitionException nsbde) { + return Optional.empty(); + } + } + AuthenticationProvider getAuthenticationProvider() { if (this.authenticationManager != null) { return null; } OpaqueTokenIntrospector introspector = getIntrospector(); - return new OpaqueTokenAuthenticationProvider(introspector); + final OpaqueTokenAuthenticationProvider opaqueTokenAuthenticationProvider = new OpaqueTokenAuthenticationProvider( + introspector); + getAuthenticationConverter().ifPresent(opaqueTokenAuthenticationProvider::setAuthenticationConverter); + return opaqueTokenAuthenticationProvider; } AuthenticationManager getAuthenticationManager(H http) { diff --git a/config/src/main/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParser.java index 32da0f24180..568ee2b131e 100644 --- a/config/src/main/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParser.java @@ -251,6 +251,9 @@ static final class OpaqueTokenBeanDefinitionParser implements BeanDefinitionPars static final String CLIENT_SECRET = "client-secret"; + static final String AUTHENTICATION_CONVERTER_REF = "authentication-converter-ref"; + static final String AUTHENTICATION_CONVERTER = "authenticationConverter"; + OpaqueTokenBeanDefinitionParser() { } @@ -258,9 +261,14 @@ static final class OpaqueTokenBeanDefinitionParser implements BeanDefinitionPars public BeanDefinition parse(Element element, ParserContext pc) { validateConfiguration(element, pc); BeanMetadataElement introspector = getIntrospector(element); + String authenticationConverterRef = element.getAttribute(AUTHENTICATION_CONVERTER_REF); BeanDefinitionBuilder opaqueTokenProviderBuilder = BeanDefinitionBuilder .rootBeanDefinition(OpaqueTokenAuthenticationProvider.class); opaqueTokenProviderBuilder.addConstructorArgValue(introspector); + if (StringUtils.hasText(authenticationConverterRef)) { + opaqueTokenProviderBuilder.addPropertyValue(AUTHENTICATION_CONVERTER, + new RuntimeBeanReference(authenticationConverterRef)); + } return opaqueTokenProviderBuilder.getBeanDefinition(); } diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java index eba6a1f3a19..85953fe08d4 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java @@ -27,6 +27,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.UUID; import java.util.function.Function; import java.util.function.Supplier; @@ -35,6 +36,7 @@ import reactor.util.context.Context; import org.springframework.beans.BeansException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContext; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; @@ -95,6 +97,7 @@ import org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenReactiveAuthenticationManager; import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverter; import org.springframework.security.oauth2.server.resource.introspection.NimbusReactiveOpaqueTokenIntrospector; +import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenAuthenticationConverter; import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector; import org.springframework.security.oauth2.server.resource.web.access.server.BearerTokenServerAccessDeniedHandler; import org.springframework.security.oauth2.server.resource.web.server.BearerTokenServerAuthenticationEntryPoint; @@ -4283,6 +4286,8 @@ public final class OpaqueTokenSpec { private Supplier introspector; + private Supplier authenticationConverter; + private OpaqueTokenSpec() { } @@ -4321,6 +4326,13 @@ public OpaqueTokenSpec introspector(ReactiveOpaqueTokenIntrospector introspector return this; } + public OpaqueTokenSpec authenticationConverter( + ReactiveOpaqueTokenAuthenticationConverter authenticationConverter) { + Assert.notNull(authenticationConverter, "authenticationConverter cannot be null"); + this.authenticationConverter = () -> authenticationConverter; + return this; + } + /** * Allows method chaining to continue configuring the * {@link ServerHttpSecurity} @@ -4331,7 +4343,11 @@ public OAuth2ResourceServerSpec and() { } protected ReactiveAuthenticationManager getAuthenticationManager() { - return new OpaqueTokenReactiveAuthenticationManager(getIntrospector()); + final OpaqueTokenReactiveAuthenticationManager authenticationManager = new OpaqueTokenReactiveAuthenticationManager( + getIntrospector()); + Optional.ofNullable(getAuthenticationConverter()) + .ifPresent(authenticationManager::setAuthenticationConverter); + return authenticationManager; } protected ReactiveOpaqueTokenIntrospector getIntrospector() { @@ -4341,6 +4357,18 @@ protected ReactiveOpaqueTokenIntrospector getIntrospector() { return getBean(ReactiveOpaqueTokenIntrospector.class); } + protected ReactiveOpaqueTokenAuthenticationConverter getAuthenticationConverter() { + if (this.authenticationConverter != null) { + return this.authenticationConverter.get(); + } + try { + return getBean(ReactiveOpaqueTokenAuthenticationConverter.class); + } + catch (NoSuchBeanDefinitionException nsbde) { + return null; + } + } + protected void configure(ServerHttpSecurity http) { ReactiveAuthenticationManager authenticationManager = getAuthenticationManager(); AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(authenticationManager); diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOpaqueTokenDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOpaqueTokenDsl.kt index 72d9bf103fb..31d8ba2c96c 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOpaqueTokenDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOpaqueTokenDsl.kt @@ -16,6 +16,7 @@ package org.springframework.security.config.web.server +import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenAuthenticationConverter import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector /** @@ -30,6 +31,7 @@ import org.springframework.security.oauth2.server.resource.introspection.Reactiv class ServerOpaqueTokenDsl { private var _introspectionUri: String? = null private var _introspector: ReactiveOpaqueTokenIntrospector? = null + private var _authenticationConverter: ReactiveOpaqueTokenAuthenticationConverter? = null private var clientCredentials: Pair? = null var introspectionUri: String? @@ -37,14 +39,21 @@ class ServerOpaqueTokenDsl { set(value) { _introspectionUri = value _introspector = null + _authenticationConverter = null } var introspector: ReactiveOpaqueTokenIntrospector? get() = _introspector set(value) { _introspector = value + _authenticationConverter = null _introspectionUri = null clientCredentials = null } + var authenticationConverter: ReactiveOpaqueTokenAuthenticationConverter? + get() = _authenticationConverter + set(value) { + _authenticationConverter = value + } /** * Configures the credentials for Introspection endpoint. @@ -55,6 +64,7 @@ class ServerOpaqueTokenDsl { fun introspectionClientCredentials(clientId: String, clientSecret: String) { clientCredentials = Pair(clientId, clientSecret) _introspector = null + _authenticationConverter = null } internal fun get(): (ServerHttpSecurity.OAuth2ResourceServerSpec.OpaqueTokenSpec) -> Unit { @@ -62,6 +72,7 @@ class ServerOpaqueTokenDsl { introspectionUri?.also { opaqueToken.introspectionUri(introspectionUri) } clientCredentials?.also { opaqueToken.introspectionClientCredentials(clientCredentials!!.first, clientCredentials!!.second) } introspector?.also { opaqueToken.introspector(introspector) } + authenticationConverter?.also { opaqueToken.authenticationConverter(authenticationConverter) } } } } diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/resourceserver/OpaqueTokenDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/resourceserver/OpaqueTokenDsl.kt index 5c8ab0c3f53..dfaaeb570f9 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/resourceserver/OpaqueTokenDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/resourceserver/OpaqueTokenDsl.kt @@ -20,6 +20,7 @@ import org.springframework.security.authentication.AuthenticationManager import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer import org.springframework.security.core.Authentication +import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenAuthenticationConverter import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector /** @@ -37,6 +38,7 @@ class OpaqueTokenDsl { private var _introspectionUri: String? = null private var _introspector: OpaqueTokenIntrospector? = null private var clientCredentials: Pair? = null + private var _authenticationConverter: OpaqueTokenAuthenticationConverter? = null var authenticationManager: AuthenticationManager? = null @@ -54,6 +56,11 @@ class OpaqueTokenDsl { clientCredentials = null } + var authenticationConverter: OpaqueTokenAuthenticationConverter? + get() = _authenticationConverter + set(value) { + _authenticationConverter = value + } /** * Configures the credentials for Introspection endpoint. @@ -70,6 +77,7 @@ class OpaqueTokenDsl { return { opaqueToken -> introspectionUri?.also { opaqueToken.introspectionUri(introspectionUri) } introspector?.also { opaqueToken.introspector(introspector) } + authenticationConverter?.also { opaqueToken.authenticationConverter(authenticationConverter) } clientCredentials?.also { opaqueToken.introspectionClientCredentials(clientCredentials!!.first, clientCredentials!!.second) } authenticationManager?.also { opaqueToken.authenticationManager(authenticationManager) } } diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.8.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-5.8.rnc index 3f48f7bcd48..dcec72a232a 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-5.8.rnc +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.8.rnc @@ -667,6 +667,9 @@ opaque-token.attlist &= opaque-token.attlist &= ## Reference to an OpaqueTokenIntrospector attribute introspector-ref {xsd:token}? +opaque-token.attlist &= + ## Reference to an OpaqueTokenAuthenticationConverter responsible for converting successful introspection result into an Authentication. + attribute authentication-converter-ref {xsd:token}? openid-login = ## Sets up form login for authentication with an Open ID identity. NOTE: The OpenID 1.0 and 2.0 protocols have been deprecated and users are encouraged to migrate to OpenID Connect, which is supported by spring-security-oauth2. diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.8.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-5.8.xsd index bef39a7c620..dc2911daac7 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-5.8.xsd +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.8.xsd @@ -2060,6 +2060,13 @@ + + + Reference to an OpaqueTokenAuthenticationConverter responsible for converting successful + introspection result into an Authentication. + + + diff --git a/config/src/test/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests.java index 702d302ef26..f89883a646c 100644 --- a/config/src/test/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests.java @@ -23,6 +23,7 @@ import java.time.Clock; import java.time.Instant; import java.time.ZoneId; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -68,13 +69,17 @@ import org.springframework.http.MediaType; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.AuthenticationManagerResolver; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.config.http.OAuth2ResourceServerBeanDefinitionParser.JwtBeanDefinitionParser; import org.springframework.security.config.http.OAuth2ResourceServerBeanDefinitionParser.OpaqueTokenBeanDefinitionParser; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolderStrategy; +import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2TokenValidator; import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; @@ -87,6 +92,7 @@ import org.springframework.security.oauth2.jwt.TestJwts; 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; @@ -662,6 +668,20 @@ public void getWhenIntrospectingThenOk() throws Exception { // @formatter:on } + @Test + public void configureWhenIntrospectingWithAuthenticationConverterThenUses() throws Exception { + this.spring.configLocations(xml("OpaqueTokenRestOperations"), xml("OpaqueTokenAndAuthenticationConverter")) + .autowire(); + mockRestOperations(json("Active")); + // @formatter:off + this.mvc.perform(get("/authenticated").header("Authorization", "Bearer token")) + .andExpect(status().isNotFound()); + + this.mvc.perform(get("/authenticated").header("Authorization", "Bearer invalidToken")) + .andExpect(status().isUnauthorized()); + // @formatter:on + } + @Test public void getWhenIntrospectionFailsThenUnauthorized() throws Exception { this.spring.configLocations(xml("OpaqueTokenRestOperations"), xml("OpaqueToken")).autowire(); @@ -1096,4 +1116,39 @@ public void setMillis(long millis) { } + public static class TestAuthentication extends AbstractAuthenticationToken { + + private final String introspectedToken; + + public TestAuthentication(String introspectedToken, Collection authorities) { + super(authorities); + this.introspectedToken = introspectedToken; + } + + @Override + public Object getCredentials() { + return this.introspectedToken; + } + + @Override + public Object getPrincipal() { + return this.introspectedToken; + } + + @Override + public boolean isAuthenticated() { + return "token".equals(this.introspectedToken); + } + + } + + public static class TestOpaqueTokenAuthenticationConverter implements OpaqueTokenAuthenticationConverter { + + @Override + public Authentication convert(String introspectedToken, OAuth2AuthenticatedPrincipal authenticatedPrincipal) { + return new TestAuthentication(introspectedToken, Collections.emptyList()); + } + + } + } diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-OpaqueTokenAndAuthenticationConverter.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-OpaqueTokenAndAuthenticationConverter.xml new file mode 100644 index 00000000000..5a22b05c8a0 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-OpaqueTokenAndAuthenticationConverter.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + diff --git a/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc b/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc index 93e9addc4d7..c000883a372 100644 --- a/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc +++ b/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc @@ -1325,6 +1325,10 @@ The Client Id to use for client authentication against the provided `introspecti * **client-secret** The Client Secret to use for client authentication against the provided `introspection-uri`. +[[nsa-opaque-token-authentication-converter-ref]] +* **authentication-converter-ref** +Reference to an `OpaqueTokenAuthenticationConverter`. Responsible for converting successful introspection result into an `Authentication` instance. + [[nsa-relying-party-registrations]] == diff --git a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc index ae711c84b72..bf5cb7ea9a8 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc @@ -295,11 +295,13 @@ fun introspector(): OpaqueTokenIntrospector { ---- ==== -If the application doesn't expose a <> bean, then Spring Boot will expose the above default one. +If the application doesn't expose an <> bean, then Spring Boot will expose the above default one. And its configuration can be overridden using `introspectionUri()` and `introspectionClientCredentials()` or replaced using `introspector()`. -Or, if you're not using Spring Boot at all, then both of these components - the filter chain and a <> can be specified in XML. +If the application doesn't expose an `OpaqueTokenAuthenticationConverter` bean, then spring-security will build `BearerTokenAuthentication`. + +Or, if you're not using Spring Boot at all, then all of these components - the filter chain, an <> and an `OpaqueTokenAuthenticationConverter` can be specified in XML. The filter chain is specified like so: diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProvider.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProvider.java index d8ece9e7402..cb131efeca5 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProvider.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProvider.java @@ -17,7 +17,6 @@ package org.springframework.security.oauth2.server.resource.authentication; import java.time.Instant; -import java.util.Collection; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -25,6 +24,7 @@ import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; @@ -35,6 +35,7 @@ import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException; import org.springframework.security.oauth2.server.resource.introspection.BadOpaqueTokenException; import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionException; +import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenAuthenticationConverter; import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; import org.springframework.util.Assert; @@ -49,16 +50,21 @@ * opaque access token, returning its attributes set as part of the {@link Authentication} * statement. *

- * Scopes are translated into {@link GrantedAuthority}s according to the following - * algorithm: - *

    - *
  1. If there is a "scope" attribute, then convert to a {@link Collection} of - * {@link String}s. - *
  2. Take the resulting {@link Collection} and prepend the "SCOPE_" keyword to each - * element, adding as {@link GrantedAuthority}s. - *
+ * This {@link ReactiveAuthenticationManager} is responsible for introspecting and + * verifying an opaque access token, returning its attributes set as part of the + * {@link Authentication} statement. + *

+ *

+ * {@link org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector} + * is responsible for retrieving token attributes from authorization-server. + *

+ *

+ * authenticationConverter is responsible for turning successful introspection into + * {@link Authentication} (which includes {@link GrantedAuthority}s mapping from token + * attributes or retrieving from an other source) * * @author Josh Cummings + * @author Jerome Wacongne <ch4mp@c4-soft.com> * @since 5.2 * @see AuthenticationProvider */ @@ -68,6 +74,8 @@ public final class OpaqueTokenAuthenticationProvider implements AuthenticationPr private final OpaqueTokenIntrospector introspector; + private OpaqueTokenAuthenticationConverter authenticationConverter; + /** * Creates a {@code OpaqueTokenAuthenticationProvider} with the provided parameters * @param introspector The {@link OpaqueTokenIntrospector} to use @@ -75,12 +83,20 @@ public final class OpaqueTokenAuthenticationProvider implements AuthenticationPr public OpaqueTokenAuthenticationProvider(OpaqueTokenIntrospector introspector) { Assert.notNull(introspector, "introspector cannot be null"); this.introspector = introspector; + this.setAuthenticationConverter(OpaqueTokenAuthenticationProvider::convert); } /** + *

* Introspect and validate the opaque * Bearer - * Token. + * Token and then delegates {@link Authentication} instantiation to + * {@link OpaqueTokenAuthenticationConverter}. + *

+ *

+ * If created Authentication is instance of {@link AbstractAuthenticationToken} and + * details are null, then introspection result details are used. + *

* @param authentication the authentication request object. * @return A successful authentication * @throws AuthenticationException if authentication failed for some reason @@ -92,8 +108,16 @@ public Authentication authenticate(Authentication authentication) throws Authent } BearerTokenAuthenticationToken bearer = (BearerTokenAuthenticationToken) authentication; OAuth2AuthenticatedPrincipal principal = getOAuth2AuthenticatedPrincipal(bearer); - AbstractAuthenticationToken result = convert(principal, bearer.getToken()); - result.setDetails(bearer.getDetails()); + Authentication result = this.authenticationConverter.convert(bearer.getToken(), principal); + if (result == null) { + return null; + } + if (AbstractAuthenticationToken.class.isAssignableFrom(result.getClass())) { + final AbstractAuthenticationToken auth = (AbstractAuthenticationToken) result; + if (auth.getDetails() == null) { + auth.setDetails(bearer.getDetails()); + } + } this.logger.debug("Authenticated token"); return result; } @@ -116,11 +140,32 @@ public boolean supports(Class authentication) { return BearerTokenAuthenticationToken.class.isAssignableFrom(authentication); } - private AbstractAuthenticationToken convert(OAuth2AuthenticatedPrincipal principal, String token) { - Instant iat = principal.getAttribute(OAuth2TokenIntrospectionClaimNames.IAT); - Instant exp = principal.getAttribute(OAuth2TokenIntrospectionClaimNames.EXP); - OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, token, iat, exp); - return new BearerTokenAuthentication(principal, accessToken, principal.getAuthorities()); + /** + * Default {@link OpaqueTokenAuthenticationConverter}. + * @param introspectedToken the bearer sring that was successfuly introspected + * @param authenticatedPrincipal the successful introspection output + * @returna {@link BearerTokenAuthentication} + */ + static BearerTokenAuthentication convert(String introspectedToken, + OAuth2AuthenticatedPrincipal authenticatedPrincipal) { + Instant iat = authenticatedPrincipal.getAttribute(OAuth2TokenIntrospectionClaimNames.IAT); + Instant exp = authenticatedPrincipal.getAttribute(OAuth2TokenIntrospectionClaimNames.EXP); + OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, introspectedToken, + iat, exp); + return new BearerTokenAuthentication(authenticatedPrincipal, accessToken, + authenticatedPrincipal.getAuthorities()); + } + + /** + * Provide with a custom bean to turn successful introspection result into an + * {@link Authentication} instance of your choice. By default, + * {@link BearerTokenAuthentication} will be built. + * @param authenticationConverter the converter to use + * @since 5.8 + */ + public void setAuthenticationConverter(OpaqueTokenAuthenticationConverter authenticationConverter) { + Assert.notNull(authenticationConverter, "authenticationConverter cannot be null"); + this.authenticationConverter = authenticationConverter; } } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenReactiveAuthenticationManager.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenReactiveAuthenticationManager.java index 79c271e9232..9fa985e3fb8 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenReactiveAuthenticationManager.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenReactiveAuthenticationManager.java @@ -16,28 +16,27 @@ package org.springframework.security.oauth2.server.resource.authentication; -import java.time.Instant; -import java.util.Collection; - import reactor.core.publisher.Mono; +import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.oauth2.core.OAuth2AccessToken; -import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames; +import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException; import org.springframework.security.oauth2.server.resource.introspection.BadOpaqueTokenException; import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionException; +import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenAuthenticationConverter; +import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenAuthenticationConverter; import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector; import org.springframework.util.Assert; /** * An {@link ReactiveAuthenticationManager} implementation for opaque - * Bearer + * Bearer * Tokens, using an * OAuth 2.0 Introspection * Endpoint to check the token's validity and reveal its attributes. @@ -46,16 +45,17 @@ * verifying an opaque access token, returning its attributes set as part of the * {@link Authentication} statement. *

- * Scopes are translated into {@link GrantedAuthority}s according to the following - * algorithm: - *

    - *
  1. If there is a "scope" attribute, then convert to a {@link Collection} of - * {@link String}s. - *
  2. Take the resulting {@link Collection} and prepend the "SCOPE_" keyword to each - * element, adding as {@link GrantedAuthority}s. - *
+ *

+ * {@link ReactiveOpaqueTokenIntrospector} is responsible for retrieving token attributes + * from authorization-server. + *

+ *

+ * authenticationConverter is responsible for turning successful introspection into + * {@link Authentication} (which includes {@link GrantedAuthority}s mapping from token + * attributes or retrieving from another source) * * @author Josh Cummings + * @author Jerome Wacongne <ch4mp@c4-soft.com> * @since 5.2 * @see ReactiveAuthenticationManager */ @@ -63,6 +63,8 @@ public class OpaqueTokenReactiveAuthenticationManager implements ReactiveAuthent private final ReactiveOpaqueTokenIntrospector introspector; + private ReactiveOpaqueTokenAuthenticationConverter authenticationConverter; + /** * Creates a {@code OpaqueTokenReactiveAuthenticationManager} with the provided * parameters @@ -71,8 +73,23 @@ public class OpaqueTokenReactiveAuthenticationManager implements ReactiveAuthent public OpaqueTokenReactiveAuthenticationManager(ReactiveOpaqueTokenIntrospector introspector) { Assert.notNull(introspector, "introspector cannot be null"); this.introspector = introspector; + this.setAuthenticationConverter(OpaqueTokenReactiveAuthenticationManager::convert); } + /** + *

+ * Introspect and validate the opaque + * Bearer + * Token and then delegates {@link Authentication} instantiation to + * {@link OpaqueTokenAuthenticationConverter}. + *

+ *

+ * If created Authentication is instance of {@link AbstractAuthenticationToken} and + * details are null, then introspection result details are used. + *

+ * @param authentication the authentication request object. + * @return A successful authentication + */ @Override public Mono authenticate(Authentication authentication) { // @formatter:off @@ -80,21 +97,14 @@ public Mono authenticate(Authentication authentication) { .filter(BearerTokenAuthenticationToken.class::isInstance) .cast(BearerTokenAuthenticationToken.class) .map(BearerTokenAuthenticationToken::getToken) - .flatMap(this::authenticate) - .cast(Authentication.class); + .flatMap(this::authenticate); // @formatter:on } - private Mono authenticate(String token) { + private Mono authenticate(String token) { // @formatter:off return this.introspector.introspect(token) - .map((principal) -> { - Instant iat = principal.getAttribute(OAuth2TokenIntrospectionClaimNames.IAT); - Instant exp = principal.getAttribute(OAuth2TokenIntrospectionClaimNames.EXP); - // construct token - OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, token, iat, exp); - return new BearerTokenAuthentication(principal, accessToken, principal.getAuthorities()); - }) + .flatMap((principal) -> this.authenticationConverter.convert(token, principal)) .onErrorMap(OAuth2IntrospectionException.class, this::onError); // @formatter:on } @@ -106,4 +116,27 @@ private AuthenticationException onError(OAuth2IntrospectionException ex) { return new AuthenticationServiceException(ex.getMessage(), ex); } + /** + * Default reactive {@link OpaqueTokenAuthenticationConverter}. + * @param introspectedToken the bearer sring that was successfuly introspected + * @param authenticatedPrincipal the successful introspection output + * @returna an async wrapper of default {@link OpaqueTokenAuthenticationConverter} + * result + */ + static Mono convert(String introspectedToken, OAuth2AuthenticatedPrincipal authenticatedPrincipal) { + return Mono.just(OpaqueTokenAuthenticationProvider.convert(introspectedToken, authenticatedPrincipal)); + } + + /** + * Provide with a custom bean to turn successful introspection result into an + * {@link Authentication} instance of your choice. By default, + * {@link BearerTokenAuthentication} will be built. + * @param authenticationConverter the converter to use + * @since 5.8 + */ + public void setAuthenticationConverter(ReactiveOpaqueTokenAuthenticationConverter authenticationConverter) { + Assert.notNull(authenticationConverter, "authenticationConverter cannot be null"); + this.authenticationConverter = authenticationConverter; + } + } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/OpaqueTokenAuthenticationConverter.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/OpaqueTokenAuthenticationConverter.java new file mode 100644 index 00000000000..8ece04d322b --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/OpaqueTokenAuthenticationConverter.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2022 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.introspection; + +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; + +/** + * Turn successful introspection result into an Authentication instance + * + * @author Jerome Wacongne <ch4mp@c4-soft.com> + * @since 5.8 + */ +@FunctionalInterface +public interface OpaqueTokenAuthenticationConverter { + + Authentication convert(String introspectedToken, OAuth2AuthenticatedPrincipal authenticatedPrincipal); + +} diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/ReactiveOpaqueTokenAuthenticationConverter.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/ReactiveOpaqueTokenAuthenticationConverter.java new file mode 100644 index 00000000000..77c481cc40d --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/ReactiveOpaqueTokenAuthenticationConverter.java @@ -0,0 +1,35 @@ +/* + * 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.introspection; + +import reactor.core.publisher.Mono; + +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; + +/** + * Turn successful introspection result into an Authentication instance + * + * @author Jerome Wacongne <ch4mp@c4-soft.com> + * @since 5.8 + */ +@FunctionalInterface +public interface ReactiveOpaqueTokenAuthenticationConverter { + + Mono convert(String introspectedToken, OAuth2AuthenticatedPrincipal authenticatedPrincipal); + +} diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProviderTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProviderTests.java index 69eaecb9b22..7d9ad2b9a1f 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProviderTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProviderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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.