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 0abfb19902..ecdbdd1644 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 @@ -44,6 +44,7 @@ import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider; import org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenAuthenticationProvider; +import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenAuthenticationConverter; import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; import org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector; import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint; @@ -105,8 +106,8 @@ import org.springframework.web.accept.HeaderContentNegotiationStrategy; * * *

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

* *

Security Filters

@@ -136,6 +137,7 @@ import org.springframework.web.accept.HeaderContentNegotiationStrategy; * * @author Josh Cummings * @author Evgeniy Cheban + * @author Jerome Wacongne <ch4mp@c4-soft.com> * @since 5.1 * @see BearerTokenAuthenticationFilter * @see JwtAuthenticationProvider @@ -448,6 +450,8 @@ public final class OAuth2ResourceServerConfigurer introspector; + private OpaqueTokenAuthenticationConverter authenticationConverter; + OpaqueTokenConfigurer(ApplicationContext context) { this.context = context; } @@ -482,6 +486,13 @@ public final class OAuth2ResourceServerConfigurer 0) { + return this.context.getBean(OpaqueTokenAuthenticationConverter.class); + } + return null; + } + AuthenticationProvider getAuthenticationProvider() { if (this.authenticationManager != null) { return null; } OpaqueTokenIntrospector introspector = getIntrospector(); - return new OpaqueTokenAuthenticationProvider(introspector); + OpaqueTokenAuthenticationProvider opaqueTokenAuthenticationProvider = new OpaqueTokenAuthenticationProvider( + introspector); + OpaqueTokenAuthenticationConverter authenticationConverter = getAuthenticationConverter(); + if (authenticationConverter != null) { + opaqueTokenAuthenticationProvider.setAuthenticationConverter(authenticationConverter); + } + return opaqueTokenAuthenticationProvider; } AuthenticationManager getAuthenticationManager(H http) { diff --git a/config/src/main/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParser.java index 44692325e9..2537910ec7 100644 --- a/config/src/main/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParser.java @@ -244,6 +244,10 @@ final class OAuth2ResourceServerBeanDefinitionParser implements BeanDefinitionPa static final String CLIENT_SECRET = "client-secret"; + static final String AUTHENTICATION_CONVERTER_REF = "authentication-converter-ref"; + + static final String AUTHENTICATION_CONVERTER = "authenticationConverter"; + OpaqueTokenBeanDefinitionParser() { } @@ -251,9 +255,13 @@ final class OAuth2ResourceServerBeanDefinitionParser implements BeanDefinitionPa public BeanDefinition parse(Element element, ParserContext pc) { validateConfiguration(element, pc); BeanMetadataElement introspector = getIntrospector(element); + String authenticationConverterRef = element.getAttribute(AUTHENTICATION_CONVERTER_REF); BeanDefinitionBuilder opaqueTokenProviderBuilder = BeanDefinitionBuilder .rootBeanDefinition(OpaqueTokenAuthenticationProvider.class); opaqueTokenProviderBuilder.addConstructorArgValue(introspector); + if (StringUtils.hasText(authenticationConverterRef)) { + opaqueTokenProviderBuilder.addPropertyReference(AUTHENTICATION_CONVERTER, authenticationConverterRef); + } return opaqueTokenProviderBuilder.getBeanDefinition(); } diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java index 4a055dc844..b6dbe53fec 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java @@ -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. @@ -95,6 +95,7 @@ import org.springframework.security.oauth2.server.resource.authentication.JwtRea import org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenReactiveAuthenticationManager; import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverter; import org.springframework.security.oauth2.server.resource.introspection.NimbusReactiveOpaqueTokenIntrospector; +import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenAuthenticationConverter; import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector; import org.springframework.security.oauth2.server.resource.web.access.server.BearerTokenServerAccessDeniedHandler; import org.springframework.security.oauth2.server.resource.web.server.BearerTokenServerAuthenticationEntryPoint; @@ -4285,6 +4286,8 @@ public class ServerHttpSecurity { private Supplier introspector; + private ReactiveOpaqueTokenAuthenticationConverter authenticationConverter; + private OpaqueTokenSpec() { } @@ -4323,6 +4326,13 @@ public class ServerHttpSecurity { return this; } + public OpaqueTokenSpec authenticationConverter( + ReactiveOpaqueTokenAuthenticationConverter authenticationConverter) { + Assert.notNull(authenticationConverter, "authenticationConverter cannot be null"); + this.authenticationConverter = authenticationConverter; + return this; + } + /** * Allows method chaining to continue configuring the * {@link ServerHttpSecurity} @@ -4333,7 +4343,13 @@ public class ServerHttpSecurity { } protected ReactiveAuthenticationManager getAuthenticationManager() { - return new OpaqueTokenReactiveAuthenticationManager(getIntrospector()); + OpaqueTokenReactiveAuthenticationManager authenticationManager = new OpaqueTokenReactiveAuthenticationManager( + getIntrospector()); + ReactiveOpaqueTokenAuthenticationConverter authenticationConverter = getAuthenticationConverter(); + if (authenticationConverter != null) { + authenticationManager.setAuthenticationConverter(authenticationConverter); + } + return authenticationManager; } protected ReactiveOpaqueTokenIntrospector getIntrospector() { @@ -4343,6 +4359,13 @@ public class ServerHttpSecurity { return getBean(ReactiveOpaqueTokenIntrospector.class); } + protected ReactiveOpaqueTokenAuthenticationConverter getAuthenticationConverter() { + if (this.authenticationConverter != null) { + return this.authenticationConverter; + } + return getBeanOrNull(ReactiveOpaqueTokenAuthenticationConverter.class); + } + protected void configure(ServerHttpSecurity http) { ReactiveAuthenticationManager authenticationManager = getAuthenticationManager(); AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(authenticationManager); diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/oauth2/resourceserver/OpaqueTokenDsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/oauth2/resourceserver/OpaqueTokenDsl.kt index a0d8417763..b6692b905f 100644 --- a/config/src/main/kotlin/org/springframework/security/config/annotation/web/oauth2/resourceserver/OpaqueTokenDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/annotation/web/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. @@ -20,6 +20,7 @@ import org.springframework.security.authentication.AuthenticationManager import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer import org.springframework.security.core.Authentication +import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenAuthenticationConverter import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector /** @@ -54,6 +55,7 @@ class OpaqueTokenDsl { clientCredentials = null } + var authenticationConverter: OpaqueTokenAuthenticationConverter? = null /** * Configures the credentials for Introspection endpoint. @@ -70,6 +72,7 @@ class OpaqueTokenDsl { return { opaqueToken -> introspectionUri?.also { opaqueToken.introspectionUri(introspectionUri) } introspector?.also { opaqueToken.introspector(introspector) } + authenticationConverter?.also { opaqueToken.authenticationConverter(authenticationConverter) } clientCredentials?.also { opaqueToken.introspectionClientCredentials(clientCredentials!!.first, clientCredentials!!.second) } authenticationManager?.also { opaqueToken.authenticationManager(authenticationManager) } } diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOpaqueTokenDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOpaqueTokenDsl.kt index 72d9bf103f..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. @@ -16,6 +16,7 @@ package org.springframework.security.config.web.server +import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenAuthenticationConverter import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector /** @@ -45,6 +46,7 @@ class ServerOpaqueTokenDsl { _introspectionUri = null clientCredentials = null } + var authenticationConverter: ReactiveOpaqueTokenAuthenticationConverter? = null /** * Configures the credentials for Introspection endpoint. @@ -62,6 +64,7 @@ class ServerOpaqueTokenDsl { introspectionUri?.also { opaqueToken.introspectionUri(introspectionUri) } clientCredentials?.also { opaqueToken.introspectionClientCredentials(clientCredentials!!.first, clientCredentials!!.second) } introspector?.also { opaqueToken.introspector(introspector) } + authenticationConverter?.also { opaqueToken.authenticationConverter(authenticationConverter) } } } } diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.8.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-5.8.rnc index 3f48f7bcd4..dcec72a232 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-5.8.rnc +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.8.rnc @@ -667,6 +667,9 @@ opaque-token.attlist &= opaque-token.attlist &= ## Reference to an OpaqueTokenIntrospector attribute introspector-ref {xsd:token}? +opaque-token.attlist &= + ## Reference to an OpaqueTokenAuthenticationConverter responsible for converting successful introspection result into an Authentication. + attribute authentication-converter-ref {xsd:token}? openid-login = ## Sets up form login for authentication with an Open ID identity. NOTE: The OpenID 1.0 and 2.0 protocols have been deprecated and users are encouraged to migrate to OpenID Connect, which is supported by spring-security-oauth2. diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.8.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-5.8.xsd index bef39a7c62..dc2911daac 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-5.8.xsd +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.8.xsd @@ -2060,6 +2060,13 @@ + + + Reference to an OpaqueTokenAuthenticationConverter responsible for converting successful + introspection result into an Authentication. + + + diff --git a/config/src/test/java/org/springframework/security/config/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 b75f9e18d6..19f98bb50c 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 @@ -80,6 +80,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.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; @@ -98,6 +99,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; @@ -116,6 +118,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; @@ -1353,6 +1356,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)); } @@ -2444,6 +2463,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/http/OAuth2ResourceServerBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests.java index cdd09eed38..8705229c58 100644 --- a/config/src/test/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests.java @@ -23,6 +23,7 @@ import java.security.interfaces.RSAPublicKey; import java.time.Clock; import java.time.Instant; import java.time.ZoneId; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -66,12 +67,16 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.AuthenticationManagerResolver; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.config.http.OAuth2ResourceServerBeanDefinitionParser.JwtBeanDefinitionParser; import org.springframework.security.config.http.OAuth2ResourceServerBeanDefinitionParser.OpaqueTokenBeanDefinitionParser; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2TokenValidator; import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; @@ -84,6 +89,7 @@ import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import org.springframework.security.oauth2.jwt.TestJwts; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector; +import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenAuthenticationConverter; import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners; @@ -643,6 +649,20 @@ public class OAuth2ResourceServerBeanDefinitionParserTests { // @formatter:on } + @Test + public void configureWhenIntrospectingWithAuthenticationConverterThenUses() throws Exception { + this.spring.configLocations(xml("OpaqueTokenRestOperations"), xml("OpaqueTokenAndAuthenticationConverter")) + .autowire(); + mockRestOperations(json("Active")); + // @formatter:off + this.mvc.perform(get("/authenticated").header("Authorization", "Bearer token")) + .andExpect(status().isNotFound()); + + this.mvc.perform(get("/authenticated").header("Authorization", "Bearer invalidToken")) + .andExpect(status().isUnauthorized()); + // @formatter:on + } + @Test public void getWhenIntrospectionFailsThenUnauthorized() throws Exception { this.spring.configLocations(xml("OpaqueTokenRestOperations"), xml("OpaqueToken")).autowire(); @@ -1077,4 +1097,39 @@ public class OAuth2ResourceServerBeanDefinitionParserTests { } + public static class TestAuthentication extends AbstractAuthenticationToken { + + private final String introspectedToken; + + public TestAuthentication(String introspectedToken, Collection authorities) { + super(authorities); + this.introspectedToken = introspectedToken; + } + + @Override + public Object getCredentials() { + return this.introspectedToken; + } + + @Override + public Object getPrincipal() { + return this.introspectedToken; + } + + @Override + public boolean isAuthenticated() { + return "token".equals(this.introspectedToken); + } + + } + + public static class TestOpaqueTokenAuthenticationConverter implements OpaqueTokenAuthenticationConverter { + + @Override + public Authentication convert(String introspectedToken, OAuth2AuthenticatedPrincipal authenticatedPrincipal) { + return new TestAuthentication(introspectedToken, Collections.emptyList()); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java index 40ad35eb2d..1d1cf35936 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; @@ -566,6 +570,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 @@ -1052,6 +1075,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/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-OpaqueTokenAndAuthenticationConverter.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-OpaqueTokenAndAuthenticationConverter.xml new file mode 100644 index 0000000000..5a22b05c8a --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-OpaqueTokenAndAuthenticationConverter.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + diff --git a/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc b/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc index 458e7dcfb1..50cd7bdab0 100644 --- a/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc +++ b/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc @@ -1324,6 +1324,10 @@ The Client Id to use for client authentication against the provided `introspecti * **client-secret** The Client Secret to use for client authentication against the provided `introspection-uri`. +[[nsa-opaque-token-authentication-converter-ref]] +* **authentication-converter-ref** +Reference to an `OpaqueTokenAuthenticationConverter`. Responsible for converting successful introspection result into an `Authentication` instance. + [[nsa-relying-party-registrations]] == diff --git a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc index 672549b72d..f77f5117a8 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 @@ -297,11 +297,13 @@ fun introspector(): OpaqueTokenIntrospector { ---- ==== -If the application doesn't expose a <> bean, then Spring Boot will expose the above default one. +If the application doesn't expose an <> bean, then Spring Boot will expose the above default one. And its configuration can be overridden using `introspectionUri()` and `introspectionClientCredentials()` or replaced using `introspector()`. -Or, if you're not using Spring Boot at all, then both of these components - the filter chain and a <> can be specified in XML. +If the application doesn't expose an `OpaqueTokenAuthenticationConverter` bean, then spring-security will build `BearerTokenAuthentication`. + +Or, if you're not using Spring Boot at all, then all of these components - the filter chain, an <> and an `OpaqueTokenAuthenticationConverter` can be specified in XML. The filter chain is specified like so: @@ -313,7 +315,8 @@ The filter chain is specified like so: - + ---- @@ -335,6 +338,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 d8ece9e740..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. @@ -35,6 +35,7 @@ import org.springframework.security.oauth2.server.resource.BearerTokenAuthentica import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException; import org.springframework.security.oauth2.server.resource.introspection.BadOpaqueTokenException; import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionException; +import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenAuthenticationConverter; import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; import org.springframework.util.Assert; @@ -57,8 +58,16 @@ import org.springframework.util.Assert; *
  • 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. + *

    + * 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> * @since 5.2 * @see AuthenticationProvider */ @@ -68,6 +77,8 @@ public final class OpaqueTokenAuthenticationProvider implements AuthenticationPr private final OpaqueTokenIntrospector introspector; + private OpaqueTokenAuthenticationConverter authenticationConverter = OpaqueTokenAuthenticationProvider::convert; + /** * Creates a {@code OpaqueTokenAuthenticationProvider} with the provided parameters * @param introspector The {@link OpaqueTokenIntrospector} to use @@ -80,7 +91,11 @@ public final class OpaqueTokenAuthenticationProvider implements AuthenticationPr /** * Introspect and validate the opaque * Bearer - * Token. + * Token and then delegates {@link Authentication} instantiation to + * {@link OpaqueTokenAuthenticationConverter}. + *

    + * If created Authentication is instance of {@link AbstractAuthenticationToken} and + * details are null, then introspection result details are used. * @param authentication the authentication request object. * @return A successful authentication * @throws AuthenticationException if authentication failed for some reason @@ -92,8 +107,16 @@ public final class OpaqueTokenAuthenticationProvider implements AuthenticationPr } BearerTokenAuthenticationToken bearer = (BearerTokenAuthenticationToken) authentication; OAuth2AuthenticatedPrincipal principal = getOAuth2AuthenticatedPrincipal(bearer); - AbstractAuthenticationToken result = convert(principal, bearer.getToken()); - result.setDetails(bearer.getDetails()); + Authentication result = this.authenticationConverter.convert(bearer.getToken(), principal); + if (result == null) { + return null; + } + if (AbstractAuthenticationToken.class.isAssignableFrom(result.getClass())) { + final AbstractAuthenticationToken auth = (AbstractAuthenticationToken) result; + if (auth.getDetails() == null) { + auth.setDetails(bearer.getDetails()); + } + } this.logger.debug("Authenticated token"); return result; } @@ -116,11 +139,32 @@ public final class OpaqueTokenAuthenticationProvider implements AuthenticationPr return BearerTokenAuthenticationToken.class.isAssignableFrom(authentication); } - private AbstractAuthenticationToken convert(OAuth2AuthenticatedPrincipal principal, String token) { - Instant iat = principal.getAttribute(OAuth2TokenIntrospectionClaimNames.IAT); - Instant exp = principal.getAttribute(OAuth2TokenIntrospectionClaimNames.EXP); - OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, token, iat, exp); - return new BearerTokenAuthentication(principal, accessToken, principal.getAuthorities()); + /** + * Default {@link OpaqueTokenAuthenticationConverter}. + * @param introspectedToken the bearer string that was successfully introspected + * @param authenticatedPrincipal the successful introspection output + * @return a {@link BearerTokenAuthentication} + */ + static BearerTokenAuthentication convert(String introspectedToken, + OAuth2AuthenticatedPrincipal authenticatedPrincipal) { + Instant iat = authenticatedPrincipal.getAttribute(OAuth2TokenIntrospectionClaimNames.IAT); + Instant exp = authenticatedPrincipal.getAttribute(OAuth2TokenIntrospectionClaimNames.EXP); + OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, introspectedToken, + iat, exp); + return new BearerTokenAuthentication(authenticatedPrincipal, accessToken, + authenticatedPrincipal.getAuthorities()); + } + + /** + * Provide with a custom bean to turn successful introspection result into an + * {@link Authentication} instance of your choice. By default, + * {@link BearerTokenAuthentication} will be built. + * @param authenticationConverter the converter to use + * @since 5.8 + */ + public void setAuthenticationConverter(OpaqueTokenAuthenticationConverter authenticationConverter) { + Assert.notNull(authenticationConverter, "authenticationConverter cannot be null"); + this.authenticationConverter = authenticationConverter; } } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenReactiveAuthenticationManager.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenReactiveAuthenticationManager.java index 79c271e923..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. @@ -16,22 +16,21 @@ package org.springframework.security.oauth2.server.resource.authentication; -import java.time.Instant; -import java.util.Collection; - import reactor.core.publisher.Mono; +import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.oauth2.core.OAuth2AccessToken; -import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames; +import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException; import org.springframework.security.oauth2.server.resource.introspection.BadOpaqueTokenException; import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionException; +import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenAuthenticationConverter; +import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenAuthenticationConverter; import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector; import org.springframework.util.Assert; @@ -46,16 +45,16 @@ import org.springframework.util.Assert; * 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. - *
    + * A {@link ReactiveOpaqueTokenIntrospector} is responsible for retrieving token + * attributes from an authorization server. + *

    + * 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> * @since 5.2 * @see ReactiveAuthenticationManager */ @@ -63,6 +62,8 @@ public class OpaqueTokenReactiveAuthenticationManager implements ReactiveAuthent private final ReactiveOpaqueTokenIntrospector introspector; + private ReactiveOpaqueTokenAuthenticationConverter authenticationConverter = OpaqueTokenReactiveAuthenticationManager::convert; + /** * Creates a {@code OpaqueTokenReactiveAuthenticationManager} with the provided * parameters @@ -73,6 +74,17 @@ public class OpaqueTokenReactiveAuthenticationManager implements ReactiveAuthent this.introspector = introspector; } + /** + * Introspect and validate the opaque + * Bearer + * Token and then delegates {@link Authentication} instantiation to + * {@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 + */ @Override public Mono authenticate(Authentication authentication) { // @formatter:off @@ -80,21 +92,14 @@ public class OpaqueTokenReactiveAuthenticationManager implements ReactiveAuthent .filter(BearerTokenAuthenticationToken.class::isInstance) .cast(BearerTokenAuthenticationToken.class) .map(BearerTokenAuthenticationToken::getToken) - .flatMap(this::authenticate) - .cast(Authentication.class); + .flatMap(this::authenticate); // @formatter:on } - private Mono authenticate(String token) { + private Mono authenticate(String token) { // @formatter:off return this.introspector.introspect(token) - .map((principal) -> { - Instant iat = principal.getAttribute(OAuth2TokenIntrospectionClaimNames.IAT); - Instant exp = principal.getAttribute(OAuth2TokenIntrospectionClaimNames.EXP); - // construct token - OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, token, iat, exp); - return new BearerTokenAuthentication(principal, accessToken, principal.getAuthorities()); - }) + .flatMap((principal) -> this.authenticationConverter.convert(token, principal)) .onErrorMap(OAuth2IntrospectionException.class, this::onError); // @formatter:on } @@ -106,4 +111,27 @@ public class OpaqueTokenReactiveAuthenticationManager implements ReactiveAuthent return new AuthenticationServiceException(ex.getMessage(), ex); } + /** + * Default {@link ReactiveOpaqueTokenAuthenticationConverter}. + * @param introspectedToken the bearer string that was successfully introspected + * @param authenticatedPrincipal the successful introspection output + * @return an async wrapper of default {@link OpaqueTokenAuthenticationConverter} + * result + */ + static Mono convert(String introspectedToken, OAuth2AuthenticatedPrincipal authenticatedPrincipal) { + return Mono.just(OpaqueTokenAuthenticationProvider.convert(introspectedToken, authenticatedPrincipal)); + } + + /** + * Provide with a custom bean to turn successful introspection result into an + * {@link Authentication} instance of your choice. By default, + * {@link BearerTokenAuthentication} will be built. + * @param authenticationConverter the converter to use + * @since 5.8 + */ + public void setAuthenticationConverter(ReactiveOpaqueTokenAuthenticationConverter authenticationConverter) { + Assert.notNull(authenticationConverter, "authenticationConverter cannot be null"); + this.authenticationConverter = authenticationConverter; + } + } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/OpaqueTokenAuthenticationConverter.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/OpaqueTokenAuthenticationConverter.java new file mode 100644 index 0000000000..d0d00f5caf --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/OpaqueTokenAuthenticationConverter.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.server.resource.introspection; + +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; + +/** + * Convert a successful introspection result into an authentication result. + * + * @author Jerome Wacongne <ch4mp@c4-soft.com> + * @since 5.8 + */ +@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 new file mode 100644 index 0000000000..9e9a63c567 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/ReactiveOpaqueTokenAuthenticationConverter.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.server.resource.introspection; + +import reactor.core.publisher.Mono; + +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; + +/** + * Convert a successful introspection result into an authentication result. + * + * @author Jerome Wacongne <ch4mp@c4-soft.com> + * @since 5.8 + */ +@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 69eaecb9b2..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 @@ -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. @@ -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); + } + }