diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java index ed49c861..bcfccb63 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java @@ -33,7 +33,6 @@ import com.nimbusds.jose.jwk.source.JWKSource; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; -import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; @@ -44,14 +43,11 @@ import org.springframework.security.core.context.SecurityContext; import org.springframework.security.oauth2.core.OAuth2Token; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; -import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; -import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationProvider; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.config.ProviderSettings; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; import org.springframework.security.oauth2.server.authorization.web.NimbusJwkSetEndpointFilter; import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationServerMetadataEndpointFilter; -import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenIntrospectionEndpointFilter; -import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenRevocationEndpointFilter; import org.springframework.security.oauth2.server.authorization.web.ProviderContextFilter; import org.springframework.security.web.authentication.HttpStatusEntryPoint; import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter; @@ -78,13 +74,12 @@ import org.springframework.util.Assert; * @see OAuth2ClientAuthenticationConfigurer * @see OAuth2AuthorizationEndpointConfigurer * @see OAuth2TokenEndpointConfigurer + * @see OAuth2TokenIntrospectionEndpointConfigurer * @see OAuth2TokenRevocationEndpointConfigurer * @see OidcConfigurer * @see RegisteredClientRepository * @see OAuth2AuthorizationService * @see OAuth2AuthorizationConsentService - * @see OAuth2TokenIntrospectionEndpointFilter - * @see OAuth2TokenRevocationEndpointFilter * @see NimbusJwkSetEndpointFilter * @see OAuth2AuthorizationServerMetadataEndpointFilter */ @@ -92,15 +87,14 @@ public final class OAuth2AuthorizationServerConfigurer, B> { private final Map, AbstractOAuth2Configurer> configurers = createConfigurers(); - private RequestMatcher tokenIntrospectionEndpointMatcher; private RequestMatcher jwkSetEndpointMatcher; private RequestMatcher authorizationServerMetadataEndpointMatcher; private final RequestMatcher endpointsMatcher = (request) -> getRequestMatcher(OAuth2AuthorizationEndpointConfigurer.class).matches(request) || getRequestMatcher(OAuth2TokenEndpointConfigurer.class).matches(request) || + getRequestMatcher(OAuth2TokenIntrospectionEndpointConfigurer.class).matches(request) || getRequestMatcher(OAuth2TokenRevocationEndpointConfigurer.class).matches(request) || getRequestMatcher(OidcConfigurer.class).matches(request) || - getRequestMatcher(OAuth2TokenIntrospectionConfigurer.class).matches(request) || this.jwkSetEndpointMatcher.matches(request) || this.authorizationServerMetadataEndpointMatcher.matches(request); @@ -198,6 +192,18 @@ public final class OAuth2AuthorizationServerConfigurer tokenIntrospectionEndpoint(Customizer tokenIntrospectionEndpointCustomizer) { + tokenIntrospectionEndpointCustomizer.customize(getConfigurer(OAuth2TokenIntrospectionEndpointConfigurer.class)); + return this; + } + /** * Configures the OAuth 2.0 Token Revocation Endpoint. * @@ -221,19 +227,6 @@ public final class OAuth2AuthorizationServerConfigurer tokenIntrospectionEndpoint(Customizer tokenIntrospectionEndpointCustomizer) { - tokenIntrospectionEndpointCustomizer.customize(getConfigurer(OAuth2TokenIntrospectionConfigurer.class)); - return this; - } - - /** * Returns a {@link RequestMatcher} for the authorization server endpoints. * @@ -251,20 +244,14 @@ public final class OAuth2AuthorizationServerConfigurer configurer.init(builder)); - OAuth2TokenIntrospectionAuthenticationProvider tokenIntrospectionAuthenticationProvider = - new OAuth2TokenIntrospectionAuthenticationProvider( - OAuth2ConfigurerUtils.getRegisteredClientRepository(builder), - OAuth2ConfigurerUtils.getAuthorizationService(builder)); - builder.authenticationProvider(postProcess(tokenIntrospectionAuthenticationProvider)); - ExceptionHandlingConfigurer exceptionHandling = builder.getConfigurer(ExceptionHandlingConfigurer.class); if (exceptionHandling != null) { exceptionHandling.defaultAuthenticationEntryPointFor( new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED), new OrRequestMatcher( getRequestMatcher(OAuth2TokenEndpointConfigurer.class), - getRequestMatcher(OAuth2TokenRevocationEndpointConfigurer.class), - this.tokenIntrospectionEndpointMatcher) + getRequestMatcher(OAuth2TokenIntrospectionEndpointConfigurer.class), + getRequestMatcher(OAuth2TokenRevocationEndpointConfigurer.class)) ); } @@ -362,8 +349,8 @@ public final class OAuth2AuthorizationServerConfigurer requestMatchers = new ArrayList<>(); requestMatchers.add(getRequestMatcher(OAuth2TokenEndpointConfigurer.class)); + requestMatchers.add(getRequestMatcher(OAuth2TokenIntrospectionEndpointConfigurer.class)); requestMatchers.add(getRequestMatcher(OAuth2TokenRevocationEndpointConfigurer.class)); - requestMatchers.add(OAuth2AuthorizationServerConfigurer.this.tokenIntrospectionEndpointMatcher); return new OrRequestMatcher(requestMatchers); } @@ -395,7 +382,6 @@ public final class OAuth2AuthorizationServerConfigurer configurer.configure(builder)); ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder); - AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class); ProviderContextFilter providerContextFilter = new ProviderContextFilter(providerSettings); builder.addFilterAfter(postProcess(providerContextFilter), SecurityContextPersistenceFilter.class); @@ -417,9 +403,9 @@ public final class OAuth2AuthorizationServerConfigurer authenticationProviders = new LinkedList<>(); - private AuthenticationSuccessHandler tokenIntrospectionResponseHandler; + private AuthenticationSuccessHandler introspectionResponseHandler; private AuthenticationFailureHandler errorResponseHandler; /** * Restrict for internal use only. */ - OAuth2TokenIntrospectionConfigurer(ObjectPostProcessor objectPostProcessor) { + OAuth2TokenIntrospectionEndpointConfigurer(ObjectPostProcessor objectPostProcessor) { super(objectPostProcessor); } /** - * Sets the {@link AuthenticationConverter} used when attempting to extract an Access Token Request from {@link HttpServletRequest} - * to an instance of {@link OAuth2AuthorizationGrantAuthenticationToken} used for authenticating the authorization grant. + * Sets the {@link AuthenticationConverter} used when attempting to extract an Introspection Request from {@link HttpServletRequest} + * to an instance of {@link OAuth2TokenIntrospectionAuthenticationToken} used for authenticating the request. * - * @param accessTokenRequestConverter the {@link AuthenticationConverter} used when attempting to extract an Access Token Request from {@link HttpServletRequest} - * @return the {@link OAuth2TokenIntrospectionConfigurer} for further configuration + * @param introspectionRequestConverter the {@link AuthenticationConverter} used when attempting to extract an Introspection Request from {@link HttpServletRequest} + * @return the {@link OAuth2TokenIntrospectionEndpointConfigurer} for further configuration */ - public OAuth2TokenIntrospectionConfigurer accessTokenRequestConverter(AuthenticationConverter accessTokenRequestConverter) { - this.accessTokenRequestConverter = accessTokenRequestConverter; + public OAuth2TokenIntrospectionEndpointConfigurer introspectionRequestConverter(AuthenticationConverter introspectionRequestConverter) { + this.introspectionRequestConverter = introspectionRequestConverter; return this; } @@ -76,9 +79,9 @@ public class OAuth2TokenIntrospectionConfigurer extends AbstractOAuth2Configurer * Adds an {@link AuthenticationProvider} used for authenticating a type of {@link OAuth2TokenIntrospectionAuthenticationToken}. * * @param authenticationProvider an {@link AuthenticationProvider} used for authenticating a type of {@link OAuth2TokenIntrospectionAuthenticationToken} - * @return the {@link OAuth2TokenIntrospectionConfigurer} for further configuration + * @return the {@link OAuth2TokenIntrospectionEndpointConfigurer} for further configuration */ - public OAuth2TokenIntrospectionConfigurer authenticationProvider(AuthenticationProvider authenticationProvider) { + public OAuth2TokenIntrospectionEndpointConfigurer authenticationProvider(AuthenticationProvider authenticationProvider) { Assert.notNull(authenticationProvider, "authenticationProvider cannot be null"); this.authenticationProviders.add(authenticationProvider); return this; @@ -87,22 +90,22 @@ public class OAuth2TokenIntrospectionConfigurer extends AbstractOAuth2Configurer /** * Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2TokenIntrospectionAuthenticationToken}. * - * @param tokenIntrospectionResponseHandler the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2TokenIntrospectionAuthenticationToken} - * @return the {@link OAuth2TokenEndpointConfigurer} for further configuration + * @param introspectionResponseHandler the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2TokenIntrospectionAuthenticationToken} + * @return the {@link OAuth2TokenIntrospectionEndpointConfigurer} for further configuration */ - public OAuth2TokenIntrospectionConfigurer accessTokenResponseHandler(AuthenticationSuccessHandler tokenIntrospectionResponseHandler) { - this.tokenIntrospectionResponseHandler = tokenIntrospectionResponseHandler; + public OAuth2TokenIntrospectionEndpointConfigurer introspectionResponseHandler(AuthenticationSuccessHandler introspectionResponseHandler) { + this.introspectionResponseHandler = introspectionResponseHandler; return this; } /** - * Sets the {@link AuthenticationFailureHandler} used for handling an {@link org.springframework.security.oauth2.core.OAuth2AuthenticationException} + * Sets the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException} * and returning the {@link OAuth2Error Error Response}. * - * @param errorResponseHandler the {@link AuthenticationFailureHandler} used for handling an {@link org.springframework.security.oauth2.core.OAuth2AuthenticationException} - * @return the {@link OAuth2TokenIntrospectionConfigurer} for further configuration + * @param errorResponseHandler the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException} + * @return the {@link OAuth2TokenIntrospectionEndpointConfigurer} for further configuration */ - public OAuth2TokenIntrospectionConfigurer errorResponseHandler(AuthenticationFailureHandler errorResponseHandler) { + public OAuth2TokenIntrospectionEndpointConfigurer errorResponseHandler(AuthenticationFailureHandler errorResponseHandler) { this.errorResponseHandler = errorResponseHandler; return this; } @@ -113,8 +116,12 @@ public class OAuth2TokenIntrospectionConfigurer extends AbstractOAuth2Configurer this.requestMatcher = new AntPathRequestMatcher( providerSettings.getTokenIntrospectionEndpoint(), HttpMethod.POST.name()); - List authenticationProviders = this.authenticationProviders.isEmpty() ? createDefaultAuthenticationProviders(builder) : this.authenticationProviders; - authenticationProviders.forEach(authenticationProvider -> builder.authenticationProvider(postProcess(authenticationProvider))); + List authenticationProviders = + !this.authenticationProviders.isEmpty() ? + this.authenticationProviders : + createDefaultAuthenticationProviders(builder); + authenticationProviders.forEach(authenticationProvider -> + builder.authenticationProvider(postProcess(authenticationProvider))); } @Override @@ -123,20 +130,17 @@ public class OAuth2TokenIntrospectionConfigurer extends AbstractOAuth2Configurer ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder); OAuth2TokenIntrospectionEndpointFilter introspectionEndpointFilter = - new OAuth2TokenIntrospectionEndpointFilter(authenticationManager, providerSettings.getTokenIntrospectionEndpoint()); - - if (accessTokenRequestConverter != null) { - introspectionEndpointFilter.setAuthenticationConverter(this.accessTokenRequestConverter); + new OAuth2TokenIntrospectionEndpointFilter( + authenticationManager, providerSettings.getTokenIntrospectionEndpoint()); + if (this.introspectionRequestConverter != null) { + introspectionEndpointFilter.setAuthenticationConverter(this.introspectionRequestConverter); } - - if (this.tokenIntrospectionResponseHandler != null) { - introspectionEndpointFilter.setAuthenticationSuccessHandler(this.tokenIntrospectionResponseHandler); + if (this.introspectionResponseHandler != null) { + introspectionEndpointFilter.setAuthenticationSuccessHandler(this.introspectionResponseHandler); } - if (this.errorResponseHandler != null) { introspectionEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler); } - builder.addFilterAfter(postProcess(introspectionEndpointFilter), FilterSecurityInterceptor.class); } @@ -148,15 +152,13 @@ public class OAuth2TokenIntrospectionConfigurer extends AbstractOAuth2Configurer private > List createDefaultAuthenticationProviders(B builder) { List authenticationProviders = new ArrayList<>(); - OAuth2TokenIntrospectionAuthenticationProvider tokenIntrospectionAuthenticationProvider - = new OAuth2TokenIntrospectionAuthenticationProvider( - OAuth2ConfigurerUtils.getRegisteredClientRepository(builder), - OAuth2ConfigurerUtils.getAuthorizationService(builder) - ); - + OAuth2TokenIntrospectionAuthenticationProvider tokenIntrospectionAuthenticationProvider = + new OAuth2TokenIntrospectionAuthenticationProvider( + OAuth2ConfigurerUtils.getRegisteredClientRepository(builder), + OAuth2ConfigurerUtils.getAuthorizationService(builder)); authenticationProviders.add(tokenIntrospectionAuthenticationProvider); return authenticationProviders; - } + } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenIntrospection.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenIntrospection.java index c236179b..4ad72f06 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenIntrospection.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenIntrospection.java @@ -34,7 +34,6 @@ import org.springframework.util.Assert; * * @author Gerardo Roza * @author Joe Grandja - * @author Gaurav Tiwari * @since 0.1.1 * @see OAuth2TokenIntrospectionClaimAccessor * @see Section 2.2 Introspection Response @@ -258,28 +257,6 @@ public final class OAuth2TokenIntrospection implements OAuth2TokenIntrospectionC return this; } - /** - * Adds custom claims if corresponding keys don't exist in present set of claims. - * - * @since 0.2.3 - * @param presentedClaims map of all claims - * @return the {@link Builder} for further configuration - */ - public Builder withCustomClaims(Map presentedClaims) { - - if (presentedClaims != null && !presentedClaims.isEmpty()) { - - presentedClaims.keySet().forEach(key -> { - if (!this.claims.containsKey(key)) { - this.claim(key, presentedClaims.get(key)); - } - }); - - } - - return this; - } - /** * Provides access to every {@link #claim(String, Object)} declared so far with * the possibility to add, replace, or remove. @@ -335,6 +312,15 @@ public final class OAuth2TokenIntrospection implements OAuth2TokenIntrospectionC ((List) this.claims.get(name)).add(value); } + @SuppressWarnings("unchecked") + private void acceptClaimValues(String name, Consumer> valuesConsumer) { + Assert.hasText(name, "name cannot be empty"); + Assert.notNull(valuesConsumer, "valuesConsumer cannot be null"); + this.claims.computeIfAbsent(name, k -> new LinkedList()); + List values = (List) this.claims.get(name); + valuesConsumer.accept(values); + } + private static void validateURL(Object url, String errorMessage) { if (URL.class.isAssignableFrom(url.getClass())) { return; @@ -346,14 +332,5 @@ public final class OAuth2TokenIntrospection implements OAuth2TokenIntrospectionC throw new IllegalArgumentException(errorMessage, ex); } } - - @SuppressWarnings("unchecked") - private void acceptClaimValues(String name, Consumer> valuesConsumer) { - Assert.hasText(name, "name cannot be empty"); - Assert.notNull(valuesConsumer, "valuesConsumer cannot be null"); - this.claims.computeIfAbsent(name, k -> new LinkedList()); - List values = (List) this.claims.get(name); - valuesConsumer.accept(values); - } } } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationProvider.java index bc7d651b..85f96385 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationProvider.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationProvider.java @@ -16,23 +16,25 @@ package org.springframework.security.oauth2.server.authorization.authentication; import java.net.URL; -import java.time.Instant; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import org.springframework.core.convert.TypeDescriptor; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.oauth2.core.AbstractOAuth2Token; import org.springframework.security.oauth2.core.OAuth2AccessToken; -import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimAccessor; import org.springframework.security.oauth2.core.OAuth2TokenIntrospection; +import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames; +import org.springframework.security.oauth2.core.converter.ClaimConversionService; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; import static org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient; @@ -41,7 +43,6 @@ import static org.springframework.security.oauth2.server.authorization.authentic * * @author Gerardo Roza * @author Joe Grandja - * @author Gaurav Tiwari * @since 0.1.1 * @see OAuth2TokenIntrospectionAuthenticationToken * @see RegisteredClientRepository @@ -49,6 +50,9 @@ import static org.springframework.security.oauth2.server.authorization.authentic * @see Section 2.1 Introspection Request */ public final class OAuth2TokenIntrospectionAuthenticationProvider implements AuthenticationProvider { + private static final TypeDescriptor OBJECT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Object.class); + private static final TypeDescriptor LIST_STRING_TYPE_DESCRIPTOR = + TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class)); private final RegisteredClientRepository registeredClientRepository; private final OAuth2AuthorizationService authorizationService; @@ -103,8 +107,15 @@ public final class OAuth2TokenIntrospectionAuthenticationProvider implements Aut private static OAuth2TokenIntrospection withActiveTokenClaims( OAuth2Authorization.Token authorizedToken, RegisteredClient authorizedClient) { - OAuth2TokenIntrospection.Builder tokenClaims = OAuth2TokenIntrospection.builder(true) - .clientId(authorizedClient.getClientId()); + OAuth2TokenIntrospection.Builder tokenClaims; + if (!CollectionUtils.isEmpty(authorizedToken.getClaims())) { + Map claims = convertClaimsIfNecessary(authorizedToken.getClaims()); + tokenClaims = OAuth2TokenIntrospection.withClaims(claims).active(true); + } else { + tokenClaims = OAuth2TokenIntrospection.builder(true); + } + + tokenClaims.clientId(authorizedClient.getClientId()); // TODO Set "username" @@ -118,34 +129,43 @@ public final class OAuth2TokenIntrospectionAuthenticationProvider implements Aut if (OAuth2AccessToken.class.isAssignableFrom(token.getClass())) { OAuth2AccessToken accessToken = (OAuth2AccessToken) token; - tokenClaims.scopes(scopes -> scopes.addAll(accessToken.getScopes())); tokenClaims.tokenType(accessToken.getTokenType().getValue()); + } + + return tokenClaims.build(); + } + + private static Map convertClaimsIfNecessary(Map claims) { + Map convertedClaims = new HashMap<>(claims); - if (!CollectionUtils.isEmpty(authorizedToken.getClaims())) { - OAuth2TokenClaimAccessor accessTokenClaims = authorizedToken::getClaims; - - Instant notBefore = accessTokenClaims.getNotBefore(); - if (notBefore != null) { - tokenClaims.notBefore(notBefore); - } - tokenClaims.subject(accessTokenClaims.getSubject()); - List audience = accessTokenClaims.getAudience(); - if (!CollectionUtils.isEmpty(audience)) { - tokenClaims.audiences(audiences -> audiences.addAll(audience)); - } - URL issuer = accessTokenClaims.getIssuer(); - if (issuer != null) { - tokenClaims.issuer(issuer.toExternalForm()); - } - String jti = accessTokenClaims.getId(); - if (StringUtils.hasText(jti)) { - tokenClaims.id(jti); - } + Object value = claims.get(OAuth2TokenIntrospectionClaimNames.ISS); + if (value != null && !(value instanceof URL)) { + URL convertedValue = ClaimConversionService.getSharedInstance() + .convert(value, URL.class); + if (convertedValue != null) { + convertedClaims.put(OAuth2TokenIntrospectionClaimNames.ISS, convertedValue); } } - tokenClaims.withCustomClaims(authorizedToken.getClaims()); + value = claims.get(OAuth2TokenIntrospectionClaimNames.SCOPE); + if (value != null && !(value instanceof List)) { + Object convertedValue = ClaimConversionService.getSharedInstance() + .convert(value, OBJECT_TYPE_DESCRIPTOR, LIST_STRING_TYPE_DESCRIPTOR); + if (convertedValue != null) { + convertedClaims.put(OAuth2TokenIntrospectionClaimNames.SCOPE, convertedValue); + } + } - return tokenClaims.build(); + value = claims.get(OAuth2TokenIntrospectionClaimNames.AUD); + if (value != null && !(value instanceof List)) { + Object convertedValue = ClaimConversionService.getSharedInstance() + .convert(value, OBJECT_TYPE_DESCRIPTOR, LIST_STRING_TYPE_DESCRIPTOR); + if (convertedValue != null) { + convertedClaims.put(OAuth2TokenIntrospectionClaimNames.AUD, convertedValue); + } + } + + return convertedClaims; } + } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenIntrospectionEndpointFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenIntrospectionEndpointFilter.java index 59d55960..c13dbe6b 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenIntrospectionEndpointFilter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenIntrospectionEndpointFilter.java @@ -70,12 +70,12 @@ public final class OAuth2TokenIntrospectionEndpointFilter extends OncePerRequest private final AuthenticationManager authenticationManager; private final RequestMatcher tokenIntrospectionEndpointMatcher; - private AuthenticationConverter tokenIntrospectionAuthenticationConverter = + private AuthenticationConverter authenticationConverter = new DefaultTokenIntrospectionAuthenticationConverter(); private final HttpMessageConverter tokenIntrospectionHttpResponseConverter = new OAuth2TokenIntrospectionHttpMessageConverter(); private final HttpMessageConverter errorHttpResponseConverter = new OAuth2ErrorHttpMessageConverter(); - private AuthenticationSuccessHandler authenticationSuccessHandler = this::sendTokenIntrospectionResponse;; + private AuthenticationSuccessHandler authenticationSuccessHandler = this::sendIntrospectionResponse; private AuthenticationFailureHandler authenticationFailureHandler = this::sendErrorResponse; /** @@ -112,14 +112,10 @@ public final class OAuth2TokenIntrospectionEndpointFilter extends OncePerRequest } try { - OAuth2TokenIntrospectionAuthenticationToken tokenIntrospectionAuthentication = - (OAuth2TokenIntrospectionAuthenticationToken) this.tokenIntrospectionAuthenticationConverter.convert(request); - - OAuth2TokenIntrospectionAuthenticationToken tokenIntrospectionAuthenticationResult = - (OAuth2TokenIntrospectionAuthenticationToken) this.authenticationManager.authenticate(tokenIntrospectionAuthentication); - + Authentication tokenIntrospectionAuthentication = this.authenticationConverter.convert(request); + Authentication tokenIntrospectionAuthenticationResult = + this.authenticationManager.authenticate(tokenIntrospectionAuthentication); this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, tokenIntrospectionAuthenticationResult); - } catch (OAuth2AuthenticationException ex) { SecurityContextHolder.clearContext(); this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex); @@ -127,50 +123,52 @@ public final class OAuth2TokenIntrospectionEndpointFilter extends OncePerRequest } /** - * Sets the {@link AuthenticationConverter} used when attempting to extract a Token Introspection Request from - * {@link HttpServletRequest} to an instance of {@link OAuth2TokenIntrospectionAuthenticationToken} used for authenticating the request. + * Sets the {@link AuthenticationConverter} used when attempting to extract an Introspection Request from {@link HttpServletRequest} + * to an instance of {@link OAuth2TokenIntrospectionAuthenticationToken} used for authenticating the request. * - * @param authenticationConverter the {@link AuthenticationConverter} used when attempting to extract a Token Introspection Request from {@link HttpServletRequest} + * @param authenticationConverter the {@link AuthenticationConverter} used when attempting to extract an Introspection Request from {@link HttpServletRequest} * @since 0.2.3 */ public void setAuthenticationConverter(AuthenticationConverter authenticationConverter) { - Assert.notNull(authenticationConverter, "authenticationConverter cannot be null."); - this.tokenIntrospectionAuthenticationConverter = authenticationConverter; + Assert.notNull(authenticationConverter, "authenticationConverter cannot be null"); + this.authenticationConverter = authenticationConverter; } /** - * Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2TokenIntrospectionAuthenticationToken} + * Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2TokenIntrospectionAuthenticationToken}. * * @param authenticationSuccessHandler the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2TokenIntrospectionAuthenticationToken} * @since 0.2.3 */ public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler authenticationSuccessHandler) { - Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null."); + Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null"); this.authenticationSuccessHandler = authenticationSuccessHandler; } /** - * Sets the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException} and - * returning {@link OAuth2Error Error Resonse}. + * Sets the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException} + * and returning the {@link OAuth2Error Error Resonse}. * - * @param authenticationFailureHandler the {@link .AuthenticationFailureHandler} used for handling {@link OAuth2AuthenticationException} + * @param authenticationFailureHandler the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException} * @since 0.2.3 */ public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) { - Assert.notNull(authenticationFailureHandler, "authenticationFailureHandler cannot be null."); + Assert.notNull(authenticationFailureHandler, "authenticationFailureHandler cannot be null"); this.authenticationFailureHandler = authenticationFailureHandler; } - private void sendTokenIntrospectionResponse(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { - - OAuth2TokenIntrospectionAuthenticationToken tokenIntrospectionAuthenticationResult = (OAuth2TokenIntrospectionAuthenticationToken) authentication; - OAuth2TokenIntrospection tokenClaims = tokenIntrospectionAuthenticationResult.getTokenClaims(); + private void sendIntrospectionResponse(HttpServletRequest request, HttpServletResponse response, + Authentication authentication) throws IOException { + OAuth2TokenIntrospectionAuthenticationToken tokenIntrospectionAuthentication = + (OAuth2TokenIntrospectionAuthenticationToken) authentication; + OAuth2TokenIntrospection tokenClaims = tokenIntrospectionAuthentication.getTokenClaims(); ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response); this.tokenIntrospectionHttpResponseConverter.write(tokenClaims, null, httpResponse); } - private void sendErrorResponse(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException { + private void sendErrorResponse(HttpServletRequest request, HttpServletResponse response, + AuthenticationException exception) throws IOException { OAuth2Error error = ((OAuth2AuthenticationException) exception).getError(); ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response); httpResponse.setStatusCode(HttpStatus.BAD_REQUEST); @@ -179,7 +177,7 @@ public final class OAuth2TokenIntrospectionEndpointFilter extends OncePerRequest private static void throwError(String errorCode, String parameterName) { OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Token Introspection Parameter: " + parameterName, - "https://tools.ietf.org/html/rfc7662#section-2.1"); + "https://datatracker.ietf.org/doc/html/rfc7662#section-2.1"); throw new OAuth2AuthenticationException(error); } @@ -217,5 +215,7 @@ public final class OAuth2TokenIntrospectionEndpointFilter extends OncePerRequest return new OAuth2TokenIntrospectionAuthenticationToken( token, clientPrincipal, tokenTypeHint, additionalParameters); } + } + } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2TokenIntrospectionTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2TokenIntrospectionTests.java index ef107a92..029a33bb 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2TokenIntrospectionTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2TokenIntrospectionTests.java @@ -26,9 +26,6 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; -import com.nimbusds.jose.jwk.JWKSet; -import com.nimbusds.jose.jwk.source.JWKSource; -import com.nimbusds.jose.proc.SecurityContext; import org.junit.After; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -49,34 +46,38 @@ import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; import org.springframework.mock.http.client.MockClientHttpResponse; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.core.Authentication; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.core.AbstractOAuth2Token; import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthorizationCode; import org.springframework.security.oauth2.core.OAuth2RefreshToken; -import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsSet; import org.springframework.security.oauth2.core.OAuth2TokenFormat; import org.springframework.security.oauth2.core.OAuth2TokenIntrospection; +import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames; import org.springframework.security.oauth2.core.OAuth2TokenType; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter; import org.springframework.security.oauth2.core.http.converter.OAuth2TokenIntrospectionHttpMessageConverter; -import org.springframework.security.oauth2.jose.TestJwks; import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService; import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; -import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsContext; import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer; import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationToken; import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository.RegisteredClientParametersMapper; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; @@ -85,14 +86,24 @@ import org.springframework.security.oauth2.server.authorization.client.TestRegis import org.springframework.security.oauth2.server.authorization.config.ProviderSettings; import org.springframework.security.oauth2.server.authorization.config.TokenSettings; import org.springframework.security.oauth2.server.authorization.jackson2.TestingAuthenticationTokenMixin; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsSet; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.AuthenticationConverter; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -104,9 +115,12 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. */ public class OAuth2TokenIntrospectionTests { private static EmbeddedDatabase db; - private static JWKSource jwkSource; private static ProviderSettings providerSettings; private static OAuth2TokenCustomizer accessTokenCustomizer; + private static AuthenticationConverter authenticationConverter; + private static AuthenticationProvider authenticationProvider; + private static AuthenticationSuccessHandler authenticationSuccessHandler; + private static AuthenticationFailureHandler authenticationFailureHandler; private static final HttpMessageConverter tokenIntrospectionHttpResponseConverter = new OAuth2TokenIntrospectionHttpMessageConverter(); private static final HttpMessageConverter accessTokenHttpResponseConverter = @@ -129,9 +143,11 @@ public class OAuth2TokenIntrospectionTests { @BeforeClass public static void init() { - JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK); - jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet); providerSettings = ProviderSettings.builder().tokenIntrospectionEndpoint("/test/introspect").build(); + authenticationConverter = mock(AuthenticationConverter.class); + authenticationProvider = mock(AuthenticationProvider.class); + authenticationSuccessHandler = mock(AuthenticationSuccessHandler.class); + authenticationFailureHandler = mock(AuthenticationFailureHandler.class); accessTokenCustomizer = mock(OAuth2TokenCustomizer.class); db = new EmbeddedDatabaseBuilder() .generateUniqueName(true) @@ -175,6 +191,7 @@ public class OAuth2TokenIntrospectionTests { .issuedAt(issuedAt) .notBefore(issuedAt) .expiresAt(expiresAt) + .claim(OAuth2TokenIntrospectionClaimNames.SCOPE, accessToken.getScopes()) .id("id") .build(); // @formatter:on @@ -314,6 +331,43 @@ public class OAuth2TokenIntrospectionTests { assertThat(tokenIntrospectionResponse.getId()).isEqualTo(accessTokenClaims.getId()); } + @Test + public void requestWhenTokenIntrospectionEndpointCustomizedThenUsed() throws Exception { + this.spring.register(AuthorizationServerConfigurationCustomTokenIntrospectionEndpoint.class).autowire(); + + RegisteredClient introspectRegisteredClient = TestRegisteredClients.registeredClient2().build(); + this.registeredClientRepository.save(introspectRegisteredClient); + + RegisteredClient authorizedRegisteredClient = TestRegisteredClients.registeredClient().build(); + this.registeredClientRepository.save(authorizedRegisteredClient); + + OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(authorizedRegisteredClient).build(); + this.authorizationService.save(authorization); + + OAuth2AccessToken accessToken = authorization.getAccessToken().getToken(); + + Authentication clientPrincipal = new OAuth2ClientAuthenticationToken( + introspectRegisteredClient, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, introspectRegisteredClient.getClientSecret()); + OAuth2TokenIntrospectionAuthenticationToken tokenIntrospectionAuthentication = + new OAuth2TokenIntrospectionAuthenticationToken( + accessToken.getTokenValue(), clientPrincipal, null, null); + + when(authenticationConverter.convert(any())).thenReturn(tokenIntrospectionAuthentication); + when(authenticationProvider.supports(eq(OAuth2TokenIntrospectionAuthenticationToken.class))).thenReturn(true); + when(authenticationProvider.authenticate(any())).thenReturn(tokenIntrospectionAuthentication); + + // @formatter:off + this.mvc.perform(post(providerSettings.getTokenIntrospectionEndpoint()) + .params(getTokenIntrospectionRequestParameters(accessToken, OAuth2TokenType.ACCESS_TOKEN)) + .header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(introspectRegisteredClient))) + .andExpect(status().isOk()); + // @formatter:on + + verify(authenticationConverter).convert(any()); + verify(authenticationProvider).authenticate(eq(tokenIntrospectionAuthentication)); + verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), eq(tokenIntrospectionAuthentication)); + } + private static MultiValueMap getTokenIntrospectionRequestParameters(AbstractOAuth2Token token, OAuth2TokenType tokenType) { MultiValueMap parameters = new LinkedMultiValueMap<>(); @@ -420,4 +474,35 @@ public class OAuth2TokenIntrospectionTests { } } + + @EnableWebSecurity + static class AuthorizationServerConfigurationCustomTokenIntrospectionEndpoint extends AuthorizationServerConfiguration { + + // @formatter:off + @Bean + public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { + OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = + new OAuth2AuthorizationServerConfigurer<>(); + authorizationServerConfigurer + .tokenIntrospectionEndpoint(tokenIntrospectionEndpoint -> + tokenIntrospectionEndpoint + .introspectionRequestConverter(authenticationConverter) + .authenticationProvider(authenticationProvider) + .introspectionResponseHandler(authenticationSuccessHandler) + .errorResponseHandler(authenticationFailureHandler)); + RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher(); + + http + .requestMatcher(endpointsMatcher) + .authorizeRequests(authorizeRequests -> + authorizeRequests.anyRequest().authenticated() + ) + .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher)) + .apply(authorizationServerConfigurer); + return http.build(); + } + // @formatter:on + + } + } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationProviderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationProviderTests.java index aa44cf83..26f9dd91 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationProviderTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationProviderTests.java @@ -31,15 +31,16 @@ import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.OAuth2RefreshToken; -import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimNames; -import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsSet; import org.springframework.security.oauth2.core.OAuth2TokenIntrospection; +import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimNames; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsSet; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -54,7 +55,6 @@ import static org.mockito.Mockito.when; * * @author Gerardo Roza * @author Joe Grandja - * @author Gaurav Tiwari */ public class OAuth2TokenIntrospectionAuthenticationProviderTests { private RegisteredClientRepository registeredClientRepository; @@ -227,6 +227,8 @@ public class OAuth2TokenIntrospectionAuthenticationProviderTests { .notBefore(issuedAt) .expiresAt(expiresAt) .id("id") + .claim(OAuth2TokenIntrospectionClaimNames.SCOPE, accessToken.getScopes()) + .claim("custom-claim", "custom-value") .build(); // @formatter:on @@ -253,68 +255,14 @@ public class OAuth2TokenIntrospectionAuthenticationProviderTests { assertThat(tokenClaims.getClientId()).isEqualTo(authorizedClient.getClientId()); assertThat(tokenClaims.getIssuedAt()).isEqualTo(accessToken.getIssuedAt()); assertThat(tokenClaims.getExpiresAt()).isEqualTo(accessToken.getExpiresAt()); - assertThat(tokenClaims.getScopes()).containsExactlyInAnyOrderElementsOf(accessToken.getScopes()); assertThat(tokenClaims.getTokenType()).isEqualTo(accessToken.getTokenType().getValue()); assertThat(tokenClaims.getNotBefore()).isEqualTo(claimsSet.getNotBefore()); assertThat(tokenClaims.getSubject()).isEqualTo(claimsSet.getSubject()); assertThat(tokenClaims.getAudience()).containsExactlyInAnyOrderElementsOf(claimsSet.getAudience()); assertThat(tokenClaims.getIssuer()).isEqualTo(claimsSet.getIssuer()); assertThat(tokenClaims.getId()).isEqualTo(claimsSet.getId()); - } - - @Test - public void authenticateWhenValidAccessTokenAndCustomClaimThenActiveAndCustomClaimInResponse() { - RegisteredClient authorizedClient = TestRegisteredClients.registeredClient().build(); - Instant issuedAt = Instant.now(); - Instant expiresAt = issuedAt.plus(Duration.ofHours(1)); - OAuth2AccessToken accessToken = new OAuth2AccessToken( - OAuth2AccessToken.TokenType.BEARER, "access-token", issuedAt, expiresAt, - new HashSet<>(Arrays.asList("scope1", "scope2"))); - - // @formatter:off - OAuth2TokenClaimsSet claimsSet = OAuth2TokenClaimsSet.builder() - .issuer("https://provider.com") - .subject("subject") - .audience(Collections.singletonList(authorizedClient.getClientId())) - .issuedAt(issuedAt) - .notBefore(issuedAt) - .expiresAt(expiresAt) - .claim("custom-claim", "custom-claim-value") - .id("id") - .build(); - // @formatter:on - - OAuth2Authorization authorization = TestOAuth2Authorizations - .authorization(authorizedClient, accessToken, claimsSet.getClaims()) - .build(); - when(this.authorizationService.findByToken(eq(accessToken.getTokenValue()), isNull())) - .thenReturn(authorization); - when(this.registeredClientRepository.findById(eq(authorizedClient.getId()))).thenReturn(authorizedClient); - RegisteredClient registeredClient = TestRegisteredClients.registeredClient2().build(); - OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken( - registeredClient, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret()); - - OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken( - accessToken.getTokenValue(), clientPrincipal, null, null); - OAuth2TokenIntrospectionAuthenticationToken authenticationResult = - (OAuth2TokenIntrospectionAuthenticationToken) this.authenticationProvider.authenticate(authentication); - - verify(this.authorizationService).findByToken(eq(authentication.getToken()), isNull()); - verify(this.registeredClientRepository).findById(eq(authorizedClient.getId())); - assertThat(authenticationResult.isAuthenticated()).isTrue(); - OAuth2TokenIntrospection tokenClaims = authenticationResult.getTokenClaims(); - assertThat(tokenClaims.isActive()).isTrue(); - assertThat(tokenClaims.getClientId()).isEqualTo(authorizedClient.getClientId()); - assertThat(tokenClaims.getIssuedAt()).isEqualTo(accessToken.getIssuedAt()); - assertThat(tokenClaims.getExpiresAt()).isEqualTo(accessToken.getExpiresAt()); assertThat(tokenClaims.getScopes()).containsExactlyInAnyOrderElementsOf(accessToken.getScopes()); - assertThat(tokenClaims.getTokenType()).isEqualTo(accessToken.getTokenType().getValue()); - assertThat(tokenClaims.getNotBefore()).isEqualTo(claimsSet.getNotBefore()); - assertThat(tokenClaims.getSubject()).isEqualTo(claimsSet.getSubject()); - assertThat(tokenClaims.getAudience()).containsExactlyInAnyOrderElementsOf(claimsSet.getAudience()); - assertThat(tokenClaims.getIssuer()).isEqualTo(claimsSet.getIssuer()); - assertThat(tokenClaims.getId()).isEqualTo(claimsSet.getId()); - assertThat((String) tokenClaims.getClaim("custom-claim")).isEqualTo("custom-claim-value"); + assertThat(tokenClaims.getClaim("custom-claim")).isEqualTo("custom-value"); } @Test diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenIntrospectionEndpointFilterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenIntrospectionEndpointFilterTests.java index 8b1f468c..61166732 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenIntrospectionEndpointFilterTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenIntrospectionEndpointFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 the original author or authors. + * Copyright 2020-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. @@ -40,6 +40,7 @@ import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.OAuth2TokenIntrospection; @@ -51,6 +52,9 @@ import org.springframework.security.oauth2.server.authorization.authentication.O import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationToken; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients; +import org.springframework.security.web.authentication.AuthenticationConverter; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -101,6 +105,27 @@ public class OAuth2TokenIntrospectionEndpointFilterTests { .hasMessage("tokenIntrospectionEndpointUri cannot be empty"); } + @Test + public void setAuthenticationConverterWhenNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.filter.setAuthenticationConverter(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("authenticationConverter cannot be null"); + } + + @Test + public void setAuthenticationSuccessHandlerWhenNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.filter.setAuthenticationSuccessHandler(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("authenticationSuccessHandler cannot be null"); + } + + @Test + public void setAuthenticationFailureHandlerWhenNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.filter.setAuthenticationFailureHandler(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("authenticationFailureHandler cannot be null"); + } + @Test public void doFilterWhenNotTokenIntrospectionRequestThenNotProcessed() throws Exception { String requestUri = "/path"; @@ -231,6 +256,100 @@ public class OAuth2TokenIntrospectionEndpointFilterTests { assertThat(tokenIntrospectionResponse.getId()).isEqualTo(tokenClaims.getId()); } + @Test + public void doFilterWhenCustomAuthenticationConverterThenUsed() throws Exception { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + Authentication clientPrincipal = new OAuth2ClientAuthenticationToken( + registeredClient, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret()); + OAuth2AccessToken accessToken = new OAuth2AccessToken( + OAuth2AccessToken.TokenType.BEARER, "token", + Instant.now(), Instant.now().plus(Duration.ofHours(1)), + new HashSet<>(Arrays.asList("scope1", "scope2"))); + OAuth2TokenIntrospectionAuthenticationToken tokenIntrospectionAuthentication = + new OAuth2TokenIntrospectionAuthenticationToken( + accessToken.getTokenValue(), clientPrincipal, OAuth2TokenType.ACCESS_TOKEN.getValue(), null); + + AuthenticationConverter authenticationConverter = mock(AuthenticationConverter.class); + when(authenticationConverter.convert(any())).thenReturn(tokenIntrospectionAuthentication); + this.filter.setAuthenticationConverter(authenticationConverter); + + when(this.authenticationManager.authenticate(any())).thenReturn(tokenIntrospectionAuthentication); + + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication(clientPrincipal); + SecurityContextHolder.setContext(securityContext); + + MockHttpServletRequest request = createTokenIntrospectionRequest( + accessToken.getTokenValue(), OAuth2TokenType.ACCESS_TOKEN.getValue()); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + this.filter.doFilter(request, response, filterChain); + + verify(authenticationConverter).convert(any()); + } + + @Test + public void doFilterWhenCustomAuthenticationSuccessHandlerThenUsed() throws Exception { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + Authentication clientPrincipal = new OAuth2ClientAuthenticationToken( + registeredClient, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret()); + OAuth2AccessToken accessToken = new OAuth2AccessToken( + OAuth2AccessToken.TokenType.BEARER, "token", + Instant.now(), Instant.now().plus(Duration.ofHours(1)), + new HashSet<>(Arrays.asList("scope1", "scope2"))); + OAuth2TokenIntrospectionAuthenticationToken tokenIntrospectionAuthentication = + new OAuth2TokenIntrospectionAuthenticationToken( + accessToken.getTokenValue(), clientPrincipal, OAuth2TokenType.ACCESS_TOKEN.getValue(), null); + + AuthenticationSuccessHandler authenticationSuccessHandler = mock(AuthenticationSuccessHandler.class); + this.filter.setAuthenticationSuccessHandler(authenticationSuccessHandler); + + when(this.authenticationManager.authenticate(any())).thenReturn(tokenIntrospectionAuthentication); + + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication(clientPrincipal); + SecurityContextHolder.setContext(securityContext); + + MockHttpServletRequest request = createTokenIntrospectionRequest( + accessToken.getTokenValue(), OAuth2TokenType.ACCESS_TOKEN.getValue()); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + this.filter.doFilter(request, response, filterChain); + + verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), any()); + } + + @Test + public void doFilterWhenCustomAuthenticationFailureHandlerThenUsed() throws Exception { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + Authentication clientPrincipal = new OAuth2ClientAuthenticationToken( + registeredClient, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret()); + OAuth2AccessToken accessToken = new OAuth2AccessToken( + OAuth2AccessToken.TokenType.BEARER, "token", + Instant.now(), Instant.now().plus(Duration.ofHours(1)), + new HashSet<>(Arrays.asList("scope1", "scope2"))); + + AuthenticationFailureHandler authenticationFailureHandler = mock(AuthenticationFailureHandler.class); + this.filter.setAuthenticationFailureHandler(authenticationFailureHandler); + + when(this.authenticationManager.authenticate(any())).thenThrow(OAuth2AuthenticationException.class); + + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication(clientPrincipal); + SecurityContextHolder.setContext(securityContext); + + MockHttpServletRequest request = createTokenIntrospectionRequest( + accessToken.getTokenValue(), OAuth2TokenType.ACCESS_TOKEN.getValue()); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + this.filter.doFilter(request, response, filterChain); + + verify(authenticationFailureHandler).onAuthenticationFailure(any(), any(), any()); + } + private void doFilterWhenTokenIntrospectionRequestInvalidParameterThenError(String parameterName, String errorCode, MockHttpServletRequest request) throws Exception {