1
0
mirror of synced 2026-08-05 01:36:56 +00:00

Add authentication converter for introspected tokens

Adds configurable authentication converter for resource-servers with
token introspection (something very similar to what
JwtAuthenticationConverter does for resource-servers with JWT decoder).

The new (Reactive)OpaqueTokenAuthenticationConverter is given
responsibility for converting successful token introspection result
into an Authentication instance (which is currently done by a private
methods of OpaqueTokenAuthenticationProvider and
OpaqueTokenReactiveAuthenticationManager).

The default (Reactive)OpaqueTokenAuthenticationConverter, behave the
same as current private convert(OAuth2AuthenticatedPrincipal principal,
String token) methods: map authorities from scope attribute and build a
BearerTokenAuthentication.

Closes gh-11661
This commit is contained in:
ch4mpy
2022-08-30 09:33:11 -10:00
committed by Steve Riesenberg
parent cc988fc287
commit 1efb63387f
16 changed files with 385 additions and 48 deletions
@@ -21,10 +21,12 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.MediaType;
@@ -46,6 +48,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;
@@ -107,8 +110,8 @@ import org.springframework.web.accept.HeaderContentNegotiationStrategy;
* </ul>
*
* <p>
* 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
* </p>
*
* <h2>Security Filters</h2>
@@ -138,6 +141,7 @@ import org.springframework.web.accept.HeaderContentNegotiationStrategy;
*
* @author Josh Cummings
* @author Evgeniy Cheban
* @author Jerome Wacongne &lt;ch4mp@c4-soft.com&gt;
* @since 5.1
* @see BearerTokenAuthenticationFilter
* @see JwtAuthenticationProvider
@@ -456,6 +460,8 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
private Supplier<OpaqueTokenIntrospector> introspector;
private Supplier<OpaqueTokenAuthenticationConverter> authenticationConverter;
OpaqueTokenConfigurer(ApplicationContext context) {
this.context = context;
}
@@ -490,6 +496,13 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
return this;
}
public OpaqueTokenConfigurer authenticationConverter(
OpaqueTokenAuthenticationConverter authenticationConverter) {
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
this.authenticationConverter = () -> authenticationConverter;
return this;
}
OpaqueTokenIntrospector getIntrospector() {
if (this.introspector != null) {
return this.introspector.get();
@@ -497,12 +510,27 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
return this.context.getBean(OpaqueTokenIntrospector.class);
}
Optional<OpaqueTokenAuthenticationConverter> getAuthenticationConverter() {
if (this.authenticationConverter != null) {
return Optional.of(this.authenticationConverter.get());
}
try {
return Optional.of(this.context.getBean(OpaqueTokenAuthenticationConverter.class));
}
catch (NoSuchBeanDefinitionException nsbde) {
return Optional.empty();
}
}
AuthenticationProvider getAuthenticationProvider() {
if (this.authenticationManager != null) {
return null;
}
OpaqueTokenIntrospector introspector = getIntrospector();
return new OpaqueTokenAuthenticationProvider(introspector);
final OpaqueTokenAuthenticationProvider opaqueTokenAuthenticationProvider = new OpaqueTokenAuthenticationProvider(
introspector);
getAuthenticationConverter().ifPresent(opaqueTokenAuthenticationProvider::setAuthenticationConverter);
return opaqueTokenAuthenticationProvider;
}
AuthenticationManager getAuthenticationManager(H http) {
@@ -251,6 +251,9 @@ 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() {
}
@@ -258,9 +261,14 @@ 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.addPropertyValue(AUTHENTICATION_CONVERTER,
new RuntimeBeanReference(authenticationConverterRef));
}
return opaqueTokenProviderBuilder.getBeanDefinition();
}
@@ -27,6 +27,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Function;
import java.util.function.Supplier;
@@ -35,6 +36,7 @@ import reactor.core.publisher.Mono;
import reactor.util.context.Context;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.core.ResolvableType;
@@ -95,6 +97,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;
@@ -4283,6 +4286,8 @@ public class ServerHttpSecurity {
private Supplier<ReactiveOpaqueTokenIntrospector> introspector;
private Supplier<ReactiveOpaqueTokenAuthenticationConverter> authenticationConverter;
private OpaqueTokenSpec() {
}
@@ -4321,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}
@@ -4331,7 +4343,11 @@ public class ServerHttpSecurity {
}
protected ReactiveAuthenticationManager getAuthenticationManager() {
return new OpaqueTokenReactiveAuthenticationManager(getIntrospector());
final OpaqueTokenReactiveAuthenticationManager authenticationManager = new OpaqueTokenReactiveAuthenticationManager(
getIntrospector());
Optional.ofNullable(getAuthenticationConverter())
.ifPresent(authenticationManager::setAuthenticationConverter);
return authenticationManager;
}
protected ReactiveOpaqueTokenIntrospector getIntrospector() {
@@ -4341,6 +4357,18 @@ public class ServerHttpSecurity {
return getBean(ReactiveOpaqueTokenIntrospector.class);
}
protected ReactiveOpaqueTokenAuthenticationConverter getAuthenticationConverter() {
if (this.authenticationConverter != null) {
return this.authenticationConverter.get();
}
try {
return getBean(ReactiveOpaqueTokenAuthenticationConverter.class);
}
catch (NoSuchBeanDefinitionException nsbde) {
return null;
}
}
protected void configure(ServerHttpSecurity http) {
ReactiveAuthenticationManager authenticationManager = getAuthenticationManager();
AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(authenticationManager);
@@ -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
/**
@@ -30,6 +31,7 @@ import org.springframework.security.oauth2.server.resource.introspection.Reactiv
class ServerOpaqueTokenDsl {
private var _introspectionUri: String? = null
private var _introspector: ReactiveOpaqueTokenIntrospector? = null
private var _authenticationConverter: ReactiveOpaqueTokenAuthenticationConverter? = null
private var clientCredentials: Pair<String, String>? = null
var introspectionUri: String?
@@ -37,14 +39,21 @@ class ServerOpaqueTokenDsl {
set(value) {
_introspectionUri = value
_introspector = null
_authenticationConverter = null
}
var introspector: ReactiveOpaqueTokenIntrospector?
get() = _introspector
set(value) {
_introspector = value
_authenticationConverter = null
_introspectionUri = null
clientCredentials = null
}
var authenticationConverter: ReactiveOpaqueTokenAuthenticationConverter?
get() = _authenticationConverter
set(value) {
_authenticationConverter = value
}
/**
* Configures the credentials for Introspection endpoint.
@@ -55,6 +64,7 @@ class ServerOpaqueTokenDsl {
fun introspectionClientCredentials(clientId: String, clientSecret: String) {
clientCredentials = Pair(clientId, clientSecret)
_introspector = null
_authenticationConverter = null
}
internal fun get(): (ServerHttpSecurity.OAuth2ResourceServerSpec.OpaqueTokenSpec) -> Unit {
@@ -62,6 +72,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) }
}
}
}
@@ -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
/**
@@ -37,6 +38,7 @@ class OpaqueTokenDsl {
private var _introspectionUri: String? = null
private var _introspector: OpaqueTokenIntrospector? = null
private var clientCredentials: Pair<String, String>? = null
private var _authenticationConverter: OpaqueTokenAuthenticationConverter? = null
var authenticationManager: AuthenticationManager? = null
@@ -54,6 +56,11 @@ class OpaqueTokenDsl {
clientCredentials = null
}
var authenticationConverter: OpaqueTokenAuthenticationConverter?
get() = _authenticationConverter
set(value) {
_authenticationConverter = value
}
/**
* Configures the credentials for Introspection endpoint.
@@ -70,6 +77,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) }
}
@@ -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 <a href="https://openid.net/specs/openid-connect-migration-1_0.html">encouraged to migrate</a> to <a href="https://openid.net/connect/">OpenID Connect</a>, which is supported by <code>spring-security-oauth2</code>.
@@ -2060,6 +2060,13 @@
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="authentication-converter-ref" type="xs:token">
<xs:annotation>
<xs:documentation>Reference to an OpaqueTokenAuthenticationConverter responsible for converting successful
introspection result into an Authentication.
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:attributeGroup>
<xs:element name="attribute-exchange">
@@ -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;
@@ -68,13 +69,17 @@ 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.core.context.SecurityContextHolderStrategy;
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;
@@ -87,6 +92,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;
@@ -662,6 +668,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();
@@ -1096,4 +1116,39 @@ public class OAuth2ResourceServerBeanDefinitionParserTests {
}
public static class TestAuthentication extends AbstractAuthenticationToken {
private final String introspectedToken;
public TestAuthentication(String introspectedToken, Collection<? extends GrantedAuthority> 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());
}
}
}
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2002-2020 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.
-->
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<b:bean name="authentication-converter"
class="org.springframework.security.config.http.OAuth2ResourceServerBeanDefinitionParserTests$TestOpaqueTokenAuthenticationConverter">
</b:bean>
<http>
<intercept-url pattern="/requires-read-scope" access="hasAuthority('SCOPE_message:read')"/>
<intercept-url pattern="/**" access="authenticated"/>
<oauth2-resource-server>
<opaque-token introspector-ref="introspector" authentication-converter-ref="authentication-converter"/>
</oauth2-resource-server>
</http>
<b:import resource="userservice.xml"/>
</b:beans>