1
0
mirror of synced 2026-08-05 09:47:05 +00:00

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:
Josh Cummings
2019-03-20 13:36:47 -06:00
parent 59acda04cf
commit b1195e7789
23 changed files with 1577 additions and 845 deletions
@@ -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);
}
}
@@ -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() {
@@ -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 {
@@ -0,0 +1,6 @@
{
"active" : true,
"sub": "test-subject",
"scope": "message:read",
"exp": 4683883211
}