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 *
* *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 @@
+ - * Scopes are translated into {@link GrantedAuthority}s according to the following - * algorithm: - *
+ *
+ * {@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: - *
+ * {@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