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 8f45f3e867..4e53d9a3e5 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,12 +21,10 @@ import java.util.Collections; 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; @@ -460,7 +458,7 @@ public final class OAuth2ResourceServerConfigurer introspector; - private Supplier authenticationConverter; + private OpaqueTokenAuthenticationConverter authenticationConverter; OpaqueTokenConfigurer(ApplicationContext context) { this.context = context; @@ -499,7 +497,7 @@ public final class OAuth2ResourceServerConfigurer authenticationConverter; + this.authenticationConverter = authenticationConverter; return this; } @@ -510,16 +508,14 @@ public final class OAuth2ResourceServerConfigurer getAuthenticationConverter() { + OpaqueTokenAuthenticationConverter getAuthenticationConverter() { if (this.authenticationConverter != null) { - return Optional.of(this.authenticationConverter.get()); + return this.authenticationConverter; } - try { - return Optional.of(this.context.getBean(OpaqueTokenAuthenticationConverter.class)); - } - catch (NoSuchBeanDefinitionException nsbde) { - return Optional.empty(); + if (this.context.getBeanNamesForType(OpaqueTokenAuthenticationConverter.class).length > 0) { + return this.context.getBean(OpaqueTokenAuthenticationConverter.class); } + return null; } AuthenticationProvider getAuthenticationProvider() { @@ -527,9 +523,12 @@ public final class OAuth2ResourceServerConfigurer introspector; - private Supplier authenticationConverter; + private ReactiveOpaqueTokenAuthenticationConverter authenticationConverter; private OpaqueTokenSpec() { } @@ -4329,7 +4327,7 @@ public class ServerHttpSecurity { public OpaqueTokenSpec authenticationConverter( ReactiveOpaqueTokenAuthenticationConverter authenticationConverter) { Assert.notNull(authenticationConverter, "authenticationConverter cannot be null"); - this.authenticationConverter = () -> authenticationConverter; + this.authenticationConverter = authenticationConverter; return this; } @@ -4343,10 +4341,12 @@ public class ServerHttpSecurity { } protected ReactiveAuthenticationManager getAuthenticationManager() { - final OpaqueTokenReactiveAuthenticationManager authenticationManager = new OpaqueTokenReactiveAuthenticationManager( + OpaqueTokenReactiveAuthenticationManager authenticationManager = new OpaqueTokenReactiveAuthenticationManager( getIntrospector()); - Optional.ofNullable(getAuthenticationConverter()) - .ifPresent(authenticationManager::setAuthenticationConverter); + ReactiveOpaqueTokenAuthenticationConverter authenticationConverter = getAuthenticationConverter(); + if (authenticationConverter != null) { + authenticationManager.setAuthenticationConverter(authenticationConverter); + } return authenticationManager; } @@ -4359,14 +4359,9 @@ public class ServerHttpSecurity { protected ReactiveOpaqueTokenAuthenticationConverter getAuthenticationConverter() { if (this.authenticationConverter != null) { - return this.authenticationConverter.get(); - } - try { - return getBean(ReactiveOpaqueTokenAuthenticationConverter.class); - } - catch (NoSuchBeanDefinitionException nsbde) { - return null; + return this.authenticationConverter; } + return getBeanOrNull(ReactiveOpaqueTokenAuthenticationConverter.class); } protected void configure(ServerHttpSecurity http) { 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 31d8ba2c96..12839a09bf 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 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. @@ -31,7 +31,6 @@ 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? @@ -39,21 +38,15 @@ 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 - } + var authenticationConverter: ReactiveOpaqueTokenAuthenticationConverter? = null /** * Configures the credentials for Introspection endpoint. @@ -64,7 +57,6 @@ class ServerOpaqueTokenDsl { fun introspectionClientCredentials(clientId: String, clientSecret: String) { clientCredentials = Pair(clientId, clientSecret) _introspector = null - _authenticationConverter = null } internal fun get(): (ServerHttpSecurity.OAuth2ResourceServerSpec.OpaqueTokenSpec) -> Unit { 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 dfaaeb570f..c7a090551e 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 @@ -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. @@ -38,7 +38,6 @@ 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 @@ -56,11 +55,7 @@ class OpaqueTokenDsl { clientCredentials = null } - var authenticationConverter: OpaqueTokenAuthenticationConverter? - get() = _authenticationConverter - set(value) { - _authenticationConverter = value - } + var authenticationConverter: OpaqueTokenAuthenticationConverter? = null /** * Configures the credentials for Introspection endpoint. diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java index 8b38914b21..d66cfbe810 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java @@ -82,6 +82,7 @@ import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManagerResolver; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; @@ -103,6 +104,7 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.TestClientRegistrations; import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal; +import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2TokenValidator; @@ -121,6 +123,7 @@ import org.springframework.security.oauth2.server.resource.authentication.JwtAut import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.security.oauth2.server.resource.authentication.JwtIssuerAuthenticationManagerResolver; 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.BearerTokenAuthenticationEntryPoint; import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter; @@ -1387,6 +1390,22 @@ public class OAuth2ResourceServerConfigurerTests { .isThrownBy(jwtConfigurer::getJwtAuthenticationConverter); } + @Test + public void getWhenCustomAuthenticationConverterThenConverts() throws Exception { + this.spring.register(RestOperationsConfig.class, OpaqueTokenAuthenticationConverterConfig.class, + BasicController.class).autowire(); + OpaqueTokenAuthenticationConverter authenticationConverter = this.spring.getContext() + .getBean(OpaqueTokenAuthenticationConverter.class); + given(authenticationConverter.convert(anyString(), any(OAuth2AuthenticatedPrincipal.class))) + .willReturn(new TestingAuthenticationToken("jdoe", null, Collections.emptyList())); + mockRestOperations(json("Active")); + // @formatter:off + this.mvc.perform(get("/authenticated").with(bearerToken("token"))) + .andExpect(status().isOk()) + .andExpect(content().string("jdoe")); + // @formatter:on + } + private static void registerMockBean(GenericApplicationContext context, String name, Class clazz) { context.registerBean(name, clazz, () -> mock(clazz)); } @@ -2441,6 +2460,30 @@ public class OAuth2ResourceServerConfigurerTests { } + @EnableWebSecurity + static class OpaqueTokenAuthenticationConverterConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .antMatchers("/requires-read-scope").hasAuthority("SCOPE_message:read") + .anyRequest().authenticated() + .and() + .oauth2ResourceServer() + .opaqueToken() + .authenticationConverter(authenticationConverter()); + // @formatter:on + } + + @Bean + OpaqueTokenAuthenticationConverter authenticationConverter() { + return mock(OpaqueTokenAuthenticationConverter.class); + } + + } + @Configuration static class JwtDecoderConfig { diff --git a/config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java index ef3ff4db4d..9b505ad64d 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java @@ -24,6 +24,7 @@ import java.security.interfaces.RSAPublicKey; import java.security.spec.InvalidKeySpecException; import java.security.spec.RSAPublicKeySpec; import java.util.Base64; +import java.util.Collections; import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -52,11 +53,13 @@ import org.springframework.http.MediaType; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver; +import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; 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.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.jwt.Jwt; @@ -66,6 +69,7 @@ import org.springframework.security.oauth2.server.resource.BearerTokenAuthentica import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverter; import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter; +import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenAuthenticationConverter; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint; import org.springframework.security.web.server.authentication.ServerAuthenticationConverter; @@ -567,6 +571,25 @@ public class OAuth2ResourceServerSpecTests { .withMessageContaining("authenticationManagerResolver"); } + @Test + public void getWhenCustomAuthenticationConverterThenConverts() { + this.spring.register(ReactiveOpaqueTokenAuthenticationConverterConfig.class, RootController.class).autowire(); + this.spring.getContext().getBean(MockWebServer.class) + .setDispatcher(requiresAuth(this.clientId, this.clientSecret, this.active)); + ReactiveOpaqueTokenAuthenticationConverter authenticationConverter = this.spring.getContext() + .getBean(ReactiveOpaqueTokenAuthenticationConverter.class); + given(authenticationConverter.convert(anyString(), any(OAuth2AuthenticatedPrincipal.class))) + .willReturn(Mono.just(new TestingAuthenticationToken("jdoe", null, Collections.emptyList()))); + // @formatter:off + this.client.get() + .headers((headers) -> headers + .setBearerAuth(this.messageReadToken) + ) + .exchange() + .expectStatus().isOk(); + // @formatter:on + } + private static Dispatcher requiresAuth(String username, String password, String response) { return new Dispatcher() { @Override @@ -1037,6 +1060,43 @@ public class OAuth2ResourceServerSpecTests { } + @EnableWebFlux + @EnableWebFluxSecurity + static class ReactiveOpaqueTokenAuthenticationConverterConfig { + + private MockWebServer mockWebServer = new MockWebServer(); + + @Bean + SecurityWebFilterChain springSecurity(ServerHttpSecurity http) { + String introspectionUri = mockWebServer().url("/introspect").toString(); + // @formatter:off + http + .oauth2ResourceServer() + .opaqueToken() + .introspectionUri(introspectionUri) + .introspectionClientCredentials("client", "secret") + .authenticationConverter(authenticationConverter()); + // @formatter:on + return http.build(); + } + + @Bean + ReactiveOpaqueTokenAuthenticationConverter authenticationConverter() { + return mock(ReactiveOpaqueTokenAuthenticationConverter.class); + } + + @Bean + MockWebServer mockWebServer() { + return this.mockWebServer; + } + + @PreDestroy + void shutdown() throws IOException { + this.mockWebServer.shutdown(); + } + + } + @RestController static class RootController { 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 bf5cb7ea9a..334f852022 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 @@ -313,7 +313,8 @@ The filter chain is specified like so: - + ---- @@ -335,6 +336,18 @@ And the < +---- +==== + [[oauth2resourceserver-opaque-introspectionuri-dsl]] === Using `introspectionUri()` 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 cb131efeca..9cbaeb6b63 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 @@ -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. @@ -17,6 +17,7 @@ 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; @@ -24,7 +25,6 @@ import org.apache.commons.logging.LogFactory; 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; @@ -50,18 +50,21 @@ import org.springframework.util.Assert; * opaque access token, returning its attributes set as part of the {@link Authentication} * statement. *

- * This {@link ReactiveAuthenticationManager} is responsible for introspecting and - * 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. + *
*

+ * An {@link OpaqueTokenIntrospector} is responsible for retrieving token attributes from + * an authorization server. *

- * {@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) + * An {@link OpaqueTokenAuthenticationConverter} is responsible for turning a successful + * introspection result into an {@link Authentication} instance (which may include mapping + * {@link GrantedAuthority}s from token attributes or retrieving from another source). * * @author Josh Cummings * @author Jerome Wacongne <ch4mp@c4-soft.com> @@ -74,7 +77,7 @@ public final class OpaqueTokenAuthenticationProvider implements AuthenticationPr private final OpaqueTokenIntrospector introspector; - private OpaqueTokenAuthenticationConverter authenticationConverter; + private OpaqueTokenAuthenticationConverter authenticationConverter = OpaqueTokenAuthenticationProvider::convert; /** * Creates a {@code OpaqueTokenAuthenticationProvider} with the provided parameters @@ -83,20 +86,16 @@ 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 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 @@ -142,9 +141,9 @@ public final class OpaqueTokenAuthenticationProvider implements AuthenticationPr /** * Default {@link OpaqueTokenAuthenticationConverter}. - * @param introspectedToken the bearer sring that was successfuly introspected + * @param introspectedToken the bearer string that was successfully introspected * @param authenticatedPrincipal the successful introspection output - * @returna {@link BearerTokenAuthentication} + * @return a {@link BearerTokenAuthentication} */ static BearerTokenAuthentication convert(String introspectedToken, OAuth2AuthenticatedPrincipal authenticatedPrincipal) { 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 9fa985e3fb..1736c6efb5 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 @@ -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. @@ -36,7 +36,7 @@ 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. @@ -45,14 +45,13 @@ import org.springframework.util.Assert; * verifying an opaque access token, returning its attributes set as part of the * {@link Authentication} statement. *

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

- * {@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) + * A {@link ReactiveOpaqueTokenAuthenticationConverter} is responsible for turning a + * successful introspection result into an {@link Authentication} instance (which may + * include mapping {@link GrantedAuthority}s from token attributes or retrieving from + * another source). * * @author Josh Cummings * @author Jerome Wacongne <ch4mp@c4-soft.com> @@ -63,7 +62,7 @@ public class OpaqueTokenReactiveAuthenticationManager implements ReactiveAuthent private final ReactiveOpaqueTokenIntrospector introspector; - private ReactiveOpaqueTokenAuthenticationConverter authenticationConverter; + private ReactiveOpaqueTokenAuthenticationConverter authenticationConverter = OpaqueTokenReactiveAuthenticationManager::convert; /** * Creates a {@code OpaqueTokenReactiveAuthenticationManager} with the provided @@ -73,20 +72,16 @@ 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}. - *

+ * {@link ReactiveOpaqueTokenAuthenticationConverter}. *

* 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 */ @@ -117,10 +112,10 @@ public class OpaqueTokenReactiveAuthenticationManager implements ReactiveAuthent } /** - * Default reactive {@link OpaqueTokenAuthenticationConverter}. - * @param introspectedToken the bearer sring that was successfuly introspected + * Default {@link ReactiveOpaqueTokenAuthenticationConverter}. + * @param introspectedToken the bearer string that was successfully introspected * @param authenticatedPrincipal the successful introspection output - * @returna an async wrapper of default {@link OpaqueTokenAuthenticationConverter} + * @return an async wrapper of default {@link OpaqueTokenAuthenticationConverter} * result */ static Mono convert(String introspectedToken, OAuth2AuthenticatedPrincipal authenticatedPrincipal) { 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 index 8ece04d322..d0d00f5caf 100644 --- 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 @@ -20,7 +20,7 @@ import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; /** - * Turn successful introspection result into an Authentication instance + * Convert a successful introspection result into an authentication result. * * @author Jerome Wacongne <ch4mp@c4-soft.com> * @since 5.8 @@ -28,6 +28,12 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; @FunctionalInterface public interface OpaqueTokenAuthenticationConverter { + /** + * Converts a successful introspection result into an authentication result. + * @param introspectedToken the bearer token used to perform token introspection + * @param authenticatedPrincipal the result of token introspection + * @return an {@link Authentication} instance + */ 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 index 77c481cc40..9e9a63c567 100644 --- 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 @@ -22,7 +22,7 @@ import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; /** - * Turn successful introspection result into an Authentication instance + * Convert a successful introspection result into an authentication result. * * @author Jerome Wacongne <ch4mp@c4-soft.com> * @since 5.8 @@ -30,6 +30,12 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; @FunctionalInterface public interface ReactiveOpaqueTokenAuthenticationConverter { + /** + * Converts a successful introspection result into an authentication result. + * @param introspectedToken the bearer token used to perform token introspection + * @param authenticatedPrincipal the result of token introspection + * @return an {@link Authentication} instance + */ 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 7d9ad2b9a1..c1c96f0ef6 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 @@ -25,6 +25,7 @@ import java.util.Map; import org.junit.jupiter.api.Test; import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames; @@ -32,6 +33,7 @@ import org.springframework.security.oauth2.core.TestOAuth2AuthenticatedPrincipal import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionAuthenticatedPrincipal; 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 static org.assertj.core.api.Assertions.assertThat; @@ -40,6 +42,8 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; /** * Tests for {@link OpaqueTokenAuthenticationProvider} @@ -114,4 +118,33 @@ public class OpaqueTokenAuthenticationProviderTests { // @formatter:on } + @Test + public void setAuthenticationConverterWhenNullThenThrowsIllegalArgumentException() { + OpaqueTokenIntrospector introspector = mock(OpaqueTokenIntrospector.class); + OpaqueTokenAuthenticationProvider provider = new OpaqueTokenAuthenticationProvider(introspector); + // @formatter:off + assertThatIllegalArgumentException() + .isThrownBy(() -> provider.setAuthenticationConverter(null)) + .withMessage("authenticationConverter cannot be null"); + // @formatter:on + } + + @Test + public void authenticateWhenCustomAuthenticationConverterThenUses() { + OpaqueTokenIntrospector introspector = mock(OpaqueTokenIntrospector.class); + OAuth2AuthenticatedPrincipal principal = TestOAuth2AuthenticatedPrincipals.active(); + given(introspector.introspect(any())).willReturn(principal); + OpaqueTokenAuthenticationProvider provider = new OpaqueTokenAuthenticationProvider(introspector); + OpaqueTokenAuthenticationConverter authenticationConverter = mock(OpaqueTokenAuthenticationConverter.class); + given(authenticationConverter.convert(any(), any(OAuth2AuthenticatedPrincipal.class))) + .willReturn(new TestingAuthenticationToken(principal, null, Collections.emptyList())); + provider.setAuthenticationConverter(authenticationConverter); + + Authentication result = provider.authenticate(new BearerTokenAuthenticationToken("token")); + assertThat(result).isNotNull(); + verify(introspector).introspect("token"); + verify(authenticationConverter).convert("token", principal); + verifyNoMoreInteractions(introspector, authenticationConverter); + } + } diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenReactiveAuthenticationManagerTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenReactiveAuthenticationManagerTests.java index 7f671a3730..f6d8fdbbd7 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenReactiveAuthenticationManagerTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenReactiveAuthenticationManagerTests.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. @@ -26,6 +26,7 @@ import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames; @@ -33,6 +34,7 @@ import org.springframework.security.oauth2.core.TestOAuth2AuthenticatedPrincipal import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionAuthenticatedPrincipal; import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionException; +import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenAuthenticationConverter; import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector; import static org.assertj.core.api.Assertions.assertThat; @@ -41,6 +43,8 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; /** * Tests for {@link OpaqueTokenReactiveAuthenticationManager} @@ -112,4 +116,34 @@ public class OpaqueTokenReactiveAuthenticationManagerTests { // @formatter:on } + @Test + public void setAuthenticationConverterWhenNullThenThrowsIllegalArgumentException() { + ReactiveOpaqueTokenIntrospector introspector = mock(ReactiveOpaqueTokenIntrospector.class); + OpaqueTokenReactiveAuthenticationManager provider = new OpaqueTokenReactiveAuthenticationManager(introspector); + // @formatter:off + assertThatIllegalArgumentException() + .isThrownBy(() -> provider.setAuthenticationConverter(null)) + .withMessage("authenticationConverter cannot be null"); + // @formatter:on + } + + @Test + public void authenticateWhenCustomAuthenticationConverterThenUses() { + ReactiveOpaqueTokenIntrospector introspector = mock(ReactiveOpaqueTokenIntrospector.class); + OAuth2AuthenticatedPrincipal principal = TestOAuth2AuthenticatedPrincipals.active(); + given(introspector.introspect(any())).willReturn(Mono.just(principal)); + OpaqueTokenReactiveAuthenticationManager provider = new OpaqueTokenReactiveAuthenticationManager(introspector); + ReactiveOpaqueTokenAuthenticationConverter authenticationConverter = mock( + ReactiveOpaqueTokenAuthenticationConverter.class); + given(authenticationConverter.convert(any(), any(OAuth2AuthenticatedPrincipal.class))) + .willReturn(Mono.just(new TestingAuthenticationToken(principal, null, Collections.emptyList()))); + provider.setAuthenticationConverter(authenticationConverter); + + Authentication result = provider.authenticate(new BearerTokenAuthenticationToken("token")).block(); + assertThat(result).isNotNull(); + verify(introspector).introspect("token"); + verify(authenticationConverter).convert("token", principal); + verifyNoMoreInteractions(introspector, authenticationConverter); + } + }