Opaque Token Intermediate Type
Introducing OAuth2TokenIntrospectionClient and also ReactiveOAuth2TokenIntrospectionClient as configuration points. The DSL looks in the application context for these types in the same way it looks for JwtDecoder and ReactiveJwtDecoder, and exposes similar configuration methods. Fixes: gh-6632
This commit is contained in:
+34
-10
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.security.config.annotation.web.configurers.oauth2.server.resource;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
@@ -23,7 +24,6 @@ import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationManagerResolver;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
|
||||
@@ -36,6 +36,8 @@ 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.OAuth2IntrospectionAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.resource.introspection.NimbusOAuth2TokenIntrospectionClient;
|
||||
import org.springframework.security.oauth2.server.resource.introspection.OAuth2TokenIntrospectionClient;
|
||||
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
|
||||
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter;
|
||||
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
|
||||
@@ -179,7 +181,7 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
|
||||
|
||||
public OpaqueTokenConfigurer opaqueToken() {
|
||||
if (this.opaqueTokenConfigurer == null) {
|
||||
this.opaqueTokenConfigurer = new OpaqueTokenConfigurer();
|
||||
this.opaqueTokenConfigurer = new OpaqueTokenConfigurer(this.context);
|
||||
}
|
||||
|
||||
return this.opaqueTokenConfigurer;
|
||||
@@ -237,7 +239,10 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
|
||||
}
|
||||
|
||||
if (this.opaqueTokenConfigurer != null) {
|
||||
http.authenticationProvider(this.opaqueTokenConfigurer.getProvider());
|
||||
OAuth2TokenIntrospectionClient introspectionClient = this.opaqueTokenConfigurer.getIntrospectionClient();
|
||||
OAuth2IntrospectionAuthenticationProvider provider =
|
||||
new OAuth2IntrospectionAuthenticationProvider(introspectionClient);
|
||||
http.authenticationProvider(provider);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,27 +293,46 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
|
||||
}
|
||||
|
||||
public class OpaqueTokenConfigurer {
|
||||
private final ApplicationContext context;
|
||||
|
||||
private String introspectionUri;
|
||||
private String introspectionClientId;
|
||||
private String introspectionClientSecret;
|
||||
private String clientId;
|
||||
private String clientSecret;
|
||||
private Supplier<OAuth2TokenIntrospectionClient> introspectionClient;
|
||||
|
||||
OpaqueTokenConfigurer(ApplicationContext context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public OpaqueTokenConfigurer introspectionUri(String introspectionUri) {
|
||||
Assert.notNull(introspectionUri, "introspectionUri cannot be null");
|
||||
this.introspectionUri = introspectionUri;
|
||||
this.introspectionClient = () ->
|
||||
new NimbusOAuth2TokenIntrospectionClient(this.introspectionUri, this.clientId, this.clientSecret);
|
||||
return this;
|
||||
}
|
||||
|
||||
public OpaqueTokenConfigurer introspectionClientCredentials(String clientId, String clientSecret) {
|
||||
Assert.notNull(clientId, "clientId cannot be null");
|
||||
Assert.notNull(clientSecret, "clientSecret cannot be null");
|
||||
this.introspectionClientId = clientId;
|
||||
this.introspectionClientSecret = clientSecret;
|
||||
this.clientId = clientId;
|
||||
this.clientSecret = clientSecret;
|
||||
this.introspectionClient = () ->
|
||||
new NimbusOAuth2TokenIntrospectionClient(this.introspectionUri, this.clientId, this.clientSecret);
|
||||
return this;
|
||||
}
|
||||
|
||||
AuthenticationProvider getProvider() {
|
||||
return new OAuth2IntrospectionAuthenticationProvider(this.introspectionUri,
|
||||
this.introspectionClientId, this.introspectionClientSecret);
|
||||
public OpaqueTokenConfigurer introspectionClient(OAuth2TokenIntrospectionClient introspectionClient) {
|
||||
Assert.notNull(introspectionClient, "introspectionClient cannot be null");
|
||||
this.introspectionClient = () -> introspectionClient;
|
||||
return this;
|
||||
}
|
||||
|
||||
OAuth2TokenIntrospectionClient getIntrospectionClient() {
|
||||
if (this.introspectionClient != null) {
|
||||
return this.introspectionClient.get();
|
||||
}
|
||||
return this.context.getBean(OAuth2TokenIntrospectionClient.class);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+32
-8
@@ -30,9 +30,8 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.context.Context;
|
||||
|
||||
@@ -61,6 +60,8 @@ import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2Authoriz
|
||||
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeReactiveAuthenticationManager;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2LoginReactiveAuthenticationManager;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
|
||||
import org.springframework.security.oauth2.client.endpoint.WebClientReactiveAuthorizationCodeTokenResponseClient;
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeReactiveAuthenticationManager;
|
||||
import org.springframework.security.oauth2.client.oidc.userinfo.OidcReactiveOAuth2UserService;
|
||||
@@ -88,6 +89,8 @@ import org.springframework.security.oauth2.server.resource.authentication.JwtAut
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtReactiveAuthenticationManager;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.OAuth2IntrospectionReactiveAuthenticationManager;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter;
|
||||
import org.springframework.security.oauth2.server.resource.introspection.NimbusReactiveOAuth2TokenIntrospectionClient;
|
||||
import org.springframework.security.oauth2.server.resource.introspection.ReactiveOAuth2TokenIntrospectionClient;
|
||||
import org.springframework.security.oauth2.server.resource.web.access.server.BearerTokenServerAccessDeniedHandler;
|
||||
import org.springframework.security.oauth2.server.resource.web.server.BearerTokenServerAuthenticationEntryPoint;
|
||||
import org.springframework.security.oauth2.server.resource.web.server.ServerBearerTokenAuthenticationConverter;
|
||||
@@ -1364,8 +1367,9 @@ public class ServerHttpSecurity {
|
||||
*/
|
||||
public class OpaqueTokenSpec {
|
||||
private String introspectionUri;
|
||||
private String introspectionClientId;
|
||||
private String introspectionClientSecret;
|
||||
private String clientId;
|
||||
private String clientSecret;
|
||||
private Supplier<ReactiveOAuth2TokenIntrospectionClient> introspectionClient;
|
||||
|
||||
/**
|
||||
* Configures the URI of the Introspection endpoint
|
||||
@@ -1375,6 +1379,9 @@ public class ServerHttpSecurity {
|
||||
public OpaqueTokenSpec introspectionUri(String introspectionUri) {
|
||||
Assert.hasText(introspectionUri, "introspectionUri cannot be empty");
|
||||
this.introspectionUri = introspectionUri;
|
||||
this.introspectionClient = () ->
|
||||
new NimbusReactiveOAuth2TokenIntrospectionClient(
|
||||
this.introspectionUri, this.clientId, this.clientSecret);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -1387,8 +1394,17 @@ public class ServerHttpSecurity {
|
||||
public OpaqueTokenSpec introspectionClientCredentials(String clientId, String clientSecret) {
|
||||
Assert.hasText(clientId, "clientId cannot be empty");
|
||||
Assert.notNull(clientSecret, "clientSecret cannot be null");
|
||||
this.introspectionClientId = clientId;
|
||||
this.introspectionClientSecret = clientSecret;
|
||||
this.clientId = clientId;
|
||||
this.clientSecret = clientSecret;
|
||||
this.introspectionClient = () ->
|
||||
new NimbusReactiveOAuth2TokenIntrospectionClient(
|
||||
this.introspectionUri, this.clientId, this.clientSecret);
|
||||
return this;
|
||||
}
|
||||
|
||||
public OpaqueTokenSpec introspectionClient(ReactiveOAuth2TokenIntrospectionClient introspectionClient) {
|
||||
Assert.notNull(introspectionClient, "introspectionClient cannot be null");
|
||||
this.introspectionClient = () -> introspectionClient;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -1401,8 +1417,14 @@ public class ServerHttpSecurity {
|
||||
}
|
||||
|
||||
protected ReactiveAuthenticationManager getAuthenticationManager() {
|
||||
return new OAuth2IntrospectionReactiveAuthenticationManager(
|
||||
this.introspectionUri, this.introspectionClientId, this.introspectionClientSecret);
|
||||
return new OAuth2IntrospectionReactiveAuthenticationManager(getIntrospectionClient());
|
||||
}
|
||||
|
||||
protected ReactiveOAuth2TokenIntrospectionClient getIntrospectionClient() {
|
||||
if (this.introspectionClient != null) {
|
||||
return this.introspectionClient.get();
|
||||
}
|
||||
return getBean(ReactiveOAuth2TokenIntrospectionClient.class);
|
||||
}
|
||||
|
||||
protected void configure(ServerHttpSecurity http) {
|
||||
@@ -1412,6 +1434,8 @@ public class ServerHttpSecurity {
|
||||
oauth2.setAuthenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler(entryPoint));
|
||||
http.addFilterAt(oauth2, SecurityWebFiltersOrder.AUTHENTICATION);
|
||||
}
|
||||
|
||||
private OpaqueTokenSpec() {}
|
||||
}
|
||||
|
||||
public ServerHttpSecurity and() {
|
||||
|
||||
+140
-1
@@ -51,6 +51,7 @@ import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.EnvironmentAware;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
import org.springframework.core.env.Environment;
|
||||
@@ -78,6 +79,8 @@ import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
|
||||
import org.springframework.security.oauth2.server.resource.introspection.NimbusOAuth2TokenIntrospectionClient;
|
||||
import org.springframework.security.oauth2.server.resource.introspection.OAuth2TokenIntrospectionClient;
|
||||
import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimNames;
|
||||
@@ -147,6 +150,10 @@ public class OAuth2ResourceServerConfigurerTests {
|
||||
private static final JwtAuthenticationToken JWT_AUTHENTICATION_TOKEN =
|
||||
new JwtAuthenticationToken(JWT, Collections.emptyList());
|
||||
|
||||
private static final String INTROSPECTION_URI = "https://idp.example.com";
|
||||
private static final String CLIENT_ID = "client-id";
|
||||
private static final String CLIENT_SECRET = "client-secret";
|
||||
|
||||
@Autowired(required = false)
|
||||
MockMvc mvc;
|
||||
|
||||
@@ -1008,6 +1015,90 @@ public class OAuth2ResourceServerConfigurerTests {
|
||||
.andExpect(invalidTokenHeader("algorithm"));
|
||||
}
|
||||
|
||||
// -- opaque
|
||||
|
||||
|
||||
@Test
|
||||
public void getWhenIntrospectingThenOk() throws Exception {
|
||||
this.spring.register(RestOperationsConfig.class, OpaqueTokenConfig.class, BasicController.class).autowire();
|
||||
mockRestOperations(json("Active"));
|
||||
|
||||
this.mvc.perform(get("/authenticated")
|
||||
.with(bearerToken("token")))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string("test-subject"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWhenIntrospectionFailsThenUnauthorized() throws Exception {
|
||||
this.spring.register(RestOperationsConfig.class, OpaqueTokenConfig.class).autowire();
|
||||
mockRestOperations(json("Inactive"));
|
||||
|
||||
this.mvc.perform(get("/")
|
||||
.with(bearerToken("token")))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE,
|
||||
containsString("Provided token [token] isn't active")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWhenIntrospectionLacksScopeThenForbidden() throws Exception {
|
||||
this.spring.register(RestOperationsConfig.class, OpaqueTokenConfig.class).autowire();
|
||||
mockRestOperations(json("ActiveNoScopes"));
|
||||
|
||||
this.mvc.perform(get("/requires-read-scope")
|
||||
.with(bearerToken("token")))
|
||||
.andExpect(status().isForbidden())
|
||||
.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, containsString("scope")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configureWhenOnlyIntrospectionUrlThenException() throws Exception {
|
||||
assertThatCode(() -> this.spring.register(OpaqueTokenHalfConfiguredConfig.class).autowire())
|
||||
.isInstanceOf(BeanCreationException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getIntrospectionClientWhenConfiguredWithClientAndIntrospectionUriThenLastOneWins() {
|
||||
ApplicationContext context = mock(ApplicationContext.class);
|
||||
|
||||
OAuth2ResourceServerConfigurer.OpaqueTokenConfigurer opaqueTokenConfigurer =
|
||||
new OAuth2ResourceServerConfigurer(context).opaqueToken();
|
||||
|
||||
OAuth2TokenIntrospectionClient client = mock(OAuth2TokenIntrospectionClient.class);
|
||||
|
||||
opaqueTokenConfigurer.introspectionUri(INTROSPECTION_URI);
|
||||
opaqueTokenConfigurer.introspectionClientCredentials(CLIENT_ID, CLIENT_SECRET);
|
||||
opaqueTokenConfigurer.introspectionClient(client);
|
||||
|
||||
assertThat(opaqueTokenConfigurer.getIntrospectionClient()).isEqualTo(client);
|
||||
|
||||
opaqueTokenConfigurer =
|
||||
new OAuth2ResourceServerConfigurer(context).opaqueToken();
|
||||
|
||||
opaqueTokenConfigurer.introspectionClient(client);
|
||||
opaqueTokenConfigurer.introspectionUri(INTROSPECTION_URI);
|
||||
opaqueTokenConfigurer.introspectionClientCredentials(CLIENT_ID, CLIENT_SECRET);
|
||||
|
||||
assertThat(opaqueTokenConfigurer.getIntrospectionClient())
|
||||
.isInstanceOf(NimbusOAuth2TokenIntrospectionClient.class);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getIntrospectionClientWhenDslAndBeanWiredThenDslTakesPrecedence() {
|
||||
GenericApplicationContext context = new GenericApplicationContext();
|
||||
registerMockBean(context, "introspectionClientOne", OAuth2TokenIntrospectionClient.class);
|
||||
registerMockBean(context, "introspectionClientTwo", OAuth2TokenIntrospectionClient.class);
|
||||
|
||||
OAuth2ResourceServerConfigurer.OpaqueTokenConfigurer opaqueToken =
|
||||
new OAuth2ResourceServerConfigurer(context).opaqueToken();
|
||||
opaqueToken.introspectionUri(INTROSPECTION_URI);
|
||||
opaqueToken.introspectionClientCredentials(CLIENT_ID, CLIENT_SECRET);
|
||||
|
||||
assertThat(opaqueToken.getIntrospectionClient()).isNotNull();
|
||||
}
|
||||
|
||||
// -- In combination with other authentication providers
|
||||
|
||||
@Test
|
||||
@@ -1628,6 +1719,22 @@ public class OAuth2ResourceServerConfigurerTests {
|
||||
}
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class OpaqueTokenConfig 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();
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class OpaqueAndJwtConfig extends WebSecurityConfigurerAdapter {
|
||||
@Override
|
||||
@@ -1641,6 +1748,22 @@ public class OAuth2ResourceServerConfigurerTests {
|
||||
}
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class OpaqueTokenHalfConfiguredConfig extends WebSecurityConfigurerAdapter {
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeRequests()
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.oauth2ResourceServer()
|
||||
.opaqueToken()
|
||||
.introspectionUri("https://idp.example.com"); // missing credentials
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class JwtDecoderConfig {
|
||||
@Bean
|
||||
@@ -1740,6 +1863,15 @@ public class OAuth2ResourceServerConfigurerTests {
|
||||
return withJwkSetUri("https://example.org/.well-known/jwks.json")
|
||||
.restOperations(this.rest).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
NimbusOAuth2TokenIntrospectionClient tokenIntrospectionClient() {
|
||||
return new NimbusOAuth2TokenIntrospectionClient("https://example.org/introspect", this.rest);
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> void registerMockBean(GenericApplicationContext context, String name, Class<T> clazz) {
|
||||
context.registerBean(name, clazz, () -> mock(clazz));
|
||||
}
|
||||
|
||||
private static class BearerTokenRequestPostProcessor implements RequestPostProcessor {
|
||||
@@ -1815,8 +1947,15 @@ public class OAuth2ResourceServerConfigurerTests {
|
||||
|
||||
private void mockRestOperations(String response) {
|
||||
RestOperations rest = this.spring.getContext().getBean(RestOperations.class);
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
ResponseEntity<String> entity = new ResponseEntity<>(response, headers, HttpStatus.OK);
|
||||
when(rest.exchange(any(RequestEntity.class), eq(String.class)))
|
||||
.thenReturn(new ResponseEntity<>(response, HttpStatus.OK));
|
||||
.thenReturn(entity);
|
||||
}
|
||||
|
||||
private String json(String name) throws IOException {
|
||||
return resource(name + ".json");
|
||||
}
|
||||
|
||||
private String jwks(String name) throws IOException {
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"active" : true,
|
||||
"sub": "test-subject",
|
||||
"scope": "message:read",
|
||||
"exp": 4683883211
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"active" : true,
|
||||
"sub": "test-subject",
|
||||
"exp": 4683883211
|
||||
}
|
||||
Reference in New Issue
Block a user