1
0
mirror of synced 2026-05-22 21:33:16 +00:00

Move OAuth2AuthorizationServerConfigurer and OAuth2AuthorizationServerConfiguration

Issue gh-17880
This commit is contained in:
Joe Grandja
2025-09-10 05:57:53 -04:00
parent b5a4cdc9eb
commit cc71be71e5
42 changed files with 98 additions and 297 deletions
+2
View File
@@ -28,6 +28,7 @@ dependencies {
optional project(':spring-security-oauth2-client')
optional project(':spring-security-oauth2-jose')
optional project(':spring-security-oauth2-resource-server')
optional project(':spring-security-oauth2-authorization-server')
optional project(':spring-security-rsocket')
optional project(':spring-security-web')
optional project(':spring-security-webauthn')
@@ -54,6 +55,7 @@ dependencies {
testImplementation project(path : ':spring-security-ldap', configuration : 'tests')
testImplementation project(path : ':spring-security-oauth2-client', configuration : 'tests')
testImplementation project(path : ':spring-security-oauth2-resource-server', configuration : 'tests')
testImplementation project(path : ':spring-security-oauth2-authorization-server', configuration : 'tests')
testImplementation project(':spring-security-saml2-service-provider')
testImplementation project(path : ':spring-security-saml2-service-provider', configuration : 'tests')
testImplementation project(path : ':spring-security-web', configuration : 'tests')
@@ -0,0 +1,91 @@
/*
* Copyright 2004-present 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.config.annotation.web.configuration;
import java.util.HashSet;
import java.util.Set;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.JWSKeySelector;
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.web.SecurityFilterChain;
/**
* {@link Configuration} for OAuth 2.0 Authorization Server support.
*
* @author Joe Grandja
* @since 7.0
* @see OAuth2AuthorizationServerConfigurer
*/
@Configuration(proxyBeanMethods = false)
public class OAuth2AuthorizationServerConfiguration {
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
// @formatter:off
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
OAuth2AuthorizationServerConfigurer.authorizationServer();
http
.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
.with(authorizationServerConfigurer, Customizer.withDefaults())
.authorizeHttpRequests((authorize) ->
authorize.anyRequest().authenticated()
);
// @formatter:on
return http.build();
}
public static JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
Set<JWSAlgorithm> jwsAlgs = new HashSet<>();
jwsAlgs.addAll(JWSAlgorithm.Family.RSA);
jwsAlgs.addAll(JWSAlgorithm.Family.EC);
jwsAlgs.addAll(JWSAlgorithm.Family.HMAC_SHA);
ConfigurableJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
JWSKeySelector<SecurityContext> jwsKeySelector = new JWSVerificationKeySelector<>(jwsAlgs, jwkSource);
jwtProcessor.setJWSKeySelector(jwsKeySelector);
// Override the default Nimbus claims set verifier as NimbusJwtDecoder handles it
// instead
jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {
});
return new NimbusJwtDecoder(jwtProcessor);
}
@Bean
RegisterMissingBeanPostProcessor registerMissingBeanPostProcessor() {
RegisterMissingBeanPostProcessor postProcessor = new RegisterMissingBeanPostProcessor();
postProcessor.addBeanDefinition(AuthorizationServerSettings.class,
() -> AuthorizationServerSettings.builder().build());
return postProcessor;
}
}
@@ -0,0 +1,75 @@
/*
* Copyright 2004-present 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.config.annotation.web.configuration;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.AnnotationBeanNameGenerator;
/**
* Post processor to register one or more bean definitions on container initialization, if
* not already present.
*
* @author Steve Riesenberg
* @since 7.0
*/
final class RegisterMissingBeanPostProcessor implements BeanDefinitionRegistryPostProcessor, BeanFactoryAware {
private final AnnotationBeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator();
private final List<AbstractBeanDefinition> beanDefinitions = new ArrayList<>();
private BeanFactory beanFactory;
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
for (AbstractBeanDefinition beanDefinition : this.beanDefinitions) {
String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
(ListableBeanFactory) this.beanFactory, beanDefinition.getBeanClass(), false, false);
if (beanNames.length == 0) {
String beanName = this.beanNameGenerator.generateBeanName(beanDefinition, registry);
registry.registerBeanDefinition(beanName, beanDefinition);
}
}
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}
<T> void addBeanDefinition(Class<T> beanClass, Supplier<T> beanSupplier) {
this.beanDefinitions.add(new RootBeanDefinition(beanClass, beanSupplier));
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
}
@@ -0,0 +1,51 @@
/*
* Copyright 2004-present 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.config.annotation.web.configurers.oauth2.server.authorization;
import org.springframework.security.config.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.util.matcher.RequestMatcher;
/**
* Base configurer for an OAuth 2.0 component (e.g. protocol endpoint).
*
* @author Joe Grandja
* @since 7.0
*/
abstract class AbstractOAuth2Configurer {
private final ObjectPostProcessor<Object> objectPostProcessor;
AbstractOAuth2Configurer(ObjectPostProcessor<Object> objectPostProcessor) {
this.objectPostProcessor = objectPostProcessor;
}
abstract void init(HttpSecurity httpSecurity);
abstract void configure(HttpSecurity httpSecurity);
abstract RequestMatcher getRequestMatcher();
protected final <T> T postProcess(T object) {
return (T) this.objectPostProcessor.postProcess(object);
}
protected final ObjectPostProcessor<Object> getObjectPostProcessor() {
return this.objectPostProcessor;
}
}
@@ -0,0 +1,156 @@
/*
* Copyright 2004-present 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.config.annotation.web.configurers.oauth2.server.authorization;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContext;
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.UriComponentsBuilder;
/**
* A {@code Filter} that associates the {@link AuthorizationServerContext} to the
* {@link AuthorizationServerContextHolder}.
*
* @author Joe Grandja
* @since 7.0
* @see AuthorizationServerContext
* @see AuthorizationServerContextHolder
* @see AuthorizationServerSettings
*/
final class AuthorizationServerContextFilter extends OncePerRequestFilter {
private final AuthorizationServerSettings authorizationServerSettings;
private final IssuerResolver issuerResolver;
AuthorizationServerContextFilter(AuthorizationServerSettings authorizationServerSettings) {
Assert.notNull(authorizationServerSettings, "authorizationServerSettings cannot be null");
this.authorizationServerSettings = authorizationServerSettings;
this.issuerResolver = new IssuerResolver(authorizationServerSettings);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
String issuer = this.issuerResolver.resolve(request);
AuthorizationServerContext authorizationServerContext = new DefaultAuthorizationServerContext(issuer,
this.authorizationServerSettings);
AuthorizationServerContextHolder.setContext(authorizationServerContext);
filterChain.doFilter(request, response);
}
finally {
AuthorizationServerContextHolder.resetContext();
}
}
private static final class IssuerResolver {
private final String issuer;
private final Set<String> endpointUris;
private IssuerResolver(AuthorizationServerSettings authorizationServerSettings) {
if (authorizationServerSettings.getIssuer() != null) {
this.issuer = authorizationServerSettings.getIssuer();
this.endpointUris = Collections.emptySet();
}
else {
this.issuer = null;
this.endpointUris = new HashSet<>();
this.endpointUris.add("/.well-known/oauth-authorization-server");
this.endpointUris.add("/.well-known/openid-configuration");
for (Map.Entry<String, Object> setting : authorizationServerSettings.getSettings().entrySet()) {
if (setting.getKey().endsWith("-endpoint")) {
this.endpointUris.add((String) setting.getValue());
}
}
}
}
private String resolve(HttpServletRequest request) {
if (this.issuer != null) {
return this.issuer;
}
// Resolve Issuer Identifier dynamically from request
String path = request.getRequestURI();
if (!StringUtils.hasText(path)) {
path = "";
}
else {
for (String endpointUri : this.endpointUris) {
if (path.contains(endpointUri)) {
path = path.replace(endpointUri, "");
break;
}
}
}
// @formatter:off
return UriComponentsBuilder.fromUriString(UrlUtils.buildFullRequestUrl(request))
.replacePath(path)
.replaceQuery(null)
.fragment(null)
.build()
.toUriString();
// @formatter:on
}
}
private static final class DefaultAuthorizationServerContext implements AuthorizationServerContext {
private final String issuer;
private final AuthorizationServerSettings authorizationServerSettings;
private DefaultAuthorizationServerContext(String issuer,
AuthorizationServerSettings authorizationServerSettings) {
this.issuer = issuer;
this.authorizationServerSettings = authorizationServerSettings;
}
@Override
public String getIssuer() {
return this.issuer;
}
@Override
public AuthorizationServerSettings getAuthorizationServerSettings() {
return this.authorizationServerSettings;
}
}
}
@@ -0,0 +1,148 @@
/*
* Copyright 2004-present 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.config.annotation.web.configurers.oauth2.server.authorization;
import java.security.MessageDigest;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import com.nimbusds.jose.jwk.JWK;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeActor;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeCompositeAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimNames;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
import org.springframework.util.CollectionUtils;
/**
* @author Joe Grandja
* @author Steve Riesenberg
* @since 7.0
*/
final class DefaultOAuth2TokenCustomizers {
private DefaultOAuth2TokenCustomizers() {
}
static OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
return (context) -> context.getClaims().claims((claims) -> customize(context, claims));
}
static OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer() {
return (context) -> context.getClaims().claims((claims) -> customize(context, claims));
}
private static void customize(OAuth2TokenContext tokenContext, Map<String, Object> claims) {
Map<String, Object> cnfClaims = null;
// Add 'cnf' claim for Mutual-TLS Client Certificate-Bound Access Tokens
if (OAuth2TokenType.ACCESS_TOKEN.equals(tokenContext.getTokenType())
&& tokenContext.getAuthorizationGrant() != null && tokenContext.getAuthorizationGrant()
.getPrincipal() instanceof OAuth2ClientAuthenticationToken clientAuthentication) {
if ((ClientAuthenticationMethod.TLS_CLIENT_AUTH.equals(clientAuthentication.getClientAuthenticationMethod())
|| ClientAuthenticationMethod.SELF_SIGNED_TLS_CLIENT_AUTH
.equals(clientAuthentication.getClientAuthenticationMethod()))
&& tokenContext.getRegisteredClient().getTokenSettings().isX509CertificateBoundAccessTokens()) {
X509Certificate[] clientCertificateChain = (X509Certificate[]) clientAuthentication.getCredentials();
try {
String sha256Thumbprint = computeSHA256Thumbprint(clientCertificateChain[0]);
cnfClaims = new HashMap<>();
cnfClaims.put("x5t#S256", sha256Thumbprint);
}
catch (Exception ex) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"Failed to compute SHA-256 Thumbprint for client X509Certificate.", null);
throw new OAuth2AuthenticationException(error, ex);
}
}
}
// Add 'cnf' claim for OAuth 2.0 Demonstrating Proof of Possession (DPoP)
Jwt dPoPProofJwt = tokenContext.get(OAuth2TokenContext.DPOP_PROOF_KEY);
if (OAuth2TokenType.ACCESS_TOKEN.equals(tokenContext.getTokenType()) && dPoPProofJwt != null) {
JWK jwk = null;
@SuppressWarnings("unchecked")
Map<String, Object> jwkJson = (Map<String, Object>) dPoPProofJwt.getHeaders().get("jwk");
try {
jwk = JWK.parse(jwkJson);
}
catch (Exception ignored) {
}
if (jwk == null) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_DPOP_PROOF,
"jwk header is missing or invalid.", null);
throw new OAuth2AuthenticationException(error);
}
try {
String sha256Thumbprint = jwk.computeThumbprint().toString();
if (cnfClaims == null) {
cnfClaims = new HashMap<>();
}
cnfClaims.put("jkt", sha256Thumbprint);
}
catch (Exception ex) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"Failed to compute SHA-256 Thumbprint for DPoP Proof PublicKey.", null);
throw new OAuth2AuthenticationException(error, ex);
}
}
if (!CollectionUtils.isEmpty(cnfClaims)) {
claims.put("cnf", cnfClaims);
}
// Add 'act' claim for delegation use case of Token Exchange Grant.
// If more than one actor is present, we create a chain of delegation by nesting
// "act" claims.
if (tokenContext
.getPrincipal() instanceof OAuth2TokenExchangeCompositeAuthenticationToken compositeAuthenticationToken) {
Map<String, Object> currentClaims = claims;
for (OAuth2TokenExchangeActor actor : compositeAuthenticationToken.getActors()) {
Map<String, Object> actorClaims = actor.getClaims();
Map<String, Object> actClaim = new LinkedHashMap<>();
actClaim.put(OAuth2TokenClaimNames.ISS, actorClaims.get(OAuth2TokenClaimNames.ISS));
actClaim.put(OAuth2TokenClaimNames.SUB, actorClaims.get(OAuth2TokenClaimNames.SUB));
currentClaims.put("act", Collections.unmodifiableMap(actClaim));
currentClaims = actClaim;
}
}
}
private static String computeSHA256Thumbprint(X509Certificate x509Certificate) throws Exception {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(x509Certificate.getEncoded());
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
}
}
@@ -0,0 +1,326 @@
/*
* Copyright 2004-present 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.config.annotation.web.configurers.oauth2.server.authorization;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationContext;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationValidator;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeRequestAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationConsentAuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.DelegatingAuthenticationConverter;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Configurer for the OAuth 2.0 Authorization Endpoint.
*
* @author Joe Grandja
* @since 7.0
* @see OAuth2AuthorizationServerConfigurer#authorizationEndpoint
* @see OAuth2AuthorizationEndpointFilter
*/
public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2Configurer {
private RequestMatcher requestMatcher;
private final List<AuthenticationConverter> authorizationRequestConverters = new ArrayList<>();
private Consumer<List<AuthenticationConverter>> authorizationRequestConvertersConsumer = (
authorizationRequestConverters) -> {
};
private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
private Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer = (authenticationProviders) -> {
};
private AuthenticationSuccessHandler authorizationResponseHandler;
private AuthenticationFailureHandler errorResponseHandler;
private String consentPage;
private Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authorizationCodeRequestAuthenticationValidator;
private SessionAuthenticationStrategy sessionAuthenticationStrategy;
/**
* Restrict for internal use only.
* @param objectPostProcessor an {@code ObjectPostProcessor}
*/
OAuth2AuthorizationEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
super(objectPostProcessor);
}
/**
* Adds an {@link AuthenticationConverter} used when attempting to extract an
* Authorization Request (or Consent) from {@link HttpServletRequest} to an instance
* of {@link OAuth2AuthorizationCodeRequestAuthenticationToken} or
* {@link OAuth2AuthorizationConsentAuthenticationToken} used for authenticating the
* request.
* @param authorizationRequestConverter an {@link AuthenticationConverter} used when
* attempting to extract an Authorization Request (or Consent) from
* {@link HttpServletRequest}
* @return the {@link OAuth2AuthorizationEndpointConfigurer} for further configuration
*/
public OAuth2AuthorizationEndpointConfigurer authorizationRequestConverter(
AuthenticationConverter authorizationRequestConverter) {
Assert.notNull(authorizationRequestConverter, "authorizationRequestConverter cannot be null");
this.authorizationRequestConverters.add(authorizationRequestConverter);
return this;
}
/**
* Sets the {@code Consumer} providing access to the {@code List} of default and
* (optionally) added {@link #authorizationRequestConverter(AuthenticationConverter)
* AuthenticationConverter}'s allowing the ability to add, remove, or customize a
* specific {@link AuthenticationConverter}.
* @param authorizationRequestConvertersConsumer the {@code Consumer} providing access
* to the {@code List} of default and (optionally) added
* {@link AuthenticationConverter}'s
* @return the {@link OAuth2AuthorizationEndpointConfigurer} for further configuration
*/
public OAuth2AuthorizationEndpointConfigurer authorizationRequestConverters(
Consumer<List<AuthenticationConverter>> authorizationRequestConvertersConsumer) {
Assert.notNull(authorizationRequestConvertersConsumer, "authorizationRequestConvertersConsumer cannot be null");
this.authorizationRequestConvertersConsumer = authorizationRequestConvertersConsumer;
return this;
}
/**
* Adds an {@link AuthenticationProvider} used for authenticating an
* {@link OAuth2AuthorizationCodeRequestAuthenticationToken}.
* @param authenticationProvider an {@link AuthenticationProvider} used for
* authenticating an {@link OAuth2AuthorizationCodeRequestAuthenticationToken}
* @return the {@link OAuth2AuthorizationEndpointConfigurer} for further configuration
*/
public OAuth2AuthorizationEndpointConfigurer authenticationProvider(AuthenticationProvider authenticationProvider) {
Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
this.authenticationProviders.add(authenticationProvider);
return this;
}
/**
* Sets the {@code Consumer} providing access to the {@code List} of default and
* (optionally) added {@link #authenticationProvider(AuthenticationProvider)
* AuthenticationProvider}'s allowing the ability to add, remove, or customize a
* specific {@link AuthenticationProvider}.
* @param authenticationProvidersConsumer the {@code Consumer} providing access to the
* {@code List} of default and (optionally) added {@link AuthenticationProvider}'s
* @return the {@link OAuth2AuthorizationEndpointConfigurer} for further configuration
*/
public OAuth2AuthorizationEndpointConfigurer authenticationProviders(
Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer) {
Assert.notNull(authenticationProvidersConsumer, "authenticationProvidersConsumer cannot be null");
this.authenticationProvidersConsumer = authenticationProvidersConsumer;
return this;
}
/**
* Sets the {@link AuthenticationSuccessHandler} used for handling an
* {@link OAuth2AuthorizationCodeRequestAuthenticationToken} and returning the
* {@link OAuth2AuthorizationResponse Authorization Response}.
* @param authorizationResponseHandler the {@link AuthenticationSuccessHandler} used
* for handling an {@link OAuth2AuthorizationCodeRequestAuthenticationToken}
* @return the {@link OAuth2AuthorizationEndpointConfigurer} for further configuration
*/
public OAuth2AuthorizationEndpointConfigurer authorizationResponseHandler(
AuthenticationSuccessHandler authorizationResponseHandler) {
this.authorizationResponseHandler = authorizationResponseHandler;
return this;
}
/**
* Sets the {@link AuthenticationFailureHandler} used for handling an
* {@link OAuth2AuthorizationCodeRequestAuthenticationException} and returning the
* {@link OAuth2Error Error Response}.
* @param errorResponseHandler the {@link AuthenticationFailureHandler} used for
* handling an {@link OAuth2AuthorizationCodeRequestAuthenticationException}
* @return the {@link OAuth2AuthorizationEndpointConfigurer} for further configuration
*/
public OAuth2AuthorizationEndpointConfigurer errorResponseHandler(
AuthenticationFailureHandler errorResponseHandler) {
this.errorResponseHandler = errorResponseHandler;
return this;
}
/**
* Specify the URI to redirect Resource Owners to if consent is required during the
* {@code authorization_code} flow. A default consent page will be generated when this
* attribute is not specified.
*
* If a URI is specified, applications are required to process the specified URI to
* generate a consent page. The query string will contain the following parameters:
*
* <ul>
* <li>{@code client_id} - the client identifier</li>
* <li>{@code scope} - a space-delimited list of scopes present in the authorization
* request</li>
* <li>{@code state} - a CSRF protection token</li>
* </ul>
*
* In general, the consent page should create a form that submits a request with the
* following requirements:
*
* <ul>
* <li>It must be an HTTP POST</li>
* <li>It must be submitted to
* {@link AuthorizationServerSettings#getAuthorizationEndpoint()}</li>
* <li>It must include the received {@code client_id} as an HTTP parameter</li>
* <li>It must include the received {@code state} as an HTTP parameter</li>
* <li>It must include the list of {@code scope}s the {@code Resource Owner} consented
* to as an HTTP parameter</li>
* </ul>
* @param consentPage the URI of the custom consent page to redirect to if consent is
* required (e.g. "/oauth2/consent")
* @return the {@link OAuth2AuthorizationEndpointConfigurer} for further configuration
*/
public OAuth2AuthorizationEndpointConfigurer consentPage(String consentPage) {
this.consentPage = consentPage;
return this;
}
void addAuthorizationCodeRequestAuthenticationValidator(
Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator) {
this.authorizationCodeRequestAuthenticationValidator = (this.authorizationCodeRequestAuthenticationValidator == null)
? authenticationValidator
: this.authorizationCodeRequestAuthenticationValidator.andThen(authenticationValidator);
}
void setSessionAuthenticationStrategy(SessionAuthenticationStrategy sessionAuthenticationStrategy) {
this.sessionAuthenticationStrategy = sessionAuthenticationStrategy;
}
@Override
void init(HttpSecurity httpSecurity) {
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
.getAuthorizationServerSettings(httpSecurity);
String authorizationEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
? OAuth2ConfigurerUtils
.withMultipleIssuersPattern(authorizationServerSettings.getAuthorizationEndpoint())
: authorizationServerSettings.getAuthorizationEndpoint();
this.requestMatcher = new OrRequestMatcher(
PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.GET, authorizationEndpointUri),
PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, authorizationEndpointUri));
List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity);
if (!this.authenticationProviders.isEmpty()) {
authenticationProviders.addAll(0, this.authenticationProviders);
}
this.authenticationProvidersConsumer.accept(authenticationProviders);
authenticationProviders.forEach(
(authenticationProvider) -> httpSecurity.authenticationProvider(postProcess(authenticationProvider)));
}
@Override
void configure(HttpSecurity httpSecurity) {
AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class);
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
.getAuthorizationServerSettings(httpSecurity);
String authorizationEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
? OAuth2ConfigurerUtils
.withMultipleIssuersPattern(authorizationServerSettings.getAuthorizationEndpoint())
: authorizationServerSettings.getAuthorizationEndpoint();
OAuth2AuthorizationEndpointFilter authorizationEndpointFilter = new OAuth2AuthorizationEndpointFilter(
authenticationManager, authorizationEndpointUri);
List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
if (!this.authorizationRequestConverters.isEmpty()) {
authenticationConverters.addAll(0, this.authorizationRequestConverters);
}
this.authorizationRequestConvertersConsumer.accept(authenticationConverters);
authorizationEndpointFilter
.setAuthenticationConverter(new DelegatingAuthenticationConverter(authenticationConverters));
if (this.authorizationResponseHandler != null) {
authorizationEndpointFilter.setAuthenticationSuccessHandler(this.authorizationResponseHandler);
}
if (this.errorResponseHandler != null) {
authorizationEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
}
if (StringUtils.hasText(this.consentPage)) {
authorizationEndpointFilter.setConsentPage(this.consentPage);
}
if (this.sessionAuthenticationStrategy != null) {
authorizationEndpointFilter.setSessionAuthenticationStrategy(this.sessionAuthenticationStrategy);
}
httpSecurity.addFilterBefore(postProcess(authorizationEndpointFilter),
AbstractPreAuthenticatedProcessingFilter.class);
}
@Override
RequestMatcher getRequestMatcher() {
return this.requestMatcher;
}
private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
authenticationConverters.add(new OAuth2AuthorizationCodeRequestAuthenticationConverter());
authenticationConverters.add(new OAuth2AuthorizationConsentAuthenticationConverter());
return authenticationConverters;
}
private List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
OAuth2AuthorizationCodeRequestAuthenticationProvider authorizationCodeRequestAuthenticationProvider = new OAuth2AuthorizationCodeRequestAuthenticationProvider(
OAuth2ConfigurerUtils.getRegisteredClientRepository(httpSecurity),
OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity),
OAuth2ConfigurerUtils.getAuthorizationConsentService(httpSecurity));
if (this.authorizationCodeRequestAuthenticationValidator != null) {
authorizationCodeRequestAuthenticationProvider
.setAuthenticationValidator(new OAuth2AuthorizationCodeRequestAuthenticationValidator()
.andThen(this.authorizationCodeRequestAuthenticationValidator));
}
authenticationProviders.add(authorizationCodeRequestAuthenticationProvider);
OAuth2AuthorizationConsentAuthenticationProvider authorizationConsentAuthenticationProvider = new OAuth2AuthorizationConsentAuthenticationProvider(
OAuth2ConfigurerUtils.getRegisteredClientRepository(httpSecurity),
OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity),
OAuth2ConfigurerUtils.getAuthorizationConsentService(httpSecurity));
authenticationProviders.add(authorizationConsentAuthenticationProvider);
return authenticationProviders;
}
}
@@ -0,0 +1,499 @@
/*
* Copyright 2004-present 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.config.annotation.web.configurers.oauth2.server.authorization;
import java.net.URI;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import com.nimbusds.jose.jwk.source.JWKSource;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.GenericApplicationListenerAdapter;
import org.springframework.context.event.SmartApplicationListener;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer;
import org.springframework.security.context.DelegatingApplicationListener;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationContext;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.security.oauth2.server.authorization.web.NimbusJwkSetEndpointFilter;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.security.web.context.SecurityContextHolderFilter;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
/**
* An {@link AbstractHttpConfigurer} for OAuth 2.0 Authorization Server support.
*
* @author Joe Grandja
* @author Daniel Garnier-Moiroux
* @author Gerardo Roza
* @author Ovidiu Popa
* @author Gaurav Tiwari
* @since 7.0
* @see AbstractHttpConfigurer
* @see OAuth2ClientAuthenticationConfigurer
* @see OAuth2AuthorizationServerMetadataEndpointConfigurer
* @see OAuth2AuthorizationEndpointConfigurer
* @see OAuth2PushedAuthorizationRequestEndpointConfigurer
* @see OAuth2TokenEndpointConfigurer
* @see OAuth2TokenIntrospectionEndpointConfigurer
* @see OAuth2TokenRevocationEndpointConfigurer
* @see OAuth2DeviceAuthorizationEndpointConfigurer
* @see OAuth2DeviceVerificationEndpointConfigurer
* @see OidcConfigurer
* @see RegisteredClientRepository
* @see OAuth2AuthorizationService
* @see OAuth2AuthorizationConsentService
* @see NimbusJwkSetEndpointFilter
*/
public final class OAuth2AuthorizationServerConfigurer
extends AbstractHttpConfigurer<OAuth2AuthorizationServerConfigurer, HttpSecurity> {
private final Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer> configurers = createConfigurers();
private RequestMatcher endpointsMatcher;
/**
* Returns a new instance of {@link OAuth2AuthorizationServerConfigurer} for
* configuring.
* @return a new instance of {@link OAuth2AuthorizationServerConfigurer} for
* configuring
*/
public static OAuth2AuthorizationServerConfigurer authorizationServer() {
return new OAuth2AuthorizationServerConfigurer();
}
/**
* Sets the repository of registered clients.
* @param registeredClientRepository the repository of registered clients
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
*/
public OAuth2AuthorizationServerConfigurer registeredClientRepository(
RegisteredClientRepository registeredClientRepository) {
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
getBuilder().setSharedObject(RegisteredClientRepository.class, registeredClientRepository);
return this;
}
/**
* Sets the authorization service.
* @param authorizationService the authorization service
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
*/
public OAuth2AuthorizationServerConfigurer authorizationService(OAuth2AuthorizationService authorizationService) {
Assert.notNull(authorizationService, "authorizationService cannot be null");
getBuilder().setSharedObject(OAuth2AuthorizationService.class, authorizationService);
return this;
}
/**
* Sets the authorization consent service.
* @param authorizationConsentService the authorization consent service
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
*/
public OAuth2AuthorizationServerConfigurer authorizationConsentService(
OAuth2AuthorizationConsentService authorizationConsentService) {
Assert.notNull(authorizationConsentService, "authorizationConsentService cannot be null");
getBuilder().setSharedObject(OAuth2AuthorizationConsentService.class, authorizationConsentService);
return this;
}
/**
* Sets the authorization server settings.
* @param authorizationServerSettings the authorization server settings
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
*/
public OAuth2AuthorizationServerConfigurer authorizationServerSettings(
AuthorizationServerSettings authorizationServerSettings) {
Assert.notNull(authorizationServerSettings, "authorizationServerSettings cannot be null");
getBuilder().setSharedObject(AuthorizationServerSettings.class, authorizationServerSettings);
return this;
}
/**
* Sets the token generator.
* @param tokenGenerator the token generator
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
*/
public OAuth2AuthorizationServerConfigurer tokenGenerator(
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
getBuilder().setSharedObject(OAuth2TokenGenerator.class, tokenGenerator);
return this;
}
/**
* Configures OAuth 2.0 Client Authentication.
* @param clientAuthenticationCustomizer the {@link Customizer} providing access to
* the {@link OAuth2ClientAuthenticationConfigurer}
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
*/
public OAuth2AuthorizationServerConfigurer clientAuthentication(
Customizer<OAuth2ClientAuthenticationConfigurer> clientAuthenticationCustomizer) {
clientAuthenticationCustomizer.customize(getConfigurer(OAuth2ClientAuthenticationConfigurer.class));
return this;
}
/**
* Configures the OAuth 2.0 Authorization Server Metadata Endpoint.
* @param authorizationServerMetadataEndpointCustomizer the {@link Customizer}
* providing access to the {@link OAuth2AuthorizationServerMetadataEndpointConfigurer}
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
*/
public OAuth2AuthorizationServerConfigurer authorizationServerMetadataEndpoint(
Customizer<OAuth2AuthorizationServerMetadataEndpointConfigurer> authorizationServerMetadataEndpointCustomizer) {
authorizationServerMetadataEndpointCustomizer
.customize(getConfigurer(OAuth2AuthorizationServerMetadataEndpointConfigurer.class));
return this;
}
/**
* Configures the OAuth 2.0 Authorization Endpoint.
* @param authorizationEndpointCustomizer the {@link Customizer} providing access to
* the {@link OAuth2AuthorizationEndpointConfigurer}
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
*/
public OAuth2AuthorizationServerConfigurer authorizationEndpoint(
Customizer<OAuth2AuthorizationEndpointConfigurer> authorizationEndpointCustomizer) {
authorizationEndpointCustomizer.customize(getConfigurer(OAuth2AuthorizationEndpointConfigurer.class));
return this;
}
/**
* Configures the OAuth 2.0 Pushed Authorization Request Endpoint.
* @param pushedAuthorizationRequestEndpointCustomizer the {@link Customizer}
* providing access to the {@link OAuth2PushedAuthorizationRequestEndpointConfigurer}
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
*/
public OAuth2AuthorizationServerConfigurer pushedAuthorizationRequestEndpoint(
Customizer<OAuth2PushedAuthorizationRequestEndpointConfigurer> pushedAuthorizationRequestEndpointCustomizer) {
OAuth2PushedAuthorizationRequestEndpointConfigurer pushedAuthorizationRequestEndpointConfigurer = getConfigurer(
OAuth2PushedAuthorizationRequestEndpointConfigurer.class);
if (pushedAuthorizationRequestEndpointConfigurer == null) {
addConfigurer(OAuth2PushedAuthorizationRequestEndpointConfigurer.class,
new OAuth2PushedAuthorizationRequestEndpointConfigurer(this::postProcess));
pushedAuthorizationRequestEndpointConfigurer = getConfigurer(
OAuth2PushedAuthorizationRequestEndpointConfigurer.class);
}
pushedAuthorizationRequestEndpointCustomizer.customize(pushedAuthorizationRequestEndpointConfigurer);
return this;
}
/**
* Configures the OAuth 2.0 Token Endpoint.
* @param tokenEndpointCustomizer the {@link Customizer} providing access to the
* {@link OAuth2TokenEndpointConfigurer}
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
*/
public OAuth2AuthorizationServerConfigurer tokenEndpoint(
Customizer<OAuth2TokenEndpointConfigurer> tokenEndpointCustomizer) {
tokenEndpointCustomizer.customize(getConfigurer(OAuth2TokenEndpointConfigurer.class));
return this;
}
/**
* Configures the OAuth 2.0 Token Introspection Endpoint.
* @param tokenIntrospectionEndpointCustomizer the {@link Customizer} providing access
* to the {@link OAuth2TokenIntrospectionEndpointConfigurer}
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
*/
public OAuth2AuthorizationServerConfigurer tokenIntrospectionEndpoint(
Customizer<OAuth2TokenIntrospectionEndpointConfigurer> tokenIntrospectionEndpointCustomizer) {
tokenIntrospectionEndpointCustomizer.customize(getConfigurer(OAuth2TokenIntrospectionEndpointConfigurer.class));
return this;
}
/**
* Configures the OAuth 2.0 Token Revocation Endpoint.
* @param tokenRevocationEndpointCustomizer the {@link Customizer} providing access to
* the {@link OAuth2TokenRevocationEndpointConfigurer}
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
*/
public OAuth2AuthorizationServerConfigurer tokenRevocationEndpoint(
Customizer<OAuth2TokenRevocationEndpointConfigurer> tokenRevocationEndpointCustomizer) {
tokenRevocationEndpointCustomizer.customize(getConfigurer(OAuth2TokenRevocationEndpointConfigurer.class));
return this;
}
/**
* Configures the OAuth 2.0 Device Authorization Endpoint.
* @param deviceAuthorizationEndpointCustomizer the {@link Customizer} providing
* access to the {@link OAuth2DeviceAuthorizationEndpointConfigurer}
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
*/
public OAuth2AuthorizationServerConfigurer deviceAuthorizationEndpoint(
Customizer<OAuth2DeviceAuthorizationEndpointConfigurer> deviceAuthorizationEndpointCustomizer) {
deviceAuthorizationEndpointCustomizer
.customize(getConfigurer(OAuth2DeviceAuthorizationEndpointConfigurer.class));
return this;
}
/**
* Configures the OAuth 2.0 Device Verification Endpoint.
* @param deviceVerificationEndpointCustomizer the {@link Customizer} providing access
* to the {@link OAuth2DeviceVerificationEndpointConfigurer}
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
*/
public OAuth2AuthorizationServerConfigurer deviceVerificationEndpoint(
Customizer<OAuth2DeviceVerificationEndpointConfigurer> deviceVerificationEndpointCustomizer) {
deviceVerificationEndpointCustomizer.customize(getConfigurer(OAuth2DeviceVerificationEndpointConfigurer.class));
return this;
}
/**
* Configures OpenID Connect 1.0 support (disabled by default).
* @param oidcCustomizer the {@link Customizer} providing access to the
* {@link OidcConfigurer}
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
*/
public OAuth2AuthorizationServerConfigurer oidc(Customizer<OidcConfigurer> oidcCustomizer) {
OidcConfigurer oidcConfigurer = getConfigurer(OidcConfigurer.class);
if (oidcConfigurer == null) {
addConfigurer(OidcConfigurer.class, new OidcConfigurer(this::postProcess));
oidcConfigurer = getConfigurer(OidcConfigurer.class);
}
oidcCustomizer.customize(oidcConfigurer);
return this;
}
/**
* Returns a {@link RequestMatcher} for the authorization server endpoints.
* @return a {@link RequestMatcher} for the authorization server endpoints
*/
public RequestMatcher getEndpointsMatcher() {
// Return a deferred RequestMatcher
// since endpointsMatcher is constructed in init(HttpSecurity).
return (request) -> this.endpointsMatcher.matches(request);
}
@Override
public void init(HttpSecurity httpSecurity) throws Exception {
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
.getAuthorizationServerSettings(httpSecurity);
validateAuthorizationServerSettings(authorizationServerSettings);
if (isOidcEnabled()) {
// Add OpenID Connect session tracking capabilities.
initSessionRegistry(httpSecurity);
SessionRegistry sessionRegistry = httpSecurity.getSharedObject(SessionRegistry.class);
OAuth2AuthorizationEndpointConfigurer authorizationEndpointConfigurer = getConfigurer(
OAuth2AuthorizationEndpointConfigurer.class);
authorizationEndpointConfigurer.setSessionAuthenticationStrategy((authentication, request, response) -> {
if (authentication instanceof OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication) {
if (authorizationCodeRequestAuthentication.getScopes().contains(OidcScopes.OPENID)) {
if (sessionRegistry.getSessionInformation(request.getSession().getId()) == null) {
sessionRegistry.registerNewSession(request.getSession().getId(),
((Authentication) authorizationCodeRequestAuthentication.getPrincipal())
.getPrincipal());
}
}
}
});
}
else {
// OpenID Connect is disabled.
// Add an authentication validator that rejects authentication requests.
Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> oidcAuthenticationRequestValidator = (
authenticationContext) -> {
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = authenticationContext
.getAuthentication();
if (authorizationCodeRequestAuthentication.getScopes().contains(OidcScopes.OPENID)) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_SCOPE,
"OpenID Connect 1.0 authentication requests are restricted.",
"https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1");
throw new OAuth2AuthorizationCodeRequestAuthenticationException(error,
authorizationCodeRequestAuthentication);
}
};
OAuth2AuthorizationEndpointConfigurer authorizationEndpointConfigurer = getConfigurer(
OAuth2AuthorizationEndpointConfigurer.class);
authorizationEndpointConfigurer
.addAuthorizationCodeRequestAuthenticationValidator(oidcAuthenticationRequestValidator);
OAuth2PushedAuthorizationRequestEndpointConfigurer pushedAuthorizationRequestEndpointConfigurer = getConfigurer(
OAuth2PushedAuthorizationRequestEndpointConfigurer.class);
if (pushedAuthorizationRequestEndpointConfigurer != null) {
pushedAuthorizationRequestEndpointConfigurer
.addAuthorizationCodeRequestAuthenticationValidator(oidcAuthenticationRequestValidator);
}
}
List<RequestMatcher> requestMatchers = new ArrayList<>();
this.configurers.values().forEach((configurer) -> {
configurer.init(httpSecurity);
requestMatchers.add(configurer.getRequestMatcher());
});
String jwkSetEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
? OAuth2ConfigurerUtils.withMultipleIssuersPattern(authorizationServerSettings.getJwkSetEndpoint())
: authorizationServerSettings.getJwkSetEndpoint();
requestMatchers.add(PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.GET, jwkSetEndpointUri));
this.endpointsMatcher = new OrRequestMatcher(requestMatchers);
ExceptionHandlingConfigurer<HttpSecurity> exceptionHandling = httpSecurity
.getConfigurer(ExceptionHandlingConfigurer.class);
if (exceptionHandling != null) {
List<RequestMatcher> preferredMatchers = new ArrayList<>();
preferredMatchers.add(getRequestMatcher(OAuth2TokenEndpointConfigurer.class));
preferredMatchers.add(getRequestMatcher(OAuth2TokenIntrospectionEndpointConfigurer.class));
preferredMatchers.add(getRequestMatcher(OAuth2TokenRevocationEndpointConfigurer.class));
preferredMatchers.add(getRequestMatcher(OAuth2DeviceAuthorizationEndpointConfigurer.class));
RequestMatcher preferredMatcher = getRequestMatcher(
OAuth2PushedAuthorizationRequestEndpointConfigurer.class);
if (preferredMatcher != null) {
preferredMatchers.add(preferredMatcher);
}
exceptionHandling.defaultAuthenticationEntryPointFor(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED),
new OrRequestMatcher(preferredMatchers));
}
httpSecurity.csrf((csrf) -> csrf.ignoringRequestMatchers(this.endpointsMatcher));
OidcConfigurer oidcConfigurer = getConfigurer(OidcConfigurer.class);
if (oidcConfigurer != null) {
if (oidcConfigurer.getConfigurer(OidcUserInfoEndpointConfigurer.class) != null
|| oidcConfigurer.getConfigurer(OidcClientRegistrationEndpointConfigurer.class) != null) {
httpSecurity
// Accept access tokens for User Info and/or Client Registration
.oauth2ResourceServer(
(oauth2ResourceServer) -> oauth2ResourceServer.jwt(Customizer.withDefaults()));
}
}
}
@Override
public void configure(HttpSecurity httpSecurity) {
this.configurers.values().forEach((configurer) -> configurer.configure(httpSecurity));
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
.getAuthorizationServerSettings(httpSecurity);
AuthorizationServerContextFilter authorizationServerContextFilter = new AuthorizationServerContextFilter(
authorizationServerSettings);
httpSecurity.addFilterAfter(postProcess(authorizationServerContextFilter), SecurityContextHolderFilter.class);
JWKSource<com.nimbusds.jose.proc.SecurityContext> jwkSource = OAuth2ConfigurerUtils.getJwkSource(httpSecurity);
if (jwkSource != null) {
String jwkSetEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
? OAuth2ConfigurerUtils.withMultipleIssuersPattern(authorizationServerSettings.getJwkSetEndpoint())
: authorizationServerSettings.getJwkSetEndpoint();
NimbusJwkSetEndpointFilter jwkSetEndpointFilter = new NimbusJwkSetEndpointFilter(jwkSource,
jwkSetEndpointUri);
httpSecurity.addFilterBefore(postProcess(jwkSetEndpointFilter),
AbstractPreAuthenticatedProcessingFilter.class);
}
}
private boolean isOidcEnabled() {
return getConfigurer(OidcConfigurer.class) != null;
}
private Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer> createConfigurers() {
Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer> configurers = new LinkedHashMap<>();
configurers.put(OAuth2ClientAuthenticationConfigurer.class,
new OAuth2ClientAuthenticationConfigurer(this::postProcess));
configurers.put(OAuth2AuthorizationServerMetadataEndpointConfigurer.class,
new OAuth2AuthorizationServerMetadataEndpointConfigurer(this::postProcess));
configurers.put(OAuth2AuthorizationEndpointConfigurer.class,
new OAuth2AuthorizationEndpointConfigurer(this::postProcess));
configurers.put(OAuth2TokenEndpointConfigurer.class, new OAuth2TokenEndpointConfigurer(this::postProcess));
configurers.put(OAuth2TokenIntrospectionEndpointConfigurer.class,
new OAuth2TokenIntrospectionEndpointConfigurer(this::postProcess));
configurers.put(OAuth2TokenRevocationEndpointConfigurer.class,
new OAuth2TokenRevocationEndpointConfigurer(this::postProcess));
configurers.put(OAuth2DeviceAuthorizationEndpointConfigurer.class,
new OAuth2DeviceAuthorizationEndpointConfigurer(this::postProcess));
configurers.put(OAuth2DeviceVerificationEndpointConfigurer.class,
new OAuth2DeviceVerificationEndpointConfigurer(this::postProcess));
return configurers;
}
@SuppressWarnings("unchecked")
private <T> T getConfigurer(Class<T> type) {
return (T) this.configurers.get(type);
}
private <T extends AbstractOAuth2Configurer> void addConfigurer(Class<T> configurerType, T configurer) {
this.configurers.put(configurerType, configurer);
}
private <T extends AbstractOAuth2Configurer> RequestMatcher getRequestMatcher(Class<T> configurerType) {
T configurer = getConfigurer(configurerType);
return (configurer != null) ? configurer.getRequestMatcher() : null;
}
private static void validateAuthorizationServerSettings(AuthorizationServerSettings authorizationServerSettings) {
if (authorizationServerSettings.getIssuer() != null) {
URI issuerUri;
try {
issuerUri = new URI(authorizationServerSettings.getIssuer());
issuerUri.toURL();
}
catch (Exception ex) {
throw new IllegalArgumentException("issuer must be a valid URL", ex);
}
// rfc8414 https://datatracker.ietf.org/doc/html/rfc8414#section-2
if (issuerUri.getQuery() != null || issuerUri.getFragment() != null) {
throw new IllegalArgumentException("issuer cannot contain query or fragment component");
}
}
}
private static void initSessionRegistry(HttpSecurity httpSecurity) {
SessionRegistry sessionRegistry = OAuth2ConfigurerUtils.getOptionalBean(httpSecurity, SessionRegistry.class);
if (sessionRegistry == null) {
sessionRegistry = new SessionRegistryImpl();
registerDelegateApplicationListener(httpSecurity, (SessionRegistryImpl) sessionRegistry);
}
httpSecurity.setSharedObject(SessionRegistry.class, sessionRegistry);
}
private static void registerDelegateApplicationListener(HttpSecurity httpSecurity,
ApplicationListener<?> delegate) {
DelegatingApplicationListener delegatingApplicationListener = OAuth2ConfigurerUtils
.getOptionalBean(httpSecurity, DelegatingApplicationListener.class);
if (delegatingApplicationListener == null) {
return;
}
SmartApplicationListener smartListener = new GenericApplicationListenerAdapter(delegate);
delegatingApplicationListener.addListener(smartListener);
}
}
@@ -0,0 +1,120 @@
/*
* Copyright 2004-present 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.config.annotation.web.configurers.oauth2.server.authorization;
import java.util.function.Consumer;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationServerMetadata;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationServerMetadataEndpointFilter;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
/**
* Configurer for the OAuth 2.0 Authorization Server Metadata Endpoint.
*
* @author Joe Grandja
* @since 7.0
* @see OAuth2AuthorizationServerConfigurer#authorizationServerMetadataEndpoint
* @see OAuth2AuthorizationServerMetadataEndpointFilter
*/
public final class OAuth2AuthorizationServerMetadataEndpointConfigurer extends AbstractOAuth2Configurer {
private RequestMatcher requestMatcher;
private Consumer<OAuth2AuthorizationServerMetadata.Builder> authorizationServerMetadataCustomizer;
private Consumer<OAuth2AuthorizationServerMetadata.Builder> defaultAuthorizationServerMetadataCustomizer;
/**
* Restrict for internal use only.
* @param objectPostProcessor an {@code ObjectPostProcessor}
*/
OAuth2AuthorizationServerMetadataEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
super(objectPostProcessor);
}
/**
* Sets the {@code Consumer} providing access to the
* {@link OAuth2AuthorizationServerMetadata.Builder} allowing the ability to customize
* the claims of the Authorization Server's configuration.
* @param authorizationServerMetadataCustomizer the {@code Consumer} providing access
* to the {@link OAuth2AuthorizationServerMetadata.Builder}
* @return the {@link OAuth2AuthorizationServerMetadataEndpointConfigurer} for further
* configuration
*/
public OAuth2AuthorizationServerMetadataEndpointConfigurer authorizationServerMetadataCustomizer(
Consumer<OAuth2AuthorizationServerMetadata.Builder> authorizationServerMetadataCustomizer) {
this.authorizationServerMetadataCustomizer = authorizationServerMetadataCustomizer;
return this;
}
void addDefaultAuthorizationServerMetadataCustomizer(
Consumer<OAuth2AuthorizationServerMetadata.Builder> defaultAuthorizationServerMetadataCustomizer) {
this.defaultAuthorizationServerMetadataCustomizer = (this.defaultAuthorizationServerMetadataCustomizer == null)
? defaultAuthorizationServerMetadataCustomizer : this.defaultAuthorizationServerMetadataCustomizer
.andThen(defaultAuthorizationServerMetadataCustomizer);
}
@Override
void init(HttpSecurity httpSecurity) {
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
.getAuthorizationServerSettings(httpSecurity);
String authorizationServerMetadataEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
? "/.well-known/oauth-authorization-server/**" : "/.well-known/oauth-authorization-server";
this.requestMatcher = PathPatternRequestMatcher.withDefaults()
.matcher(HttpMethod.GET, authorizationServerMetadataEndpointUri);
}
@Override
void configure(HttpSecurity httpSecurity) {
OAuth2AuthorizationServerMetadataEndpointFilter authorizationServerMetadataEndpointFilter = new OAuth2AuthorizationServerMetadataEndpointFilter();
Consumer<OAuth2AuthorizationServerMetadata.Builder> authorizationServerMetadataCustomizer = getAuthorizationServerMetadataCustomizer();
if (authorizationServerMetadataCustomizer != null) {
authorizationServerMetadataEndpointFilter
.setAuthorizationServerMetadataCustomizer(authorizationServerMetadataCustomizer);
}
httpSecurity.addFilterBefore(postProcess(authorizationServerMetadataEndpointFilter),
AbstractPreAuthenticatedProcessingFilter.class);
}
private Consumer<OAuth2AuthorizationServerMetadata.Builder> getAuthorizationServerMetadataCustomizer() {
Consumer<OAuth2AuthorizationServerMetadata.Builder> authorizationServerMetadataCustomizer = null;
if (this.defaultAuthorizationServerMetadataCustomizer != null
|| this.authorizationServerMetadataCustomizer != null) {
if (this.defaultAuthorizationServerMetadataCustomizer != null) {
authorizationServerMetadataCustomizer = this.defaultAuthorizationServerMetadataCustomizer;
}
if (this.authorizationServerMetadataCustomizer != null) {
authorizationServerMetadataCustomizer = (authorizationServerMetadataCustomizer != null)
? authorizationServerMetadataCustomizer.andThen(this.authorizationServerMetadataCustomizer)
: this.authorizationServerMetadataCustomizer;
}
}
return authorizationServerMetadataCustomizer;
}
@Override
RequestMatcher getRequestMatcher() {
return this.requestMatcher;
}
}
@@ -0,0 +1,287 @@
/*
* Copyright 2004-present 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.config.annotation.web.configurers.oauth2.server.authorization;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.authentication.ClientSecretAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.JwtClientAssertionAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.PublicClientAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.X509ClientCertificateAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.web.OAuth2ClientAuthenticationFilter;
import org.springframework.security.oauth2.server.authorization.web.authentication.ClientSecretBasicAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.ClientSecretPostAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.JwtClientAssertionAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.PublicClientAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.X509ClientCertificateAuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.DelegatingAuthenticationConverter;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
/**
* Configurer for OAuth 2.0 Client Authentication.
*
* @author Joe Grandja
* @since 7.0
* @see OAuth2AuthorizationServerConfigurer#clientAuthentication
* @see OAuth2ClientAuthenticationFilter
*/
public final class OAuth2ClientAuthenticationConfigurer extends AbstractOAuth2Configurer {
private RequestMatcher requestMatcher;
private final List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
private Consumer<List<AuthenticationConverter>> authenticationConvertersConsumer = (authenticationConverters) -> {
};
private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
private Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer = (authenticationProviders) -> {
};
private AuthenticationSuccessHandler authenticationSuccessHandler;
private AuthenticationFailureHandler errorResponseHandler;
/**
* Restrict for internal use only.
* @param objectPostProcessor an {@code ObjectPostProcessor}
*/
OAuth2ClientAuthenticationConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
super(objectPostProcessor);
}
/**
* Adds an {@link AuthenticationConverter} used when attempting to extract client
* credentials from {@link HttpServletRequest} to an instance of
* {@link OAuth2ClientAuthenticationToken} used for authenticating the client.
* @param authenticationConverter an {@link AuthenticationConverter} used when
* attempting to extract client credentials from {@link HttpServletRequest}
* @return the {@link OAuth2ClientAuthenticationConfigurer} for further configuration
*/
public OAuth2ClientAuthenticationConfigurer authenticationConverter(
AuthenticationConverter authenticationConverter) {
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
this.authenticationConverters.add(authenticationConverter);
return this;
}
/**
* Sets the {@code Consumer} providing access to the {@code List} of default and
* (optionally) added {@link #authenticationConverter(AuthenticationConverter)
* AuthenticationConverter}'s allowing the ability to add, remove, or customize a
* specific {@link AuthenticationConverter}.
* @param authenticationConvertersConsumer the {@code Consumer} providing access to
* the {@code List} of default and (optionally) added
* {@link AuthenticationConverter}'s
* @return the {@link OAuth2ClientAuthenticationConfigurer} for further configuration
*/
public OAuth2ClientAuthenticationConfigurer authenticationConverters(
Consumer<List<AuthenticationConverter>> authenticationConvertersConsumer) {
Assert.notNull(authenticationConvertersConsumer, "authenticationConvertersConsumer cannot be null");
this.authenticationConvertersConsumer = authenticationConvertersConsumer;
return this;
}
/**
* Adds an {@link AuthenticationProvider} used for authenticating an
* {@link OAuth2ClientAuthenticationToken}.
* @param authenticationProvider an {@link AuthenticationProvider} used for
* authenticating an {@link OAuth2ClientAuthenticationToken}
* @return the {@link OAuth2ClientAuthenticationConfigurer} for further configuration
*/
public OAuth2ClientAuthenticationConfigurer authenticationProvider(AuthenticationProvider authenticationProvider) {
Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
this.authenticationProviders.add(authenticationProvider);
return this;
}
/**
* Sets the {@code Consumer} providing access to the {@code List} of default and
* (optionally) added {@link #authenticationProvider(AuthenticationProvider)
* AuthenticationProvider}'s allowing the ability to add, remove, or customize a
* specific {@link AuthenticationProvider}.
* @param authenticationProvidersConsumer the {@code Consumer} providing access to the
* {@code List} of default and (optionally) added {@link AuthenticationProvider}'s
* @return the {@link OAuth2ClientAuthenticationConfigurer} for further configuration
*/
public OAuth2ClientAuthenticationConfigurer authenticationProviders(
Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer) {
Assert.notNull(authenticationProvidersConsumer, "authenticationProvidersConsumer cannot be null");
this.authenticationProvidersConsumer = authenticationProvidersConsumer;
return this;
}
/**
* Sets the {@link AuthenticationSuccessHandler} used for handling a successful client
* authentication and associating the {@link OAuth2ClientAuthenticationToken} to the
* {@link SecurityContext}.
* @param authenticationSuccessHandler the {@link AuthenticationSuccessHandler} used
* for handling a successful client authentication
* @return the {@link OAuth2ClientAuthenticationConfigurer} for further configuration
*/
public OAuth2ClientAuthenticationConfigurer authenticationSuccessHandler(
AuthenticationSuccessHandler authenticationSuccessHandler) {
this.authenticationSuccessHandler = authenticationSuccessHandler;
return this;
}
/**
* Sets the {@link AuthenticationFailureHandler} used for handling a failed client
* authentication and returning the {@link OAuth2Error Error Response}.
* @param errorResponseHandler the {@link AuthenticationFailureHandler} used for
* handling a failed client authentication
* @return the {@link OAuth2ClientAuthenticationConfigurer} for further configuration
*/
public OAuth2ClientAuthenticationConfigurer errorResponseHandler(
AuthenticationFailureHandler errorResponseHandler) {
this.errorResponseHandler = errorResponseHandler;
return this;
}
@Override
void init(HttpSecurity httpSecurity) {
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
.getAuthorizationServerSettings(httpSecurity);
String tokenEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
? OAuth2ConfigurerUtils.withMultipleIssuersPattern(authorizationServerSettings.getTokenEndpoint())
: authorizationServerSettings.getTokenEndpoint();
String tokenIntrospectionEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
? OAuth2ConfigurerUtils
.withMultipleIssuersPattern(authorizationServerSettings.getTokenIntrospectionEndpoint())
: authorizationServerSettings.getTokenIntrospectionEndpoint();
String tokenRevocationEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
? OAuth2ConfigurerUtils
.withMultipleIssuersPattern(authorizationServerSettings.getTokenRevocationEndpoint())
: authorizationServerSettings.getTokenRevocationEndpoint();
String deviceAuthorizationEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
? OAuth2ConfigurerUtils
.withMultipleIssuersPattern(authorizationServerSettings.getDeviceAuthorizationEndpoint())
: authorizationServerSettings.getDeviceAuthorizationEndpoint();
String pushedAuthorizationRequestEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
? OAuth2ConfigurerUtils
.withMultipleIssuersPattern(authorizationServerSettings.getPushedAuthorizationRequestEndpoint())
: authorizationServerSettings.getPushedAuthorizationRequestEndpoint();
this.requestMatcher = new OrRequestMatcher(
PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, tokenEndpointUri),
PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, tokenIntrospectionEndpointUri),
PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, tokenRevocationEndpointUri),
PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, deviceAuthorizationEndpointUri),
PathPatternRequestMatcher.withDefaults()
.matcher(HttpMethod.POST, pushedAuthorizationRequestEndpointUri));
List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity);
if (!this.authenticationProviders.isEmpty()) {
authenticationProviders.addAll(0, this.authenticationProviders);
}
this.authenticationProvidersConsumer.accept(authenticationProviders);
authenticationProviders.forEach(
(authenticationProvider) -> httpSecurity.authenticationProvider(postProcess(authenticationProvider)));
}
@Override
void configure(HttpSecurity httpSecurity) {
AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class);
OAuth2ClientAuthenticationFilter clientAuthenticationFilter = new OAuth2ClientAuthenticationFilter(
authenticationManager, this.requestMatcher);
List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
if (!this.authenticationConverters.isEmpty()) {
authenticationConverters.addAll(0, this.authenticationConverters);
}
this.authenticationConvertersConsumer.accept(authenticationConverters);
clientAuthenticationFilter
.setAuthenticationConverter(new DelegatingAuthenticationConverter(authenticationConverters));
if (this.authenticationSuccessHandler != null) {
clientAuthenticationFilter.setAuthenticationSuccessHandler(this.authenticationSuccessHandler);
}
if (this.errorResponseHandler != null) {
clientAuthenticationFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
}
httpSecurity.addFilterAfter(postProcess(clientAuthenticationFilter),
AbstractPreAuthenticatedProcessingFilter.class);
}
@Override
RequestMatcher getRequestMatcher() {
return this.requestMatcher;
}
private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
authenticationConverters.add(new JwtClientAssertionAuthenticationConverter());
authenticationConverters.add(new ClientSecretBasicAuthenticationConverter());
authenticationConverters.add(new ClientSecretPostAuthenticationConverter());
authenticationConverters.add(new PublicClientAuthenticationConverter());
authenticationConverters.add(new X509ClientCertificateAuthenticationConverter());
return authenticationConverters;
}
private static List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
RegisteredClientRepository registeredClientRepository = OAuth2ConfigurerUtils
.getRegisteredClientRepository(httpSecurity);
OAuth2AuthorizationService authorizationService = OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity);
JwtClientAssertionAuthenticationProvider jwtClientAssertionAuthenticationProvider = new JwtClientAssertionAuthenticationProvider(
registeredClientRepository, authorizationService);
authenticationProviders.add(jwtClientAssertionAuthenticationProvider);
X509ClientCertificateAuthenticationProvider x509ClientCertificateAuthenticationProvider = new X509ClientCertificateAuthenticationProvider(
registeredClientRepository, authorizationService);
authenticationProviders.add(x509ClientCertificateAuthenticationProvider);
ClientSecretAuthenticationProvider clientSecretAuthenticationProvider = new ClientSecretAuthenticationProvider(
registeredClientRepository, authorizationService);
PasswordEncoder passwordEncoder = OAuth2ConfigurerUtils.getOptionalBean(httpSecurity, PasswordEncoder.class);
if (passwordEncoder != null) {
clientSecretAuthenticationProvider.setPasswordEncoder(passwordEncoder);
}
authenticationProviders.add(clientSecretAuthenticationProvider);
PublicClientAuthenticationProvider publicClientAuthenticationProvider = new PublicClientAuthenticationProvider(
registeredClientRepository, authorizationService);
authenticationProviders.add(publicClientAuthenticationProvider);
return authenticationProviders;
}
}
@@ -0,0 +1,247 @@
/*
* Copyright 2004-present 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.config.annotation.web.configurers.oauth2.server.authorization;
import java.util.Map;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.core.ResolvableType;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator;
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.JwtGenerator;
import org.springframework.security.oauth2.server.authorization.token.OAuth2AccessTokenGenerator;
import org.springframework.security.oauth2.server.authorization.token.OAuth2RefreshTokenGenerator;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Utility methods for the OAuth 2.0 Configurers.
*
* @author Joe Grandja
* @since 7.0
*/
final class OAuth2ConfigurerUtils {
private OAuth2ConfigurerUtils() {
}
static String withMultipleIssuersPattern(String endpointUri) {
Assert.hasText(endpointUri, "endpointUri cannot be empty");
return endpointUri.startsWith("/") ? "/**" + endpointUri : "/**/" + endpointUri;
}
static RegisteredClientRepository getRegisteredClientRepository(HttpSecurity httpSecurity) {
RegisteredClientRepository registeredClientRepository = httpSecurity
.getSharedObject(RegisteredClientRepository.class);
if (registeredClientRepository == null) {
registeredClientRepository = getBean(httpSecurity, RegisteredClientRepository.class);
httpSecurity.setSharedObject(RegisteredClientRepository.class, registeredClientRepository);
}
return registeredClientRepository;
}
static OAuth2AuthorizationService getAuthorizationService(HttpSecurity httpSecurity) {
OAuth2AuthorizationService authorizationService = httpSecurity
.getSharedObject(OAuth2AuthorizationService.class);
if (authorizationService == null) {
authorizationService = getOptionalBean(httpSecurity, OAuth2AuthorizationService.class);
if (authorizationService == null) {
authorizationService = new InMemoryOAuth2AuthorizationService();
}
httpSecurity.setSharedObject(OAuth2AuthorizationService.class, authorizationService);
}
return authorizationService;
}
static OAuth2AuthorizationConsentService getAuthorizationConsentService(HttpSecurity httpSecurity) {
OAuth2AuthorizationConsentService authorizationConsentService = httpSecurity
.getSharedObject(OAuth2AuthorizationConsentService.class);
if (authorizationConsentService == null) {
authorizationConsentService = getOptionalBean(httpSecurity, OAuth2AuthorizationConsentService.class);
if (authorizationConsentService == null) {
authorizationConsentService = new InMemoryOAuth2AuthorizationConsentService();
}
httpSecurity.setSharedObject(OAuth2AuthorizationConsentService.class, authorizationConsentService);
}
return authorizationConsentService;
}
@SuppressWarnings("unchecked")
static OAuth2TokenGenerator<? extends OAuth2Token> getTokenGenerator(HttpSecurity httpSecurity) {
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator = httpSecurity
.getSharedObject(OAuth2TokenGenerator.class);
if (tokenGenerator == null) {
tokenGenerator = getOptionalBean(httpSecurity, OAuth2TokenGenerator.class);
if (tokenGenerator == null) {
JwtGenerator jwtGenerator = getJwtGenerator(httpSecurity);
OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
accessTokenGenerator.setAccessTokenCustomizer(getAccessTokenCustomizer(httpSecurity));
OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
if (jwtGenerator != null) {
tokenGenerator = new DelegatingOAuth2TokenGenerator(jwtGenerator, accessTokenGenerator,
refreshTokenGenerator);
}
else {
tokenGenerator = new DelegatingOAuth2TokenGenerator(accessTokenGenerator, refreshTokenGenerator);
}
}
httpSecurity.setSharedObject(OAuth2TokenGenerator.class, tokenGenerator);
}
return tokenGenerator;
}
private static JwtGenerator getJwtGenerator(HttpSecurity httpSecurity) {
JwtGenerator jwtGenerator = httpSecurity.getSharedObject(JwtGenerator.class);
if (jwtGenerator == null) {
JwtEncoder jwtEncoder = getJwtEncoder(httpSecurity);
if (jwtEncoder != null) {
jwtGenerator = new JwtGenerator(jwtEncoder);
jwtGenerator.setJwtCustomizer(getJwtCustomizer(httpSecurity));
httpSecurity.setSharedObject(JwtGenerator.class, jwtGenerator);
}
}
return jwtGenerator;
}
private static JwtEncoder getJwtEncoder(HttpSecurity httpSecurity) {
JwtEncoder jwtEncoder = httpSecurity.getSharedObject(JwtEncoder.class);
if (jwtEncoder == null) {
jwtEncoder = getOptionalBean(httpSecurity, JwtEncoder.class);
if (jwtEncoder == null) {
JWKSource<SecurityContext> jwkSource = getJwkSource(httpSecurity);
if (jwkSource != null) {
jwtEncoder = new NimbusJwtEncoder(jwkSource);
}
}
if (jwtEncoder != null) {
httpSecurity.setSharedObject(JwtEncoder.class, jwtEncoder);
}
}
return jwtEncoder;
}
@SuppressWarnings("unchecked")
static JWKSource<SecurityContext> getJwkSource(HttpSecurity httpSecurity) {
JWKSource<SecurityContext> jwkSource = httpSecurity.getSharedObject(JWKSource.class);
if (jwkSource == null) {
ResolvableType type = ResolvableType.forClassWithGenerics(JWKSource.class, SecurityContext.class);
jwkSource = getOptionalBean(httpSecurity, type);
if (jwkSource != null) {
httpSecurity.setSharedObject(JWKSource.class, jwkSource);
}
}
return jwkSource;
}
private static OAuth2TokenCustomizer<JwtEncodingContext> getJwtCustomizer(HttpSecurity httpSecurity) {
final OAuth2TokenCustomizer<JwtEncodingContext> defaultJwtCustomizer = DefaultOAuth2TokenCustomizers
.jwtCustomizer();
ResolvableType type = ResolvableType.forClassWithGenerics(OAuth2TokenCustomizer.class,
JwtEncodingContext.class);
final OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer = getOptionalBean(httpSecurity, type);
if (jwtCustomizer == null) {
return defaultJwtCustomizer;
}
return (context) -> {
defaultJwtCustomizer.customize(context);
jwtCustomizer.customize(context);
};
}
private static OAuth2TokenCustomizer<OAuth2TokenClaimsContext> getAccessTokenCustomizer(HttpSecurity httpSecurity) {
final OAuth2TokenCustomizer<OAuth2TokenClaimsContext> defaultAccessTokenCustomizer = DefaultOAuth2TokenCustomizers
.accessTokenCustomizer();
ResolvableType type = ResolvableType.forClassWithGenerics(OAuth2TokenCustomizer.class,
OAuth2TokenClaimsContext.class);
OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer = getOptionalBean(httpSecurity, type);
if (accessTokenCustomizer == null) {
return defaultAccessTokenCustomizer;
}
return (context) -> {
defaultAccessTokenCustomizer.customize(context);
accessTokenCustomizer.customize(context);
};
}
static AuthorizationServerSettings getAuthorizationServerSettings(HttpSecurity httpSecurity) {
AuthorizationServerSettings authorizationServerSettings = httpSecurity
.getSharedObject(AuthorizationServerSettings.class);
if (authorizationServerSettings == null) {
authorizationServerSettings = getBean(httpSecurity, AuthorizationServerSettings.class);
httpSecurity.setSharedObject(AuthorizationServerSettings.class, authorizationServerSettings);
}
return authorizationServerSettings;
}
static <T> T getBean(HttpSecurity httpSecurity, Class<T> type) {
return httpSecurity.getSharedObject(ApplicationContext.class).getBean(type);
}
@SuppressWarnings("unchecked")
static <T> T getBean(HttpSecurity httpSecurity, ResolvableType type) {
ApplicationContext context = httpSecurity.getSharedObject(ApplicationContext.class);
String[] names = context.getBeanNamesForType(type);
if (names.length == 1) {
return (T) context.getBean(names[0]);
}
if (names.length > 1) {
throw new NoUniqueBeanDefinitionException(type, names);
}
throw new NoSuchBeanDefinitionException(type);
}
static <T> T getOptionalBean(HttpSecurity httpSecurity, Class<T> type) {
Map<String, T> beansMap = BeanFactoryUtils
.beansOfTypeIncludingAncestors(httpSecurity.getSharedObject(ApplicationContext.class), type);
if (beansMap.size() > 1) {
throw new NoUniqueBeanDefinitionException(type, beansMap.size(),
"Expected single matching bean of type '" + type.getName() + "' but found " + beansMap.size() + ": "
+ StringUtils.collectionToCommaDelimitedString(beansMap.keySet()));
}
return (!beansMap.isEmpty() ? beansMap.values().iterator().next() : null);
}
@SuppressWarnings("unchecked")
static <T> T getOptionalBean(HttpSecurity httpSecurity, ResolvableType type) {
ApplicationContext context = httpSecurity.getSharedObject(ApplicationContext.class);
String[] names = context.getBeanNamesForType(type);
if (names.length > 1) {
throw new NoUniqueBeanDefinitionException(type, names);
}
return (names.length == 1) ? (T) context.getBean(names[0]) : null;
}
}
@@ -0,0 +1,274 @@
/*
* Copyright 2004-present 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.config.annotation.web.configurers.oauth2.server.authorization;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.endpoint.OAuth2DeviceAuthorizationResponse;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceAuthorizationRequestAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceAuthorizationRequestAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.web.OAuth2DeviceAuthorizationEndpointFilter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2DeviceAuthorizationRequestAuthenticationConverter;
import org.springframework.security.web.access.intercept.AuthorizationFilter;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.DelegatingAuthenticationConverter;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Configurer for the OAuth 2.0 Device Authorization Endpoint.
*
* @author Steve Riesenberg
* @since 7.0
* @see OAuth2AuthorizationServerConfigurer#deviceAuthorizationEndpoint
* @see OAuth2DeviceAuthorizationEndpointFilter
*/
public final class OAuth2DeviceAuthorizationEndpointConfigurer extends AbstractOAuth2Configurer {
private RequestMatcher requestMatcher;
private final List<AuthenticationConverter> deviceAuthorizationRequestConverters = new ArrayList<>();
private Consumer<List<AuthenticationConverter>> deviceAuthorizationRequestConvertersConsumer = (
deviceAuthorizationRequestConverters) -> {
};
private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
private Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer = (authenticationProviders) -> {
};
private AuthenticationSuccessHandler deviceAuthorizationResponseHandler;
private AuthenticationFailureHandler errorResponseHandler;
private String verificationUri;
/**
* Restrict for internal use only.
* @param objectPostProcessor an {@code ObjectPostProcessor}
*/
OAuth2DeviceAuthorizationEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
super(objectPostProcessor);
}
/**
* Sets the {@link AuthenticationConverter} used when attempting to extract a Device
* Authorization Request from {@link HttpServletRequest} to an instance of
* {@link OAuth2DeviceAuthorizationRequestAuthenticationToken} used for authenticating
* the request.
* @param deviceAuthorizationRequestConverter the {@link AuthenticationConverter} used
* when attempting to extract a Device Authorization Request from
* {@link HttpServletRequest}
* @return the {@link OAuth2DeviceAuthorizationEndpointConfigurer} for further
* configuration
*/
public OAuth2DeviceAuthorizationEndpointConfigurer deviceAuthorizationRequestConverter(
AuthenticationConverter deviceAuthorizationRequestConverter) {
Assert.notNull(deviceAuthorizationRequestConverter, "deviceAuthorizationRequestConverter cannot be null");
this.deviceAuthorizationRequestConverters.add(deviceAuthorizationRequestConverter);
return this;
}
/**
* Sets the {@code Consumer} providing access to the {@code List} of default and
* (optionally) added
* {@link #deviceAuthorizationRequestConverter(AuthenticationConverter)
* AuthenticationConverter}'s allowing the ability to add, remove, or customize a
* specific {@link AuthenticationConverter}.
* @param deviceAuthorizationRequestConvertersConsumer the {@code Consumer} providing
* access to the {@code List} of default and (optionally) added
* {@link AuthenticationConverter}'s
* @return the {@link OAuth2DeviceAuthorizationEndpointConfigurer} for further
* configuration
*/
public OAuth2DeviceAuthorizationEndpointConfigurer deviceAuthorizationRequestConverters(
Consumer<List<AuthenticationConverter>> deviceAuthorizationRequestConvertersConsumer) {
Assert.notNull(deviceAuthorizationRequestConvertersConsumer,
"deviceAuthorizationRequestConvertersConsumer cannot be null");
this.deviceAuthorizationRequestConvertersConsumer = deviceAuthorizationRequestConvertersConsumer;
return this;
}
/**
* Adds an {@link AuthenticationProvider} used for authenticating an
* {@link OAuth2DeviceAuthorizationRequestAuthenticationToken}.
* @param authenticationProvider an {@link AuthenticationProvider} used for
* authenticating an {@link OAuth2DeviceAuthorizationRequestAuthenticationToken}
* @return the {@link OAuth2DeviceAuthorizationEndpointConfigurer} for further
* configuration
*/
public OAuth2DeviceAuthorizationEndpointConfigurer authenticationProvider(
AuthenticationProvider authenticationProvider) {
Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
this.authenticationProviders.add(authenticationProvider);
return this;
}
/**
* Sets the {@code Consumer} providing access to the {@code List} of default and
* (optionally) added {@link #authenticationProvider(AuthenticationProvider)
* AuthenticationProvider}'s allowing the ability to add, remove, or customize a
* specific {@link AuthenticationProvider}.
* @param authenticationProvidersConsumer the {@code Consumer} providing access to the
* {@code List} of default and (optionally) added {@link AuthenticationProvider}'s
* @return the {@link OAuth2DeviceAuthorizationEndpointConfigurer} for further
* configuration
*/
public OAuth2DeviceAuthorizationEndpointConfigurer authenticationProviders(
Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer) {
Assert.notNull(authenticationProvidersConsumer, "authenticationProvidersConsumer cannot be null");
this.authenticationProvidersConsumer = authenticationProvidersConsumer;
return this;
}
/**
* Sets the {@link AuthenticationSuccessHandler} used for handling an
* {@link OAuth2DeviceAuthorizationRequestAuthenticationToken} and returning the
* {@link OAuth2DeviceAuthorizationResponse Device Authorization Response}.
* @param deviceAuthorizationResponseHandler the {@link AuthenticationSuccessHandler}
* used for handling an {@link OAuth2DeviceAuthorizationRequestAuthenticationToken}
* @return the {@link OAuth2DeviceAuthorizationEndpointConfigurer} for further
* configuration
*/
public OAuth2DeviceAuthorizationEndpointConfigurer deviceAuthorizationResponseHandler(
AuthenticationSuccessHandler deviceAuthorizationResponseHandler) {
this.deviceAuthorizationResponseHandler = deviceAuthorizationResponseHandler;
return this;
}
/**
* Sets the {@link AuthenticationFailureHandler} used for handling an
* {@link OAuth2AuthenticationException} and returning the {@link OAuth2Error Error
* Response}.
* @param errorResponseHandler the {@link AuthenticationFailureHandler} used for
* handling an {@link OAuth2AuthenticationException}
* @return the {@link OAuth2DeviceAuthorizationEndpointConfigurer} for further
* configuration
*/
public OAuth2DeviceAuthorizationEndpointConfigurer errorResponseHandler(
AuthenticationFailureHandler errorResponseHandler) {
this.errorResponseHandler = errorResponseHandler;
return this;
}
/**
* Sets the end-user verification {@code URI} on the authorization server.
* @param verificationUri the end-user verification {@code URI} on the authorization
* server
* @return the {@link OAuth2DeviceAuthorizationEndpointConfigurer} for further
* configuration
*/
public OAuth2DeviceAuthorizationEndpointConfigurer verificationUri(String verificationUri) {
this.verificationUri = verificationUri;
return this;
}
@Override
public void init(HttpSecurity builder) {
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
.getAuthorizationServerSettings(builder);
String deviceAuthorizationEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
? OAuth2ConfigurerUtils
.withMultipleIssuersPattern(authorizationServerSettings.getDeviceAuthorizationEndpoint())
: authorizationServerSettings.getDeviceAuthorizationEndpoint();
this.requestMatcher = PathPatternRequestMatcher.withDefaults()
.matcher(HttpMethod.POST, deviceAuthorizationEndpointUri);
List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(builder);
if (!this.authenticationProviders.isEmpty()) {
authenticationProviders.addAll(0, this.authenticationProviders);
}
this.authenticationProvidersConsumer.accept(authenticationProviders);
authenticationProviders
.forEach((authenticationProvider) -> builder.authenticationProvider(postProcess(authenticationProvider)));
}
@Override
public void configure(HttpSecurity builder) {
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
.getAuthorizationServerSettings(builder);
String deviceAuthorizationEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
? OAuth2ConfigurerUtils
.withMultipleIssuersPattern(authorizationServerSettings.getDeviceAuthorizationEndpoint())
: authorizationServerSettings.getDeviceAuthorizationEndpoint();
OAuth2DeviceAuthorizationEndpointFilter deviceAuthorizationEndpointFilter = new OAuth2DeviceAuthorizationEndpointFilter(
authenticationManager, deviceAuthorizationEndpointUri);
List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
if (!this.deviceAuthorizationRequestConverters.isEmpty()) {
authenticationConverters.addAll(0, this.deviceAuthorizationRequestConverters);
}
this.deviceAuthorizationRequestConvertersConsumer.accept(authenticationConverters);
deviceAuthorizationEndpointFilter
.setAuthenticationConverter(new DelegatingAuthenticationConverter(authenticationConverters));
if (this.deviceAuthorizationResponseHandler != null) {
deviceAuthorizationEndpointFilter.setAuthenticationSuccessHandler(this.deviceAuthorizationResponseHandler);
}
if (this.errorResponseHandler != null) {
deviceAuthorizationEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
}
if (StringUtils.hasText(this.verificationUri)) {
deviceAuthorizationEndpointFilter.setVerificationUri(this.verificationUri);
}
builder.addFilterAfter(postProcess(deviceAuthorizationEndpointFilter), AuthorizationFilter.class);
}
@Override
RequestMatcher getRequestMatcher() {
return this.requestMatcher;
}
private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
authenticationConverters.add(new OAuth2DeviceAuthorizationRequestAuthenticationConverter());
return authenticationConverters;
}
private static List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity builder) {
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
OAuth2AuthorizationService authorizationService = OAuth2ConfigurerUtils.getAuthorizationService(builder);
OAuth2DeviceAuthorizationRequestAuthenticationProvider deviceAuthorizationRequestAuthenticationProvider = new OAuth2DeviceAuthorizationRequestAuthenticationProvider(
authorizationService);
authenticationProviders.add(deviceAuthorizationRequestAuthenticationProvider);
return authenticationProviders;
}
}
@@ -0,0 +1,326 @@
/*
* Copyright 2004-present 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.config.annotation.web.configurers.oauth2.server.authorization;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceAuthorizationConsentAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceAuthorizationConsentAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceVerificationAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceVerificationAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.web.OAuth2DeviceVerificationEndpointFilter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2DeviceAuthorizationConsentAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2DeviceVerificationAuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.DelegatingAuthenticationConverter;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Configurer for the OAuth 2.0 Device Verification Endpoint.
*
* @author Steve Riesenberg
* @since 7.0
* @see OAuth2AuthorizationServerConfigurer#deviceVerificationEndpoint
* @see OAuth2DeviceVerificationEndpointFilter
*/
public final class OAuth2DeviceVerificationEndpointConfigurer extends AbstractOAuth2Configurer {
private RequestMatcher requestMatcher;
private final List<AuthenticationConverter> deviceVerificationRequestConverters = new ArrayList<>();
private Consumer<List<AuthenticationConverter>> deviceVerificationRequestConvertersConsumer = (
deviceVerificationRequestConverters) -> {
};
private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
private Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer = (authenticationProviders) -> {
};
private AuthenticationSuccessHandler deviceVerificationResponseHandler;
private AuthenticationFailureHandler errorResponseHandler;
private String consentPage;
/**
* Restrict for internal use only.
* @param objectPostProcessor an {@code ObjectPostProcessor}
*/
OAuth2DeviceVerificationEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
super(objectPostProcessor);
}
/**
* Sets the {@link AuthenticationConverter} used when attempting to extract a Device
* Verification Request (or Device Authorization Consent) from
* {@link HttpServletRequest} to an instance of
* {@link OAuth2DeviceVerificationAuthenticationToken} or
* {@link OAuth2DeviceAuthorizationConsentAuthenticationToken} used for authenticating
* the request.
* @param deviceVerificationRequestConverter the {@link AuthenticationConverter} used
* when attempting to extract a Device Verification Request (or Device Authorization
* Consent) from {@link HttpServletRequest}
* @return the {@link OAuth2DeviceVerificationEndpointConfigurer} for further
* configuration
*/
public OAuth2DeviceVerificationEndpointConfigurer deviceVerificationRequestConverter(
AuthenticationConverter deviceVerificationRequestConverter) {
Assert.notNull(deviceVerificationRequestConverter, "deviceVerificationRequestConverter cannot be null");
this.deviceVerificationRequestConverters.add(deviceVerificationRequestConverter);
return this;
}
/**
* Sets the {@code Consumer} providing access to the {@code List} of default and
* (optionally) added
* {@link #deviceVerificationRequestConverter(AuthenticationConverter)
* AuthenticationConverter}'s allowing the ability to add, remove, or customize a
* specific {@link AuthenticationConverter}.
* @param deviceVerificationRequestConvertersConsumer the {@code Consumer} providing
* access to the {@code List} of default and (optionally) added
* {@link AuthenticationConverter}'s
* @return the {@link OAuth2DeviceVerificationEndpointConfigurer} for further
* configuration
*/
public OAuth2DeviceVerificationEndpointConfigurer deviceVerificationRequestConverters(
Consumer<List<AuthenticationConverter>> deviceVerificationRequestConvertersConsumer) {
Assert.notNull(deviceVerificationRequestConvertersConsumer,
"deviceVerificationRequestConvertersConsumer cannot be null");
this.deviceVerificationRequestConvertersConsumer = deviceVerificationRequestConvertersConsumer;
return this;
}
/**
* Adds an {@link AuthenticationProvider} used for authenticating an
* {@link OAuth2DeviceVerificationAuthenticationToken} or
* {@link OAuth2DeviceAuthorizationConsentAuthenticationToken}.
* @param authenticationProvider an {@link AuthenticationProvider} used for
* authenticating an {@link OAuth2DeviceVerificationAuthenticationToken} or
* {@link OAuth2DeviceAuthorizationConsentAuthenticationToken}
* @return the {@link OAuth2DeviceVerificationEndpointConfigurer} for further
* configuration
*/
public OAuth2DeviceVerificationEndpointConfigurer authenticationProvider(
AuthenticationProvider authenticationProvider) {
Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
this.authenticationProviders.add(authenticationProvider);
return this;
}
/**
* Sets the {@code Consumer} providing access to the {@code List} of default and
* (optionally) added {@link #authenticationProvider(AuthenticationProvider)
* AuthenticationProvider}'s allowing the ability to add, remove, or customize a
* specific {@link AuthenticationProvider}.
* @param authenticationProvidersConsumer the {@code Consumer} providing access to the
* {@code List} of default and (optionally) added {@link AuthenticationProvider}'s
* @return the {@link OAuth2DeviceVerificationEndpointConfigurer} for further
* configuration
*/
public OAuth2DeviceVerificationEndpointConfigurer authenticationProviders(
Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer) {
Assert.notNull(authenticationProvidersConsumer, "authenticationProvidersConsumer cannot be null");
this.authenticationProvidersConsumer = authenticationProvidersConsumer;
return this;
}
/**
* Sets the {@link AuthenticationSuccessHandler} used for handling an
* {@link OAuth2DeviceVerificationAuthenticationToken} and returning the response.
* @param deviceVerificationResponseHandler the {@link AuthenticationSuccessHandler}
* used for handling an {@link OAuth2DeviceVerificationAuthenticationToken}
* @return the {@link OAuth2DeviceVerificationEndpointConfigurer} for further
* configuration
*/
public OAuth2DeviceVerificationEndpointConfigurer deviceVerificationResponseHandler(
AuthenticationSuccessHandler deviceVerificationResponseHandler) {
this.deviceVerificationResponseHandler = deviceVerificationResponseHandler;
return this;
}
/**
* Sets the {@link AuthenticationFailureHandler} used for handling an
* {@link OAuth2AuthenticationException} and returning the {@link OAuth2Error Error
* Response}.
* @param errorResponseHandler the {@link AuthenticationFailureHandler} used for
* handling an {@link OAuth2AuthenticationException}
* @return the {@link OAuth2DeviceVerificationEndpointConfigurer} for further
* configuration
*/
public OAuth2DeviceVerificationEndpointConfigurer errorResponseHandler(
AuthenticationFailureHandler errorResponseHandler) {
this.errorResponseHandler = errorResponseHandler;
return this;
}
/**
* Specify the URI to redirect Resource Owners to if consent is required during the
* {@code device_code} flow. A default consent page will be generated when this
* attribute is not specified.
*
* If a URI is specified, applications are required to process the specified URI to
* generate a consent page. The query string will contain the following parameters:
*
* <ul>
* <li>{@code client_id} - the client identifier</li>
* <li>{@code scope} - a space-delimited list of scopes present in the device
* authorization request</li>
* <li>{@code state} - a CSRF protection token</li>
* <li>{@code user_code} - the user code</li>
* </ul>
*
* In general, the consent page should create a form that submits a request with the
* following requirements:
*
* <ul>
* <li>It must be an HTTP POST</li>
* <li>It must be submitted to
* {@link AuthorizationServerSettings#getDeviceVerificationEndpoint()}</li>
* <li>It must include the received {@code client_id} as an HTTP parameter</li>
* <li>It must include the received {@code state} as an HTTP parameter</li>
* <li>It must include the list of {@code scope}s the {@code Resource Owner} consented
* to as an HTTP parameter</li>
* <li>It must include the received {@code user_code} as an HTTP parameter</li>
* </ul>
* @param consentPage the URI of the custom consent page to redirect to if consent is
* required (e.g. "/oauth2/consent")
* @return the {@link OAuth2DeviceVerificationEndpointConfigurer} for further
* configuration
*/
public OAuth2DeviceVerificationEndpointConfigurer consentPage(String consentPage) {
this.consentPage = consentPage;
return this;
}
@Override
public void init(HttpSecurity builder) {
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
.getAuthorizationServerSettings(builder);
String deviceVerificationEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
? OAuth2ConfigurerUtils
.withMultipleIssuersPattern(authorizationServerSettings.getDeviceVerificationEndpoint())
: authorizationServerSettings.getDeviceVerificationEndpoint();
this.requestMatcher = new OrRequestMatcher(
PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.GET, deviceVerificationEndpointUri),
PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, deviceVerificationEndpointUri));
List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(builder);
if (!this.authenticationProviders.isEmpty()) {
authenticationProviders.addAll(0, this.authenticationProviders);
}
this.authenticationProvidersConsumer.accept(authenticationProviders);
authenticationProviders
.forEach((authenticationProvider) -> builder.authenticationProvider(postProcess(authenticationProvider)));
}
@Override
public void configure(HttpSecurity builder) {
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
.getAuthorizationServerSettings(builder);
String deviceVerificationEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
? OAuth2ConfigurerUtils
.withMultipleIssuersPattern(authorizationServerSettings.getDeviceVerificationEndpoint())
: authorizationServerSettings.getDeviceVerificationEndpoint();
OAuth2DeviceVerificationEndpointFilter deviceVerificationEndpointFilter = new OAuth2DeviceVerificationEndpointFilter(
authenticationManager, deviceVerificationEndpointUri);
List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
if (!this.deviceVerificationRequestConverters.isEmpty()) {
authenticationConverters.addAll(0, this.deviceVerificationRequestConverters);
}
this.deviceVerificationRequestConvertersConsumer.accept(authenticationConverters);
deviceVerificationEndpointFilter
.setAuthenticationConverter(new DelegatingAuthenticationConverter(authenticationConverters));
if (this.deviceVerificationResponseHandler != null) {
deviceVerificationEndpointFilter.setAuthenticationSuccessHandler(this.deviceVerificationResponseHandler);
}
if (this.errorResponseHandler != null) {
deviceVerificationEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
}
if (StringUtils.hasText(this.consentPage)) {
deviceVerificationEndpointFilter.setConsentPage(this.consentPage);
}
builder.addFilterBefore(postProcess(deviceVerificationEndpointFilter),
AbstractPreAuthenticatedProcessingFilter.class);
}
@Override
RequestMatcher getRequestMatcher() {
return this.requestMatcher;
}
private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
authenticationConverters.add(new OAuth2DeviceVerificationAuthenticationConverter());
authenticationConverters.add(new OAuth2DeviceAuthorizationConsentAuthenticationConverter());
return authenticationConverters;
}
private static List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity builder) {
RegisteredClientRepository registeredClientRepository = OAuth2ConfigurerUtils
.getRegisteredClientRepository(builder);
OAuth2AuthorizationService authorizationService = OAuth2ConfigurerUtils.getAuthorizationService(builder);
OAuth2AuthorizationConsentService authorizationConsentService = OAuth2ConfigurerUtils
.getAuthorizationConsentService(builder);
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
// @formatter:off
OAuth2DeviceVerificationAuthenticationProvider deviceVerificationAuthenticationProvider =
new OAuth2DeviceVerificationAuthenticationProvider(
registeredClientRepository, authorizationService, authorizationConsentService);
// @formatter:on
authenticationProviders.add(deviceVerificationAuthenticationProvider);
// @formatter:off
OAuth2DeviceAuthorizationConsentAuthenticationProvider deviceAuthorizationConsentAuthenticationProvider =
new OAuth2DeviceAuthorizationConsentAuthenticationProvider(
registeredClientRepository, authorizationService, authorizationConsentService);
// @formatter:on
authenticationProviders.add(deviceAuthorizationConsentAuthenticationProvider);
return authenticationProviders;
}
}
@@ -0,0 +1,267 @@
/*
* Copyright 2004-present 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.config.annotation.web.configurers.oauth2.server.authorization;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationContext;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationValidator;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2PushedAuthorizationRequestAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2PushedAuthorizationRequestAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.web.OAuth2PushedAuthorizationRequestEndpointFilter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeRequestAuthenticationConverter;
import org.springframework.security.web.access.intercept.AuthorizationFilter;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.DelegatingAuthenticationConverter;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
/**
* Configurer for the OAuth 2.0 Pushed Authorization Request Endpoint.
*
* @author Joe Grandja
* @since 7.0
* @see OAuth2AuthorizationServerConfigurer#pushedAuthorizationRequestEndpoint
* @see OAuth2PushedAuthorizationRequestEndpointFilter
*/
public final class OAuth2PushedAuthorizationRequestEndpointConfigurer extends AbstractOAuth2Configurer {
private RequestMatcher requestMatcher;
private final List<AuthenticationConverter> pushedAuthorizationRequestConverters = new ArrayList<>();
private Consumer<List<AuthenticationConverter>> pushedAuthorizationRequestConvertersConsumer = (
authorizationRequestConverters) -> {
};
private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
private Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer = (authenticationProviders) -> {
};
private AuthenticationSuccessHandler pushedAuthorizationResponseHandler;
private AuthenticationFailureHandler errorResponseHandler;
private Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authorizationCodeRequestAuthenticationValidator;
/**
* Restrict for internal use only.
* @param objectPostProcessor an {@code ObjectPostProcessor}
*/
OAuth2PushedAuthorizationRequestEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
super(objectPostProcessor);
}
/**
* Adds an {@link AuthenticationConverter} used when attempting to extract a Pushed
* Authorization Request from {@link HttpServletRequest} to an instance of
* {@link OAuth2PushedAuthorizationRequestAuthenticationToken} used for authenticating
* the request.
* @param pushedAuthorizationRequestConverter an {@link AuthenticationConverter} used
* when attempting to extract a Pushed Authorization Request from
* {@link HttpServletRequest}
* @return the {@link OAuth2PushedAuthorizationRequestEndpointConfigurer} for further
* configuration
*/
public OAuth2PushedAuthorizationRequestEndpointConfigurer pushedAuthorizationRequestConverter(
AuthenticationConverter pushedAuthorizationRequestConverter) {
Assert.notNull(pushedAuthorizationRequestConverter, "pushedAuthorizationRequestConverter cannot be null");
this.pushedAuthorizationRequestConverters.add(pushedAuthorizationRequestConverter);
return this;
}
/**
* Sets the {@code Consumer} providing access to the {@code List} of default and
* (optionally) added
* {@link #pushedAuthorizationRequestConverter(AuthenticationConverter)
* AuthenticationConverter}'s allowing the ability to add, remove, or customize a
* specific {@link AuthenticationConverter}.
* @param pushedAuthorizationRequestConvertersConsumer the {@code Consumer} providing
* access to the {@code List} of default and (optionally) added
* {@link AuthenticationConverter}'s
* @return the {@link OAuth2PushedAuthorizationRequestEndpointConfigurer} for further
* configuration
*/
public OAuth2PushedAuthorizationRequestEndpointConfigurer pushedAuthorizationRequestConverters(
Consumer<List<AuthenticationConverter>> pushedAuthorizationRequestConvertersConsumer) {
Assert.notNull(pushedAuthorizationRequestConvertersConsumer,
"pushedAuthorizationRequestConvertersConsumer cannot be null");
this.pushedAuthorizationRequestConvertersConsumer = pushedAuthorizationRequestConvertersConsumer;
return this;
}
/**
* Adds an {@link AuthenticationProvider} used for authenticating an
* {@link OAuth2PushedAuthorizationRequestAuthenticationToken}.
* @param authenticationProvider an {@link AuthenticationProvider} used for
* authenticating an {@link OAuth2PushedAuthorizationRequestAuthenticationToken}
* @return the {@link OAuth2PushedAuthorizationRequestEndpointConfigurer} for further
* configuration
*/
public OAuth2PushedAuthorizationRequestEndpointConfigurer authenticationProvider(
AuthenticationProvider authenticationProvider) {
Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
this.authenticationProviders.add(authenticationProvider);
return this;
}
/**
* Sets the {@code Consumer} providing access to the {@code List} of default and
* (optionally) added {@link #authenticationProvider(AuthenticationProvider)
* AuthenticationProvider}'s allowing the ability to add, remove, or customize a
* specific {@link AuthenticationProvider}.
* @param authenticationProvidersConsumer the {@code Consumer} providing access to the
* {@code List} of default and (optionally) added {@link AuthenticationProvider}'s
* @return the {@link OAuth2PushedAuthorizationRequestEndpointConfigurer} for further
* configuration
*/
public OAuth2PushedAuthorizationRequestEndpointConfigurer authenticationProviders(
Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer) {
Assert.notNull(authenticationProvidersConsumer, "authenticationProvidersConsumer cannot be null");
this.authenticationProvidersConsumer = authenticationProvidersConsumer;
return this;
}
/**
* Sets the {@link AuthenticationSuccessHandler} used for handling an
* {@link OAuth2PushedAuthorizationRequestAuthenticationToken} and returning the
* Pushed Authorization Response.
* @param pushedAuthorizationResponseHandler the {@link AuthenticationSuccessHandler}
* used for handling an {@link OAuth2PushedAuthorizationRequestAuthenticationToken}
* @return the {@link OAuth2PushedAuthorizationRequestEndpointConfigurer} for further
* configuration
*/
public OAuth2PushedAuthorizationRequestEndpointConfigurer pushedAuthorizationResponseHandler(
AuthenticationSuccessHandler pushedAuthorizationResponseHandler) {
this.pushedAuthorizationResponseHandler = pushedAuthorizationResponseHandler;
return this;
}
/**
* Sets the {@link AuthenticationFailureHandler} used for handling an
* {@link OAuth2AuthorizationCodeRequestAuthenticationException} and returning the
* {@link OAuth2Error Error Response}.
* @param errorResponseHandler the {@link AuthenticationFailureHandler} used for
* handling an {@link OAuth2AuthorizationCodeRequestAuthenticationException}
* @return the {@link OAuth2PushedAuthorizationRequestEndpointConfigurer} for further
* configuration
*/
public OAuth2PushedAuthorizationRequestEndpointConfigurer errorResponseHandler(
AuthenticationFailureHandler errorResponseHandler) {
this.errorResponseHandler = errorResponseHandler;
return this;
}
void addAuthorizationCodeRequestAuthenticationValidator(
Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator) {
this.authorizationCodeRequestAuthenticationValidator = (this.authorizationCodeRequestAuthenticationValidator == null)
? authenticationValidator
: this.authorizationCodeRequestAuthenticationValidator.andThen(authenticationValidator);
}
@Override
void init(HttpSecurity httpSecurity) {
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
.getAuthorizationServerSettings(httpSecurity);
String pushedAuthorizationRequestEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
? OAuth2ConfigurerUtils
.withMultipleIssuersPattern(authorizationServerSettings.getPushedAuthorizationRequestEndpoint())
: authorizationServerSettings.getPushedAuthorizationRequestEndpoint();
this.requestMatcher = PathPatternRequestMatcher.withDefaults()
.matcher(HttpMethod.POST, pushedAuthorizationRequestEndpointUri);
List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity);
if (!this.authenticationProviders.isEmpty()) {
authenticationProviders.addAll(0, this.authenticationProviders);
}
this.authenticationProvidersConsumer.accept(authenticationProviders);
authenticationProviders.forEach(
(authenticationProvider) -> httpSecurity.authenticationProvider(postProcess(authenticationProvider)));
}
@Override
void configure(HttpSecurity httpSecurity) {
AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class);
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
.getAuthorizationServerSettings(httpSecurity);
String pushedAuthorizationRequestEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
? OAuth2ConfigurerUtils
.withMultipleIssuersPattern(authorizationServerSettings.getPushedAuthorizationRequestEndpoint())
: authorizationServerSettings.getPushedAuthorizationRequestEndpoint();
OAuth2PushedAuthorizationRequestEndpointFilter pushedAuthorizationRequestEndpointFilter = new OAuth2PushedAuthorizationRequestEndpointFilter(
authenticationManager, pushedAuthorizationRequestEndpointUri);
List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
if (!this.pushedAuthorizationRequestConverters.isEmpty()) {
authenticationConverters.addAll(0, this.pushedAuthorizationRequestConverters);
}
this.pushedAuthorizationRequestConvertersConsumer.accept(authenticationConverters);
pushedAuthorizationRequestEndpointFilter
.setAuthenticationConverter(new DelegatingAuthenticationConverter(authenticationConverters));
if (this.pushedAuthorizationResponseHandler != null) {
pushedAuthorizationRequestEndpointFilter
.setAuthenticationSuccessHandler(this.pushedAuthorizationResponseHandler);
}
if (this.errorResponseHandler != null) {
pushedAuthorizationRequestEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
}
httpSecurity.addFilterAfter(postProcess(pushedAuthorizationRequestEndpointFilter), AuthorizationFilter.class);
}
@Override
RequestMatcher getRequestMatcher() {
return this.requestMatcher;
}
private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
authenticationConverters.add(new OAuth2AuthorizationCodeRequestAuthenticationConverter());
return authenticationConverters;
}
private List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
OAuth2PushedAuthorizationRequestAuthenticationProvider pushedAuthorizationRequestAuthenticationProvider = new OAuth2PushedAuthorizationRequestAuthenticationProvider(
OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity));
if (this.authorizationCodeRequestAuthenticationValidator != null) {
pushedAuthorizationRequestAuthenticationProvider
.setAuthenticationValidator(new OAuth2AuthorizationCodeRequestAuthenticationValidator()
.andThen(this.authorizationCodeRequestAuthenticationValidator));
}
authenticationProviders.add(pushedAuthorizationRequestAuthenticationProvider);
return authenticationProviders;
}
}
@@ -0,0 +1,279 @@
/*
* Copyright 2004-present 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.config.annotation.web.configurers.oauth2.server.authorization;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceCodeAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2RefreshTokenAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ClientCredentialsAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2DeviceCodeAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2RefreshTokenAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2TokenExchangeAuthenticationConverter;
import org.springframework.security.web.access.intercept.AuthorizationFilter;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.DelegatingAuthenticationConverter;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
/**
* Configurer for the OAuth 2.0 Token Endpoint.
*
* @author Joe Grandja
* @since 7.0
* @see OAuth2AuthorizationServerConfigurer#tokenEndpoint
* @see OAuth2TokenEndpointFilter
*/
public final class OAuth2TokenEndpointConfigurer extends AbstractOAuth2Configurer {
private RequestMatcher requestMatcher;
private final List<AuthenticationConverter> accessTokenRequestConverters = new ArrayList<>();
private Consumer<List<AuthenticationConverter>> accessTokenRequestConvertersConsumer = (
accessTokenRequestConverters) -> {
};
private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
private Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer = (authenticationProviders) -> {
};
private AuthenticationSuccessHandler accessTokenResponseHandler;
private AuthenticationFailureHandler errorResponseHandler;
/**
* Restrict for internal use only.
* @param objectPostProcessor an {@code ObjectPostProcessor}
*/
OAuth2TokenEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
super(objectPostProcessor);
}
/**
* Adds an {@link AuthenticationConverter} used when attempting to extract an Access
* Token Request from {@link HttpServletRequest} to an instance of
* {@link OAuth2AuthorizationGrantAuthenticationToken} used for authenticating the
* authorization grant.
* @param accessTokenRequestConverter an {@link AuthenticationConverter} used when
* attempting to extract an Access Token Request from {@link HttpServletRequest}
* @return the {@link OAuth2TokenEndpointConfigurer} for further configuration
*/
public OAuth2TokenEndpointConfigurer accessTokenRequestConverter(
AuthenticationConverter accessTokenRequestConverter) {
Assert.notNull(accessTokenRequestConverter, "accessTokenRequestConverter cannot be null");
this.accessTokenRequestConverters.add(accessTokenRequestConverter);
return this;
}
/**
* Sets the {@code Consumer} providing access to the {@code List} of default and
* (optionally) added {@link #accessTokenRequestConverter(AuthenticationConverter)
* AuthenticationConverter}'s allowing the ability to add, remove, or customize a
* specific {@link AuthenticationConverter}.
* @param accessTokenRequestConvertersConsumer the {@code Consumer} providing access
* to the {@code List} of default and (optionally) added
* {@link AuthenticationConverter}'s
* @return the {@link OAuth2TokenEndpointConfigurer} for further configuration
*/
public OAuth2TokenEndpointConfigurer accessTokenRequestConverters(
Consumer<List<AuthenticationConverter>> accessTokenRequestConvertersConsumer) {
Assert.notNull(accessTokenRequestConvertersConsumer, "accessTokenRequestConvertersConsumer cannot be null");
this.accessTokenRequestConvertersConsumer = accessTokenRequestConvertersConsumer;
return this;
}
/**
* Adds an {@link AuthenticationProvider} used for authenticating a type of
* {@link OAuth2AuthorizationGrantAuthenticationToken}.
* @param authenticationProvider an {@link AuthenticationProvider} used for
* authenticating a type of {@link OAuth2AuthorizationGrantAuthenticationToken}
* @return the {@link OAuth2TokenEndpointConfigurer} for further configuration
*/
public OAuth2TokenEndpointConfigurer authenticationProvider(AuthenticationProvider authenticationProvider) {
Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
this.authenticationProviders.add(authenticationProvider);
return this;
}
/**
* Sets the {@code Consumer} providing access to the {@code List} of default and
* (optionally) added {@link #authenticationProvider(AuthenticationProvider)
* AuthenticationProvider}'s allowing the ability to add, remove, or customize a
* specific {@link AuthenticationProvider}.
* @param authenticationProvidersConsumer the {@code Consumer} providing access to the
* {@code List} of default and (optionally) added {@link AuthenticationProvider}'s
* @return the {@link OAuth2TokenEndpointConfigurer} for further configuration
*/
public OAuth2TokenEndpointConfigurer authenticationProviders(
Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer) {
Assert.notNull(authenticationProvidersConsumer, "authenticationProvidersConsumer cannot be null");
this.authenticationProvidersConsumer = authenticationProvidersConsumer;
return this;
}
/**
* Sets the {@link AuthenticationSuccessHandler} used for handling an
* {@link OAuth2AccessTokenAuthenticationToken} and returning the
* {@link OAuth2AccessTokenResponse Access Token Response}.
* @param accessTokenResponseHandler the {@link AuthenticationSuccessHandler} used for
* handling an {@link OAuth2AccessTokenAuthenticationToken}
* @return the {@link OAuth2TokenEndpointConfigurer} for further configuration
*/
public OAuth2TokenEndpointConfigurer accessTokenResponseHandler(
AuthenticationSuccessHandler accessTokenResponseHandler) {
this.accessTokenResponseHandler = accessTokenResponseHandler;
return this;
}
/**
* Sets the {@link AuthenticationFailureHandler} used for handling an
* {@link OAuth2AuthenticationException} and returning the {@link OAuth2Error Error
* Response}.
* @param errorResponseHandler the {@link AuthenticationFailureHandler} used for
* handling an {@link OAuth2AuthenticationException}
* @return the {@link OAuth2TokenEndpointConfigurer} for further configuration
*/
public OAuth2TokenEndpointConfigurer errorResponseHandler(AuthenticationFailureHandler errorResponseHandler) {
this.errorResponseHandler = errorResponseHandler;
return this;
}
@Override
void init(HttpSecurity httpSecurity) {
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
.getAuthorizationServerSettings(httpSecurity);
String tokenEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
? OAuth2ConfigurerUtils.withMultipleIssuersPattern(authorizationServerSettings.getTokenEndpoint())
: authorizationServerSettings.getTokenEndpoint();
this.requestMatcher = PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, tokenEndpointUri);
List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity);
if (!this.authenticationProviders.isEmpty()) {
authenticationProviders.addAll(0, this.authenticationProviders);
}
this.authenticationProvidersConsumer.accept(authenticationProviders);
authenticationProviders.forEach(
(authenticationProvider) -> httpSecurity.authenticationProvider(postProcess(authenticationProvider)));
}
@Override
void configure(HttpSecurity httpSecurity) {
AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class);
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
.getAuthorizationServerSettings(httpSecurity);
String tokenEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
? OAuth2ConfigurerUtils.withMultipleIssuersPattern(authorizationServerSettings.getTokenEndpoint())
: authorizationServerSettings.getTokenEndpoint();
OAuth2TokenEndpointFilter tokenEndpointFilter = new OAuth2TokenEndpointFilter(authenticationManager,
tokenEndpointUri);
List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
if (!this.accessTokenRequestConverters.isEmpty()) {
authenticationConverters.addAll(0, this.accessTokenRequestConverters);
}
this.accessTokenRequestConvertersConsumer.accept(authenticationConverters);
tokenEndpointFilter.setAuthenticationConverter(new DelegatingAuthenticationConverter(authenticationConverters));
if (this.accessTokenResponseHandler != null) {
tokenEndpointFilter.setAuthenticationSuccessHandler(this.accessTokenResponseHandler);
}
if (this.errorResponseHandler != null) {
tokenEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
}
httpSecurity.addFilterAfter(postProcess(tokenEndpointFilter), AuthorizationFilter.class);
}
@Override
RequestMatcher getRequestMatcher() {
return this.requestMatcher;
}
private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
authenticationConverters.add(new OAuth2AuthorizationCodeAuthenticationConverter());
authenticationConverters.add(new OAuth2RefreshTokenAuthenticationConverter());
authenticationConverters.add(new OAuth2ClientCredentialsAuthenticationConverter());
authenticationConverters.add(new OAuth2DeviceCodeAuthenticationConverter());
authenticationConverters.add(new OAuth2TokenExchangeAuthenticationConverter());
return authenticationConverters;
}
private static List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
OAuth2AuthorizationService authorizationService = OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity);
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator = OAuth2ConfigurerUtils
.getTokenGenerator(httpSecurity);
OAuth2AuthorizationCodeAuthenticationProvider authorizationCodeAuthenticationProvider = new OAuth2AuthorizationCodeAuthenticationProvider(
authorizationService, tokenGenerator);
SessionRegistry sessionRegistry = httpSecurity.getSharedObject(SessionRegistry.class);
if (sessionRegistry != null) {
authorizationCodeAuthenticationProvider.setSessionRegistry(sessionRegistry);
}
authenticationProviders.add(authorizationCodeAuthenticationProvider);
OAuth2RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider = new OAuth2RefreshTokenAuthenticationProvider(
authorizationService, tokenGenerator);
authenticationProviders.add(refreshTokenAuthenticationProvider);
OAuth2ClientCredentialsAuthenticationProvider clientCredentialsAuthenticationProvider = new OAuth2ClientCredentialsAuthenticationProvider(
authorizationService, tokenGenerator);
authenticationProviders.add(clientCredentialsAuthenticationProvider);
OAuth2DeviceCodeAuthenticationProvider deviceCodeAuthenticationProvider = new OAuth2DeviceCodeAuthenticationProvider(
authorizationService, tokenGenerator);
authenticationProviders.add(deviceCodeAuthenticationProvider);
OAuth2TokenExchangeAuthenticationProvider tokenExchangeAuthenticationProvider = new OAuth2TokenExchangeAuthenticationProvider(
authorizationService, tokenGenerator);
authenticationProviders.add(tokenExchangeAuthenticationProvider);
return authenticationProviders;
}
}
@@ -0,0 +1,250 @@
/*
* Copyright 2004-present 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.config.annotation.web.configurers.oauth2.server.authorization;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenIntrospectionEndpointFilter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2TokenIntrospectionAuthenticationConverter;
import org.springframework.security.web.access.intercept.AuthorizationFilter;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.DelegatingAuthenticationConverter;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
/**
* Configurer for the OAuth 2.0 Token Introspection Endpoint.
*
* @author Gaurav Tiwari
* @author Joe Grandja
* @since 7.0
* @see OAuth2AuthorizationServerConfigurer#tokenIntrospectionEndpoint(Customizer)
* @see OAuth2TokenIntrospectionEndpointFilter
*/
public final class OAuth2TokenIntrospectionEndpointConfigurer extends AbstractOAuth2Configurer {
private RequestMatcher requestMatcher;
private final List<AuthenticationConverter> introspectionRequestConverters = new ArrayList<>();
private Consumer<List<AuthenticationConverter>> introspectionRequestConvertersConsumer = (
introspectionRequestConverters) -> {
};
private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
private Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer = (authenticationProviders) -> {
};
private AuthenticationSuccessHandler introspectionResponseHandler;
private AuthenticationFailureHandler errorResponseHandler;
/**
* Restrict for internal use only.
* @param objectPostProcessor an {@code ObjectPostProcessor}
*/
OAuth2TokenIntrospectionEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
super(objectPostProcessor);
}
/**
* Adds an {@link AuthenticationConverter} used when attempting to extract an
* Introspection Request from {@link HttpServletRequest} to an instance of
* {@link OAuth2TokenIntrospectionAuthenticationToken} used for authenticating the
* request.
* @param introspectionRequestConverter an {@link AuthenticationConverter} used when
* attempting to extract an Introspection Request from {@link HttpServletRequest}
* @return the {@link OAuth2TokenIntrospectionEndpointConfigurer} for further
* configuration
*/
public OAuth2TokenIntrospectionEndpointConfigurer introspectionRequestConverter(
AuthenticationConverter introspectionRequestConverter) {
Assert.notNull(introspectionRequestConverter, "introspectionRequestConverter cannot be null");
this.introspectionRequestConverters.add(introspectionRequestConverter);
return this;
}
/**
* Sets the {@code Consumer} providing access to the {@code List} of default and
* (optionally) added {@link #introspectionRequestConverter(AuthenticationConverter)
* AuthenticationConverter}'s allowing the ability to add, remove, or customize a
* specific {@link AuthenticationConverter}.
* @param introspectionRequestConvertersConsumer the {@code Consumer} providing access
* to the {@code List} of default and (optionally) added
* {@link AuthenticationConverter}'s
* @return the {@link OAuth2TokenIntrospectionEndpointConfigurer} for further
* configuration
*/
public OAuth2TokenIntrospectionEndpointConfigurer introspectionRequestConverters(
Consumer<List<AuthenticationConverter>> introspectionRequestConvertersConsumer) {
Assert.notNull(introspectionRequestConvertersConsumer, "introspectionRequestConvertersConsumer cannot be null");
this.introspectionRequestConvertersConsumer = introspectionRequestConvertersConsumer;
return this;
}
/**
* Adds an {@link AuthenticationProvider} used for authenticating a type of
* {@link OAuth2TokenIntrospectionAuthenticationToken}.
* @param authenticationProvider an {@link AuthenticationProvider} used for
* authenticating a type of {@link OAuth2TokenIntrospectionAuthenticationToken}
* @return the {@link OAuth2TokenIntrospectionEndpointConfigurer} for further
* configuration
*/
public OAuth2TokenIntrospectionEndpointConfigurer authenticationProvider(
AuthenticationProvider authenticationProvider) {
Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
this.authenticationProviders.add(authenticationProvider);
return this;
}
/**
* Sets the {@code Consumer} providing access to the {@code List} of default and
* (optionally) added {@link #authenticationProvider(AuthenticationProvider)
* AuthenticationProvider}'s allowing the ability to add, remove, or customize a
* specific {@link AuthenticationProvider}.
* @param authenticationProvidersConsumer the {@code Consumer} providing access to the
* {@code List} of default and (optionally) added {@link AuthenticationProvider}'s
* @return the {@link OAuth2TokenIntrospectionEndpointConfigurer} for further
* configuration
*/
public OAuth2TokenIntrospectionEndpointConfigurer authenticationProviders(
Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer) {
Assert.notNull(authenticationProvidersConsumer, "authenticationProvidersConsumer cannot be null");
this.authenticationProvidersConsumer = authenticationProvidersConsumer;
return this;
}
/**
* Sets the {@link AuthenticationSuccessHandler} used for handling an
* {@link OAuth2TokenIntrospectionAuthenticationToken}.
* @param introspectionResponseHandler the {@link AuthenticationSuccessHandler} used
* for handling an {@link OAuth2TokenIntrospectionAuthenticationToken}
* @return the {@link OAuth2TokenIntrospectionEndpointConfigurer} for further
* configuration
*/
public OAuth2TokenIntrospectionEndpointConfigurer introspectionResponseHandler(
AuthenticationSuccessHandler introspectionResponseHandler) {
this.introspectionResponseHandler = introspectionResponseHandler;
return this;
}
/**
* Sets the {@link AuthenticationFailureHandler} used for handling an
* {@link OAuth2AuthenticationException} and returning the {@link OAuth2Error Error
* Response}.
* @param errorResponseHandler the {@link AuthenticationFailureHandler} used for
* handling an {@link OAuth2AuthenticationException}
* @return the {@link OAuth2TokenIntrospectionEndpointConfigurer} for further
* configuration
*/
public OAuth2TokenIntrospectionEndpointConfigurer errorResponseHandler(
AuthenticationFailureHandler errorResponseHandler) {
this.errorResponseHandler = errorResponseHandler;
return this;
}
@Override
void init(HttpSecurity httpSecurity) {
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
.getAuthorizationServerSettings(httpSecurity);
String tokenIntrospectionEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
? OAuth2ConfigurerUtils
.withMultipleIssuersPattern(authorizationServerSettings.getTokenIntrospectionEndpoint())
: authorizationServerSettings.getTokenIntrospectionEndpoint();
this.requestMatcher = PathPatternRequestMatcher.withDefaults()
.matcher(HttpMethod.POST, tokenIntrospectionEndpointUri);
List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity);
if (!this.authenticationProviders.isEmpty()) {
authenticationProviders.addAll(0, this.authenticationProviders);
}
this.authenticationProvidersConsumer.accept(authenticationProviders);
authenticationProviders.forEach(
(authenticationProvider) -> httpSecurity.authenticationProvider(postProcess(authenticationProvider)));
}
@Override
void configure(HttpSecurity httpSecurity) {
AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class);
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
.getAuthorizationServerSettings(httpSecurity);
String tokenIntrospectionEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
? OAuth2ConfigurerUtils
.withMultipleIssuersPattern(authorizationServerSettings.getTokenIntrospectionEndpoint())
: authorizationServerSettings.getTokenIntrospectionEndpoint();
OAuth2TokenIntrospectionEndpointFilter introspectionEndpointFilter = new OAuth2TokenIntrospectionEndpointFilter(
authenticationManager, tokenIntrospectionEndpointUri);
List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
if (!this.introspectionRequestConverters.isEmpty()) {
authenticationConverters.addAll(0, this.introspectionRequestConverters);
}
this.introspectionRequestConvertersConsumer.accept(authenticationConverters);
introspectionEndpointFilter
.setAuthenticationConverter(new DelegatingAuthenticationConverter(authenticationConverters));
if (this.introspectionResponseHandler != null) {
introspectionEndpointFilter.setAuthenticationSuccessHandler(this.introspectionResponseHandler);
}
if (this.errorResponseHandler != null) {
introspectionEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
}
httpSecurity.addFilterAfter(postProcess(introspectionEndpointFilter), AuthorizationFilter.class);
}
@Override
RequestMatcher getRequestMatcher() {
return this.requestMatcher;
}
private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
authenticationConverters.add(new OAuth2TokenIntrospectionAuthenticationConverter());
return authenticationConverters;
}
private static List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
OAuth2TokenIntrospectionAuthenticationProvider tokenIntrospectionAuthenticationProvider = new OAuth2TokenIntrospectionAuthenticationProvider(
OAuth2ConfigurerUtils.getRegisteredClientRepository(httpSecurity),
OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity));
authenticationProviders.add(tokenIntrospectionAuthenticationProvider);
return authenticationProviders;
}
}
@@ -0,0 +1,249 @@
/*
* Copyright 2004-present 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.config.annotation.web.configurers.oauth2.server.authorization;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenRevocationEndpointFilter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2TokenRevocationAuthenticationConverter;
import org.springframework.security.web.access.intercept.AuthorizationFilter;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.DelegatingAuthenticationConverter;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
/**
* Configurer for the OAuth 2.0 Token Revocation Endpoint.
*
* @author Arfat Chaus
* @author Joe Grandja
* @since 7.0
* @see OAuth2AuthorizationServerConfigurer#tokenRevocationEndpoint
* @see OAuth2TokenRevocationEndpointFilter
*/
public final class OAuth2TokenRevocationEndpointConfigurer extends AbstractOAuth2Configurer {
private RequestMatcher requestMatcher;
private final List<AuthenticationConverter> revocationRequestConverters = new ArrayList<>();
private Consumer<List<AuthenticationConverter>> revocationRequestConvertersConsumer = (
revocationRequestConverters) -> {
};
private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
private Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer = (authenticationProviders) -> {
};
private AuthenticationSuccessHandler revocationResponseHandler;
private AuthenticationFailureHandler errorResponseHandler;
/**
* Restrict for internal use only.
* @param objectPostProcessor an {@code ObjectPostProcessor}
*/
OAuth2TokenRevocationEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
super(objectPostProcessor);
}
/**
* Adds an {@link AuthenticationConverter} used when attempting to extract a Revoke
* Token Request from {@link HttpServletRequest} to an instance of
* {@link OAuth2TokenRevocationAuthenticationToken} used for authenticating the
* request.
* @param revocationRequestConverter an {@link AuthenticationConverter} used when
* attempting to extract a Revoke Token Request from {@link HttpServletRequest}
* @return the {@link OAuth2TokenRevocationEndpointConfigurer} for further
* configuration
*/
public OAuth2TokenRevocationEndpointConfigurer revocationRequestConverter(
AuthenticationConverter revocationRequestConverter) {
Assert.notNull(revocationRequestConverter, "revocationRequestConverter cannot be null");
this.revocationRequestConverters.add(revocationRequestConverter);
return this;
}
/**
* Sets the {@code Consumer} providing access to the {@code List} of default and
* (optionally) added {@link #revocationRequestConverter(AuthenticationConverter)
* AuthenticationConverter}'s allowing the ability to add, remove, or customize a
* specific {@link AuthenticationConverter}.
* @param revocationRequestConvertersConsumer the {@code Consumer} providing access to
* the {@code List} of default and (optionally) added
* {@link AuthenticationConverter}'s
* @return the {@link OAuth2TokenRevocationEndpointConfigurer} for further
* configuration
*/
public OAuth2TokenRevocationEndpointConfigurer revocationRequestConverters(
Consumer<List<AuthenticationConverter>> revocationRequestConvertersConsumer) {
Assert.notNull(revocationRequestConvertersConsumer, "revocationRequestConvertersConsumer cannot be null");
this.revocationRequestConvertersConsumer = revocationRequestConvertersConsumer;
return this;
}
/**
* Adds an {@link AuthenticationProvider} used for authenticating a type of
* {@link OAuth2TokenRevocationAuthenticationToken}.
* @param authenticationProvider an {@link AuthenticationProvider} used for
* authenticating a type of {@link OAuth2TokenRevocationAuthenticationToken}
* @return the {@link OAuth2TokenRevocationEndpointConfigurer} for further
* configuration
*/
public OAuth2TokenRevocationEndpointConfigurer authenticationProvider(
AuthenticationProvider authenticationProvider) {
Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
this.authenticationProviders.add(authenticationProvider);
return this;
}
/**
* Sets the {@code Consumer} providing access to the {@code List} of default and
* (optionally) added {@link #authenticationProvider(AuthenticationProvider)
* AuthenticationProvider}'s allowing the ability to add, remove, or customize a
* specific {@link AuthenticationProvider}.
* @param authenticationProvidersConsumer the {@code Consumer} providing access to the
* {@code List} of default and (optionally) added {@link AuthenticationProvider}'s
* @return the {@link OAuth2TokenRevocationEndpointConfigurer} for further
* configuration
*/
public OAuth2TokenRevocationEndpointConfigurer authenticationProviders(
Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer) {
Assert.notNull(authenticationProvidersConsumer, "authenticationProvidersConsumer cannot be null");
this.authenticationProvidersConsumer = authenticationProvidersConsumer;
return this;
}
/**
* Sets the {@link AuthenticationSuccessHandler} used for handling an
* {@link OAuth2TokenRevocationAuthenticationToken}.
* @param revocationResponseHandler the {@link AuthenticationSuccessHandler} used for
* handling an {@link OAuth2TokenRevocationAuthenticationToken}
* @return the {@link OAuth2TokenRevocationEndpointConfigurer} for further
* configuration
*/
public OAuth2TokenRevocationEndpointConfigurer revocationResponseHandler(
AuthenticationSuccessHandler revocationResponseHandler) {
this.revocationResponseHandler = revocationResponseHandler;
return this;
}
/**
* Sets the {@link AuthenticationFailureHandler} used for handling an
* {@link OAuth2AuthenticationException} and returning the {@link OAuth2Error Error
* Response}.
* @param errorResponseHandler the {@link AuthenticationFailureHandler} used for
* handling an {@link OAuth2AuthenticationException}
* @return the {@link OAuth2TokenRevocationEndpointConfigurer} for further
* configuration
*/
public OAuth2TokenRevocationEndpointConfigurer errorResponseHandler(
AuthenticationFailureHandler errorResponseHandler) {
this.errorResponseHandler = errorResponseHandler;
return this;
}
@Override
void init(HttpSecurity httpSecurity) {
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
.getAuthorizationServerSettings(httpSecurity);
String tokenRevocationEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
? OAuth2ConfigurerUtils
.withMultipleIssuersPattern(authorizationServerSettings.getTokenRevocationEndpoint())
: authorizationServerSettings.getTokenRevocationEndpoint();
this.requestMatcher = PathPatternRequestMatcher.withDefaults()
.matcher(HttpMethod.POST, tokenRevocationEndpointUri);
List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity);
if (!this.authenticationProviders.isEmpty()) {
authenticationProviders.addAll(0, this.authenticationProviders);
}
this.authenticationProvidersConsumer.accept(authenticationProviders);
authenticationProviders.forEach(
(authenticationProvider) -> httpSecurity.authenticationProvider(postProcess(authenticationProvider)));
}
@Override
void configure(HttpSecurity httpSecurity) {
AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class);
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
.getAuthorizationServerSettings(httpSecurity);
String tokenRevocationEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
? OAuth2ConfigurerUtils
.withMultipleIssuersPattern(authorizationServerSettings.getTokenRevocationEndpoint())
: authorizationServerSettings.getTokenRevocationEndpoint();
OAuth2TokenRevocationEndpointFilter revocationEndpointFilter = new OAuth2TokenRevocationEndpointFilter(
authenticationManager, tokenRevocationEndpointUri);
List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
if (!this.revocationRequestConverters.isEmpty()) {
authenticationConverters.addAll(0, this.revocationRequestConverters);
}
this.revocationRequestConvertersConsumer.accept(authenticationConverters);
revocationEndpointFilter
.setAuthenticationConverter(new DelegatingAuthenticationConverter(authenticationConverters));
if (this.revocationResponseHandler != null) {
revocationEndpointFilter.setAuthenticationSuccessHandler(this.revocationResponseHandler);
}
if (this.errorResponseHandler != null) {
revocationEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
}
httpSecurity.addFilterAfter(postProcess(revocationEndpointFilter), AuthorizationFilter.class);
}
@Override
RequestMatcher getRequestMatcher() {
return this.requestMatcher;
}
private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
authenticationConverters.add(new OAuth2TokenRevocationAuthenticationConverter());
return authenticationConverters;
}
private static List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
OAuth2TokenRevocationAuthenticationProvider tokenRevocationAuthenticationProvider = new OAuth2TokenRevocationAuthenticationProvider(
OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity));
authenticationProviders.add(tokenRevocationAuthenticationProvider);
return authenticationProviders;
}
}
@@ -0,0 +1,269 @@
/*
* Copyright 2004-present 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.config.annotation.web.configurers.oauth2.server.authorization;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.server.authorization.oidc.OidcClientRegistration;
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientConfigurationAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcClientRegistrationEndpointFilter;
import org.springframework.security.oauth2.server.authorization.oidc.web.authentication.OidcClientRegistrationAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.web.access.intercept.AuthorizationFilter;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.DelegatingAuthenticationConverter;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
/**
* Configurer for OpenID Connect 1.0 Dynamic Client Registration Endpoint.
*
* @author Joe Grandja
* @author Daniel Garnier-Moiroux
* @since 7.0
* @see OidcConfigurer#clientRegistrationEndpoint
* @see OidcClientRegistrationEndpointFilter
*/
public final class OidcClientRegistrationEndpointConfigurer extends AbstractOAuth2Configurer {
private RequestMatcher requestMatcher;
private final List<AuthenticationConverter> clientRegistrationRequestConverters = new ArrayList<>();
private Consumer<List<AuthenticationConverter>> clientRegistrationRequestConvertersConsumer = (
clientRegistrationRequestConverters) -> {
};
private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
private Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer = (authenticationProviders) -> {
};
private AuthenticationSuccessHandler clientRegistrationResponseHandler;
private AuthenticationFailureHandler errorResponseHandler;
/**
* Restrict for internal use only.
* @param objectPostProcessor an {@code ObjectPostProcessor}
*/
OidcClientRegistrationEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
super(objectPostProcessor);
}
/**
* Adds an {@link AuthenticationConverter} used when attempting to extract a Client
* Registration Request from {@link HttpServletRequest} to an instance of
* {@link OidcClientRegistrationAuthenticationToken} used for authenticating the
* request.
* @param clientRegistrationRequestConverter an {@link AuthenticationConverter} used
* when attempting to extract a Client Registration Request from
* {@link HttpServletRequest}
* @return the {@link OidcClientRegistrationEndpointConfigurer} for further
* configuration
*/
public OidcClientRegistrationEndpointConfigurer clientRegistrationRequestConverter(
AuthenticationConverter clientRegistrationRequestConverter) {
Assert.notNull(clientRegistrationRequestConverter, "clientRegistrationRequestConverter cannot be null");
this.clientRegistrationRequestConverters.add(clientRegistrationRequestConverter);
return this;
}
/**
* Sets the {@code Consumer} providing access to the {@code List} of default and
* (optionally) added
* {@link #clientRegistrationRequestConverter(AuthenticationConverter)
* AuthenticationConverter}'s allowing the ability to add, remove, or customize a
* specific {@link AuthenticationConverter}.
* @param clientRegistrationRequestConvertersConsumer the {@code Consumer} providing
* access to the {@code List} of default and (optionally) added
* {@link AuthenticationConverter}'s
* @return the {@link OidcUserInfoEndpointConfigurer} for further configuration
*/
public OidcClientRegistrationEndpointConfigurer clientRegistrationRequestConverters(
Consumer<List<AuthenticationConverter>> clientRegistrationRequestConvertersConsumer) {
Assert.notNull(clientRegistrationRequestConvertersConsumer,
"clientRegistrationRequestConvertersConsumer cannot be null");
this.clientRegistrationRequestConvertersConsumer = clientRegistrationRequestConvertersConsumer;
return this;
}
/**
* Adds an {@link AuthenticationProvider} used for authenticating an
* {@link OidcClientRegistrationAuthenticationToken}.
* @param authenticationProvider an {@link AuthenticationProvider} used for
* authenticating an {@link OidcClientRegistrationAuthenticationToken}
* @return the {@link OidcClientRegistrationEndpointConfigurer} for further
* configuration
*/
public OidcClientRegistrationEndpointConfigurer authenticationProvider(
AuthenticationProvider authenticationProvider) {
Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
this.authenticationProviders.add(authenticationProvider);
return this;
}
/**
* Sets the {@code Consumer} providing access to the {@code List} of default and
* (optionally) added {@link #authenticationProvider(AuthenticationProvider)
* AuthenticationProvider}'s allowing the ability to add, remove, or customize a
* specific {@link AuthenticationProvider}.
* @param authenticationProvidersConsumer the {@code Consumer} providing access to the
* {@code List} of default and (optionally) added {@link AuthenticationProvider}'s
* @return the {@link OidcClientRegistrationEndpointConfigurer} for further
* configuration
*/
public OidcClientRegistrationEndpointConfigurer authenticationProviders(
Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer) {
Assert.notNull(authenticationProvidersConsumer, "authenticationProvidersConsumer cannot be null");
this.authenticationProvidersConsumer = authenticationProvidersConsumer;
return this;
}
/**
* Sets the {@link AuthenticationSuccessHandler} used for handling an
* {@link OidcClientRegistrationAuthenticationToken} and returning the
* {@link OidcClientRegistration Client Registration Response}.
* @param clientRegistrationResponseHandler the {@link AuthenticationSuccessHandler}
* used for handling an {@link OidcClientRegistrationAuthenticationToken}
* @return the {@link OidcClientRegistrationEndpointConfigurer} for further
* configuration
*/
public OidcClientRegistrationEndpointConfigurer clientRegistrationResponseHandler(
AuthenticationSuccessHandler clientRegistrationResponseHandler) {
this.clientRegistrationResponseHandler = clientRegistrationResponseHandler;
return this;
}
/**
* Sets the {@link AuthenticationFailureHandler} used for handling an
* {@link OAuth2AuthenticationException} and returning the {@link OAuth2Error Error
* Response}.
* @param errorResponseHandler the {@link AuthenticationFailureHandler} used for
* handling an {@link OAuth2AuthenticationException}
* @return the {@link OidcClientRegistrationEndpointConfigurer} for further
* configuration
*/
public OidcClientRegistrationEndpointConfigurer errorResponseHandler(
AuthenticationFailureHandler errorResponseHandler) {
this.errorResponseHandler = errorResponseHandler;
return this;
}
@Override
void init(HttpSecurity httpSecurity) {
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
.getAuthorizationServerSettings(httpSecurity);
String clientRegistrationEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
? OAuth2ConfigurerUtils
.withMultipleIssuersPattern(authorizationServerSettings.getOidcClientRegistrationEndpoint())
: authorizationServerSettings.getOidcClientRegistrationEndpoint();
this.requestMatcher = new OrRequestMatcher(
PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, clientRegistrationEndpointUri),
PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.GET, clientRegistrationEndpointUri));
List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity);
if (!this.authenticationProviders.isEmpty()) {
authenticationProviders.addAll(0, this.authenticationProviders);
}
this.authenticationProvidersConsumer.accept(authenticationProviders);
authenticationProviders.forEach(
(authenticationProvider) -> httpSecurity.authenticationProvider(postProcess(authenticationProvider)));
}
@Override
void configure(HttpSecurity httpSecurity) {
AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class);
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
.getAuthorizationServerSettings(httpSecurity);
String clientRegistrationEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
? OAuth2ConfigurerUtils
.withMultipleIssuersPattern(authorizationServerSettings.getOidcClientRegistrationEndpoint())
: authorizationServerSettings.getOidcClientRegistrationEndpoint();
OidcClientRegistrationEndpointFilter oidcClientRegistrationEndpointFilter = new OidcClientRegistrationEndpointFilter(
authenticationManager, clientRegistrationEndpointUri);
List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
if (!this.clientRegistrationRequestConverters.isEmpty()) {
authenticationConverters.addAll(0, this.clientRegistrationRequestConverters);
}
this.clientRegistrationRequestConvertersConsumer.accept(authenticationConverters);
oidcClientRegistrationEndpointFilter
.setAuthenticationConverter(new DelegatingAuthenticationConverter(authenticationConverters));
if (this.clientRegistrationResponseHandler != null) {
oidcClientRegistrationEndpointFilter
.setAuthenticationSuccessHandler(this.clientRegistrationResponseHandler);
}
if (this.errorResponseHandler != null) {
oidcClientRegistrationEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
}
httpSecurity.addFilterAfter(postProcess(oidcClientRegistrationEndpointFilter), AuthorizationFilter.class);
}
@Override
RequestMatcher getRequestMatcher() {
return this.requestMatcher;
}
private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
authenticationConverters.add(new OidcClientRegistrationAuthenticationConverter());
return authenticationConverters;
}
private static List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
OidcClientRegistrationAuthenticationProvider oidcClientRegistrationAuthenticationProvider = new OidcClientRegistrationAuthenticationProvider(
OAuth2ConfigurerUtils.getRegisteredClientRepository(httpSecurity),
OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity),
OAuth2ConfigurerUtils.getTokenGenerator(httpSecurity));
PasswordEncoder passwordEncoder = OAuth2ConfigurerUtils.getOptionalBean(httpSecurity, PasswordEncoder.class);
if (passwordEncoder != null) {
oidcClientRegistrationAuthenticationProvider.setPasswordEncoder(passwordEncoder);
}
authenticationProviders.add(oidcClientRegistrationAuthenticationProvider);
OidcClientConfigurationAuthenticationProvider oidcClientConfigurationAuthenticationProvider = new OidcClientConfigurationAuthenticationProvider(
OAuth2ConfigurerUtils.getRegisteredClientRepository(httpSecurity),
OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity));
authenticationProviders.add(oidcClientConfigurationAuthenticationProvider);
return authenticationProviders;
}
}
@@ -0,0 +1,167 @@
/*
* Copyright 2004-present 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.config.annotation.web.configurers.oauth2.server.authorization;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContext;
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.web.util.UriComponentsBuilder;
/**
* Configurer for OpenID Connect 1.0 support.
*
* @author Joe Grandja
* @since 7.0
* @see OAuth2AuthorizationServerConfigurer#oidc
* @see OidcProviderConfigurationEndpointConfigurer
* @see OidcLogoutEndpointConfigurer
* @see OidcClientRegistrationEndpointConfigurer
* @see OidcUserInfoEndpointConfigurer
*/
public final class OidcConfigurer extends AbstractOAuth2Configurer {
private final Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer> configurers = new LinkedHashMap<>();
private RequestMatcher requestMatcher;
/**
* Restrict for internal use only.
* @param objectPostProcessor an {@code ObjectPostProcessor}
*/
OidcConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
super(objectPostProcessor);
addConfigurer(OidcProviderConfigurationEndpointConfigurer.class,
new OidcProviderConfigurationEndpointConfigurer(objectPostProcessor));
addConfigurer(OidcLogoutEndpointConfigurer.class, new OidcLogoutEndpointConfigurer(objectPostProcessor));
addConfigurer(OidcUserInfoEndpointConfigurer.class, new OidcUserInfoEndpointConfigurer(objectPostProcessor));
}
/**
* Configures the OpenID Connect 1.0 Provider Configuration Endpoint.
* @param providerConfigurationEndpointCustomizer the {@link Customizer} providing
* access to the {@link OidcProviderConfigurationEndpointConfigurer}
* @return the {@link OidcConfigurer} for further configuration
*/
public OidcConfigurer providerConfigurationEndpoint(
Customizer<OidcProviderConfigurationEndpointConfigurer> providerConfigurationEndpointCustomizer) {
providerConfigurationEndpointCustomizer
.customize(getConfigurer(OidcProviderConfigurationEndpointConfigurer.class));
return this;
}
/**
* Configures the OpenID Connect 1.0 RP-Initiated Logout Endpoint.
* @param logoutEndpointCustomizer the {@link Customizer} providing access to the
* {@link OidcLogoutEndpointConfigurer}
* @return the {@link OidcConfigurer} for further configuration
*/
public OidcConfigurer logoutEndpoint(Customizer<OidcLogoutEndpointConfigurer> logoutEndpointCustomizer) {
logoutEndpointCustomizer.customize(getConfigurer(OidcLogoutEndpointConfigurer.class));
return this;
}
/**
* Configures the OpenID Connect Dynamic Client Registration 1.0 Endpoint.
* @param clientRegistrationEndpointCustomizer the {@link Customizer} providing access
* to the {@link OidcClientRegistrationEndpointConfigurer}
* @return the {@link OidcConfigurer} for further configuration
*/
public OidcConfigurer clientRegistrationEndpoint(
Customizer<OidcClientRegistrationEndpointConfigurer> clientRegistrationEndpointCustomizer) {
OidcClientRegistrationEndpointConfigurer clientRegistrationEndpointConfigurer = getConfigurer(
OidcClientRegistrationEndpointConfigurer.class);
if (clientRegistrationEndpointConfigurer == null) {
addConfigurer(OidcClientRegistrationEndpointConfigurer.class,
new OidcClientRegistrationEndpointConfigurer(getObjectPostProcessor()));
clientRegistrationEndpointConfigurer = getConfigurer(OidcClientRegistrationEndpointConfigurer.class);
}
clientRegistrationEndpointCustomizer.customize(clientRegistrationEndpointConfigurer);
return this;
}
/**
* Configures the OpenID Connect 1.0 UserInfo Endpoint.
* @param userInfoEndpointCustomizer the {@link Customizer} providing access to the
* {@link OidcUserInfoEndpointConfigurer}
* @return the {@link OidcConfigurer} for further configuration
*/
public OidcConfigurer userInfoEndpoint(Customizer<OidcUserInfoEndpointConfigurer> userInfoEndpointCustomizer) {
userInfoEndpointCustomizer.customize(getConfigurer(OidcUserInfoEndpointConfigurer.class));
return this;
}
@Override
void init(HttpSecurity httpSecurity) {
List<RequestMatcher> requestMatchers = new ArrayList<>();
this.configurers.values().forEach((configurer) -> {
configurer.init(httpSecurity);
requestMatchers.add(configurer.getRequestMatcher());
});
this.requestMatcher = new OrRequestMatcher(requestMatchers);
}
@Override
void configure(HttpSecurity httpSecurity) {
OidcClientRegistrationEndpointConfigurer clientRegistrationEndpointConfigurer = getConfigurer(
OidcClientRegistrationEndpointConfigurer.class);
if (clientRegistrationEndpointConfigurer != null) {
OidcProviderConfigurationEndpointConfigurer providerConfigurationEndpointConfigurer = getConfigurer(
OidcProviderConfigurationEndpointConfigurer.class);
providerConfigurationEndpointConfigurer.addDefaultProviderConfigurationCustomizer((builder) -> {
AuthorizationServerContext authorizationServerContext = AuthorizationServerContextHolder.getContext();
String issuer = authorizationServerContext.getIssuer();
AuthorizationServerSettings authorizationServerSettings = authorizationServerContext
.getAuthorizationServerSettings();
String clientRegistrationEndpoint = UriComponentsBuilder.fromUriString(issuer)
.path(authorizationServerSettings.getOidcClientRegistrationEndpoint())
.build()
.toUriString();
builder.clientRegistrationEndpoint(clientRegistrationEndpoint);
});
}
this.configurers.values().forEach((configurer) -> configurer.configure(httpSecurity));
}
@Override
RequestMatcher getRequestMatcher() {
return this.requestMatcher;
}
@SuppressWarnings("unchecked")
<T> T getConfigurer(Class<T> type) {
return (T) this.configurers.get(type);
}
private <T extends AbstractOAuth2Configurer> void addConfigurer(Class<T> configurerType, T configurer) {
this.configurers.put(configurerType, configurer);
}
}
@@ -0,0 +1,238 @@
/*
* Copyright 2004-present 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.config.annotation.web.configurers.oauth2.server.authorization;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcLogoutAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcLogoutAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcLogoutEndpointFilter;
import org.springframework.security.oauth2.server.authorization.oidc.web.authentication.OidcLogoutAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.DelegatingAuthenticationConverter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
/**
* Configurer for OpenID Connect 1.0 RP-Initiated Logout Endpoint.
*
* @author Joe Grandja
* @since 7.0
* @see OidcConfigurer#logoutEndpoint
* @see OidcLogoutEndpointFilter
*/
public final class OidcLogoutEndpointConfigurer extends AbstractOAuth2Configurer {
private RequestMatcher requestMatcher;
private final List<AuthenticationConverter> logoutRequestConverters = new ArrayList<>();
private Consumer<List<AuthenticationConverter>> logoutRequestConvertersConsumer = (logoutRequestConverters) -> {
};
private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
private Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer = (authenticationProviders) -> {
};
private AuthenticationSuccessHandler logoutResponseHandler;
private AuthenticationFailureHandler errorResponseHandler;
/**
* Restrict for internal use only.
* @param objectPostProcessor an {@code ObjectPostProcessor}
*/
OidcLogoutEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
super(objectPostProcessor);
}
/**
* Adds an {@link AuthenticationConverter} used when attempting to extract a Logout
* Request from {@link HttpServletRequest} to an instance of
* {@link OidcLogoutAuthenticationToken} used for authenticating the request.
* @param logoutRequestConverter an {@link AuthenticationConverter} used when
* attempting to extract a Logout Request from {@link HttpServletRequest}
* @return the {@link OidcLogoutEndpointConfigurer} for further configuration
*/
public OidcLogoutEndpointConfigurer logoutRequestConverter(AuthenticationConverter logoutRequestConverter) {
Assert.notNull(logoutRequestConverter, "logoutRequestConverter cannot be null");
this.logoutRequestConverters.add(logoutRequestConverter);
return this;
}
/**
* Sets the {@code Consumer} providing access to the {@code List} of default and
* (optionally) added {@link #logoutRequestConverter(AuthenticationConverter)
* AuthenticationConverter}'s allowing the ability to add, remove, or customize a
* specific {@link AuthenticationConverter}.
* @param logoutRequestConvertersConsumer the {@code Consumer} providing access to the
* {@code List} of default and (optionally) added {@link AuthenticationConverter}'s
* @return the {@link OidcLogoutEndpointConfigurer} for further configuration
*/
public OidcLogoutEndpointConfigurer logoutRequestConverters(
Consumer<List<AuthenticationConverter>> logoutRequestConvertersConsumer) {
Assert.notNull(logoutRequestConvertersConsumer, "logoutRequestConvertersConsumer cannot be null");
this.logoutRequestConvertersConsumer = logoutRequestConvertersConsumer;
return this;
}
/**
* Adds an {@link AuthenticationProvider} used for authenticating an
* {@link OidcLogoutAuthenticationToken}.
* @param authenticationProvider an {@link AuthenticationProvider} used for
* authenticating an {@link OidcLogoutAuthenticationToken}
* @return the {@link OidcLogoutEndpointConfigurer} for further configuration
*/
public OidcLogoutEndpointConfigurer authenticationProvider(AuthenticationProvider authenticationProvider) {
Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
this.authenticationProviders.add(authenticationProvider);
return this;
}
/**
* Sets the {@code Consumer} providing access to the {@code List} of default and
* (optionally) added {@link #authenticationProvider(AuthenticationProvider)
* AuthenticationProvider}'s allowing the ability to add, remove, or customize a
* specific {@link AuthenticationProvider}.
* @param authenticationProvidersConsumer the {@code Consumer} providing access to the
* {@code List} of default and (optionally) added {@link AuthenticationProvider}'s
* @return the {@link OidcLogoutEndpointConfigurer} for further configuration
*/
public OidcLogoutEndpointConfigurer authenticationProviders(
Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer) {
Assert.notNull(authenticationProvidersConsumer, "authenticationProvidersConsumer cannot be null");
this.authenticationProvidersConsumer = authenticationProvidersConsumer;
return this;
}
/**
* Sets the {@link AuthenticationSuccessHandler} used for handling an
* {@link OidcLogoutAuthenticationToken} and performing the logout.
* @param logoutResponseHandler the {@link AuthenticationSuccessHandler} used for
* handling an {@link OidcLogoutAuthenticationToken}
* @return the {@link OidcLogoutEndpointConfigurer} for further configuration
*/
public OidcLogoutEndpointConfigurer logoutResponseHandler(AuthenticationSuccessHandler logoutResponseHandler) {
this.logoutResponseHandler = logoutResponseHandler;
return this;
}
/**
* Sets the {@link AuthenticationFailureHandler} used for handling an
* {@link OAuth2AuthenticationException} and returning the {@link OAuth2Error Error
* Response}.
* @param errorResponseHandler the {@link AuthenticationFailureHandler} used for
* handling an {@link OAuth2AuthenticationException}
* @return the {@link OidcLogoutEndpointConfigurer} for further configuration
*/
public OidcLogoutEndpointConfigurer errorResponseHandler(AuthenticationFailureHandler errorResponseHandler) {
this.errorResponseHandler = errorResponseHandler;
return this;
}
@Override
void init(HttpSecurity httpSecurity) {
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
.getAuthorizationServerSettings(httpSecurity);
String logoutEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
? OAuth2ConfigurerUtils.withMultipleIssuersPattern(authorizationServerSettings.getOidcLogoutEndpoint())
: authorizationServerSettings.getOidcLogoutEndpoint();
this.requestMatcher = new OrRequestMatcher(
PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.GET, logoutEndpointUri),
PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, logoutEndpointUri));
List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity);
if (!this.authenticationProviders.isEmpty()) {
authenticationProviders.addAll(0, this.authenticationProviders);
}
this.authenticationProvidersConsumer.accept(authenticationProviders);
authenticationProviders.forEach(
(authenticationProvider) -> httpSecurity.authenticationProvider(postProcess(authenticationProvider)));
}
@Override
void configure(HttpSecurity httpSecurity) {
AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class);
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
.getAuthorizationServerSettings(httpSecurity);
String logoutEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
? OAuth2ConfigurerUtils.withMultipleIssuersPattern(authorizationServerSettings.getOidcLogoutEndpoint())
: authorizationServerSettings.getOidcLogoutEndpoint();
OidcLogoutEndpointFilter oidcLogoutEndpointFilter = new OidcLogoutEndpointFilter(authenticationManager,
logoutEndpointUri);
List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
if (!this.logoutRequestConverters.isEmpty()) {
authenticationConverters.addAll(0, this.logoutRequestConverters);
}
this.logoutRequestConvertersConsumer.accept(authenticationConverters);
oidcLogoutEndpointFilter
.setAuthenticationConverter(new DelegatingAuthenticationConverter(authenticationConverters));
if (this.logoutResponseHandler != null) {
oidcLogoutEndpointFilter.setAuthenticationSuccessHandler(this.logoutResponseHandler);
}
if (this.errorResponseHandler != null) {
oidcLogoutEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
}
httpSecurity.addFilterBefore(postProcess(oidcLogoutEndpointFilter), LogoutFilter.class);
}
@Override
RequestMatcher getRequestMatcher() {
return this.requestMatcher;
}
private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
authenticationConverters.add(new OidcLogoutAuthenticationConverter());
return authenticationConverters;
}
private static List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
OidcLogoutAuthenticationProvider oidcLogoutAuthenticationProvider = new OidcLogoutAuthenticationProvider(
OAuth2ConfigurerUtils.getRegisteredClientRepository(httpSecurity),
OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity),
httpSecurity.getSharedObject(SessionRegistry.class));
authenticationProviders.add(oidcLogoutAuthenticationProvider);
return authenticationProviders;
}
}
@@ -0,0 +1,118 @@
/*
* Copyright 2004-present 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.config.annotation.web.configurers.oauth2.server.authorization;
import java.util.function.Consumer;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.server.authorization.oidc.OidcProviderConfiguration;
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcProviderConfigurationEndpointFilter;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
/**
* Configurer for the OpenID Connect 1.0 Provider Configuration Endpoint.
*
* @author Joe Grandja
* @since 7.0
* @see OidcConfigurer#providerConfigurationEndpoint
* @see OidcProviderConfigurationEndpointFilter
*/
public final class OidcProviderConfigurationEndpointConfigurer extends AbstractOAuth2Configurer {
private RequestMatcher requestMatcher;
private Consumer<OidcProviderConfiguration.Builder> providerConfigurationCustomizer;
private Consumer<OidcProviderConfiguration.Builder> defaultProviderConfigurationCustomizer;
/**
* Restrict for internal use only.
* @param objectPostProcessor an {@code ObjectPostProcessor}
*/
OidcProviderConfigurationEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
super(objectPostProcessor);
}
/**
* Sets the {@code Consumer} providing access to the
* {@link OidcProviderConfiguration.Builder} allowing the ability to customize the
* claims of the OpenID Provider's configuration.
* @param providerConfigurationCustomizer the {@code Consumer} providing access to the
* {@link OidcProviderConfiguration.Builder}
* @return the {@link OidcProviderConfigurationEndpointConfigurer} for further
* configuration
*/
public OidcProviderConfigurationEndpointConfigurer providerConfigurationCustomizer(
Consumer<OidcProviderConfiguration.Builder> providerConfigurationCustomizer) {
this.providerConfigurationCustomizer = providerConfigurationCustomizer;
return this;
}
void addDefaultProviderConfigurationCustomizer(
Consumer<OidcProviderConfiguration.Builder> defaultProviderConfigurationCustomizer) {
this.defaultProviderConfigurationCustomizer = (this.defaultProviderConfigurationCustomizer == null)
? defaultProviderConfigurationCustomizer
: this.defaultProviderConfigurationCustomizer.andThen(defaultProviderConfigurationCustomizer);
}
@Override
void init(HttpSecurity httpSecurity) {
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
.getAuthorizationServerSettings(httpSecurity);
String oidcProviderConfigurationEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
? "/**/.well-known/openid-configuration" : "/.well-known/openid-configuration";
this.requestMatcher = PathPatternRequestMatcher.withDefaults()
.matcher(HttpMethod.GET, oidcProviderConfigurationEndpointUri);
}
@Override
void configure(HttpSecurity httpSecurity) {
OidcProviderConfigurationEndpointFilter oidcProviderConfigurationEndpointFilter = new OidcProviderConfigurationEndpointFilter();
Consumer<OidcProviderConfiguration.Builder> providerConfigurationCustomizer = getProviderConfigurationCustomizer();
if (providerConfigurationCustomizer != null) {
oidcProviderConfigurationEndpointFilter.setProviderConfigurationCustomizer(providerConfigurationCustomizer);
}
httpSecurity.addFilterBefore(postProcess(oidcProviderConfigurationEndpointFilter),
AbstractPreAuthenticatedProcessingFilter.class);
}
private Consumer<OidcProviderConfiguration.Builder> getProviderConfigurationCustomizer() {
Consumer<OidcProviderConfiguration.Builder> providerConfigurationCustomizer = null;
if (this.defaultProviderConfigurationCustomizer != null || this.providerConfigurationCustomizer != null) {
if (this.defaultProviderConfigurationCustomizer != null) {
providerConfigurationCustomizer = this.defaultProviderConfigurationCustomizer;
}
if (this.providerConfigurationCustomizer != null) {
providerConfigurationCustomizer = (providerConfigurationCustomizer != null)
? providerConfigurationCustomizer.andThen(this.providerConfigurationCustomizer)
: this.providerConfigurationCustomizer;
}
}
return providerConfigurationCustomizer;
}
@Override
RequestMatcher getRequestMatcher() {
return this.requestMatcher;
}
}
@@ -0,0 +1,281 @@
/*
* Copyright 2004-present 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.config.annotation.web.configurers.oauth2.server.authorization;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationContext;
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcUserInfoEndpointFilter;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.web.access.intercept.AuthorizationFilter;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.DelegatingAuthenticationConverter;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
/**
* Configurer for OpenID Connect 1.0 UserInfo Endpoint.
*
* @author Steve Riesenberg
* @author Daniel Garnier-Moiroux
* @since 7.0
* @see OidcConfigurer#userInfoEndpoint
* @see OidcUserInfoEndpointFilter
*/
public final class OidcUserInfoEndpointConfigurer extends AbstractOAuth2Configurer {
private RequestMatcher requestMatcher;
private final List<AuthenticationConverter> userInfoRequestConverters = new ArrayList<>();
private Consumer<List<AuthenticationConverter>> userInfoRequestConvertersConsumer = (userInfoRequestConverters) -> {
};
private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
private Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer = (authenticationProviders) -> {
};
private AuthenticationSuccessHandler userInfoResponseHandler;
private AuthenticationFailureHandler errorResponseHandler;
private Function<OidcUserInfoAuthenticationContext, OidcUserInfo> userInfoMapper;
/**
* Restrict for internal use only.
* @param objectPostProcessor an {@code ObjectPostProcessor}
*/
OidcUserInfoEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
super(objectPostProcessor);
}
/**
* Adds an {@link AuthenticationConverter} used when attempting to extract an UserInfo
* Request from {@link HttpServletRequest} to an instance of
* {@link OidcUserInfoAuthenticationToken} used for authenticating the request.
* @param userInfoRequestConverter an {@link AuthenticationConverter} used when
* attempting to extract an UserInfo Request from {@link HttpServletRequest}
* @return the {@link OidcUserInfoEndpointConfigurer} for further configuration
*/
public OidcUserInfoEndpointConfigurer userInfoRequestConverter(AuthenticationConverter userInfoRequestConverter) {
Assert.notNull(userInfoRequestConverter, "userInfoRequestConverter cannot be null");
this.userInfoRequestConverters.add(userInfoRequestConverter);
return this;
}
/**
* Sets the {@code Consumer} providing access to the {@code List} of default and
* (optionally) added {@link #userInfoRequestConverter(AuthenticationConverter)
* AuthenticationConverter}'s allowing the ability to add, remove, or customize a
* specific {@link AuthenticationConverter}.
* @param userInfoRequestConvertersConsumer the {@code Consumer} providing access to
* the {@code List} of default and (optionally) added
* {@link AuthenticationConverter}'s
* @return the {@link OidcUserInfoEndpointConfigurer} for further configuration
*/
public OidcUserInfoEndpointConfigurer userInfoRequestConverters(
Consumer<List<AuthenticationConverter>> userInfoRequestConvertersConsumer) {
Assert.notNull(userInfoRequestConvertersConsumer, "userInfoRequestConvertersConsumer cannot be null");
this.userInfoRequestConvertersConsumer = userInfoRequestConvertersConsumer;
return this;
}
/**
* Adds an {@link AuthenticationProvider} used for authenticating an
* {@link OidcUserInfoAuthenticationToken}.
* @param authenticationProvider an {@link AuthenticationProvider} used for
* authenticating an {@link OidcUserInfoAuthenticationToken}
* @return the {@link OidcUserInfoEndpointConfigurer} for further configuration
*/
public OidcUserInfoEndpointConfigurer authenticationProvider(AuthenticationProvider authenticationProvider) {
Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
this.authenticationProviders.add(authenticationProvider);
return this;
}
/**
* Sets the {@code Consumer} providing access to the {@code List} of default and
* (optionally) added {@link #authenticationProvider(AuthenticationProvider)
* AuthenticationProvider}'s allowing the ability to add, remove, or customize a
* specific {@link AuthenticationProvider}.
* @param authenticationProvidersConsumer the {@code Consumer} providing access to the
* {@code List} of default and (optionally) added {@link AuthenticationProvider}'s
* @return the {@link OidcUserInfoEndpointConfigurer} for further configuration
*/
public OidcUserInfoEndpointConfigurer authenticationProviders(
Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer) {
Assert.notNull(authenticationProvidersConsumer, "authenticationProvidersConsumer cannot be null");
this.authenticationProvidersConsumer = authenticationProvidersConsumer;
return this;
}
/**
* Sets the {@link AuthenticationSuccessHandler} used for handling an
* {@link OidcUserInfoAuthenticationToken} and returning the {@link OidcUserInfo
* UserInfo Response}.
* @param userInfoResponseHandler the {@link AuthenticationSuccessHandler} used for
* handling an {@link OidcUserInfoAuthenticationToken}
* @return the {@link OidcUserInfoEndpointConfigurer} for further configuration
*/
public OidcUserInfoEndpointConfigurer userInfoResponseHandler(
AuthenticationSuccessHandler userInfoResponseHandler) {
this.userInfoResponseHandler = userInfoResponseHandler;
return this;
}
/**
* Sets the {@link AuthenticationFailureHandler} used for handling an
* {@link OAuth2AuthenticationException} and returning the {@link OAuth2Error Error
* Response}.
* @param errorResponseHandler the {@link AuthenticationFailureHandler} used for
* handling an {@link OAuth2AuthenticationException}
* @return the {@link OidcUserInfoEndpointConfigurer} for further configuration
*/
public OidcUserInfoEndpointConfigurer errorResponseHandler(AuthenticationFailureHandler errorResponseHandler) {
this.errorResponseHandler = errorResponseHandler;
return this;
}
/**
* Sets the {@link Function} used to extract claims from
* {@link OidcUserInfoAuthenticationContext} to an instance of {@link OidcUserInfo}
* for the UserInfo response.
*
* <p>
* The {@link OidcUserInfoAuthenticationContext} gives the mapper access to the
* {@link OidcUserInfoAuthenticationToken}, as well as, the following context
* attributes:
* <ul>
* <li>{@link OidcUserInfoAuthenticationContext#getAccessToken()} containing the
* bearer token used to make the request.</li>
* <li>{@link OidcUserInfoAuthenticationContext#getAuthorization()} containing the
* {@link OidcIdToken} and {@link OAuth2AccessToken} associated with the bearer token
* used to make the request.</li>
* </ul>
* @param userInfoMapper the {@link Function} used to extract claims from
* {@link OidcUserInfoAuthenticationContext} to an instance of {@link OidcUserInfo}
* @return the {@link OidcUserInfoEndpointConfigurer} for further configuration
*/
public OidcUserInfoEndpointConfigurer userInfoMapper(
Function<OidcUserInfoAuthenticationContext, OidcUserInfo> userInfoMapper) {
this.userInfoMapper = userInfoMapper;
return this;
}
@Override
void init(HttpSecurity httpSecurity) {
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
.getAuthorizationServerSettings(httpSecurity);
String userInfoEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
? OAuth2ConfigurerUtils
.withMultipleIssuersPattern(authorizationServerSettings.getOidcUserInfoEndpoint())
: authorizationServerSettings.getOidcUserInfoEndpoint();
this.requestMatcher = new OrRequestMatcher(
PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.GET, userInfoEndpointUri),
PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, userInfoEndpointUri));
List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity);
if (!this.authenticationProviders.isEmpty()) {
authenticationProviders.addAll(0, this.authenticationProviders);
}
this.authenticationProvidersConsumer.accept(authenticationProviders);
authenticationProviders.forEach(
(authenticationProvider) -> httpSecurity.authenticationProvider(postProcess(authenticationProvider)));
}
@Override
void configure(HttpSecurity httpSecurity) {
AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class);
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
.getAuthorizationServerSettings(httpSecurity);
String userInfoEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
? OAuth2ConfigurerUtils
.withMultipleIssuersPattern(authorizationServerSettings.getOidcUserInfoEndpoint())
: authorizationServerSettings.getOidcUserInfoEndpoint();
OidcUserInfoEndpointFilter oidcUserInfoEndpointFilter = new OidcUserInfoEndpointFilter(authenticationManager,
userInfoEndpointUri);
List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
if (!this.userInfoRequestConverters.isEmpty()) {
authenticationConverters.addAll(0, this.userInfoRequestConverters);
}
this.userInfoRequestConvertersConsumer.accept(authenticationConverters);
oidcUserInfoEndpointFilter
.setAuthenticationConverter(new DelegatingAuthenticationConverter(authenticationConverters));
if (this.userInfoResponseHandler != null) {
oidcUserInfoEndpointFilter.setAuthenticationSuccessHandler(this.userInfoResponseHandler);
}
if (this.errorResponseHandler != null) {
oidcUserInfoEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
}
httpSecurity.addFilterAfter(postProcess(oidcUserInfoEndpointFilter), AuthorizationFilter.class);
}
@Override
RequestMatcher getRequestMatcher() {
return this.requestMatcher;
}
private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
authenticationConverters.add((request) -> {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return new OidcUserInfoAuthenticationToken(authentication);
});
return authenticationConverters;
}
private List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
OidcUserInfoAuthenticationProvider oidcUserInfoAuthenticationProvider = new OidcUserInfoAuthenticationProvider(
OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity));
if (this.userInfoMapper != null) {
oidcUserInfoAuthenticationProvider.setUserInfoMapper(this.userInfoMapper);
}
authenticationProviders.add(oidcUserInfoAuthenticationProvider);
return authenticationProviders;
}
}
@@ -0,0 +1,46 @@
/*
* Copyright 2004-present 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.config.annotation.web.configuration;
import java.lang.reflect.Method;
import org.junit.jupiter.api.Test;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.OrderUtils;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.util.ClassUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link OAuth2AuthorizationServerConfiguration}.
*
* @author Joe Grandja
*/
public class OAuth2AuthorizationServerConfigurationTests {
@Test
public void assertOrderHighestPrecedence() {
Method authorizationServerSecurityFilterChainMethod = ClassUtils.getMethod(
OAuth2AuthorizationServerConfiguration.class, "authorizationServerSecurityFilterChain",
HttpSecurity.class);
Integer order = OrderUtils.getOrder(authorizationServerSecurityFilterChainMethod);
assertThat(order).isEqualTo(Ordered.HIGHEST_PRECEDENCE);
}
}
@@ -0,0 +1,112 @@
/*
* Copyright 2004-present 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.config.annotation.web.configuration;
import java.util.function.Supplier;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.endsWith;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
/**
* Tests for {@link RegisterMissingBeanPostProcessor}.
*
* @author Steve Riesenberg
*/
public class RegisterMissingBeanPostProcessorTests {
private final RegisterMissingBeanPostProcessor postProcessor = new RegisterMissingBeanPostProcessor();
@Test
public void postProcessBeanDefinitionRegistryWhenClassAddedThenRegisteredWithClass() {
this.postProcessor.addBeanDefinition(SimpleBean.class, null);
this.postProcessor.setBeanFactory(new DefaultListableBeanFactory());
BeanDefinitionRegistry beanDefinitionRegistry = mock(BeanDefinitionRegistry.class);
this.postProcessor.postProcessBeanDefinitionRegistry(beanDefinitionRegistry);
ArgumentCaptor<BeanDefinition> beanDefinitionCaptor = ArgumentCaptor.forClass(BeanDefinition.class);
verify(beanDefinitionRegistry).registerBeanDefinition(endsWith("SimpleBean"), beanDefinitionCaptor.capture());
RootBeanDefinition beanDefinition = (RootBeanDefinition) beanDefinitionCaptor.getValue();
assertThat(beanDefinition.getBeanClass()).isEqualTo(SimpleBean.class);
assertThat(beanDefinition.getInstanceSupplier()).isNull();
}
@Test
public void postProcessBeanDefinitionRegistryWhenSupplierAddedThenRegisteredWithSupplier() {
Supplier<SimpleBean> beanSupplier = () -> new SimpleBean("string");
this.postProcessor.addBeanDefinition(SimpleBean.class, beanSupplier);
this.postProcessor.setBeanFactory(new DefaultListableBeanFactory());
BeanDefinitionRegistry beanDefinitionRegistry = mock(BeanDefinitionRegistry.class);
this.postProcessor.postProcessBeanDefinitionRegistry(beanDefinitionRegistry);
ArgumentCaptor<BeanDefinition> beanDefinitionCaptor = ArgumentCaptor.forClass(BeanDefinition.class);
verify(beanDefinitionRegistry).registerBeanDefinition(endsWith("SimpleBean"), beanDefinitionCaptor.capture());
RootBeanDefinition beanDefinition = (RootBeanDefinition) beanDefinitionCaptor.getValue();
assertThat(beanDefinition.getBeanClass()).isEqualTo(SimpleBean.class);
assertThat(beanDefinition.getInstanceSupplier()).isEqualTo(beanSupplier);
}
@Test
public void postProcessBeanDefinitionRegistryWhenNoBeanDefinitionsAddedThenNoneRegistered() {
this.postProcessor.setBeanFactory(new DefaultListableBeanFactory());
BeanDefinitionRegistry beanDefinitionRegistry = mock(BeanDefinitionRegistry.class);
this.postProcessor.postProcessBeanDefinitionRegistry(beanDefinitionRegistry);
verifyNoInteractions(beanDefinitionRegistry);
}
@Test
public void postProcessBeanDefinitionRegistryWhenBeanDefinitionAlreadyExistsThenNoneRegistered() {
this.postProcessor.addBeanDefinition(SimpleBean.class, null);
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
beanFactory.registerBeanDefinition("simpleBean", new RootBeanDefinition(SimpleBean.class));
this.postProcessor.setBeanFactory(beanFactory);
BeanDefinitionRegistry beanDefinitionRegistry = mock(BeanDefinitionRegistry.class);
this.postProcessor.postProcessBeanDefinitionRegistry(beanDefinitionRegistry);
verifyNoInteractions(beanDefinitionRegistry);
}
private static final class SimpleBean {
private final String field;
private SimpleBean(String field) {
this.field = field;
}
private String getField() {
return this.field;
}
}
}
@@ -0,0 +1,138 @@
/*
* Copyright 2004-present 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.config.annotation.web.configurers.oauth2.server.authorization;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import jakarta.servlet.FilterChain;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link AuthorizationServerContextFilter}.
*
* @author Joe Grandja
*/
class AuthorizationServerContextFilterTests {
private static final String SCHEME = "https";
private static final String HOST = "example.com";
private static final int PORT = 8443;
private static final String DEFAULT_ISSUER = SCHEME + "://" + HOST + ":" + PORT;
private AuthorizationServerContextFilter filter;
@Test
void doFilterWhenDefaultEndpointsThenIssuerResolved() throws Exception {
AuthorizationServerSettings authorizationServerSettings = AuthorizationServerSettings.builder().build();
this.filter = new AuthorizationServerContextFilter(authorizationServerSettings);
String issuerPath = "/issuer1";
String issuerWithPath = DEFAULT_ISSUER.concat(issuerPath);
Set<String> endpointUris = getEndpointUris(authorizationServerSettings);
for (String endpointUri : endpointUris) {
assertResolvedIssuer(issuerPath.concat(endpointUri), issuerWithPath);
}
}
@Test
void doFilterWhenCustomEndpointsThenIssuerResolved() throws Exception {
AuthorizationServerSettings authorizationServerSettings = AuthorizationServerSettings.builder()
.authorizationEndpoint("/oauth2/v1/authorize")
.deviceAuthorizationEndpoint("/oauth2/v1/device_authorization")
.deviceVerificationEndpoint("/oauth2/v1/device_verification")
.tokenEndpoint("/oauth2/v1/token")
.jwkSetEndpoint("/oauth2/v1/jwks")
.tokenRevocationEndpoint("/oauth2/v1/revoke")
.tokenIntrospectionEndpoint("/oauth2/v1/introspect")
.oidcClientRegistrationEndpoint("/connect/v1/register")
.oidcUserInfoEndpoint("/v1/userinfo")
.oidcLogoutEndpoint("/connect/v1/logout")
.build();
this.filter = new AuthorizationServerContextFilter(authorizationServerSettings);
String issuerPath = "/issuer2";
String issuerWithPath = DEFAULT_ISSUER.concat(issuerPath);
Set<String> endpointUris = getEndpointUris(authorizationServerSettings);
for (String endpointUri : endpointUris) {
assertResolvedIssuer(issuerPath.concat(endpointUri), issuerWithPath);
}
}
@Test
void doFilterWhenIssuerHasMultiplePathsThenIssuerResolved() throws Exception {
AuthorizationServerSettings authorizationServerSettings = AuthorizationServerSettings.builder().build();
this.filter = new AuthorizationServerContextFilter(authorizationServerSettings);
String issuerPath = "/path1/path2/issuer3";
String issuerWithPath = DEFAULT_ISSUER.concat(issuerPath);
Set<String> endpointUris = getEndpointUris(authorizationServerSettings);
for (String endpointUri : endpointUris) {
assertResolvedIssuer(issuerPath.concat(endpointUri), issuerWithPath);
}
}
private void assertResolvedIssuer(String requestUri, String expectedIssuer) throws Exception {
MockHttpServletRequest request = createRequest(requestUri);
MockHttpServletResponse response = new MockHttpServletResponse();
AtomicReference<String> resolvedIssuer = new AtomicReference<>();
FilterChain filterChain = (req, resp) -> resolvedIssuer
.set(AuthorizationServerContextHolder.getContext().getIssuer());
this.filter.doFilter(request, response, filterChain);
assertThat(resolvedIssuer.get()).isEqualTo(expectedIssuer);
}
private static Set<String> getEndpointUris(AuthorizationServerSettings authorizationServerSettings) {
Set<String> endpointUris = new HashSet<>();
endpointUris.add("/.well-known/oauth-authorization-server");
endpointUris.add("/.well-known/openid-configuration");
for (Map.Entry<String, Object> setting : authorizationServerSettings.getSettings().entrySet()) {
if (setting.getKey().endsWith("-endpoint")) {
endpointUris.add((String) setting.getValue());
}
}
return endpointUris;
}
private static MockHttpServletRequest createRequest(String requestUri) {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setRequestURI(requestUri);
request.setScheme(SCHEME);
request.setServerName(HOST);
request.setServerPort(PORT);
return request;
}
}
@@ -0,0 +1,243 @@
/*
* Copyright 2004-present 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.config.annotation.web.configurers.oauth2.server.authorization;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.JwsHeader;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeActor;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeCompositeAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.util.TestX509Certificates;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link DefaultOAuth2TokenCustomizers}.
*
* @author Steve Riesenberg
* @author Joe Grandja
*/
class DefaultOAuth2TokenCustomizersTests {
private static final String ISSUER_1 = "issuer-1";
private static final String ISSUER_2 = "issuer-2";
private JwsHeader.Builder jwsHeaderBuilder;
private JwtClaimsSet.Builder jwtClaimsBuilder;
@BeforeEach
void setUp() {
this.jwsHeaderBuilder = JwsHeader.with(SignatureAlgorithm.RS256);
this.jwtClaimsBuilder = JwtClaimsSet.builder().issuer(ISSUER_1);
}
@Test
void customizeWhenTokenTypeIsRefreshTokenThenNoClaimsAdded() {
// @formatter:off
JwtEncodingContext tokenContext = JwtEncodingContext.with(this.jwsHeaderBuilder, this.jwtClaimsBuilder)
.tokenType(OAuth2TokenType.REFRESH_TOKEN)
.build();
// @formatter:on
DefaultOAuth2TokenCustomizers.jwtCustomizer().customize(tokenContext);
JwtClaimsSet jwtClaimsSet = this.jwtClaimsBuilder.build();
assertThat(jwtClaimsSet.getClaims()).containsOnly(entry(JwtClaimNames.ISS, ISSUER_1));
}
@Test
void customizeWhenAuthorizationGrantIsNullThenNoClaimsAdded() {
// @formatter:off
JwtEncodingContext tokenContext = JwtEncodingContext.with(this.jwsHeaderBuilder, this.jwtClaimsBuilder)
.tokenType(OAuth2TokenType.ACCESS_TOKEN)
.build();
// @formatter:on
DefaultOAuth2TokenCustomizers.jwtCustomizer().customize(tokenContext);
JwtClaimsSet jwtClaimsSet = this.jwtClaimsBuilder.build();
assertThat(jwtClaimsSet.getClaims()).containsOnly(entry(JwtClaimNames.ISS, ISSUER_1));
}
@Test
void customizeWhenTokenExchangeGrantAndResourcesThenNoClaimsAdded() {
OAuth2TokenExchangeAuthenticationToken tokenExchangeAuthentication = mock(
OAuth2TokenExchangeAuthenticationToken.class);
given(tokenExchangeAuthentication.getResources()).willReturn(Set.of("resource1", "resource2"));
// @formatter:off
JwtEncodingContext tokenContext = JwtEncodingContext.with(this.jwsHeaderBuilder, this.jwtClaimsBuilder)
.tokenType(OAuth2TokenType.ACCESS_TOKEN)
.authorizationGrant(tokenExchangeAuthentication)
.build();
// @formatter:on
DefaultOAuth2TokenCustomizers.jwtCustomizer().customize(tokenContext);
JwtClaimsSet jwtClaimsSet = this.jwtClaimsBuilder.build();
// We do not populate claims (e.g. `aud`) based on the resource parameter
assertThat(jwtClaimsSet.getClaims()).containsOnly(entry(JwtClaimNames.ISS, ISSUER_1));
}
@Test
void customizeWhenTokenExchangeGrantAndAudiencesThenNoClaimsAdded() {
OAuth2TokenExchangeAuthenticationToken tokenExchangeAuthentication = mock(
OAuth2TokenExchangeAuthenticationToken.class);
given(tokenExchangeAuthentication.getAudiences()).willReturn(Set.of("audience1", "audience2"));
// @formatter:off
JwtEncodingContext tokenContext = JwtEncodingContext.with(this.jwsHeaderBuilder, this.jwtClaimsBuilder)
.tokenType(OAuth2TokenType.ACCESS_TOKEN)
.authorizationGrant(tokenExchangeAuthentication)
.build();
// @formatter:on
DefaultOAuth2TokenCustomizers.jwtCustomizer().customize(tokenContext);
JwtClaimsSet jwtClaimsSet = this.jwtClaimsBuilder.build();
// NOTE: We do not populate claims (e.g. `aud`) based on the audience parameter
assertThat(jwtClaimsSet.getClaims()).containsOnly(entry(JwtClaimNames.ISS, ISSUER_1));
}
@Test
void customizeWhenTokenExchangeGrantAndDelegationThenActClaimAdded() {
OAuth2TokenExchangeAuthenticationToken tokenExchangeAuthentication = mock(
OAuth2TokenExchangeAuthenticationToken.class);
given(tokenExchangeAuthentication.getAudiences()).willReturn(Collections.emptySet());
Authentication subject = new TestingAuthenticationToken("subject", null);
OAuth2TokenExchangeActor actor1 = new OAuth2TokenExchangeActor(
Map.of(JwtClaimNames.ISS, ISSUER_1, JwtClaimNames.SUB, "actor1"));
OAuth2TokenExchangeActor actor2 = new OAuth2TokenExchangeActor(
Map.of(JwtClaimNames.ISS, ISSUER_2, JwtClaimNames.SUB, "actor2"));
OAuth2TokenExchangeCompositeAuthenticationToken principal = new OAuth2TokenExchangeCompositeAuthenticationToken(
subject, List.of(actor1, actor2));
// @formatter:off
JwtEncodingContext tokenContext = JwtEncodingContext.with(this.jwsHeaderBuilder, this.jwtClaimsBuilder)
.tokenType(OAuth2TokenType.ACCESS_TOKEN)
.principal(principal)
.authorizationGrant(tokenExchangeAuthentication)
.build();
// @formatter:on
DefaultOAuth2TokenCustomizers.jwtCustomizer().customize(tokenContext);
JwtClaimsSet jwtClaimsSet = this.jwtClaimsBuilder.build();
assertThat(jwtClaimsSet.getClaims()).isNotEmpty();
assertThat(jwtClaimsSet.getClaims()).hasSize(2);
assertThat(jwtClaimsSet.getClaims().get("act")).isNotNull();
@SuppressWarnings("unchecked")
Map<String, Object> actClaim1 = (Map<String, Object>) jwtClaimsSet.getClaims().get("act");
assertThat(actClaim1.get(JwtClaimNames.ISS)).isEqualTo(ISSUER_1);
assertThat(actClaim1.get(JwtClaimNames.SUB)).isEqualTo("actor1");
@SuppressWarnings("unchecked")
Map<String, Object> actClaim2 = (Map<String, Object>) actClaim1.get("act");
assertThat(actClaim2.get(JwtClaimNames.ISS)).isEqualTo(ISSUER_2);
assertThat(actClaim2.get(JwtClaimNames.SUB)).isEqualTo("actor2");
}
@Test
void customizeWhenPKIX509ClientCertificateAndCertificateBoundAccessTokensThenX5tClaimAdded() {
// @formatter:off
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.clientAuthenticationMethod(ClientAuthenticationMethod.TLS_CLIENT_AUTH)
.clientSettings(
ClientSettings.builder()
.x509CertificateSubjectDN(TestX509Certificates.DEMO_CLIENT_PKI_CERTIFICATE[0].getSubjectX500Principal().getName())
.build()
)
.tokenSettings(
TokenSettings.builder()
.x509CertificateBoundAccessTokens(true)
.build()
)
.build();
// @formatter:on
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
ClientAuthenticationMethod.TLS_CLIENT_AUTH, TestX509Certificates.DEMO_CLIENT_PKI_CERTIFICATE);
OAuth2ClientCredentialsAuthenticationToken clientCredentialsAuthentication = new OAuth2ClientCredentialsAuthenticationToken(
clientPrincipal, null, null);
// @formatter:off
JwtEncodingContext tokenContext = JwtEncodingContext.with(this.jwsHeaderBuilder, this.jwtClaimsBuilder)
.tokenType(OAuth2TokenType.ACCESS_TOKEN)
.registeredClient(registeredClient)
.authorizationGrant(clientCredentialsAuthentication)
.build();
// @formatter:on
DefaultOAuth2TokenCustomizers.jwtCustomizer().customize(tokenContext);
JwtClaimsSet jwtClaimsSet = this.jwtClaimsBuilder.build();
assertThat(jwtClaimsSet.getClaims()).isNotEmpty();
assertThat(jwtClaimsSet.getClaims()).hasSize(2);
Map<String, Object> cnfClaim = jwtClaimsSet.getClaim("cnf");
assertThat(cnfClaim).isNotEmpty();
assertThat(cnfClaim.get("x5t#S256")).isNotNull();
}
@Test
void customizeWhenSelfSignedX509ClientCertificateAndCertificateBoundAccessTokensThenX5tClaimAdded() {
// @formatter:off
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.clientAuthenticationMethod(ClientAuthenticationMethod.SELF_SIGNED_TLS_CLIENT_AUTH)
.clientSettings(
ClientSettings.builder()
.jwkSetUrl("https://client.example.com/jwks")
.build()
)
.tokenSettings(
TokenSettings.builder()
.x509CertificateBoundAccessTokens(true)
.build()
)
.build();
// @formatter:on
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
ClientAuthenticationMethod.SELF_SIGNED_TLS_CLIENT_AUTH,
TestX509Certificates.DEMO_CLIENT_SELF_SIGNED_CERTIFICATE);
OAuth2ClientCredentialsAuthenticationToken clientCredentialsAuthentication = new OAuth2ClientCredentialsAuthenticationToken(
clientPrincipal, null, null);
// @formatter:off
JwtEncodingContext tokenContext = JwtEncodingContext.with(this.jwsHeaderBuilder, this.jwtClaimsBuilder)
.tokenType(OAuth2TokenType.ACCESS_TOKEN)
.registeredClient(registeredClient)
.authorizationGrant(clientCredentialsAuthentication)
.build();
// @formatter:on
DefaultOAuth2TokenCustomizers.jwtCustomizer().customize(tokenContext);
JwtClaimsSet jwtClaimsSet = this.jwtClaimsBuilder.build();
assertThat(jwtClaimsSet.getClaims()).isNotEmpty();
assertThat(jwtClaimsSet.getClaims()).hasSize(2);
Map<String, Object> cnfClaim = jwtClaimsSet.getClaim("cnf");
assertThat(cnfClaim).isNotEmpty();
assertThat(cnfClaim.get("x5t#S256")).isNotNull();
}
}
@@ -0,0 +1,200 @@
/*
* Copyright 2004-present 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.config.annotation.web.configurers.oauth2.server.authorization;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpHeaders;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.oauth2.jose.TestJwks;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.jackson2.TestingAuthenticationTokenMixin;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.CoreMatchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Integration tests for the JWK Set endpoint.
*
* @author Florian Berthe
*/
@ExtendWith(SpringTestContextExtension.class)
public class JwkSetTests {
private static final String DEFAULT_JWK_SET_ENDPOINT_URI = "/oauth2/jwks";
private static EmbeddedDatabase db;
private static JWKSource<SecurityContext> jwkSource;
public final SpringTestContext spring = new SpringTestContext(this);
@Autowired
private MockMvc mvc;
@Autowired
private JdbcOperations jdbcOperations;
@Autowired
private AuthorizationServerSettings authorizationServerSettings;
@BeforeAll
public static void init() {
JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
db = new EmbeddedDatabaseBuilder().generateUniqueName(true)
.setType(EmbeddedDatabaseType.HSQL)
.setScriptEncoding("UTF-8")
.addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-schema.sql")
.addScript(
"org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql")
.build();
}
@AfterEach
public void tearDown() {
this.jdbcOperations.update("truncate table oauth2_authorization");
this.jdbcOperations.update("truncate table oauth2_registered_client");
}
@AfterAll
public static void destroy() {
db.shutdown();
}
@Test
public void requestWhenJwkSetThenReturnKeys() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
assertJwkSetRequestThenReturnKeys(DEFAULT_JWK_SET_ENDPOINT_URI);
}
@Test
public void requestWhenJwkSetCustomEndpointThenReturnKeys() throws Exception {
this.spring.register(AuthorizationServerConfigurationCustomEndpoints.class).autowire();
assertJwkSetRequestThenReturnKeys(this.authorizationServerSettings.getJwkSetEndpoint());
}
@Test
public void requestWhenJwkSetRequestIncludesIssuerPathThenReturnKeys() throws Exception {
this.spring.register(AuthorizationServerConfigurationCustomEndpoints.class).autowire();
String issuer = "https://example.com:8443/issuer1";
assertJwkSetRequestThenReturnKeys(issuer.concat(this.authorizationServerSettings.getJwkSetEndpoint()));
}
private void assertJwkSetRequestThenReturnKeys(String jwkSetEndpointUri) throws Exception {
this.mvc.perform(get(jwkSetEndpointUri))
.andExpect(status().isOk())
.andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store")))
.andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache")))
.andExpect(jsonPath("$.keys").isNotEmpty())
.andExpect(jsonPath("$.keys").isArray());
}
@EnableWebSecurity
@Import(OAuth2AuthorizationServerConfiguration.class)
static class AuthorizationServerConfiguration {
@Bean
OAuth2AuthorizationService authorizationService(JdbcOperations jdbcOperations,
RegisteredClientRepository registeredClientRepository) {
JdbcOAuth2AuthorizationService authorizationService = new JdbcOAuth2AuthorizationService(jdbcOperations,
registeredClientRepository);
authorizationService.setAuthorizationRowMapper(new RowMapper(registeredClientRepository));
authorizationService.setAuthorizationParametersMapper(new ParametersMapper());
return authorizationService;
}
@Bean
RegisteredClientRepository registeredClientRepository(JdbcOperations jdbcOperations) {
return new JdbcRegisteredClientRepository(jdbcOperations);
}
@Bean
JdbcOperations jdbcOperations() {
return new JdbcTemplate(db);
}
@Bean
JWKSource<SecurityContext> jwkSource() {
return jwkSource;
}
static class RowMapper extends JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper {
RowMapper(RegisteredClientRepository registeredClientRepository) {
super(registeredClientRepository);
getObjectMapper().addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class);
}
}
static class ParametersMapper extends JdbcOAuth2AuthorizationService.OAuth2AuthorizationParametersMapper {
ParametersMapper() {
super();
getObjectMapper().addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class);
}
}
}
@EnableWebSecurity
@Import(OAuth2AuthorizationServerConfiguration.class)
static class AuthorizationServerConfigurationCustomEndpoints extends AuthorizationServerConfiguration {
@Bean
AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder()
.jwkSetEndpoint("/test/jwks")
.multipleIssuersAllowed(true)
.build();
}
}
}
@@ -0,0 +1,230 @@
/*
* Copyright 2004-present 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.config.annotation.web.configurers.oauth2.server.authorization;
import java.util.function.Consumer;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.oauth2.jose.TestJwks;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationServerMetadata;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationServerMetadataClaimNames;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.CoreMatchers.hasItems;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Integration tests for the OAuth 2.0 Authorization Server Metadata endpoint.
*
* @author Daniel Garnier-Moiroux
*/
@ExtendWith(SpringTestContextExtension.class)
public class OAuth2AuthorizationServerMetadataTests {
private static final String DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI = "/.well-known/oauth-authorization-server";
private static final String ISSUER = "https://example.com";
private static EmbeddedDatabase db;
private static JWKSource<SecurityContext> jwkSource;
public final SpringTestContext spring = new SpringTestContext(this);
@Autowired
private MockMvc mvc;
@Autowired
private JdbcOperations jdbcOperations;
@BeforeAll
public static void setupClass() {
JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
db = new EmbeddedDatabaseBuilder().generateUniqueName(true)
.setType(EmbeddedDatabaseType.HSQL)
.setScriptEncoding("UTF-8")
.addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-schema.sql")
.addScript(
"org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql")
.build();
}
@AfterEach
public void tearDown() {
this.jdbcOperations.update("truncate table oauth2_authorization");
this.jdbcOperations.update("truncate table oauth2_registered_client");
}
@AfterAll
public static void destroy() {
db.shutdown();
}
@Test
public void requestWhenAuthorizationServerMetadataRequestAndIssuerSetThenUsed() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
this.mvc.perform(get(ISSUER.concat(DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI)))
.andExpect(status().is2xxSuccessful())
.andExpect(jsonPath("issuer").value(ISSUER))
.andReturn();
}
@Test
public void requestWhenAuthorizationServerMetadataRequestIncludesIssuerPathThenMetadataResponseHasIssuerPath()
throws Exception {
this.spring.register(AuthorizationServerConfigurationWithMultipleIssuersAllowed.class).autowire();
String host = "https://example.com:8443";
String issuerPath = "/issuer1";
String issuer = host.concat(issuerPath);
this.mvc.perform(get(host.concat(DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI).concat(issuerPath)))
.andExpect(status().is2xxSuccessful())
.andExpect(jsonPath("issuer").value(issuer))
.andReturn();
issuerPath = "/path1/issuer2";
issuer = host.concat(issuerPath);
this.mvc.perform(get(host.concat(DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI).concat(issuerPath)))
.andExpect(status().is2xxSuccessful())
.andExpect(jsonPath("issuer").value(issuer))
.andReturn();
issuerPath = "/path1/path2/issuer3";
issuer = host.concat(issuerPath);
this.mvc.perform(get(host.concat(DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI).concat(issuerPath)))
.andExpect(status().is2xxSuccessful())
.andExpect(jsonPath("issuer").value(issuer))
.andReturn();
}
// gh-616
@Test
public void requestWhenAuthorizationServerMetadataRequestAndMetadataCustomizerSetThenReturnCustomMetadataResponse()
throws Exception {
this.spring.register(AuthorizationServerConfigurationWithMetadataCustomizer.class).autowire();
this.mvc.perform(get(ISSUER.concat(DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI)))
.andExpect(status().is2xxSuccessful())
.andExpect(jsonPath(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED,
hasItems("scope1", "scope2")));
}
@EnableWebSecurity
@Import(OAuth2AuthorizationServerConfiguration.class)
static class AuthorizationServerConfiguration {
@Bean
RegisteredClientRepository registeredClientRepository(JdbcOperations jdbcOperations) {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(
jdbcOperations);
registeredClientRepository.save(registeredClient);
return registeredClientRepository;
}
@Bean
JdbcOperations jdbcOperations() {
return new JdbcTemplate(db);
}
@Bean
JWKSource<SecurityContext> jwkSource() {
return jwkSource;
}
@Bean
AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().issuer(ISSUER).build();
}
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class AuthorizationServerConfigurationWithMetadataCustomizer extends AuthorizationServerConfiguration {
// @formatter:off
@Bean
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
OAuth2AuthorizationServerConfigurer.authorizationServer();
http
.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
.with(authorizationServerConfigurer, (authorizationServer) ->
authorizationServer
.authorizationServerMetadataEndpoint((authorizationServerMetadataEndpoint) ->
authorizationServerMetadataEndpoint
.authorizationServerMetadataCustomizer(authorizationServerMetadataCustomizer()))
)
.authorizeHttpRequests((authorize) ->
authorize.anyRequest().authenticated()
);
return http.build();
}
// @formatter:on
private Consumer<OAuth2AuthorizationServerMetadata.Builder> authorizationServerMetadataCustomizer() {
return (authorizationServerMetadata) -> authorizationServerMetadata.scope("scope1").scope("scope2");
}
}
@EnableWebSecurity
@Import(OAuth2AuthorizationServerConfiguration.class)
static class AuthorizationServerConfigurationWithMultipleIssuersAllowed extends AuthorizationServerConfiguration {
@Bean
AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().multipleIssuersAllowed(true).build();
}
}
}
@@ -0,0 +1,691 @@
/*
* Copyright 2004-present 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.config.annotation.web.configurers.oauth2.server.authorization;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.function.Consumer;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpHeaders;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
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.context.SecurityContextHolder;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.jose.TestJwks;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.JwsHeader;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.authentication.ClientSecretAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.JwtClientAssertionAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceCodeAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2RefreshTokenAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.PublicClientAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.X509ClientCertificateAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository.RegisteredClientParametersMapper;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import org.springframework.security.oauth2.server.authorization.jackson2.TestingAuthenticationTokenMixin;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
import org.springframework.security.oauth2.server.authorization.util.TestX509Certificates;
import org.springframework.security.oauth2.server.authorization.web.authentication.ClientSecretBasicAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.ClientSecretPostAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.JwtClientAssertionAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ClientCredentialsAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2DeviceCodeAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2RefreshTokenAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2TokenExchangeAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.PublicClientAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.X509ClientCertificateAuthenticationConverter;
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Integration tests for the OAuth 2.0 Client Credentials Grant.
*
* @author Alexey Nesterov
* @author Joe Grandja
*/
@ExtendWith(SpringTestContextExtension.class)
public class OAuth2ClientCredentialsGrantTests {
private static final String DEFAULT_TOKEN_ENDPOINT_URI = "/oauth2/token";
private static EmbeddedDatabase db;
private static JWKSource<SecurityContext> jwkSource;
private static OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer;
private static NimbusJwtEncoder dPoPProofJwtEncoder;
private static AuthenticationConverter authenticationConverter;
private static Consumer<List<AuthenticationConverter>> authenticationConvertersConsumer;
private static AuthenticationProvider authenticationProvider;
private static Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer;
private static AuthenticationSuccessHandler authenticationSuccessHandler;
private static AuthenticationFailureHandler authenticationFailureHandler;
public final SpringTestContext spring = new SpringTestContext(this);
@Autowired
private MockMvc mvc;
@Autowired
private JdbcOperations jdbcOperations;
@Autowired
private RegisteredClientRepository registeredClientRepository;
@BeforeAll
public static void init() {
JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
jwtCustomizer = mock(OAuth2TokenCustomizer.class);
JWKSet clientJwkSet = new JWKSet(TestJwks.DEFAULT_EC_JWK);
JWKSource<SecurityContext> clientJwkSource = (jwkSelector, securityContext) -> jwkSelector.select(clientJwkSet);
dPoPProofJwtEncoder = new NimbusJwtEncoder(clientJwkSource);
authenticationConverter = mock(AuthenticationConverter.class);
authenticationConvertersConsumer = mock(Consumer.class);
authenticationProvider = mock(AuthenticationProvider.class);
authenticationProvidersConsumer = mock(Consumer.class);
authenticationSuccessHandler = mock(AuthenticationSuccessHandler.class);
authenticationFailureHandler = mock(AuthenticationFailureHandler.class);
db = new EmbeddedDatabaseBuilder().generateUniqueName(true)
.setType(EmbeddedDatabaseType.HSQL)
.setScriptEncoding("UTF-8")
.addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-schema.sql")
.addScript(
"org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql")
.build();
}
@SuppressWarnings("unchecked")
@BeforeEach
public void setup() {
reset(jwtCustomizer);
reset(authenticationConverter);
reset(authenticationConvertersConsumer);
reset(authenticationProvider);
reset(authenticationProvidersConsumer);
reset(authenticationSuccessHandler);
reset(authenticationFailureHandler);
}
@AfterEach
public void tearDown() {
this.jdbcOperations.update("truncate table oauth2_authorization");
this.jdbcOperations.update("truncate table oauth2_registered_client");
}
@AfterAll
public static void destroy() {
db.shutdown();
}
@Test
public void requestWhenTokenRequestNotAuthenticatedThenUnauthorized() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
this.mvc
.perform(MockMvcRequestBuilders.post(DEFAULT_TOKEN_ENDPOINT_URI)
.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()))
.andExpect(status().isUnauthorized());
}
@Test
public void requestWhenTokenRequestValidThenTokenResponse() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredClient2().build();
this.registeredClientRepository.save(registeredClient);
this.mvc
.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.param(OAuth2ParameterNames.SCOPE, "scope1 scope2")
.header(HttpHeaders.AUTHORIZATION,
"Basic " + encodeBasicAuth(registeredClient.getClientId(), registeredClient.getClientSecret())))
.andExpect(status().isOk())
.andExpect(jsonPath("$.access_token").isNotEmpty())
.andExpect(jsonPath("$.scope").value("scope1 scope2"));
verify(jwtCustomizer).customize(any());
}
@Test
public void requestWhenTokenRequestPostsClientCredentialsThenTokenResponse() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredClient2().build();
this.registeredClientRepository.save(registeredClient);
this.mvc
.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.param(OAuth2ParameterNames.SCOPE, "scope1 scope2")
.param(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())
.param(OAuth2ParameterNames.CLIENT_SECRET, registeredClient.getClientSecret()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.access_token").isNotEmpty())
.andExpect(jsonPath("$.scope").value("scope1 scope2"));
verify(jwtCustomizer).customize(any());
}
@Test
public void requestWhenTokenRequestPostsClientCredentialsAndRequiresUpgradingThenClientSecretUpgraded()
throws Exception {
this.spring.register(AuthorizationServerConfigurationCustomPasswordEncoder.class).autowire();
String clientSecret = "secret-2";
RegisteredClient registeredClient = TestRegisteredClients.registeredClient2()
.clientSecret("{noop}" + clientSecret)
.build();
this.registeredClientRepository.save(registeredClient);
this.mvc
.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.param(OAuth2ParameterNames.SCOPE, "scope1 scope2")
.param(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())
.param(OAuth2ParameterNames.CLIENT_SECRET, clientSecret))
.andExpect(status().isOk())
.andExpect(jsonPath("$.access_token").isNotEmpty())
.andExpect(jsonPath("$.scope").value("scope1 scope2"));
verify(jwtCustomizer).customize(any());
RegisteredClient updatedRegisteredClient = this.registeredClientRepository
.findByClientId(registeredClient.getClientId());
assertThat(updatedRegisteredClient.getClientSecret()).startsWith("{bcrypt}");
}
@Test
public void requestWhenTokenRequestWithPKIX509ClientCertificateThenTokenResponse() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
// @formatter:off
RegisteredClient registeredClient = TestRegisteredClients.registeredClient2()
.clientAuthenticationMethod(ClientAuthenticationMethod.TLS_CLIENT_AUTH)
.clientSettings(
ClientSettings.builder()
.x509CertificateSubjectDN(TestX509Certificates.DEMO_CLIENT_PKI_CERTIFICATE[0].getSubjectX500Principal().getName())
.build()
)
.build();
// @formatter:on
this.registeredClientRepository.save(registeredClient);
this.mvc
.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
.with(SecurityMockMvcRequestPostProcessors.x509(TestX509Certificates.DEMO_CLIENT_PKI_CERTIFICATE))
.param(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())
.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.param(OAuth2ParameterNames.SCOPE, "scope1 scope2"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.access_token").isNotEmpty())
.andExpect(jsonPath("$.scope").value("scope1 scope2"));
verify(jwtCustomizer).customize(any());
}
// gh-1635
@Test
public void requestWhenTokenRequestIncludesBasicClientCredentialsAndX509ClientCertificateThenTokenResponse()
throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredClient2().build();
this.registeredClientRepository.save(registeredClient);
this.mvc
.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
.with(SecurityMockMvcRequestPostProcessors.x509(TestX509Certificates.DEMO_CLIENT_PKI_CERTIFICATE))
.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.param(OAuth2ParameterNames.SCOPE, "scope1 scope2")
.header(HttpHeaders.AUTHORIZATION,
"Basic " + encodeBasicAuth(registeredClient.getClientId(), registeredClient.getClientSecret())))
.andExpect(status().isOk())
.andExpect(jsonPath("$.access_token").isNotEmpty())
.andExpect(jsonPath("$.scope").value("scope1 scope2"));
verify(jwtCustomizer).customize(any());
}
@Test
public void requestWhenTokenEndpointCustomizedThenUsed() throws Exception {
this.spring.register(AuthorizationServerConfigurationCustomTokenEndpoint.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredClient2().build();
this.registeredClientRepository.save(registeredClient);
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
OAuth2ClientCredentialsAuthenticationToken clientCredentialsAuthentication = new OAuth2ClientCredentialsAuthenticationToken(
clientPrincipal, null, null);
given(authenticationConverter.convert(any())).willReturn(clientCredentialsAuthentication);
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "token",
Instant.now(), Instant.now().plus(Duration.ofHours(1)));
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = new OAuth2AccessTokenAuthenticationToken(
registeredClient, clientPrincipal, accessToken);
given(authenticationProvider.supports(eq(OAuth2ClientCredentialsAuthenticationToken.class))).willReturn(true);
given(authenticationProvider.authenticate(any())).willReturn(accessTokenAuthentication);
this.mvc
.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.header(HttpHeaders.AUTHORIZATION,
"Basic " + encodeBasicAuth(registeredClient.getClientId(), registeredClient.getClientSecret())))
.andExpect(status().isOk());
verify(authenticationConverter).convert(any());
@SuppressWarnings("unchecked")
ArgumentCaptor<List<AuthenticationConverter>> authenticationConvertersCaptor = ArgumentCaptor
.forClass(List.class);
verify(authenticationConvertersConsumer).accept(authenticationConvertersCaptor.capture());
List<AuthenticationConverter> authenticationConverters = authenticationConvertersCaptor.getValue();
assertThat(authenticationConverters).allMatch((converter) -> converter == authenticationConverter
|| converter instanceof OAuth2AuthorizationCodeAuthenticationConverter
|| converter instanceof OAuth2RefreshTokenAuthenticationConverter
|| converter instanceof OAuth2ClientCredentialsAuthenticationConverter
|| converter instanceof OAuth2DeviceCodeAuthenticationConverter
|| converter instanceof OAuth2TokenExchangeAuthenticationConverter);
verify(authenticationProvider).authenticate(eq(clientCredentialsAuthentication));
@SuppressWarnings("unchecked")
ArgumentCaptor<List<AuthenticationProvider>> authenticationProvidersCaptor = ArgumentCaptor
.forClass(List.class);
verify(authenticationProvidersConsumer).accept(authenticationProvidersCaptor.capture());
List<AuthenticationProvider> authenticationProviders = authenticationProvidersCaptor.getValue();
assertThat(authenticationProviders).allMatch((provider) -> provider == authenticationProvider
|| provider instanceof OAuth2AuthorizationCodeAuthenticationProvider
|| provider instanceof OAuth2RefreshTokenAuthenticationProvider
|| provider instanceof OAuth2ClientCredentialsAuthenticationProvider
|| provider instanceof OAuth2DeviceCodeAuthenticationProvider
|| provider instanceof OAuth2TokenExchangeAuthenticationProvider);
verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), eq(accessTokenAuthentication));
}
@Test
public void requestWhenClientAuthenticationCustomizedThenUsed() throws Exception {
this.spring.register(AuthorizationServerConfigurationCustomClientAuthentication.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredClient2().build();
this.registeredClientRepository.save(registeredClient);
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
new ClientAuthenticationMethod("custom"), null);
given(authenticationConverter.convert(any())).willReturn(clientPrincipal);
given(authenticationProvider.supports(eq(OAuth2ClientAuthenticationToken.class))).willReturn(true);
given(authenticationProvider.authenticate(any())).willReturn(clientPrincipal);
this.mvc
.perform(post(DEFAULT_TOKEN_ENDPOINT_URI).param(OAuth2ParameterNames.GRANT_TYPE,
AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()))
.andExpect(status().isOk());
verify(authenticationConverter).convert(any());
@SuppressWarnings("unchecked")
ArgumentCaptor<List<AuthenticationConverter>> authenticationConvertersCaptor = ArgumentCaptor
.forClass(List.class);
verify(authenticationConvertersConsumer).accept(authenticationConvertersCaptor.capture());
List<AuthenticationConverter> authenticationConverters = authenticationConvertersCaptor.getValue();
assertThat(authenticationConverters).allMatch((converter) -> converter == authenticationConverter
|| converter instanceof JwtClientAssertionAuthenticationConverter
|| converter instanceof ClientSecretBasicAuthenticationConverter
|| converter instanceof ClientSecretPostAuthenticationConverter
|| converter instanceof PublicClientAuthenticationConverter
|| converter instanceof X509ClientCertificateAuthenticationConverter);
verify(authenticationProvider).authenticate(eq(clientPrincipal));
@SuppressWarnings("unchecked")
ArgumentCaptor<List<AuthenticationProvider>> authenticationProvidersCaptor = ArgumentCaptor
.forClass(List.class);
verify(authenticationProvidersConsumer).accept(authenticationProvidersCaptor.capture());
List<AuthenticationProvider> authenticationProviders = authenticationProvidersCaptor.getValue();
assertThat(authenticationProviders).allMatch((provider) -> provider == authenticationProvider
|| provider instanceof JwtClientAssertionAuthenticationProvider
|| provider instanceof X509ClientCertificateAuthenticationProvider
|| provider instanceof ClientSecretAuthenticationProvider
|| provider instanceof PublicClientAuthenticationProvider);
verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), eq(clientPrincipal));
}
@Test
public void requestWhenTokenRequestIncludesIssuerPathThenIssuerResolvedWithPath() throws Exception {
this.spring.register(AuthorizationServerConfigurationWithMultipleIssuersAllowed.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredClient2().build();
this.registeredClientRepository.save(registeredClient);
String issuer = "https://example.com:8443/issuer1";
this.mvc
.perform(post(issuer.concat(DEFAULT_TOKEN_ENDPOINT_URI))
.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.param(OAuth2ParameterNames.SCOPE, "scope1 scope2")
.header(HttpHeaders.AUTHORIZATION,
"Basic " + encodeBasicAuth(registeredClient.getClientId(), registeredClient.getClientSecret())))
.andExpect(status().isOk())
.andExpect(jsonPath("$.access_token").isNotEmpty())
.andExpect(jsonPath("$.scope").value("scope1 scope2"));
ArgumentCaptor<JwtEncodingContext> jwtEncodingContextCaptor = ArgumentCaptor.forClass(JwtEncodingContext.class);
verify(jwtCustomizer).customize(jwtEncodingContextCaptor.capture());
JwtEncodingContext jwtEncodingContext = jwtEncodingContextCaptor.getValue();
assertThat(jwtEncodingContext.getAuthorizationServerContext().getIssuer()).isEqualTo(issuer);
}
@Test
public void requestWhenTokenRequestWithDPoPProofThenReturnDPoPBoundAccessToken() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredClient2().build();
this.registeredClientRepository.save(registeredClient);
String tokenEndpointUri = "http://localhost" + DEFAULT_TOKEN_ENDPOINT_URI;
String dPoPProof = generateDPoPProof(tokenEndpointUri);
this.mvc
.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.param(OAuth2ParameterNames.SCOPE, "scope1 scope2")
.header(HttpHeaders.AUTHORIZATION,
"Basic " + encodeBasicAuth(registeredClient.getClientId(), registeredClient.getClientSecret()))
.header(OAuth2AccessToken.TokenType.DPOP.getValue(), dPoPProof))
.andExpect(status().isOk())
.andExpect(jsonPath("$.token_type").value(OAuth2AccessToken.TokenType.DPOP.getValue()));
}
private static String generateDPoPProof(String tokenEndpointUri) {
// @formatter:off
Map<String, Object> publicJwk = TestJwks.DEFAULT_EC_JWK
.toPublicJWK()
.toJSONObject();
JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.ES256)
.type("dpop+jwt")
.jwk(publicJwk)
.build();
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuedAt(Instant.now())
.claim("htm", "POST")
.claim("htu", tokenEndpointUri)
.id(UUID.randomUUID().toString())
.build();
// @formatter:on
Jwt jwt = dPoPProofJwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, claims));
return jwt.getTokenValue();
}
private static String encodeBasicAuth(String clientId, String secret) throws Exception {
clientId = URLEncoder.encode(clientId, StandardCharsets.UTF_8.name());
secret = URLEncoder.encode(secret, StandardCharsets.UTF_8.name());
String credentialsString = clientId + ":" + secret;
byte[] encodedBytes = Base64.getEncoder().encode(credentialsString.getBytes(StandardCharsets.UTF_8));
return new String(encodedBytes, StandardCharsets.UTF_8);
}
@EnableWebSecurity
@Import(OAuth2AuthorizationServerConfiguration.class)
static class AuthorizationServerConfiguration {
@Bean
OAuth2AuthorizationService authorizationService(JdbcOperations jdbcOperations,
RegisteredClientRepository registeredClientRepository) {
JdbcOAuth2AuthorizationService authorizationService = new JdbcOAuth2AuthorizationService(jdbcOperations,
registeredClientRepository);
authorizationService.setAuthorizationRowMapper(new RowMapper(registeredClientRepository));
authorizationService.setAuthorizationParametersMapper(new ParametersMapper());
return authorizationService;
}
@Bean
RegisteredClientRepository registeredClientRepository(JdbcOperations jdbcOperations) {
JdbcRegisteredClientRepository jdbcRegisteredClientRepository = new JdbcRegisteredClientRepository(
jdbcOperations);
RegisteredClientParametersMapper registeredClientParametersMapper = new RegisteredClientParametersMapper();
jdbcRegisteredClientRepository.setRegisteredClientParametersMapper(registeredClientParametersMapper);
return jdbcRegisteredClientRepository;
}
@Bean
JdbcOperations jdbcOperations() {
return new JdbcTemplate(db);
}
@Bean
JWKSource<SecurityContext> jwkSource() {
return jwkSource;
}
@Bean
OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
return jwtCustomizer;
}
@Bean
PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
static class RowMapper extends JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper {
RowMapper(RegisteredClientRepository registeredClientRepository) {
super(registeredClientRepository);
getObjectMapper().addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class);
}
}
static class ParametersMapper extends JdbcOAuth2AuthorizationService.OAuth2AuthorizationParametersMapper {
ParametersMapper() {
super();
getObjectMapper().addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class);
}
}
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class AuthorizationServerConfigurationCustomTokenEndpoint extends AuthorizationServerConfiguration {
// @formatter:off
@Bean
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
OAuth2AuthorizationServerConfigurer.authorizationServer();
http
.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
.with(authorizationServerConfigurer, (authorizationServer) ->
authorizationServer
.tokenEndpoint((tokenEndpoint) ->
tokenEndpoint
.accessTokenRequestConverter(authenticationConverter)
.accessTokenRequestConverters(authenticationConvertersConsumer)
.authenticationProvider(authenticationProvider)
.authenticationProviders(authenticationProvidersConsumer)
.accessTokenResponseHandler(authenticationSuccessHandler)
.errorResponseHandler(authenticationFailureHandler))
)
.authorizeHttpRequests((authorize) ->
authorize.anyRequest().authenticated()
);
return http.build();
}
// @formatter:on
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class AuthorizationServerConfigurationCustomPasswordEncoder extends AuthorizationServerConfiguration {
@Override
PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class AuthorizationServerConfigurationCustomClientAuthentication extends AuthorizationServerConfiguration {
// @formatter:off
@Bean
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
authenticationSuccessHandler = spy(authenticationSuccessHandler());
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
OAuth2AuthorizationServerConfigurer.authorizationServer();
http
.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
.with(authorizationServerConfigurer, (authorizationServer) ->
authorizationServer
.clientAuthentication((clientAuthentication) ->
clientAuthentication
.authenticationConverter(authenticationConverter)
.authenticationConverters(authenticationConvertersConsumer)
.authenticationProvider(authenticationProvider)
.authenticationProviders(authenticationProvidersConsumer)
.authenticationSuccessHandler(authenticationSuccessHandler)
.errorResponseHandler(authenticationFailureHandler))
)
.authorizeHttpRequests((authorize) ->
authorize.anyRequest().authenticated()
);
return http.build();
}
// @formatter:on
private AuthenticationSuccessHandler authenticationSuccessHandler() {
return new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
org.springframework.security.core.context.SecurityContext securityContext = SecurityContextHolder
.createEmptyContext();
securityContext.setAuthentication(authentication);
SecurityContextHolder.setContext(securityContext);
}
};
}
}
@EnableWebSecurity
@Import(OAuth2AuthorizationServerConfiguration.class)
static class AuthorizationServerConfigurationWithMultipleIssuersAllowed extends AuthorizationServerConfiguration {
@Bean
AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().multipleIssuersAllowed(true).build();
}
}
}
@@ -0,0 +1,696 @@
/*
* Copyright 2004-present 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.config.annotation.web.configurers.oauth2.server.authorization;
import java.security.Principal;
import java.time.Instant;
import java.util.Map;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.function.Function;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.mock.http.client.MockClientHttpResponse;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2DeviceCode;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.security.oauth2.core.OAuth2UserCode;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2DeviceAuthorizationResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
import org.springframework.security.oauth2.core.http.converter.OAuth2DeviceAuthorizationResponseHttpMessageConverter;
import org.springframework.security.oauth2.jose.TestJwks;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.JwsHeader;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Integration tests for OAuth 2.0 Device Grant.
*
* @author Steve Riesenberg
*/
@ExtendWith(SpringTestContextExtension.class)
public class OAuth2DeviceCodeGrantTests {
private static final String DEFAULT_DEVICE_AUTHORIZATION_ENDPOINT_URI = "/oauth2/device_authorization";
private static final String DEFAULT_DEVICE_VERIFICATION_ENDPOINT_URI = "/oauth2/device_verification";
private static final String DEFAULT_TOKEN_ENDPOINT_URI = "/oauth2/token";
private static final OAuth2TokenType DEVICE_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.DEVICE_CODE);
private static final String USER_CODE = "ABCD-EFGH";
private static final String STATE = "123";
private static final String DEVICE_CODE = "abc-XYZ";
private static EmbeddedDatabase db;
private static JWKSource<SecurityContext> jwkSource;
private static NimbusJwtEncoder dPoPProofJwtEncoder;
private static final HttpMessageConverter<OAuth2DeviceAuthorizationResponse> deviceAuthorizationResponseHttpMessageConverter = new OAuth2DeviceAuthorizationResponseHttpMessageConverter();
private static final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenResponseHttpMessageConverter = new OAuth2AccessTokenResponseHttpMessageConverter();
public final SpringTestContext spring = new SpringTestContext(this);
@Autowired
private MockMvc mvc;
@Autowired
private JdbcOperations jdbcOperations;
@Autowired
private RegisteredClientRepository registeredClientRepository;
@Autowired
private OAuth2AuthorizationService authorizationService;
@Autowired
private OAuth2AuthorizationConsentService authorizationConsentService;
@BeforeAll
public static void init() {
JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
JWKSet clientJwkSet = new JWKSet(TestJwks.DEFAULT_EC_JWK);
JWKSource<SecurityContext> clientJwkSource = (jwkSelector, securityContext) -> jwkSelector.select(clientJwkSet);
dPoPProofJwtEncoder = new NimbusJwtEncoder(clientJwkSource);
// @formatter:off
db = new EmbeddedDatabaseBuilder()
.generateUniqueName(true)
.setType(EmbeddedDatabaseType.HSQL)
.setScriptEncoding("UTF-8")
.addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-schema.sql")
.addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-consent-schema.sql")
.addScript("org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql")
.build();
// @formatter:on
}
@AfterEach
public void tearDown() {
this.jdbcOperations.update("truncate table oauth2_authorization");
this.jdbcOperations.update("truncate table oauth2_authorization_consent");
this.jdbcOperations.update("truncate table oauth2_registered_client");
}
@AfterAll
public static void destroy() {
db.shutdown();
}
@Test
public void requestWhenDeviceAuthorizationRequestNotAuthenticatedThenUnauthorized() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
// @formatter:off
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
.build();
// @formatter:on
this.registeredClientRepository.save(registeredClient);
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.set(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
parameters.set(OAuth2ParameterNames.SCOPE,
StringUtils.collectionToDelimitedString(registeredClient.getScopes(), " "));
// @formatter:off
this.mvc.perform(post(DEFAULT_DEVICE_AUTHORIZATION_ENDPOINT_URI)
.params(parameters))
.andExpect(status().isUnauthorized());
// @formatter:on
}
@Test
public void requestWhenRegisteredClientMissingThenUnauthorized() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
// @formatter:off
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
.build();
// @formatter:on
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.set(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
parameters.set(OAuth2ParameterNames.SCOPE,
StringUtils.collectionToDelimitedString(registeredClient.getScopes(), " "));
// @formatter:off
this.mvc.perform(post(DEFAULT_DEVICE_AUTHORIZATION_ENDPOINT_URI)
.params(parameters)
.headers(withClientAuth(registeredClient)))
.andExpect(status().isUnauthorized());
// @formatter:on
}
@Test
public void requestWhenDeviceAuthorizationRequestValidThenReturnDeviceAuthorizationResponse() throws Exception {
this.spring.register(AuthorizationServerConfigurationWithMultipleIssuersAllowed.class).autowire();
// @formatter:off
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
.build();
// @formatter:on
this.registeredClientRepository.save(registeredClient);
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.set(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
parameters.set(OAuth2ParameterNames.SCOPE,
StringUtils.collectionToDelimitedString(registeredClient.getScopes(), " "));
String issuer = "https://example.com:8443/issuer1";
// @formatter:off
MvcResult mvcResult = this.mvc.perform(post(issuer.concat(DEFAULT_DEVICE_AUTHORIZATION_ENDPOINT_URI))
.params(parameters)
.headers(withClientAuth(registeredClient)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.device_code").isNotEmpty())
.andExpect(jsonPath("$.user_code").isNotEmpty())
.andExpect(jsonPath("$.expires_in").isNumber())
.andExpect(jsonPath("$.verification_uri").isNotEmpty())
.andExpect(jsonPath("$.verification_uri_complete").isNotEmpty())
.andReturn();
// @formatter:on
MockHttpServletResponse servletResponse = mvcResult.getResponse();
MockClientHttpResponse httpResponse = new MockClientHttpResponse(servletResponse.getContentAsByteArray(),
HttpStatus.OK);
OAuth2DeviceAuthorizationResponse deviceAuthorizationResponse = deviceAuthorizationResponseHttpMessageConverter
.read(OAuth2DeviceAuthorizationResponse.class, httpResponse);
String userCode = deviceAuthorizationResponse.getUserCode().getTokenValue();
assertThat(userCode).matches("[A-Z]{4}-[A-Z]{4}");
assertThat(deviceAuthorizationResponse.getVerificationUri())
.isEqualTo("https://example.com:8443/oauth2/device_verification");
assertThat(deviceAuthorizationResponse.getVerificationUriComplete())
.isEqualTo("https://example.com:8443/oauth2/device_verification?user_code=" + userCode);
String deviceCode = deviceAuthorizationResponse.getDeviceCode().getTokenValue();
OAuth2Authorization authorization = this.authorizationService.findByToken(deviceCode, DEVICE_CODE_TOKEN_TYPE);
assertThat(authorization.getToken(OAuth2DeviceCode.class)).isNotNull();
assertThat(authorization.getToken(OAuth2UserCode.class)).isNotNull();
}
@Test
public void requestWhenDeviceVerificationRequestUnauthenticatedThenUnauthorized() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
// @formatter:off
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
.build();
// @formatter:on
this.registeredClientRepository.save(registeredClient);
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plusSeconds(300);
// @formatter:off
OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName(registeredClient.getClientId())
.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
.token(new OAuth2DeviceCode(DEVICE_CODE, issuedAt, expiresAt))
.token(new OAuth2UserCode(USER_CODE, issuedAt, expiresAt))
.attribute(OAuth2ParameterNames.SCOPE, registeredClient.getScopes())
.build();
// @formatter:on
this.authorizationService.save(authorization);
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.set(OAuth2ParameterNames.USER_CODE, USER_CODE);
// @formatter:off
this.mvc.perform(get(DEFAULT_DEVICE_VERIFICATION_ENDPOINT_URI)
.queryParams(parameters))
.andExpect(status().isUnauthorized());
// @formatter:on
}
@Test
public void requestWhenDeviceVerificationRequestValidThenDisplaysConsentPage() throws Exception {
this.spring.register(AuthorizationServerConfigurationWithMultipleIssuersAllowed.class).autowire();
// @formatter:off
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
.build();
// @formatter:on
this.registeredClientRepository.save(registeredClient);
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plusSeconds(300);
// @formatter:off
OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName(registeredClient.getClientId())
.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
.token(new OAuth2DeviceCode(DEVICE_CODE, issuedAt, expiresAt))
.token(new OAuth2UserCode(USER_CODE, issuedAt, expiresAt))
.attribute(OAuth2ParameterNames.SCOPE, registeredClient.getScopes())
.build();
// @formatter:on
this.authorizationService.save(authorization);
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.set(OAuth2ParameterNames.USER_CODE, USER_CODE);
String issuer = "https://example.com:8443/issuer1";
// @formatter:off
MvcResult mvcResult = this.mvc.perform(get(issuer.concat(DEFAULT_DEVICE_VERIFICATION_ENDPOINT_URI))
.queryParams(parameters)
.with(user("user")))
.andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML))
.andReturn();
// @formatter:on
String responseHtml = mvcResult.getResponse().getContentAsString();
assertThat(responseHtml).contains("Consent required");
OAuth2Authorization updatedAuthorization = this.authorizationService.findById(authorization.getId());
assertThat(updatedAuthorization.getPrincipalName()).isEqualTo("user");
assertThat(updatedAuthorization).isNotNull();
// @formatter:off
assertThat(updatedAuthorization.getToken(OAuth2UserCode.class))
.extracting(isInvalidated())
.isEqualTo(false);
// @formatter:on
}
@Test
public void requestWhenDeviceAuthorizationConsentRequestUnauthenticatedThenBadRequest() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
// @formatter:off
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
.build();
// @formatter:on
this.registeredClientRepository.save(registeredClient);
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plusSeconds(300);
// @formatter:off
OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName("user")
.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
.token(new OAuth2DeviceCode(DEVICE_CODE, issuedAt, expiresAt))
.token(new OAuth2UserCode(USER_CODE, issuedAt, expiresAt))
.attribute(OAuth2ParameterNames.SCOPE, registeredClient.getScopes())
.attribute(OAuth2ParameterNames.STATE, STATE)
.build();
// @formatter:on
this.authorizationService.save(authorization);
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.set(OAuth2ParameterNames.USER_CODE, USER_CODE);
parameters.set(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
parameters.set(OAuth2ParameterNames.SCOPE, registeredClient.getScopes().iterator().next());
parameters.set(OAuth2ParameterNames.STATE, STATE);
// @formatter:off
this.mvc.perform(post(DEFAULT_DEVICE_VERIFICATION_ENDPOINT_URI)
.params(parameters))
.andExpect(status().isBadRequest());
// @formatter:on
}
@Test
public void requestWhenDeviceAuthorizationConsentRequestValidThenRedirectsToSuccessPage() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
// @formatter:off
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
.build();
// @formatter:on
this.registeredClientRepository.save(registeredClient);
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plusSeconds(300);
// @formatter:off
OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName("user")
.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
.token(new OAuth2DeviceCode(DEVICE_CODE, issuedAt, expiresAt))
.token(new OAuth2UserCode(USER_CODE, issuedAt, expiresAt))
.attribute(OAuth2ParameterNames.SCOPE, registeredClient.getScopes())
.attribute(OAuth2ParameterNames.STATE, STATE)
.build();
// @formatter:on
this.authorizationService.save(authorization);
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.set(OAuth2ParameterNames.USER_CODE, USER_CODE);
parameters.set(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
parameters.set(OAuth2ParameterNames.SCOPE, registeredClient.getScopes().iterator().next());
parameters.set(OAuth2ParameterNames.STATE, STATE);
// @formatter:off
MvcResult mvcResult = this.mvc.perform(post(DEFAULT_DEVICE_VERIFICATION_ENDPOINT_URI)
.params(parameters)
.with(user("user")))
.andExpect(status().is3xxRedirection())
.andReturn();
// @formatter:on
assertThat(mvcResult.getResponse().getHeader(HttpHeaders.LOCATION)).isEqualTo("/?success");
OAuth2Authorization updatedAuthorization = this.authorizationService.findById(authorization.getId());
assertThat(updatedAuthorization).isNotNull();
// @formatter:off
assertThat(updatedAuthorization.getToken(OAuth2UserCode.class))
.extracting(isInvalidated())
.isEqualTo(true);
// @formatter:on
}
@Test
public void requestWhenAccessTokenRequestUnauthenticatedThenUnauthorized() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
// @formatter:off
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
.build();
// @formatter:on
this.registeredClientRepository.save(registeredClient);
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plusSeconds(300);
// @formatter:off
OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName(registeredClient.getClientId())
.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
.token(new OAuth2DeviceCode(DEVICE_CODE, issuedAt, expiresAt))
.token(new OAuth2UserCode(USER_CODE, issuedAt, expiresAt), withInvalidated())
.authorizedScopes(registeredClient.getScopes())
.attribute(Principal.class.getName(), new UsernamePasswordAuthenticationToken("user", null))
.build();
// @formatter:on
this.authorizationService.save(authorization);
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.set(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.DEVICE_CODE.getValue());
parameters.set(OAuth2ParameterNames.DEVICE_CODE, DEVICE_CODE);
// @formatter:off
this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
.params(parameters))
.andExpect(status().isUnauthorized());
// @formatter:on
}
@Test
public void requestWhenAccessTokenRequestValidThenReturnAccessTokenResponse() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
// @formatter:off
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
.build();
// @formatter:on
this.registeredClientRepository.save(registeredClient);
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plusSeconds(300);
// @formatter:off
OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName(registeredClient.getClientId())
.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
.token(new OAuth2DeviceCode(DEVICE_CODE, issuedAt, expiresAt))
.token(new OAuth2UserCode(USER_CODE, issuedAt, expiresAt), withInvalidated())
.authorizedScopes(registeredClient.getScopes())
.attribute(Principal.class.getName(), new UsernamePasswordAuthenticationToken("user", null))
.build();
// @formatter:on
this.authorizationService.save(authorization);
// @formatter:off
OAuth2AuthorizationConsent authorizationConsent =
OAuth2AuthorizationConsent.withId(registeredClient.getClientId(), "user")
.scope(registeredClient.getScopes().iterator().next())
.build();
// @formatter:on
this.authorizationConsentService.save(authorizationConsent);
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.set(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.DEVICE_CODE.getValue());
parameters.set(OAuth2ParameterNames.DEVICE_CODE, DEVICE_CODE);
// @formatter:off
MvcResult mvcResult = this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
.params(parameters)
.headers(withClientAuth(registeredClient)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.access_token").isNotEmpty())
.andExpect(jsonPath("$.refresh_token").isNotEmpty())
.andExpect(jsonPath("$.expires_in").isNumber())
.andExpect(jsonPath("$.scope").isNotEmpty())
.andExpect(jsonPath("$.token_type").isNotEmpty())
.andReturn();
// @formatter:on
OAuth2Authorization updatedAuthorization = this.authorizationService.findById(authorization.getId());
assertThat(updatedAuthorization).isNotNull();
assertThat(updatedAuthorization.getAccessToken()).isNotNull();
assertThat(updatedAuthorization.getRefreshToken()).isNotNull();
// @formatter:off
assertThat(updatedAuthorization.getToken(OAuth2DeviceCode.class))
.extracting(isInvalidated())
.isEqualTo(true);
// @formatter:on
MockHttpServletResponse servletResponse = mvcResult.getResponse();
MockClientHttpResponse httpResponse = new MockClientHttpResponse(servletResponse.getContentAsByteArray(),
HttpStatus.OK);
OAuth2AccessTokenResponse accessTokenResponse = accessTokenResponseHttpMessageConverter
.read(OAuth2AccessTokenResponse.class, httpResponse);
String accessToken = accessTokenResponse.getAccessToken().getTokenValue();
OAuth2Authorization accessTokenAuthorization = this.authorizationService.findByToken(accessToken,
OAuth2TokenType.ACCESS_TOKEN);
assertThat(accessTokenAuthorization).isEqualTo(updatedAuthorization);
}
@Test
public void requestWhenAccessTokenRequestWithDPoPProofThenReturnDPoPBoundAccessToken() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
// @formatter:off
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
.build();
// @formatter:on
this.registeredClientRepository.save(registeredClient);
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plusSeconds(300);
// @formatter:off
OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName(registeredClient.getClientId())
.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
.token(new OAuth2DeviceCode(DEVICE_CODE, issuedAt, expiresAt))
.token(new OAuth2UserCode(USER_CODE, issuedAt, expiresAt), withInvalidated())
.authorizedScopes(registeredClient.getScopes())
.attribute(Principal.class.getName(), new UsernamePasswordAuthenticationToken("user", null))
.build();
// @formatter:on
this.authorizationService.save(authorization);
// @formatter:off
OAuth2AuthorizationConsent authorizationConsent =
OAuth2AuthorizationConsent.withId(registeredClient.getClientId(), "user")
.scope(registeredClient.getScopes().iterator().next())
.build();
// @formatter:on
this.authorizationConsentService.save(authorizationConsent);
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.set(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.DEVICE_CODE.getValue());
parameters.set(OAuth2ParameterNames.DEVICE_CODE, DEVICE_CODE);
String tokenEndpointUri = "http://localhost" + DEFAULT_TOKEN_ENDPOINT_URI;
String dPoPProof = generateDPoPProof(tokenEndpointUri);
// @formatter:off
this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
.params(parameters)
.headers(withClientAuth(registeredClient))
.header(OAuth2AccessToken.TokenType.DPOP.getValue(), dPoPProof))
.andExpect(status().isOk())
.andExpect(jsonPath("$.token_type").value(OAuth2AccessToken.TokenType.DPOP.getValue()));
// @formatter:on
authorization = this.authorizationService.findById(authorization.getId());
assertThat(authorization.getAccessToken().getClaims()).containsKey("cnf");
@SuppressWarnings("unchecked")
Map<String, Object> cnfClaims = (Map<String, Object>) authorization.getAccessToken().getClaims().get("cnf");
assertThat(cnfClaims).containsKey("jkt");
String jwkThumbprintClaim = (String) cnfClaims.get("jkt");
assertThat(jwkThumbprintClaim).isEqualTo(TestJwks.DEFAULT_EC_JWK.toPublicJWK().computeThumbprint().toString());
}
private static String generateDPoPProof(String tokenEndpointUri) {
// @formatter:off
Map<String, Object> publicJwk = TestJwks.DEFAULT_EC_JWK
.toPublicJWK()
.toJSONObject();
JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.ES256)
.type("dpop+jwt")
.jwk(publicJwk)
.build();
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuedAt(Instant.now())
.claim("htm", "POST")
.claim("htu", tokenEndpointUri)
.id(UUID.randomUUID().toString())
.build();
// @formatter:on
Jwt jwt = dPoPProofJwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, claims));
return jwt.getTokenValue();
}
private static HttpHeaders withClientAuth(RegisteredClient registeredClient) {
HttpHeaders headers = new HttpHeaders();
headers.setBasicAuth(registeredClient.getClientId(), registeredClient.getClientSecret());
return headers;
}
private static Consumer<Map<String, Object>> withInvalidated() {
return (metadata) -> metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true);
}
private static Function<OAuth2Authorization.Token<? extends OAuth2Token>, Boolean> isInvalidated() {
return (token) -> token.getMetadata(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME);
}
@EnableWebSecurity
@Import(OAuth2AuthorizationServerConfiguration.class)
static class AuthorizationServerConfiguration {
@Bean
RegisteredClientRepository registeredClientRepository(JdbcOperations jdbcOperations) {
return new JdbcRegisteredClientRepository(jdbcOperations);
}
@Bean
OAuth2AuthorizationService authorizationService(JdbcOperations jdbcOperations,
RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationService(jdbcOperations, registeredClientRepository);
}
@Bean
OAuth2AuthorizationConsentService authorizationConsentService(JdbcOperations jdbcOperations,
RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcOperations, registeredClientRepository);
}
@Bean
JdbcOperations jdbcOperations() {
return new JdbcTemplate(db);
}
@Bean
JWKSource<SecurityContext> jwkSource() {
return jwkSource;
}
@Bean
PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
@EnableWebSecurity
@Import(OAuth2AuthorizationServerConfiguration.class)
static class AuthorizationServerConfigurationWithMultipleIssuersAllowed extends AuthorizationServerConfiguration {
@Bean
AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().multipleIssuersAllowed(true).build();
}
}
}
@@ -0,0 +1,646 @@
/*
* Copyright 2004-present 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.config.annotation.web.configurers.oauth2.server.authorization;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.Principal;
import java.security.PublicKey;
import java.time.Instant;
import java.util.Base64;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import jakarta.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.lang.Nullable;
import org.springframework.mock.http.client.MockClientHttpResponse;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
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.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.Transient;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
import org.springframework.security.oauth2.jose.TestJwks;
import org.springframework.security.oauth2.jose.TestKeys;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.JwsHeader;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository.RegisteredClientParametersMapper;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import org.springframework.security.oauth2.server.authorization.jackson2.TestingAuthenticationTokenMixin;
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.CoreMatchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Integration tests for the OAuth 2.0 Refresh Token Grant.
*
* @author Alexey Nesterov
* @since 7.0
*/
@ExtendWith(SpringTestContextExtension.class)
public class OAuth2RefreshTokenGrantTests {
private static final String DEFAULT_TOKEN_ENDPOINT_URI = "/oauth2/token";
private static final String DEFAULT_TOKEN_REVOCATION_ENDPOINT_URI = "/oauth2/revoke";
private static final String AUTHORITIES_CLAIM = "authorities";
private static EmbeddedDatabase db;
private static JWKSource<SecurityContext> jwkSource;
private static NimbusJwtDecoder jwtDecoder;
private static NimbusJwtEncoder dPoPProofJwtEncoder;
private static HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter = new OAuth2AccessTokenResponseHttpMessageConverter();
public final SpringTestContext spring = new SpringTestContext(this);
@Autowired
private MockMvc mvc;
@Autowired
private JdbcOperations jdbcOperations;
@Autowired
private RegisteredClientRepository registeredClientRepository;
@Autowired
private OAuth2AuthorizationService authorizationService;
@BeforeAll
public static void init() {
JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
jwtDecoder = NimbusJwtDecoder.withPublicKey(TestKeys.DEFAULT_PUBLIC_KEY).build();
JWKSet clientJwkSet = new JWKSet(TestJwks.DEFAULT_EC_JWK);
JWKSource<SecurityContext> clientJwkSource = (jwkSelector, securityContext) -> jwkSelector.select(clientJwkSet);
dPoPProofJwtEncoder = new NimbusJwtEncoder(clientJwkSource);
db = new EmbeddedDatabaseBuilder().generateUniqueName(true)
.setType(EmbeddedDatabaseType.HSQL)
.setScriptEncoding("UTF-8")
.addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-schema.sql")
.addScript(
"org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql")
.build();
}
@AfterEach
public void tearDown() {
this.jdbcOperations.update("truncate table oauth2_authorization");
this.jdbcOperations.update("truncate table oauth2_registered_client");
}
@AfterAll
public static void destroy() {
db.shutdown();
}
@Test
public void requestWhenRefreshTokenRequestValidThenReturnAccessTokenResponse() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
this.registeredClientRepository.save(registeredClient);
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build();
this.authorizationService.save(authorization);
MvcResult mvcResult = this.mvc
.perform(post(DEFAULT_TOKEN_ENDPOINT_URI).params(getRefreshTokenRequestParameters(authorization))
.header(HttpHeaders.AUTHORIZATION,
"Basic " + encodeBasicAuth(registeredClient.getClientId(), registeredClient.getClientSecret())))
.andExpect(status().isOk())
.andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store")))
.andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache")))
.andExpect(jsonPath("$.access_token").isNotEmpty())
.andExpect(jsonPath("$.token_type").isNotEmpty())
.andExpect(jsonPath("$.expires_in").isNotEmpty())
.andExpect(jsonPath("$.refresh_token").isNotEmpty())
.andExpect(jsonPath("$.scope").isNotEmpty())
.andReturn();
MockHttpServletResponse servletResponse = mvcResult.getResponse();
MockClientHttpResponse httpResponse = new MockClientHttpResponse(servletResponse.getContentAsByteArray(),
HttpStatus.valueOf(servletResponse.getStatus()));
OAuth2AccessTokenResponse accessTokenResponse = accessTokenHttpResponseConverter
.read(OAuth2AccessTokenResponse.class, httpResponse);
// Assert user authorities was propagated as claim in JWT
Jwt jwt = jwtDecoder.decode(accessTokenResponse.getAccessToken().getTokenValue());
List<String> authoritiesClaim = jwt.getClaim(AUTHORITIES_CLAIM);
Authentication principal = authorization.getAttribute(Principal.class.getName());
Set<String> userAuthorities = new HashSet<>();
for (GrantedAuthority authority : principal.getAuthorities()) {
userAuthorities.add(authority.getAuthority());
}
assertThat(authoritiesClaim).containsExactlyInAnyOrderElementsOf(userAuthorities);
}
// gh-432
@Test
public void requestWhenRevokeAndRefreshThenAccessTokenActive() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
this.registeredClientRepository.save(registeredClient);
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build();
this.authorizationService.save(authorization);
OAuth2AccessToken token = authorization.getAccessToken().getToken();
OAuth2TokenType tokenType = OAuth2TokenType.ACCESS_TOKEN;
this.mvc
.perform(post(DEFAULT_TOKEN_REVOCATION_ENDPOINT_URI)
.params(getTokenRevocationRequestParameters(token, tokenType))
.header(HttpHeaders.AUTHORIZATION,
"Basic " + encodeBasicAuth(registeredClient.getClientId(), registeredClient.getClientSecret())))
.andExpect(status().isOk());
this.mvc
.perform(post(DEFAULT_TOKEN_ENDPOINT_URI).params(getRefreshTokenRequestParameters(authorization))
.header(HttpHeaders.AUTHORIZATION,
"Basic " + encodeBasicAuth(registeredClient.getClientId(), registeredClient.getClientSecret())))
.andExpect(status().isOk());
OAuth2Authorization updatedAuthorization = this.authorizationService.findById(authorization.getId());
OAuth2Authorization.Token<OAuth2AccessToken> accessToken = updatedAuthorization.getAccessToken();
assertThat(accessToken.isActive()).isTrue();
}
// gh-1430
@Test
public void requestWhenRefreshTokenRequestWithPublicClientThenReturnAccessTokenResponse() throws Exception {
this.spring.register(AuthorizationServerConfigurationWithPublicClientAuthentication.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredPublicClient()
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.build();
this.registeredClientRepository.save(registeredClient);
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build();
this.authorizationService.save(authorization);
this.mvc
.perform(post(DEFAULT_TOKEN_ENDPOINT_URI).params(getRefreshTokenRequestParameters(authorization))
.param(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId()))
.andExpect(status().isOk())
.andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store")))
.andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache")))
.andExpect(jsonPath("$.access_token").isNotEmpty())
.andExpect(jsonPath("$.token_type").isNotEmpty())
.andExpect(jsonPath("$.expires_in").isNotEmpty())
.andExpect(jsonPath("$.refresh_token").isNotEmpty())
.andExpect(jsonPath("$.scope").isNotEmpty());
}
@Test
public void requestWhenRefreshTokenRequestWithPublicClientAndDPoPProofThenReturnDPoPBoundAccessToken()
throws Exception {
this.spring.register(AuthorizationServerConfigurationWithPublicClientAuthentication.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredPublicClient()
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.build();
this.registeredClientRepository.save(registeredClient);
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.DPOP,
"dpop-bound-access-token", Instant.now(), Instant.now().plusSeconds(300));
Map<String, Object> accessTokenClaims = new HashMap<>();
Map<String, Object> cnfClaim = new HashMap<>();
cnfClaim.put("jkt", TestJwks.DEFAULT_EC_JWK.toPublicJWK().computeThumbprint().toString());
accessTokenClaims.put("cnf", cnfClaim);
OAuth2Authorization authorization = TestOAuth2Authorizations
.authorization(registeredClient, accessToken, accessTokenClaims)
.build();
this.authorizationService.save(authorization);
String tokenEndpointUri = "http://localhost" + DEFAULT_TOKEN_ENDPOINT_URI;
String dPoPProof = generateDPoPProof(tokenEndpointUri);
this.mvc
.perform(post(DEFAULT_TOKEN_ENDPOINT_URI).params(getRefreshTokenRequestParameters(authorization))
.param(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())
.header(OAuth2AccessToken.TokenType.DPOP.getValue(), dPoPProof))
.andExpect(status().isOk())
.andExpect(jsonPath("$.token_type").value(OAuth2AccessToken.TokenType.DPOP.getValue()));
authorization = this.authorizationService.findById(authorization.getId());
assertThat(authorization.getAccessToken().getClaims()).containsKey("cnf");
@SuppressWarnings("unchecked")
Map<String, Object> cnfClaims = (Map<String, Object>) authorization.getAccessToken().getClaims().get("cnf");
assertThat(cnfClaims).containsKey("jkt");
String jwkThumbprintClaim = (String) cnfClaims.get("jkt");
assertThat(jwkThumbprintClaim).isEqualTo(TestJwks.DEFAULT_EC_JWK.toPublicJWK().computeThumbprint().toString());
}
@Test
public void requestWhenRefreshTokenRequestWithPublicClientAndDPoPProofAndAccessTokenNotBoundThenBadRequest()
throws Exception {
this.spring.register(AuthorizationServerConfigurationWithPublicClientAuthentication.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredPublicClient()
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.build();
this.registeredClientRepository.save(registeredClient);
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build();
this.authorizationService.save(authorization);
String tokenEndpointUri = "http://localhost" + DEFAULT_TOKEN_ENDPOINT_URI;
String dPoPProof = generateDPoPProof(tokenEndpointUri);
this.mvc
.perform(post(DEFAULT_TOKEN_ENDPOINT_URI).params(getRefreshTokenRequestParameters(authorization))
.param(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())
.header(OAuth2AccessToken.TokenType.DPOP.getValue(), dPoPProof))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.error").value(OAuth2ErrorCodes.INVALID_DPOP_PROOF))
.andExpect(jsonPath("$.error_description").value("jkt claim is missing."));
}
@Test
public void requestWhenRefreshTokenRequestWithPublicClientAndDPoPProofAndDifferentPublicKeyThenBadRequest()
throws Exception {
this.spring.register(AuthorizationServerConfigurationWithPublicClientAuthentication.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredPublicClient()
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.build();
this.registeredClientRepository.save(registeredClient);
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.DPOP,
"dpop-bound-access-token", Instant.now(), Instant.now().plusSeconds(300));
Map<String, Object> accessTokenClaims = new HashMap<>();
// Bind access token to different public key
PublicKey publicKey = TestJwks.DEFAULT_RSA_JWK.toPublicKey();
Map<String, Object> cnfClaim = new HashMap<>();
cnfClaim.put("jkt", computeSHA256(publicKey));
accessTokenClaims.put("cnf", cnfClaim);
OAuth2Authorization authorization = TestOAuth2Authorizations
.authorization(registeredClient, accessToken, accessTokenClaims)
.build();
this.authorizationService.save(authorization);
String tokenEndpointUri = "http://localhost" + DEFAULT_TOKEN_ENDPOINT_URI;
String dPoPProof = generateDPoPProof(tokenEndpointUri);
this.mvc
.perform(post(DEFAULT_TOKEN_ENDPOINT_URI).params(getRefreshTokenRequestParameters(authorization))
.param(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())
.header(OAuth2AccessToken.TokenType.DPOP.getValue(), dPoPProof))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.error").value(OAuth2ErrorCodes.INVALID_DPOP_PROOF))
.andExpect(jsonPath("$.error_description").value("jwk header is invalid."));
}
@Test
public void requestWhenRefreshTokenRequestWithDPoPProofThenReturnDPoPBoundAccessToken() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
this.registeredClientRepository.save(registeredClient);
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build();
this.authorizationService.save(authorization);
String tokenEndpointUri = "http://localhost" + DEFAULT_TOKEN_ENDPOINT_URI;
String dPoPProof = generateDPoPProof(tokenEndpointUri);
this.mvc
.perform(post(DEFAULT_TOKEN_ENDPOINT_URI).params(getRefreshTokenRequestParameters(authorization))
.header(HttpHeaders.AUTHORIZATION,
"Basic " + encodeBasicAuth(registeredClient.getClientId(), registeredClient.getClientSecret()))
.header(OAuth2AccessToken.TokenType.DPOP.getValue(), dPoPProof))
.andExpect(status().isOk())
.andExpect(jsonPath("$.token_type").value(OAuth2AccessToken.TokenType.DPOP.getValue()));
authorization = this.authorizationService.findById(authorization.getId());
assertThat(authorization.getAccessToken().getClaims()).containsKey("cnf");
@SuppressWarnings("unchecked")
Map<String, Object> cnfClaims = (Map<String, Object>) authorization.getAccessToken().getClaims().get("cnf");
assertThat(cnfClaims).containsKey("jkt");
}
private static String generateDPoPProof(String tokenEndpointUri) {
// @formatter:off
Map<String, Object> publicJwk = TestJwks.DEFAULT_EC_JWK
.toPublicJWK()
.toJSONObject();
JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.ES256)
.type("dpop+jwt")
.jwk(publicJwk)
.build();
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuedAt(Instant.now())
.claim("htm", "POST")
.claim("htu", tokenEndpointUri)
.id(UUID.randomUUID().toString())
.build();
// @formatter:on
Jwt jwt = dPoPProofJwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, claims));
return jwt.getTokenValue();
}
private static String computeSHA256(PublicKey publicKey) throws Exception {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(publicKey.getEncoded());
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
}
private static MultiValueMap<String, String> getRefreshTokenRequestParameters(OAuth2Authorization authorization) {
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.set(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.REFRESH_TOKEN.getValue());
parameters.set(OAuth2ParameterNames.REFRESH_TOKEN, authorization.getRefreshToken().getToken().getTokenValue());
return parameters;
}
private static MultiValueMap<String, String> getTokenRevocationRequestParameters(OAuth2Token token,
OAuth2TokenType tokenType) {
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.set(OAuth2ParameterNames.TOKEN, token.getTokenValue());
parameters.set(OAuth2ParameterNames.TOKEN_TYPE_HINT, tokenType.getValue());
return parameters;
}
private static String encodeBasicAuth(String clientId, String secret) throws Exception {
clientId = URLEncoder.encode(clientId, StandardCharsets.UTF_8.name());
secret = URLEncoder.encode(secret, StandardCharsets.UTF_8.name());
String credentialsString = clientId + ":" + secret;
byte[] encodedBytes = Base64.getEncoder().encode(credentialsString.getBytes(StandardCharsets.UTF_8));
return new String(encodedBytes, StandardCharsets.UTF_8);
}
@EnableWebSecurity
@Import(OAuth2AuthorizationServerConfiguration.class)
static class AuthorizationServerConfiguration {
@Bean
OAuth2AuthorizationService authorizationService(JdbcOperations jdbcOperations,
RegisteredClientRepository registeredClientRepository) {
JdbcOAuth2AuthorizationService authorizationService = new JdbcOAuth2AuthorizationService(jdbcOperations,
registeredClientRepository);
authorizationService.setAuthorizationRowMapper(new RowMapper(registeredClientRepository));
authorizationService.setAuthorizationParametersMapper(new ParametersMapper());
return authorizationService;
}
@Bean
RegisteredClientRepository registeredClientRepository(JdbcOperations jdbcOperations) {
JdbcRegisteredClientRepository jdbcRegisteredClientRepository = new JdbcRegisteredClientRepository(
jdbcOperations);
RegisteredClientParametersMapper registeredClientParametersMapper = new RegisteredClientParametersMapper();
jdbcRegisteredClientRepository.setRegisteredClientParametersMapper(registeredClientParametersMapper);
return jdbcRegisteredClientRepository;
}
@Bean
JdbcOperations jdbcOperations() {
return new JdbcTemplate(db);
}
@Bean
JWKSource<SecurityContext> jwkSource() {
return jwkSource;
}
@Bean
OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
return (context) -> {
if (AuthorizationGrantType.REFRESH_TOKEN.equals(context.getAuthorizationGrantType())) {
Authentication principal = context.getPrincipal();
Set<String> authorities = new HashSet<>();
for (GrantedAuthority authority : principal.getAuthorities()) {
authorities.add(authority.getAuthority());
}
context.getClaims().claim(AUTHORITIES_CLAIM, authorities);
}
};
}
@Bean
PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
static class RowMapper extends JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper {
RowMapper(RegisteredClientRepository registeredClientRepository) {
super(registeredClientRepository);
getObjectMapper().addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class);
}
}
static class ParametersMapper extends JdbcOAuth2AuthorizationService.OAuth2AuthorizationParametersMapper {
ParametersMapper() {
super();
getObjectMapper().addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class);
}
}
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class AuthorizationServerConfigurationWithPublicClientAuthentication
extends AuthorizationServerConfiguration {
// @formatter:off
@Bean
SecurityFilterChain authorizationServerSecurityFilterChain(
HttpSecurity http, RegisteredClientRepository registeredClientRepository) throws Exception {
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
OAuth2AuthorizationServerConfigurer.authorizationServer();
http
.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
.with(authorizationServerConfigurer, (authorizationServer) ->
authorizationServer
.clientAuthentication((clientAuthentication) ->
clientAuthentication
.authenticationConverter(
new PublicClientRefreshTokenAuthenticationConverter())
.authenticationProvider(
new PublicClientRefreshTokenAuthenticationProvider(registeredClientRepository)))
)
.authorizeHttpRequests((authorize) ->
authorize.anyRequest().authenticated()
);
return http.build();
}
// @formatter:on
}
@Transient
private static final class PublicClientRefreshTokenAuthenticationToken extends OAuth2ClientAuthenticationToken {
private PublicClientRefreshTokenAuthenticationToken(String clientId) {
super(clientId, ClientAuthenticationMethod.NONE, null, null);
}
private PublicClientRefreshTokenAuthenticationToken(RegisteredClient registeredClient) {
super(registeredClient, ClientAuthenticationMethod.NONE, null);
}
}
private static final class PublicClientRefreshTokenAuthenticationConverter implements AuthenticationConverter {
@Nullable
@Override
public Authentication convert(HttpServletRequest request) {
// grant_type (REQUIRED)
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
if (!AuthorizationGrantType.REFRESH_TOKEN.getValue().equals(grantType)) {
return null;
}
// client_id (REQUIRED)
String clientId = request.getParameter(OAuth2ParameterNames.CLIENT_ID);
if (!StringUtils.hasText(clientId)) {
return null;
}
return new PublicClientRefreshTokenAuthenticationToken(clientId);
}
}
private static final class PublicClientRefreshTokenAuthenticationProvider implements AuthenticationProvider {
private final RegisteredClientRepository registeredClientRepository;
private PublicClientRefreshTokenAuthenticationProvider(RegisteredClientRepository registeredClientRepository) {
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
this.registeredClientRepository = registeredClientRepository;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
PublicClientRefreshTokenAuthenticationToken publicClientAuthentication = (PublicClientRefreshTokenAuthenticationToken) authentication;
if (!ClientAuthenticationMethod.NONE.equals(publicClientAuthentication.getClientAuthenticationMethod())) {
return null;
}
String clientId = publicClientAuthentication.getPrincipal().toString();
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
if (registeredClient == null) {
throwInvalidClient(OAuth2ParameterNames.CLIENT_ID);
}
if (!registeredClient.getClientAuthenticationMethods()
.contains(publicClientAuthentication.getClientAuthenticationMethod())) {
throwInvalidClient("authentication_method");
}
return new PublicClientRefreshTokenAuthenticationToken(registeredClient);
}
@Override
public boolean supports(Class<?> authentication) {
return PublicClientRefreshTokenAuthenticationToken.class.isAssignableFrom(authentication);
}
private static void throwInvalidClient(String parameterName) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
"Public client authentication failed: " + parameterName, null);
throw new OAuth2AuthenticationException(error);
}
}
}
@@ -0,0 +1,472 @@
/*
* Copyright 2004-present 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.config.annotation.web.configurers.oauth2.server.authorization;
import java.security.Principal;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.function.Function;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.mock.http.client.MockClientHttpResponse;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
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.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
import org.springframework.security.oauth2.jose.TestJwks;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.JwsHeader;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeCompositeAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimNames;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Integration tests for OAuth 2.0 Token Exchange Grant.
*
* @author Steve Riesenberg
*/
@ExtendWith(SpringTestContextExtension.class)
public class OAuth2TokenExchangeGrantTests {
private static final String DEFAULT_TOKEN_ENDPOINT_URI = "/oauth2/token";
private static final String RESOURCE = "https://mydomain.com/resource";
private static final String AUDIENCE = "audience";
private static final String SUBJECT_TOKEN = "EfYu_0jEL";
private static final String ACTOR_TOKEN = "JlNE_xR1f";
private static final String ACCESS_TOKEN_TYPE_VALUE = "urn:ietf:params:oauth:token-type:access_token";
private static final String JWT_TOKEN_TYPE_VALUE = "urn:ietf:params:oauth:token-type:jwt";
private static NimbusJwtEncoder dPoPProofJwtEncoder;
public final SpringTestContext spring = new SpringTestContext(this);
private final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenResponseHttpMessageConverter = new OAuth2AccessTokenResponseHttpMessageConverter();
@Autowired
private MockMvc mvc;
@Autowired
private JdbcOperations jdbcOperations;
@Autowired
private RegisteredClientRepository registeredClientRepository;
@Autowired
private OAuth2AuthorizationService authorizationService;
@BeforeAll
public static void init() {
JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
AuthorizationServerConfiguration.JWK_SOURCE = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
JWKSet clientJwkSet = new JWKSet(TestJwks.DEFAULT_EC_JWK);
JWKSource<SecurityContext> clientJwkSource = (jwkSelector, securityContext) -> jwkSelector.select(clientJwkSet);
dPoPProofJwtEncoder = new NimbusJwtEncoder(clientJwkSource);
// @formatter:off
AuthorizationServerConfiguration.DB = new EmbeddedDatabaseBuilder()
.generateUniqueName(true)
.setType(EmbeddedDatabaseType.HSQL)
.setScriptEncoding("UTF-8")
.addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-schema.sql")
.addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-consent-schema.sql")
.addScript("org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql")
.build();
// @formatter:on
}
@AfterEach
public void tearDown() {
this.jdbcOperations.update("truncate table oauth2_authorization");
this.jdbcOperations.update("truncate table oauth2_authorization_consent");
this.jdbcOperations.update("truncate table oauth2_registered_client");
}
@AfterAll
public static void destroy() {
AuthorizationServerConfiguration.DB.shutdown();
}
@Test
public void requestWhenAccessTokenRequestNotAuthenticatedThenUnauthorized() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.authorizationGrantType(AuthorizationGrantType.TOKEN_EXCHANGE)
.build();
this.registeredClientRepository.save(registeredClient);
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.set(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.TOKEN_EXCHANGE.getValue());
parameters.set(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
parameters.set(OAuth2ParameterNames.SCOPE,
StringUtils.collectionToDelimitedString(registeredClient.getScopes(), " "));
// @formatter:off
this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI).params(parameters))
.andExpect(status().isUnauthorized());
// @formatter:on
}
@Test
public void requestWhenAccessTokenRequestValidAndNoActorTokenThenReturnAccessTokenResponseForImpersonation()
throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.authorizationGrantType(AuthorizationGrantType.TOKEN_EXCHANGE)
.build();
this.registeredClientRepository.save(registeredClient);
UsernamePasswordAuthenticationToken userPrincipal = createUserPrincipal("user");
OAuth2Authorization subjectAuthorization = TestOAuth2Authorizations.authorization(registeredClient)
.attribute(Principal.class.getName(), userPrincipal)
.build();
this.authorizationService.save(subjectAuthorization);
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.set(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.TOKEN_EXCHANGE.getValue());
parameters.set(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
parameters.set(OAuth2ParameterNames.REQUESTED_TOKEN_TYPE, JWT_TOKEN_TYPE_VALUE);
parameters.set(OAuth2ParameterNames.SUBJECT_TOKEN,
subjectAuthorization.getAccessToken().getToken().getTokenValue());
parameters.set(OAuth2ParameterNames.SUBJECT_TOKEN_TYPE, JWT_TOKEN_TYPE_VALUE);
parameters.set(OAuth2ParameterNames.RESOURCE, RESOURCE);
parameters.set(OAuth2ParameterNames.AUDIENCE, AUDIENCE);
parameters.set(OAuth2ParameterNames.SCOPE,
StringUtils.collectionToDelimitedString(registeredClient.getScopes(), " "));
// @formatter:off
MvcResult mvcResult = this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
.params(parameters)
.headers(withClientAuth(registeredClient)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.access_token").isNotEmpty())
.andExpect(jsonPath("$.refresh_token").doesNotExist())
.andExpect(jsonPath("$.expires_in").isNumber())
.andExpect(jsonPath("$.scope").isNotEmpty())
.andExpect(jsonPath("$.token_type").isNotEmpty())
.andExpect(jsonPath("$.issued_token_type").isNotEmpty())
.andReturn();
// @formatter:on
MockHttpServletResponse servletResponse = mvcResult.getResponse();
MockClientHttpResponse httpResponse = new MockClientHttpResponse(servletResponse.getContentAsByteArray(),
HttpStatus.OK);
OAuth2AccessTokenResponse accessTokenResponse = this.accessTokenResponseHttpMessageConverter
.read(OAuth2AccessTokenResponse.class, httpResponse);
String accessToken = accessTokenResponse.getAccessToken().getTokenValue();
OAuth2Authorization authorization = this.authorizationService.findByToken(accessToken,
OAuth2TokenType.ACCESS_TOKEN);
assertThat(authorization).isNotNull();
assertThat(authorization.getAccessToken()).isNotNull();
assertThat(authorization.getAccessToken().getClaims()).isNotNull();
// We do not populate claims (e.g. `aud`) based on the resource or audience
// parameters
assertThat(authorization.getAccessToken().getClaims().get(OAuth2TokenClaimNames.AUD))
.isEqualTo(List.of(registeredClient.getClientId()));
assertThat(authorization.getRefreshToken()).isNull();
assertThat(authorization.<Authentication>getAttribute(Principal.class.getName())).isEqualTo(userPrincipal);
}
@Test
public void requestWhenAccessTokenRequestValidAndActorTokenThenReturnAccessTokenResponseForDelegation()
throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.authorizationGrantType(AuthorizationGrantType.TOKEN_EXCHANGE)
.build();
this.registeredClientRepository.save(registeredClient);
UsernamePasswordAuthenticationToken userPrincipal = createUserPrincipal("user");
UsernamePasswordAuthenticationToken adminPrincipal = createUserPrincipal("admin");
Map<String, Object> actorTokenClaims = new HashMap<>();
actorTokenClaims.put(OAuth2TokenClaimNames.ISS, "issuer2");
actorTokenClaims.put(OAuth2TokenClaimNames.SUB, "admin");
Map<String, Object> subjectTokenClaims = new HashMap<>();
subjectTokenClaims.put(OAuth2TokenClaimNames.ISS, "issuer1");
subjectTokenClaims.put(OAuth2TokenClaimNames.SUB, "user");
subjectTokenClaims.put("may_act", actorTokenClaims);
OAuth2AccessToken subjectToken = createAccessToken(SUBJECT_TOKEN);
OAuth2AccessToken actorToken = createAccessToken(ACTOR_TOKEN);
// @formatter:off
OAuth2Authorization subjectAuthorization = TestOAuth2Authorizations.authorization(registeredClient, subjectToken, subjectTokenClaims)
.id(UUID.randomUUID().toString())
.attribute(Principal.class.getName(), userPrincipal)
.build();
OAuth2Authorization actorAuthorization = TestOAuth2Authorizations.authorization(registeredClient, actorToken, actorTokenClaims)
.id(UUID.randomUUID().toString())
.attribute(Principal.class.getName(), adminPrincipal)
.build();
// @formatter:on
this.authorizationService.save(subjectAuthorization);
this.authorizationService.save(actorAuthorization);
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.set(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.TOKEN_EXCHANGE.getValue());
parameters.set(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
parameters.set(OAuth2ParameterNames.REQUESTED_TOKEN_TYPE, JWT_TOKEN_TYPE_VALUE);
parameters.set(OAuth2ParameterNames.SUBJECT_TOKEN, SUBJECT_TOKEN);
parameters.set(OAuth2ParameterNames.SUBJECT_TOKEN_TYPE, JWT_TOKEN_TYPE_VALUE);
parameters.set(OAuth2ParameterNames.ACTOR_TOKEN, ACTOR_TOKEN);
parameters.set(OAuth2ParameterNames.ACTOR_TOKEN_TYPE, ACCESS_TOKEN_TYPE_VALUE);
parameters.set(OAuth2ParameterNames.SCOPE,
StringUtils.collectionToDelimitedString(registeredClient.getScopes(), " "));
// @formatter:off
MvcResult mvcResult = this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
.params(parameters)
.headers(withClientAuth(registeredClient)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.access_token").isNotEmpty())
.andExpect(jsonPath("$.refresh_token").doesNotExist())
.andExpect(jsonPath("$.expires_in").isNumber())
.andExpect(jsonPath("$.scope").isNotEmpty())
.andExpect(jsonPath("$.token_type").isNotEmpty())
.andExpect(jsonPath("$.issued_token_type").isNotEmpty())
.andReturn();
// @formatter:on
MockHttpServletResponse servletResponse = mvcResult.getResponse();
MockClientHttpResponse httpResponse = new MockClientHttpResponse(servletResponse.getContentAsByteArray(),
HttpStatus.OK);
OAuth2AccessTokenResponse accessTokenResponse = this.accessTokenResponseHttpMessageConverter
.read(OAuth2AccessTokenResponse.class, httpResponse);
String accessToken = accessTokenResponse.getAccessToken().getTokenValue();
OAuth2Authorization authorization = this.authorizationService.findByToken(accessToken,
OAuth2TokenType.ACCESS_TOKEN);
assertThat(authorization).isNotNull();
assertThat(authorization.getAccessToken()).isNotNull();
assertThat(authorization.getAccessToken().getClaims()).isNotNull();
assertThat(authorization.getAccessToken().getClaims().get("act")).isNotNull();
assertThat(authorization.getRefreshToken()).isNull();
assertThat(authorization.<Authentication>getAttribute(Principal.class.getName()))
.isInstanceOf(OAuth2TokenExchangeCompositeAuthenticationToken.class);
}
@Test
public void requestWhenAccessTokenRequestWithDPoPProofThenReturnDPoPBoundAccessToken() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.authorizationGrantType(AuthorizationGrantType.TOKEN_EXCHANGE)
.build();
this.registeredClientRepository.save(registeredClient);
UsernamePasswordAuthenticationToken userPrincipal = createUserPrincipal("user");
UsernamePasswordAuthenticationToken adminPrincipal = createUserPrincipal("admin");
Map<String, Object> actorTokenClaims = new HashMap<>();
actorTokenClaims.put(OAuth2TokenClaimNames.ISS, "issuer2");
actorTokenClaims.put(OAuth2TokenClaimNames.SUB, "admin");
Map<String, Object> subjectTokenClaims = new HashMap<>();
subjectTokenClaims.put(OAuth2TokenClaimNames.ISS, "issuer1");
subjectTokenClaims.put(OAuth2TokenClaimNames.SUB, "user");
subjectTokenClaims.put("may_act", actorTokenClaims);
OAuth2AccessToken subjectToken = createAccessToken(SUBJECT_TOKEN);
OAuth2AccessToken actorToken = createAccessToken(ACTOR_TOKEN);
// @formatter:off
OAuth2Authorization subjectAuthorization = TestOAuth2Authorizations.authorization(registeredClient, subjectToken, subjectTokenClaims)
.id(UUID.randomUUID().toString())
.attribute(Principal.class.getName(), userPrincipal)
.build();
OAuth2Authorization actorAuthorization = TestOAuth2Authorizations.authorization(registeredClient, actorToken, actorTokenClaims)
.id(UUID.randomUUID().toString())
.attribute(Principal.class.getName(), adminPrincipal)
.build();
// @formatter:on
this.authorizationService.save(subjectAuthorization);
this.authorizationService.save(actorAuthorization);
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.set(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.TOKEN_EXCHANGE.getValue());
parameters.set(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
parameters.set(OAuth2ParameterNames.REQUESTED_TOKEN_TYPE, JWT_TOKEN_TYPE_VALUE);
parameters.set(OAuth2ParameterNames.SUBJECT_TOKEN, SUBJECT_TOKEN);
parameters.set(OAuth2ParameterNames.SUBJECT_TOKEN_TYPE, JWT_TOKEN_TYPE_VALUE);
parameters.set(OAuth2ParameterNames.ACTOR_TOKEN, ACTOR_TOKEN);
parameters.set(OAuth2ParameterNames.ACTOR_TOKEN_TYPE, ACCESS_TOKEN_TYPE_VALUE);
parameters.set(OAuth2ParameterNames.SCOPE,
StringUtils.collectionToDelimitedString(registeredClient.getScopes(), " "));
String tokenEndpointUri = "http://localhost" + DEFAULT_TOKEN_ENDPOINT_URI;
String dPoPProof = generateDPoPProof(tokenEndpointUri);
// @formatter:off
this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
.params(parameters)
.headers(withClientAuth(registeredClient))
.header(OAuth2AccessToken.TokenType.DPOP.getValue(), dPoPProof))
.andExpect(status().isOk())
.andExpect(jsonPath("$.token_type").value(OAuth2AccessToken.TokenType.DPOP.getValue()));
// @formatter:on
}
private static OAuth2AccessToken createAccessToken(String tokenValue) {
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plusSeconds(300);
return new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, tokenValue, issuedAt, expiresAt);
}
private static UsernamePasswordAuthenticationToken createUserPrincipal(String username) {
User user = new User(username, "", AuthorityUtils.createAuthorityList("ROLE_USER"));
return UsernamePasswordAuthenticationToken.authenticated(user, null, user.getAuthorities());
}
private static HttpHeaders withClientAuth(RegisteredClient registeredClient) {
HttpHeaders headers = new HttpHeaders();
headers.setBasicAuth(registeredClient.getClientId(), registeredClient.getClientSecret());
return headers;
}
private static Consumer<Map<String, Object>> withInvalidated() {
return (metadata) -> metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true);
}
private static Function<OAuth2Authorization.Token<? extends OAuth2Token>, Boolean> isInvalidated() {
return (token) -> token.getMetadata(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME);
}
private static String generateDPoPProof(String tokenEndpointUri) {
// @formatter:off
Map<String, Object> publicJwk = TestJwks.DEFAULT_EC_JWK
.toPublicJWK()
.toJSONObject();
JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.ES256)
.type("dpop+jwt")
.jwk(publicJwk)
.build();
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuedAt(Instant.now())
.claim("htm", "POST")
.claim("htu", tokenEndpointUri)
.id(UUID.randomUUID().toString())
.build();
// @formatter:on
Jwt jwt = dPoPProofJwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, claims));
return jwt.getTokenValue();
}
@EnableWebSecurity
@Import(OAuth2AuthorizationServerConfiguration.class)
static class AuthorizationServerConfiguration {
static JWKSource<SecurityContext> JWK_SOURCE;
static EmbeddedDatabase DB;
@Bean
RegisteredClientRepository registeredClientRepository(JdbcOperations jdbcOperations) {
return new JdbcRegisteredClientRepository(jdbcOperations);
}
@Bean
OAuth2AuthorizationService authorizationService(JdbcOperations jdbcOperations,
RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationService(jdbcOperations, registeredClientRepository);
}
@Bean
OAuth2AuthorizationConsentService authorizationConsentService(JdbcOperations jdbcOperations,
RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcOperations, registeredClientRepository);
}
@Bean
JdbcOperations jdbcOperations() {
return new JdbcTemplate(DB);
}
@Bean
JWKSource<SecurityContext> jwkSource() {
return JWK_SOURCE;
}
@Bean
PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
}
@@ -0,0 +1,610 @@
/*
* Copyright 2004-present 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.config.annotation.web.configurers.oauth2.server.authorization;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.function.Consumer;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.mock.http.client.MockClientHttpResponse;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenIntrospection;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository.RegisteredClientParametersMapper;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import org.springframework.security.oauth2.server.authorization.http.converter.OAuth2TokenIntrospectionHttpMessageConverter;
import org.springframework.security.oauth2.server.authorization.jackson2.TestingAuthenticationTokenMixin;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsSet;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2TokenIntrospectionAuthenticationConverter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Integration tests for the OAuth 2.0 Token Introspection endpoint.
*
* @author Gerardo Roza
* @author Joe Grandja
*/
@ExtendWith(SpringTestContextExtension.class)
public class OAuth2TokenIntrospectionTests {
private static EmbeddedDatabase db;
private static OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer;
private static AuthenticationConverter authenticationConverter;
private static Consumer<List<AuthenticationConverter>> authenticationConvertersConsumer;
private static AuthenticationProvider authenticationProvider;
private static Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer;
private static AuthenticationSuccessHandler authenticationSuccessHandler;
private static AuthenticationFailureHandler authenticationFailureHandler;
private static final HttpMessageConverter<OAuth2TokenIntrospection> tokenIntrospectionHttpResponseConverter = new OAuth2TokenIntrospectionHttpMessageConverter();
private static final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter = new OAuth2AccessTokenResponseHttpMessageConverter();
public final SpringTestContext spring = new SpringTestContext(this);
@Autowired
private MockMvc mvc;
@Autowired
private JdbcOperations jdbcOperations;
@Autowired
private RegisteredClientRepository registeredClientRepository;
@Autowired
private OAuth2AuthorizationService authorizationService;
@Autowired
private AuthorizationServerSettings authorizationServerSettings;
@BeforeAll
public static void init() {
authenticationConverter = mock(AuthenticationConverter.class);
authenticationConvertersConsumer = mock(Consumer.class);
authenticationProvider = mock(AuthenticationProvider.class);
authenticationProvidersConsumer = mock(Consumer.class);
authenticationSuccessHandler = mock(AuthenticationSuccessHandler.class);
authenticationFailureHandler = mock(AuthenticationFailureHandler.class);
accessTokenCustomizer = mock(OAuth2TokenCustomizer.class);
db = new EmbeddedDatabaseBuilder().generateUniqueName(true)
.setType(EmbeddedDatabaseType.HSQL)
.setScriptEncoding("UTF-8")
.addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-schema.sql")
.addScript(
"org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql")
.build();
}
@SuppressWarnings("unchecked")
@BeforeEach
public void setup() {
reset(authenticationConverter);
reset(authenticationConvertersConsumer);
reset(authenticationProvider);
reset(authenticationProvidersConsumer);
reset(authenticationSuccessHandler);
reset(authenticationFailureHandler);
reset(accessTokenCustomizer);
}
@AfterEach
public void tearDown() {
this.jdbcOperations.update("truncate table oauth2_authorization");
this.jdbcOperations.update("truncate table oauth2_registered_client");
}
@AfterAll
public static void destroy() {
db.shutdown();
}
@Test
public void requestWhenIntrospectValidAccessTokenThenActive() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
RegisteredClient introspectRegisteredClient = TestRegisteredClients.registeredClient2()
.clientSecret("secret-2")
.build();
this.registeredClientRepository.save(introspectRegisteredClient);
RegisteredClient authorizedRegisteredClient = TestRegisteredClients.registeredClient().build();
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(Duration.ofHours(1));
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "access-token",
issuedAt, expiresAt, new HashSet<>(Arrays.asList("scope1", "scope2")));
// @formatter:off
OAuth2TokenClaimsSet accessTokenClaims = OAuth2TokenClaimsSet.builder()
.issuer("https://provider.com")
.subject("subject")
.audience(Collections.singletonList(authorizedRegisteredClient.getClientId()))
.issuedAt(issuedAt)
.notBefore(issuedAt)
.expiresAt(expiresAt)
.claim(OAuth2TokenIntrospectionClaimNames.SCOPE, accessToken.getScopes())
.id("id")
.build();
// @formatter:on
OAuth2Authorization authorization = TestOAuth2Authorizations
.authorization(authorizedRegisteredClient, accessToken, accessTokenClaims.getClaims())
.build();
this.registeredClientRepository.save(authorizedRegisteredClient);
this.authorizationService.save(authorization);
// @formatter:off
MvcResult mvcResult = this.mvc.perform(post(this.authorizationServerSettings.getTokenIntrospectionEndpoint())
.params(getTokenIntrospectionRequestParameters(accessToken, OAuth2TokenType.ACCESS_TOKEN))
.header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(introspectRegisteredClient)))
.andExpect(status().isOk())
.andReturn();
// @formatter:on
OAuth2TokenIntrospection tokenIntrospectionResponse = readTokenIntrospectionResponse(mvcResult);
assertThat(tokenIntrospectionResponse.isActive()).isTrue();
assertThat(tokenIntrospectionResponse.getClientId()).isEqualTo(authorizedRegisteredClient.getClientId());
assertThat(tokenIntrospectionResponse.getUsername()).isNull();
assertThat(tokenIntrospectionResponse.getIssuedAt()).isBetween(accessTokenClaims.getIssuedAt().minusSeconds(1),
accessTokenClaims.getIssuedAt().plusSeconds(1));
assertThat(tokenIntrospectionResponse.getExpiresAt()).isBetween(
accessTokenClaims.getExpiresAt().minusSeconds(1), accessTokenClaims.getExpiresAt().plusSeconds(1));
assertThat(tokenIntrospectionResponse.getScopes()).containsExactlyInAnyOrderElementsOf(accessToken.getScopes());
assertThat(tokenIntrospectionResponse.getTokenType()).isEqualTo(accessToken.getTokenType().getValue());
assertThat(tokenIntrospectionResponse.getNotBefore()).isBetween(
accessTokenClaims.getNotBefore().minusSeconds(1), accessTokenClaims.getNotBefore().plusSeconds(1));
assertThat(tokenIntrospectionResponse.getSubject()).isEqualTo(accessTokenClaims.getSubject());
assertThat(tokenIntrospectionResponse.getAudience())
.containsExactlyInAnyOrderElementsOf(accessTokenClaims.getAudience());
assertThat(tokenIntrospectionResponse.getIssuer()).isEqualTo(accessTokenClaims.getIssuer());
assertThat(tokenIntrospectionResponse.getId()).isEqualTo(accessTokenClaims.getId());
}
@Test
public void requestWhenIntrospectValidRefreshTokenThenActive() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
RegisteredClient introspectRegisteredClient = TestRegisteredClients.registeredClient2()
.clientSecret("secret-2")
.build();
this.registeredClientRepository.save(introspectRegisteredClient);
RegisteredClient authorizedRegisteredClient = TestRegisteredClients.registeredClient().build();
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(authorizedRegisteredClient).build();
OAuth2RefreshToken refreshToken = authorization.getRefreshToken().getToken();
this.registeredClientRepository.save(authorizedRegisteredClient);
this.authorizationService.save(authorization);
// @formatter:off
MvcResult mvcResult = this.mvc.perform(post(this.authorizationServerSettings.getTokenIntrospectionEndpoint())
.params(getTokenIntrospectionRequestParameters(refreshToken, OAuth2TokenType.REFRESH_TOKEN))
.header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(introspectRegisteredClient)))
.andExpect(status().isOk())
.andReturn();
// @formatter:on
OAuth2TokenIntrospection tokenIntrospectionResponse = readTokenIntrospectionResponse(mvcResult);
assertThat(tokenIntrospectionResponse.isActive()).isTrue();
assertThat(tokenIntrospectionResponse.getClientId()).isEqualTo(authorizedRegisteredClient.getClientId());
assertThat(tokenIntrospectionResponse.getUsername()).isNull();
assertThat(tokenIntrospectionResponse.getIssuedAt()).isBetween(refreshToken.getIssuedAt().minusSeconds(1),
refreshToken.getIssuedAt().plusSeconds(1));
assertThat(tokenIntrospectionResponse.getExpiresAt()).isBetween(refreshToken.getExpiresAt().minusSeconds(1),
refreshToken.getExpiresAt().plusSeconds(1));
assertThat(tokenIntrospectionResponse.getScopes()).isNull();
assertThat(tokenIntrospectionResponse.getTokenType()).isNull();
assertThat(tokenIntrospectionResponse.getNotBefore()).isNull();
assertThat(tokenIntrospectionResponse.getSubject()).isNull();
assertThat(tokenIntrospectionResponse.getAudience()).isNull();
assertThat(tokenIntrospectionResponse.getIssuer()).isNull();
assertThat(tokenIntrospectionResponse.getId()).isNull();
}
@Test
public void requestWhenObtainReferenceAccessTokenAndIntrospectThenActive() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
// @formatter:off
TokenSettings tokenSettings = TokenSettings.builder()
.accessTokenFormat(OAuth2TokenFormat.REFERENCE)
.build();
RegisteredClient authorizedRegisteredClient = TestRegisteredClients.registeredClient()
.tokenSettings(tokenSettings)
.build();
// @formatter:on
this.registeredClientRepository.save(authorizedRegisteredClient);
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(authorizedRegisteredClient).build();
this.authorizationService.save(authorization);
// @formatter:off
MvcResult mvcResult = this.mvc.perform(post(this.authorizationServerSettings.getTokenEndpoint())
.params(getAuthorizationCodeTokenRequestParameters(authorizedRegisteredClient, authorization))
.header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(authorizedRegisteredClient)))
.andExpect(status().isOk())
.andReturn();
// @formatter:on
OAuth2AccessTokenResponse accessTokenResponse = readAccessTokenResponse(mvcResult);
OAuth2AccessToken accessToken = accessTokenResponse.getAccessToken();
RegisteredClient introspectRegisteredClient = TestRegisteredClients.registeredClient2().build();
this.registeredClientRepository.save(introspectRegisteredClient);
// @formatter:off
mvcResult = this.mvc.perform(post(this.authorizationServerSettings.getTokenIntrospectionEndpoint())
.params(getTokenIntrospectionRequestParameters(accessToken, OAuth2TokenType.ACCESS_TOKEN))
.header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(introspectRegisteredClient)))
.andExpect(status().isOk())
.andReturn();
// @formatter:on
OAuth2TokenIntrospection tokenIntrospectionResponse = readTokenIntrospectionResponse(mvcResult);
ArgumentCaptor<OAuth2TokenClaimsContext> accessTokenClaimsContextCaptor = ArgumentCaptor
.forClass(OAuth2TokenClaimsContext.class);
verify(accessTokenCustomizer).customize(accessTokenClaimsContextCaptor.capture());
OAuth2TokenClaimsContext accessTokenClaimsContext = accessTokenClaimsContextCaptor.getValue();
OAuth2TokenClaimsSet accessTokenClaims = accessTokenClaimsContext.getClaims().build();
assertThat(tokenIntrospectionResponse.isActive()).isTrue();
assertThat(tokenIntrospectionResponse.getClientId()).isEqualTo(authorizedRegisteredClient.getClientId());
assertThat(tokenIntrospectionResponse.getUsername()).isNull();
assertThat(tokenIntrospectionResponse.getIssuedAt()).isBetween(accessTokenClaims.getIssuedAt().minusSeconds(1),
accessTokenClaims.getIssuedAt().plusSeconds(1));
assertThat(tokenIntrospectionResponse.getExpiresAt()).isBetween(
accessTokenClaims.getExpiresAt().minusSeconds(1), accessTokenClaims.getExpiresAt().plusSeconds(1));
List<String> scopes = new ArrayList<>(accessTokenClaims.getClaim(OAuth2ParameterNames.SCOPE));
assertThat(tokenIntrospectionResponse.getScopes()).containsExactlyInAnyOrderElementsOf(scopes);
assertThat(tokenIntrospectionResponse.getTokenType()).isEqualTo(accessToken.getTokenType().getValue());
assertThat(tokenIntrospectionResponse.getNotBefore()).isBetween(
accessTokenClaims.getNotBefore().minusSeconds(1), accessTokenClaims.getNotBefore().plusSeconds(1));
assertThat(tokenIntrospectionResponse.getSubject()).isEqualTo(accessTokenClaims.getSubject());
assertThat(tokenIntrospectionResponse.getAudience())
.containsExactlyInAnyOrderElementsOf(accessTokenClaims.getAudience());
assertThat(tokenIntrospectionResponse.getIssuer()).isEqualTo(accessTokenClaims.getIssuer());
assertThat(tokenIntrospectionResponse.getId()).isEqualTo(accessTokenClaims.getId());
}
@Test
public void requestWhenTokenIntrospectionEndpointCustomizedThenUsed() throws Exception {
this.spring.register(AuthorizationServerConfigurationCustomTokenIntrospectionEndpoint.class).autowire();
RegisteredClient introspectRegisteredClient = TestRegisteredClients.registeredClient2().build();
this.registeredClientRepository.save(introspectRegisteredClient);
RegisteredClient authorizedRegisteredClient = TestRegisteredClients.registeredClient().build();
this.registeredClientRepository.save(authorizedRegisteredClient);
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(authorizedRegisteredClient).build();
this.authorizationService.save(authorization);
OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
Authentication clientPrincipal = new OAuth2ClientAuthenticationToken(introspectRegisteredClient,
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, introspectRegisteredClient.getClientSecret());
OAuth2TokenIntrospectionAuthenticationToken tokenIntrospectionAuthentication = new OAuth2TokenIntrospectionAuthenticationToken(
accessToken.getTokenValue(), clientPrincipal, null, null);
given(authenticationConverter.convert(any())).willReturn(tokenIntrospectionAuthentication);
given(authenticationProvider.supports(eq(OAuth2TokenIntrospectionAuthenticationToken.class))).willReturn(true);
given(authenticationProvider.authenticate(any())).willReturn(tokenIntrospectionAuthentication);
// @formatter:off
this.mvc.perform(post(this.authorizationServerSettings.getTokenIntrospectionEndpoint())
.params(getTokenIntrospectionRequestParameters(accessToken, OAuth2TokenType.ACCESS_TOKEN))
.header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(introspectRegisteredClient)))
.andExpect(status().isOk());
// @formatter:on
verify(authenticationConverter).convert(any());
@SuppressWarnings("unchecked")
ArgumentCaptor<List<AuthenticationConverter>> authenticationConvertersCaptor = ArgumentCaptor
.forClass(List.class);
verify(authenticationConvertersConsumer).accept(authenticationConvertersCaptor.capture());
List<AuthenticationConverter> authenticationConverters = authenticationConvertersCaptor.getValue();
assertThat(authenticationConverters).allMatch((converter) -> converter == authenticationConverter
|| converter instanceof OAuth2TokenIntrospectionAuthenticationConverter);
verify(authenticationProvider).authenticate(eq(tokenIntrospectionAuthentication));
@SuppressWarnings("unchecked")
ArgumentCaptor<List<AuthenticationProvider>> authenticationProvidersCaptor = ArgumentCaptor
.forClass(List.class);
verify(authenticationProvidersConsumer).accept(authenticationProvidersCaptor.capture());
List<AuthenticationProvider> authenticationProviders = authenticationProvidersCaptor.getValue();
assertThat(authenticationProviders).allMatch((provider) -> provider == authenticationProvider
|| provider instanceof OAuth2TokenIntrospectionAuthenticationProvider);
verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(),
eq(tokenIntrospectionAuthentication));
}
@Test
public void requestWhenIntrospectionRequestIncludesIssuerPathThenActive() throws Exception {
this.spring.register(AuthorizationServerConfigurationCustomTokenIntrospectionEndpoint.class).autowire();
RegisteredClient introspectRegisteredClient = TestRegisteredClients.registeredClient2().build();
this.registeredClientRepository.save(introspectRegisteredClient);
RegisteredClient authorizedRegisteredClient = TestRegisteredClients.registeredClient().build();
this.registeredClientRepository.save(authorizedRegisteredClient);
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(authorizedRegisteredClient).build();
this.authorizationService.save(authorization);
OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
Authentication clientPrincipal = new OAuth2ClientAuthenticationToken(introspectRegisteredClient,
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, introspectRegisteredClient.getClientSecret());
OAuth2TokenIntrospectionAuthenticationToken tokenIntrospectionAuthentication = new OAuth2TokenIntrospectionAuthenticationToken(
accessToken.getTokenValue(), clientPrincipal, null, null);
given(authenticationConverter.convert(any())).willReturn(tokenIntrospectionAuthentication);
given(authenticationProvider.supports(eq(OAuth2TokenIntrospectionAuthenticationToken.class))).willReturn(true);
given(authenticationProvider.authenticate(any())).willReturn(tokenIntrospectionAuthentication);
String issuer = "https://example.com:8443/issuer1";
// @formatter:off
this.mvc.perform(post(issuer.concat(this.authorizationServerSettings.getTokenIntrospectionEndpoint()))
.params(getTokenIntrospectionRequestParameters(accessToken, OAuth2TokenType.ACCESS_TOKEN))
.header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(introspectRegisteredClient)))
.andExpect(status().isOk());
// @formatter:on
}
private static MultiValueMap<String, String> getTokenIntrospectionRequestParameters(OAuth2Token token,
OAuth2TokenType tokenType) {
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.set(OAuth2ParameterNames.TOKEN, token.getTokenValue());
parameters.set(OAuth2ParameterNames.TOKEN_TYPE_HINT, tokenType.getValue());
return parameters;
}
private static OAuth2TokenIntrospection readTokenIntrospectionResponse(MvcResult mvcResult) throws Exception {
MockHttpServletResponse servletResponse = mvcResult.getResponse();
MockClientHttpResponse httpResponse = new MockClientHttpResponse(servletResponse.getContentAsByteArray(),
HttpStatus.valueOf(servletResponse.getStatus()));
return tokenIntrospectionHttpResponseConverter.read(OAuth2TokenIntrospection.class, httpResponse);
}
private static MultiValueMap<String, String> getAuthorizationCodeTokenRequestParameters(
RegisteredClient registeredClient, OAuth2Authorization authorization) {
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.set(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
parameters.set(OAuth2ParameterNames.CODE,
authorization.getToken(OAuth2AuthorizationCode.class).getToken().getTokenValue());
parameters.set(OAuth2ParameterNames.REDIRECT_URI, registeredClient.getRedirectUris().iterator().next());
return parameters;
}
private static OAuth2AccessTokenResponse readAccessTokenResponse(MvcResult mvcResult) throws Exception {
MockHttpServletResponse servletResponse = mvcResult.getResponse();
MockClientHttpResponse httpResponse = new MockClientHttpResponse(servletResponse.getContentAsByteArray(),
HttpStatus.valueOf(servletResponse.getStatus()));
return accessTokenHttpResponseConverter.read(OAuth2AccessTokenResponse.class, httpResponse);
}
private static String getAuthorizationHeader(RegisteredClient registeredClient) throws Exception {
String clientId = registeredClient.getClientId();
String clientSecret = registeredClient.getClientSecret();
clientId = URLEncoder.encode(clientId, StandardCharsets.UTF_8.name());
clientSecret = URLEncoder.encode(clientSecret, StandardCharsets.UTF_8.name());
String credentialsString = clientId + ":" + clientSecret;
byte[] encodedBytes = Base64.getEncoder().encode(credentialsString.getBytes(StandardCharsets.UTF_8));
return "Basic " + new String(encodedBytes, StandardCharsets.UTF_8);
}
@EnableWebSecurity
@Import(OAuth2AuthorizationServerConfiguration.class)
static class AuthorizationServerConfiguration {
@Bean
OAuth2AuthorizationService authorizationService(JdbcOperations jdbcOperations,
RegisteredClientRepository registeredClientRepository) {
JdbcOAuth2AuthorizationService authorizationService = new JdbcOAuth2AuthorizationService(jdbcOperations,
registeredClientRepository);
authorizationService.setAuthorizationRowMapper(new RowMapper(registeredClientRepository));
authorizationService.setAuthorizationParametersMapper(new ParametersMapper());
return authorizationService;
}
@Bean
OAuth2AuthorizationConsentService authorizationConsentService(JdbcOperations jdbcOperations,
RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcOperations, registeredClientRepository);
}
@Bean
RegisteredClientRepository registeredClientRepository(JdbcOperations jdbcOperations) {
JdbcRegisteredClientRepository jdbcRegisteredClientRepository = new JdbcRegisteredClientRepository(
jdbcOperations);
RegisteredClientParametersMapper registeredClientParametersMapper = new RegisteredClientParametersMapper();
jdbcRegisteredClientRepository.setRegisteredClientParametersMapper(registeredClientParametersMapper);
return jdbcRegisteredClientRepository;
}
@Bean
JdbcOperations jdbcOperations() {
return new JdbcTemplate(db);
}
@Bean
AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().tokenIntrospectionEndpoint("/test/introspect").build();
}
@Bean
OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer() {
return accessTokenCustomizer;
}
@Bean
PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
static class RowMapper extends JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper {
RowMapper(RegisteredClientRepository registeredClientRepository) {
super(registeredClientRepository);
getObjectMapper().addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class);
}
}
static class ParametersMapper extends JdbcOAuth2AuthorizationService.OAuth2AuthorizationParametersMapper {
ParametersMapper() {
super();
getObjectMapper().addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class);
}
}
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class AuthorizationServerConfigurationCustomTokenIntrospectionEndpoint
extends AuthorizationServerConfiguration {
// @formatter:off
@Bean
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
OAuth2AuthorizationServerConfigurer.authorizationServer();
http
.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
.with(authorizationServerConfigurer, (authorizationServer) ->
authorizationServer
.tokenIntrospectionEndpoint((tokenIntrospectionEndpoint) ->
tokenIntrospectionEndpoint
.introspectionRequestConverter(authenticationConverter)
.introspectionRequestConverters(authenticationConvertersConsumer)
.authenticationProvider(authenticationProvider)
.authenticationProviders(authenticationProvidersConsumer)
.introspectionResponseHandler(authenticationSuccessHandler)
.errorResponseHandler(authenticationFailureHandler))
)
.authorizeHttpRequests((authorize) ->
authorize.anyRequest().authenticated()
);
return http.build();
}
// @formatter:on
@Override
AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder()
.multipleIssuersAllowed(true)
.tokenIntrospectionEndpoint("/test/introspect")
.build();
}
}
}
@@ -0,0 +1,413 @@
/*
* Copyright 2004-present 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.config.annotation.web.configurers.oauth2.server.authorization;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.List;
import java.util.function.Consumer;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpHeaders;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.jose.TestJwks;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository.RegisteredClientParametersMapper;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import org.springframework.security.oauth2.server.authorization.jackson2.TestingAuthenticationTokenMixin;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2TokenRevocationAuthenticationConverter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Integration tests for the OAuth 2.0 Token Revocation endpoint.
*
* @author Joe Grandja
*/
@ExtendWith(SpringTestContextExtension.class)
public class OAuth2TokenRevocationTests {
private static final String DEFAULT_TOKEN_REVOCATION_ENDPOINT_URI = "/oauth2/revoke";
private static EmbeddedDatabase db;
private static JWKSource<SecurityContext> jwkSource;
private static AuthenticationConverter authenticationConverter;
private static Consumer<List<AuthenticationConverter>> authenticationConvertersConsumer;
private static AuthenticationProvider authenticationProvider;
private static Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer;
private static AuthenticationSuccessHandler authenticationSuccessHandler;
private static AuthenticationFailureHandler authenticationFailureHandler;
public final SpringTestContext spring = new SpringTestContext(this);
@Autowired
private MockMvc mvc;
@Autowired
private JdbcOperations jdbcOperations;
@Autowired
private RegisteredClientRepository registeredClientRepository;
@Autowired
private OAuth2AuthorizationService authorizationService;
@BeforeAll
public static void init() {
JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
authenticationConverter = mock(AuthenticationConverter.class);
authenticationConvertersConsumer = mock(Consumer.class);
authenticationProvider = mock(AuthenticationProvider.class);
authenticationProvidersConsumer = mock(Consumer.class);
authenticationSuccessHandler = mock(AuthenticationSuccessHandler.class);
authenticationFailureHandler = mock(AuthenticationFailureHandler.class);
db = new EmbeddedDatabaseBuilder().generateUniqueName(true)
.setType(EmbeddedDatabaseType.HSQL)
.setScriptEncoding("UTF-8")
.addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-schema.sql")
.addScript(
"org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql")
.build();
}
@AfterEach
public void tearDown() {
this.jdbcOperations.update("truncate table oauth2_authorization");
this.jdbcOperations.update("truncate table oauth2_registered_client");
}
@AfterAll
public static void destroy() {
db.shutdown();
}
@Test
public void requestWhenRevokeRefreshTokenThenRevoked() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
this.registeredClientRepository.save(registeredClient);
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build();
OAuth2RefreshToken token = authorization.getRefreshToken().getToken();
OAuth2TokenType tokenType = OAuth2TokenType.REFRESH_TOKEN;
this.authorizationService.save(authorization);
this.mvc
.perform(post(DEFAULT_TOKEN_REVOCATION_ENDPOINT_URI)
.params(getTokenRevocationRequestParameters(token, tokenType))
.header(HttpHeaders.AUTHORIZATION,
"Basic " + encodeBasicAuth(registeredClient.getClientId(), registeredClient.getClientSecret())))
.andExpect(status().isOk());
OAuth2Authorization updatedAuthorization = this.authorizationService.findById(authorization.getId());
OAuth2Authorization.Token<OAuth2RefreshToken> refreshToken = updatedAuthorization.getRefreshToken();
assertThat(refreshToken.isInvalidated()).isTrue();
OAuth2Authorization.Token<OAuth2AccessToken> accessToken = updatedAuthorization.getAccessToken();
assertThat(accessToken.isInvalidated()).isTrue();
}
@Test
public void requestWhenRevokeAccessTokenThenRevoked() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
this.registeredClientRepository.save(registeredClient);
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build();
OAuth2AccessToken token = authorization.getAccessToken().getToken();
OAuth2TokenType tokenType = OAuth2TokenType.ACCESS_TOKEN;
this.authorizationService.save(authorization);
this.mvc
.perform(post(DEFAULT_TOKEN_REVOCATION_ENDPOINT_URI)
.params(getTokenRevocationRequestParameters(token, tokenType))
.header(HttpHeaders.AUTHORIZATION,
"Basic " + encodeBasicAuth(registeredClient.getClientId(), registeredClient.getClientSecret())))
.andExpect(status().isOk());
OAuth2Authorization updatedAuthorization = this.authorizationService.findById(authorization.getId());
OAuth2Authorization.Token<OAuth2AccessToken> accessToken = updatedAuthorization.getAccessToken();
assertThat(accessToken.isInvalidated()).isTrue();
OAuth2Authorization.Token<OAuth2RefreshToken> refreshToken = updatedAuthorization.getRefreshToken();
assertThat(refreshToken.isInvalidated()).isFalse();
}
@Test
public void requestWhenRevokeAccessTokenAndRequestIncludesIssuerPathThenRevoked() throws Exception {
this.spring.register(AuthorizationServerConfigurationWithMultipleIssuersAllowed.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
this.registeredClientRepository.save(registeredClient);
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build();
OAuth2AccessToken token = authorization.getAccessToken().getToken();
OAuth2TokenType tokenType = OAuth2TokenType.ACCESS_TOKEN;
this.authorizationService.save(authorization);
String issuer = "https://example.com:8443/issuer1";
// @formatter:off
this.mvc.perform(post(issuer.concat(DEFAULT_TOKEN_REVOCATION_ENDPOINT_URI))
.params(getTokenRevocationRequestParameters(token, tokenType))
.header(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth(
registeredClient.getClientId(), registeredClient.getClientSecret())))
.andExpect(status().isOk());
// @formatter:on
OAuth2Authorization updatedAuthorization = this.authorizationService.findById(authorization.getId());
OAuth2Authorization.Token<OAuth2AccessToken> accessToken = updatedAuthorization.getAccessToken();
assertThat(accessToken.isInvalidated()).isTrue();
OAuth2Authorization.Token<OAuth2RefreshToken> refreshToken = updatedAuthorization.getRefreshToken();
assertThat(refreshToken.isInvalidated()).isFalse();
}
@Test
public void requestWhenTokenRevocationEndpointCustomizedThenUsed() throws Exception {
this.spring.register(AuthorizationServerConfigurationCustomTokenRevocationEndpoint.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
this.registeredClientRepository.save(registeredClient);
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build();
OAuth2AccessToken token = authorization.getAccessToken().getToken();
OAuth2TokenType tokenType = OAuth2TokenType.ACCESS_TOKEN;
this.authorizationService.save(authorization);
Authentication clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
OAuth2TokenRevocationAuthenticationToken tokenRevocationAuthentication = new OAuth2TokenRevocationAuthenticationToken(
token, clientPrincipal);
given(authenticationConverter.convert(any())).willReturn(tokenRevocationAuthentication);
given(authenticationProvider.supports(eq(OAuth2TokenRevocationAuthenticationToken.class))).willReturn(true);
given(authenticationProvider.authenticate(any())).willReturn(tokenRevocationAuthentication);
this.mvc
.perform(post(DEFAULT_TOKEN_REVOCATION_ENDPOINT_URI)
.params(getTokenRevocationRequestParameters(token, tokenType))
.header(HttpHeaders.AUTHORIZATION,
"Basic " + encodeBasicAuth(registeredClient.getClientId(), registeredClient.getClientSecret())))
.andExpect(status().isOk());
verify(authenticationConverter).convert(any());
@SuppressWarnings("unchecked")
ArgumentCaptor<List<AuthenticationConverter>> authenticationConvertersCaptor = ArgumentCaptor
.forClass(List.class);
verify(authenticationConvertersConsumer).accept(authenticationConvertersCaptor.capture());
List<AuthenticationConverter> authenticationConverters = authenticationConvertersCaptor.getValue();
assertThat(authenticationConverters).allMatch((converter) -> converter == authenticationConverter
|| converter instanceof OAuth2TokenRevocationAuthenticationConverter);
verify(authenticationProvider).authenticate(eq(tokenRevocationAuthentication));
@SuppressWarnings("unchecked")
ArgumentCaptor<List<AuthenticationProvider>> authenticationProvidersCaptor = ArgumentCaptor
.forClass(List.class);
verify(authenticationProvidersConsumer).accept(authenticationProvidersCaptor.capture());
List<AuthenticationProvider> authenticationProviders = authenticationProvidersCaptor.getValue();
assertThat(authenticationProviders).allMatch((provider) -> provider == authenticationProvider
|| provider instanceof OAuth2TokenRevocationAuthenticationProvider);
verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), eq(tokenRevocationAuthentication));
}
private static MultiValueMap<String, String> getTokenRevocationRequestParameters(OAuth2Token token,
OAuth2TokenType tokenType) {
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.set(OAuth2ParameterNames.TOKEN, token.getTokenValue());
parameters.set(OAuth2ParameterNames.TOKEN_TYPE_HINT, tokenType.getValue());
return parameters;
}
private static String encodeBasicAuth(String clientId, String secret) throws Exception {
clientId = URLEncoder.encode(clientId, StandardCharsets.UTF_8.name());
secret = URLEncoder.encode(secret, StandardCharsets.UTF_8.name());
String credentialsString = clientId + ":" + secret;
byte[] encodedBytes = Base64.getEncoder().encode(credentialsString.getBytes(StandardCharsets.UTF_8));
return new String(encodedBytes, StandardCharsets.UTF_8);
}
@EnableWebSecurity
@Import(OAuth2AuthorizationServerConfiguration.class)
static class AuthorizationServerConfiguration {
@Bean
OAuth2AuthorizationService authorizationService(JdbcOperations jdbcOperations,
RegisteredClientRepository registeredClientRepository) {
JdbcOAuth2AuthorizationService authorizationService = new JdbcOAuth2AuthorizationService(jdbcOperations,
registeredClientRepository);
authorizationService.setAuthorizationRowMapper(new RowMapper(registeredClientRepository));
authorizationService.setAuthorizationParametersMapper(new ParametersMapper());
return authorizationService;
}
@Bean
RegisteredClientRepository registeredClientRepository(JdbcOperations jdbcOperations) {
JdbcRegisteredClientRepository jdbcRegisteredClientRepository = new JdbcRegisteredClientRepository(
jdbcOperations);
RegisteredClientParametersMapper registeredClientParametersMapper = new RegisteredClientParametersMapper();
jdbcRegisteredClientRepository.setRegisteredClientParametersMapper(registeredClientParametersMapper);
return jdbcRegisteredClientRepository;
}
@Bean
JdbcOperations jdbcOperations() {
return new JdbcTemplate(db);
}
@Bean
JWKSource<SecurityContext> jwkSource() {
return jwkSource;
}
@Bean
PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
static class RowMapper extends JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper {
RowMapper(RegisteredClientRepository registeredClientRepository) {
super(registeredClientRepository);
getObjectMapper().addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class);
}
}
static class ParametersMapper extends JdbcOAuth2AuthorizationService.OAuth2AuthorizationParametersMapper {
ParametersMapper() {
super();
getObjectMapper().addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class);
}
}
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class AuthorizationServerConfigurationCustomTokenRevocationEndpoint
extends AuthorizationServerConfiguration {
// @formatter:off
@Bean
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
OAuth2AuthorizationServerConfigurer.authorizationServer();
http
.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
.with(authorizationServerConfigurer, (authorizationServer) ->
authorizationServer
.tokenRevocationEndpoint((tokenRevocationEndpoint) ->
tokenRevocationEndpoint
.revocationRequestConverter(authenticationConverter)
.revocationRequestConverters(authenticationConvertersConsumer)
.authenticationProvider(authenticationProvider)
.authenticationProviders(authenticationProvidersConsumer)
.revocationResponseHandler(authenticationSuccessHandler)
.errorResponseHandler(authenticationFailureHandler))
)
.authorizeHttpRequests((authorize) ->
authorize.anyRequest().authenticated()
);
return http.build();
}
// @formatter:on
}
@EnableWebSecurity
@Import(OAuth2AuthorizationServerConfiguration.class)
static class AuthorizationServerConfigurationWithMultipleIssuersAllowed extends AuthorizationServerConfiguration {
@Bean
AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().multipleIssuersAllowed(true).build();
}
}
}
@@ -0,0 +1,919 @@
/*
* Copyright 2004-present 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.config.annotation.web.configurers.oauth2.server.authorization;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import javax.crypto.spec.SecretKeySpec;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import jakarta.servlet.http.HttpServletResponse;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.assertj.core.data.TemporalUnitWithinOffset;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.mock.http.MockHttpOutputMessage;
import org.springframework.mock.http.client.MockClientHttpResponse;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
import org.springframework.security.oauth2.jose.TestJwks;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.JwsHeader;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository.RegisteredClientParametersMapper;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import org.springframework.security.oauth2.server.authorization.oidc.OidcClientRegistration;
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientConfigurationAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.oidc.converter.OidcClientRegistrationRegisteredClientConverter;
import org.springframework.security.oauth2.server.authorization.oidc.converter.RegisteredClientOidcClientRegistrationConverter;
import org.springframework.security.oauth2.server.authorization.oidc.http.converter.OidcClientRegistrationHttpMessageConverter;
import org.springframework.security.oauth2.server.authorization.oidc.web.authentication.OidcClientRegistrationAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.CollectionUtils;
import org.springframework.web.util.UriComponentsBuilder;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.CoreMatchers.containsString;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Integration tests for OpenID Connect Dynamic Client Registration 1.0.
*
* @author Ovidiu Popa
* @author Joe Grandja
* @author Dmitriy Dubson
*/
@ExtendWith(SpringTestContextExtension.class)
public class OidcClientRegistrationTests {
private static final String ISSUER = "https://example.com:8443/issuer1";
private static final String DEFAULT_TOKEN_ENDPOINT_URI = "/oauth2/token";
private static final String DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI = "/connect/register";
private static final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter = new OAuth2AccessTokenResponseHttpMessageConverter();
private static final HttpMessageConverter<OidcClientRegistration> clientRegistrationHttpMessageConverter = new OidcClientRegistrationHttpMessageConverter();
private static EmbeddedDatabase db;
private static JWKSource<SecurityContext> jwkSource;
private static JWKSet clientJwkSet;
private static JwtEncoder jwtClientAssertionEncoder;
public final SpringTestContext spring = new SpringTestContext(this);
@Autowired
private MockMvc mvc;
@Autowired
private JdbcOperations jdbcOperations;
@Autowired
private RegisteredClientRepository registeredClientRepository;
@Autowired
private AuthorizationServerSettings authorizationServerSettings;
private static AuthenticationConverter authenticationConverter;
private static Consumer<List<AuthenticationConverter>> authenticationConvertersConsumer;
private static AuthenticationProvider authenticationProvider;
private static Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer;
private static AuthenticationSuccessHandler authenticationSuccessHandler;
private static AuthenticationFailureHandler authenticationFailureHandler;
private MockWebServer server;
private String clientJwkSetUrl;
@BeforeAll
public static void init() {
JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
clientJwkSet = new JWKSet(TestJwks.generateRsaJwk().build());
jwtClientAssertionEncoder = new NimbusJwtEncoder(
(jwkSelector, securityContext) -> jwkSelector.select(clientJwkSet));
db = new EmbeddedDatabaseBuilder().generateUniqueName(true)
.setType(EmbeddedDatabaseType.HSQL)
.setScriptEncoding("UTF-8")
.addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-schema.sql")
.addScript(
"org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql")
.build();
authenticationConverter = mock(AuthenticationConverter.class);
authenticationConvertersConsumer = mock(Consumer.class);
authenticationProvider = mock(AuthenticationProvider.class);
authenticationProvidersConsumer = mock(Consumer.class);
authenticationSuccessHandler = mock(AuthenticationSuccessHandler.class);
authenticationFailureHandler = mock(AuthenticationFailureHandler.class);
}
@BeforeEach
public void setup() throws Exception {
this.server = new MockWebServer();
this.server.start();
this.clientJwkSetUrl = this.server.url("/jwks").toString();
// @formatter:off
MockResponse response = new MockResponse()
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.setBody(clientJwkSet.toString());
// @formatter:on
this.server.enqueue(response);
given(authenticationProvider.supports(OidcClientRegistrationAuthenticationToken.class)).willReturn(true);
}
@AfterEach
public void tearDown() throws Exception {
this.server.shutdown();
this.jdbcOperations.update("truncate table oauth2_authorization");
this.jdbcOperations.update("truncate table oauth2_registered_client");
reset(authenticationConverter);
reset(authenticationConvertersConsumer);
reset(authenticationProvider);
reset(authenticationProvidersConsumer);
reset(authenticationSuccessHandler);
reset(authenticationFailureHandler);
}
@AfterAll
public static void destroy() {
db.shutdown();
}
@Test
public void requestWhenClientRegistrationRequestAuthorizedThenClientRegistrationResponse() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
// @formatter:off
OidcClientRegistration clientRegistration = OidcClientRegistration.builder()
.clientName("client-name")
.redirectUri("https://client.example.com")
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.scope("scope1")
.scope("scope2")
.build();
// @formatter:on
OidcClientRegistration clientRegistrationResponse = registerClient(clientRegistration);
assertThat(clientRegistrationResponse.getClientId()).isNotNull();
assertThat(clientRegistrationResponse.getClientIdIssuedAt()).isNotNull();
assertThat(clientRegistrationResponse.getClientSecret()).isNotNull();
assertThat(clientRegistrationResponse.getClientSecretExpiresAt()).isNull();
assertThat(clientRegistrationResponse.getClientName()).isEqualTo(clientRegistration.getClientName());
assertThat(clientRegistrationResponse.getRedirectUris())
.containsExactlyInAnyOrderElementsOf(clientRegistration.getRedirectUris());
assertThat(clientRegistrationResponse.getGrantTypes())
.containsExactlyInAnyOrderElementsOf(clientRegistration.getGrantTypes());
assertThat(clientRegistrationResponse.getResponseTypes())
.containsExactly(OAuth2AuthorizationResponseType.CODE.getValue());
assertThat(clientRegistrationResponse.getScopes())
.containsExactlyInAnyOrderElementsOf(clientRegistration.getScopes());
assertThat(clientRegistrationResponse.getTokenEndpointAuthenticationMethod())
.isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue());
assertThat(clientRegistrationResponse.getIdTokenSignedResponseAlgorithm())
.isEqualTo(SignatureAlgorithm.RS256.getName());
assertThat(clientRegistrationResponse.getRegistrationClientUrl()).isNotNull();
assertThat(clientRegistrationResponse.getRegistrationAccessToken()).isNotEmpty();
}
@Test
public void requestWhenClientConfigurationRequestAuthorizedThenClientRegistrationResponse() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
// @formatter:off
OidcClientRegistration clientRegistration = OidcClientRegistration.builder()
.clientName("client-name")
.redirectUri("https://client.example.com")
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.scope("scope1")
.scope("scope2")
.build();
// @formatter:on
OidcClientRegistration clientRegistrationResponse = registerClient(clientRegistration);
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setBearerAuth(clientRegistrationResponse.getRegistrationAccessToken());
MvcResult mvcResult = this.mvc
.perform(get(clientRegistrationResponse.getRegistrationClientUrl().toURI()).headers(httpHeaders))
.andExpect(status().isOk())
.andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store")))
.andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache")))
.andReturn();
OidcClientRegistration clientConfigurationResponse = readClientRegistrationResponse(mvcResult.getResponse());
assertThat(clientConfigurationResponse.getClientId()).isEqualTo(clientRegistrationResponse.getClientId());
assertThat(clientConfigurationResponse.getClientIdIssuedAt())
.isEqualTo(clientRegistrationResponse.getClientIdIssuedAt());
assertThat(clientConfigurationResponse.getClientSecret()).isNotNull();
assertThat(clientConfigurationResponse.getClientSecretExpiresAt())
.isEqualTo(clientRegistrationResponse.getClientSecretExpiresAt());
assertThat(clientConfigurationResponse.getClientName()).isEqualTo(clientRegistrationResponse.getClientName());
assertThat(clientConfigurationResponse.getRedirectUris())
.containsExactlyInAnyOrderElementsOf(clientRegistrationResponse.getRedirectUris());
assertThat(clientConfigurationResponse.getGrantTypes())
.containsExactlyInAnyOrderElementsOf(clientRegistrationResponse.getGrantTypes());
assertThat(clientConfigurationResponse.getResponseTypes())
.containsExactlyInAnyOrderElementsOf(clientRegistrationResponse.getResponseTypes());
assertThat(clientConfigurationResponse.getScopes())
.containsExactlyInAnyOrderElementsOf(clientRegistrationResponse.getScopes());
assertThat(clientConfigurationResponse.getTokenEndpointAuthenticationMethod())
.isEqualTo(clientRegistrationResponse.getTokenEndpointAuthenticationMethod());
assertThat(clientConfigurationResponse.getIdTokenSignedResponseAlgorithm())
.isEqualTo(clientRegistrationResponse.getIdTokenSignedResponseAlgorithm());
assertThat(clientConfigurationResponse.getRegistrationClientUrl())
.isEqualTo(clientRegistrationResponse.getRegistrationClientUrl());
assertThat(clientConfigurationResponse.getRegistrationAccessToken()).isNull();
}
@Test
public void requestWhenClientRegistrationEndpointCustomizedThenUsed() throws Exception {
this.spring.register(CustomClientRegistrationConfiguration.class).autowire();
// @formatter:off
OidcClientRegistration clientRegistration = OidcClientRegistration.builder()
.clientName("client-name")
.redirectUri("https://client.example.com")
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.scope("scope1")
.scope("scope2")
.build();
// @formatter:on
willAnswer((invocation) -> {
HttpServletResponse response = invocation.getArgument(1, HttpServletResponse.class);
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
httpResponse.setStatusCode(HttpStatus.CREATED);
new OidcClientRegistrationHttpMessageConverter().write(clientRegistration, null, httpResponse);
return null;
}).given(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), any());
registerClient(clientRegistration);
verify(authenticationConverter).convert(any());
ArgumentCaptor<List<AuthenticationConverter>> authenticationConvertersCaptor = ArgumentCaptor
.forClass(List.class);
verify(authenticationConvertersConsumer).accept(authenticationConvertersCaptor.capture());
List<AuthenticationConverter> authenticationConverters = authenticationConvertersCaptor.getValue();
assertThat(authenticationConverters).hasSize(2)
.allMatch((converter) -> converter == authenticationConverter
|| converter instanceof OidcClientRegistrationAuthenticationConverter);
verify(authenticationProvider).authenticate(any());
ArgumentCaptor<List<AuthenticationProvider>> authenticationProvidersCaptor = ArgumentCaptor
.forClass(List.class);
verify(authenticationProvidersConsumer).accept(authenticationProvidersCaptor.capture());
List<AuthenticationProvider> authenticationProviders = authenticationProvidersCaptor.getValue();
assertThat(authenticationProviders).hasSize(3)
.allMatch((provider) -> provider == authenticationProvider
|| provider instanceof OidcClientRegistrationAuthenticationProvider
|| provider instanceof OidcClientConfigurationAuthenticationProvider);
verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), any());
verifyNoInteractions(authenticationFailureHandler);
}
@Test
public void requestWhenClientRegistrationEndpointCustomizedWithAuthenticationFailureHandlerThenUsed()
throws Exception {
this.spring.register(CustomClientRegistrationConfiguration.class).autowire();
given(authenticationProvider.authenticate(any())).willThrow(new OAuth2AuthenticationException("error"));
this.mvc.perform(get(ISSUER.concat(DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI))
.param(OAuth2ParameterNames.CLIENT_ID, "invalid")
.with(jwt()));
verify(authenticationFailureHandler).onAuthenticationFailure(any(), any(), any());
verifyNoInteractions(authenticationSuccessHandler);
}
// gh-1056
@Test
public void requestWhenClientRegistersWithSecretThenClientAuthenticationSuccess() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
// @formatter:off
OidcClientRegistration clientRegistration = OidcClientRegistration.builder()
.clientName("client-name")
.redirectUri("https://client.example.com")
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.scope("scope1")
.scope("scope2")
.build();
// @formatter:on
OidcClientRegistration clientRegistrationResponse = registerClient(clientRegistration);
this.mvc
.perform(post(ISSUER.concat(DEFAULT_TOKEN_ENDPOINT_URI))
.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.param(OAuth2ParameterNames.SCOPE, "scope1")
.with(httpBasic(clientRegistrationResponse.getClientId(),
clientRegistrationResponse.getClientSecret())))
.andExpect(status().isOk())
.andExpect(jsonPath("$.access_token").isNotEmpty())
.andExpect(jsonPath("$.scope").value("scope1"))
.andReturn();
}
// gh-1344
@Test
public void requestWhenClientRegistersWithClientSecretJwtThenClientAuthenticationSuccess() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
// @formatter:off
OidcClientRegistration clientRegistration = OidcClientRegistration.builder()
.clientName("client-name")
.redirectUri("https://client.example.com")
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue())
.scope("scope1")
.scope("scope2")
.build();
// @formatter:on
OidcClientRegistration clientRegistrationResponse = registerClient(clientRegistration);
JwsHeader jwsHeader = JwsHeader.with(MacAlgorithm.HS256).build();
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS);
JwtClaimsSet jwtClaimsSet = JwtClaimsSet.builder()
.issuer(clientRegistrationResponse.getClientId())
.subject(clientRegistrationResponse.getClientId())
.audience(Collections.singletonList(asUrl(ISSUER, this.authorizationServerSettings.getTokenEndpoint())))
.issuedAt(issuedAt)
.expiresAt(expiresAt)
.build();
JWKSet jwkSet = new JWKSet(
TestJwks.jwk(new SecretKeySpec(clientRegistrationResponse.getClientSecret().getBytes(), "HS256"))
.build());
JwtEncoder jwtClientAssertionEncoder = new NimbusJwtEncoder(
(jwkSelector, securityContext) -> jwkSelector.select(jwkSet));
Jwt jwtAssertion = jwtClientAssertionEncoder.encode(JwtEncoderParameters.from(jwsHeader, jwtClaimsSet));
this.mvc
.perform(post(ISSUER.concat(DEFAULT_TOKEN_ENDPOINT_URI))
.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.param(OAuth2ParameterNames.SCOPE, "scope1")
.param(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE,
"urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
.param(OAuth2ParameterNames.CLIENT_ASSERTION, jwtAssertion.getTokenValue())
.param(OAuth2ParameterNames.CLIENT_ID, clientRegistrationResponse.getClientId()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.access_token").isNotEmpty())
.andExpect(jsonPath("$.scope").value("scope1"));
}
@Test
public void requestWhenClientRegistersWithCustomMetadataThenSavedToRegisteredClient() throws Exception {
this.spring.register(CustomClientMetadataConfiguration.class).autowire();
// @formatter:off
OidcClientRegistration clientRegistration = OidcClientRegistration.builder()
.clientName("client-name")
.redirectUri("https://client.example.com")
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.scope("scope1")
.scope("scope2")
.claim("custom-metadata-name-1", "value-1")
.claim("custom-metadata-name-2", "value-2")
.claim("non-registered-custom-metadata", "value-3")
.build();
// @formatter:on
OidcClientRegistration clientRegistrationResponse = registerClient(clientRegistration);
RegisteredClient registeredClient = this.registeredClientRepository
.findByClientId(clientRegistrationResponse.getClientId());
assertThat(clientRegistrationResponse.<String>getClaim("custom-metadata-name-1")).isEqualTo("value-1");
assertThat(clientRegistrationResponse.<String>getClaim("custom-metadata-name-2")).isEqualTo("value-2");
assertThat(clientRegistrationResponse.<String>getClaim("non-registered-custom-metadata")).isNull();
assertThat(registeredClient.getClientSettings().<String>getSetting("custom-metadata-name-1"))
.isEqualTo("value-1");
assertThat(registeredClient.getClientSettings().<String>getSetting("custom-metadata-name-2"))
.isEqualTo("value-2");
assertThat(registeredClient.getClientSettings().<String>getSetting("non-registered-custom-metadata")).isNull();
}
// gh-2111
@Test
public void requestWhenClientRegistersWithSecretExpirationThenClientRegistrationResponse() throws Exception {
this.spring.register(ClientSecretExpirationConfiguration.class).autowire();
// @formatter:off
OidcClientRegistration clientRegistration = OidcClientRegistration.builder()
.clientName("client-name")
.redirectUri("https://client.example.com")
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.scope("scope1")
.scope("scope2")
.build();
// @formatter:on
OidcClientRegistration clientRegistrationResponse = registerClient(clientRegistration);
Instant expectedSecretExpiryDate = Instant.now().plus(Duration.ofHours(24));
TemporalUnitWithinOffset allowedDelta = new TemporalUnitWithinOffset(1, ChronoUnit.MINUTES);
// Returned response contains expiration date
assertThat(clientRegistrationResponse.getClientSecretExpiresAt()).isNotNull()
.isCloseTo(expectedSecretExpiryDate, allowedDelta);
RegisteredClient registeredClient = this.registeredClientRepository
.findByClientId(clientRegistrationResponse.getClientId());
// Persisted RegisteredClient contains expiration date
assertThat(registeredClient).isNotNull();
assertThat(registeredClient.getClientSecretExpiresAt()).isNotNull()
.isCloseTo(expectedSecretExpiryDate, allowedDelta);
}
private OidcClientRegistration registerClient(OidcClientRegistration clientRegistration) throws Exception {
// ***** (1) Obtain the "initial" access token used for registering the client
String clientRegistrationScope = "client.create";
// @formatter:off
RegisteredClient clientRegistrar = RegisteredClient.withId("client-registrar-1")
.clientId("client-registrar-1")
.clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.scope(clientRegistrationScope)
.clientSettings(
ClientSettings.builder()
.jwkSetUrl(this.clientJwkSetUrl)
.tokenEndpointAuthenticationSigningAlgorithm(SignatureAlgorithm.RS256)
.build()
)
.build();
// @formatter:on
this.registeredClientRepository.save(clientRegistrar);
// @formatter:off
JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.RS256)
.build();
JwtClaimsSet jwtClaimsSet = jwtClientAssertionClaims(clientRegistrar)
.build();
// @formatter:on
Jwt jwtAssertion = jwtClientAssertionEncoder.encode(JwtEncoderParameters.from(jwsHeader, jwtClaimsSet));
MvcResult mvcResult = this.mvc
.perform(post(ISSUER.concat(DEFAULT_TOKEN_ENDPOINT_URI))
.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.param(OAuth2ParameterNames.SCOPE, clientRegistrationScope)
.param(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE,
"urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
.param(OAuth2ParameterNames.CLIENT_ASSERTION, jwtAssertion.getTokenValue())
.param(OAuth2ParameterNames.CLIENT_ID, clientRegistrar.getClientId()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.access_token").isNotEmpty())
.andExpect(jsonPath("$.scope").value(clientRegistrationScope))
.andReturn();
OAuth2AccessToken accessToken = readAccessTokenResponse(mvcResult.getResponse()).getAccessToken();
// ***** (2) Register the client
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setBearerAuth(accessToken.getTokenValue());
// Register the client
mvcResult = this.mvc
.perform(post(ISSUER.concat(DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI)).headers(httpHeaders)
.contentType(MediaType.APPLICATION_JSON)
.content(getClientRegistrationRequestContent(clientRegistration)))
.andExpect(status().isCreated())
.andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store")))
.andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache")))
.andReturn();
return readClientRegistrationResponse(mvcResult.getResponse());
}
private JwtClaimsSet.Builder jwtClientAssertionClaims(RegisteredClient registeredClient) {
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS);
return JwtClaimsSet.builder()
.issuer(registeredClient.getClientId())
.subject(registeredClient.getClientId())
.audience(Collections.singletonList(asUrl(ISSUER, this.authorizationServerSettings.getTokenEndpoint())))
.issuedAt(issuedAt)
.expiresAt(expiresAt);
}
private static String asUrl(String uri, String path) {
return UriComponentsBuilder.fromUriString(uri).path(path).build().toUriString();
}
private static OAuth2AccessTokenResponse readAccessTokenResponse(MockHttpServletResponse response)
throws Exception {
MockClientHttpResponse httpResponse = new MockClientHttpResponse(response.getContentAsByteArray(),
HttpStatus.valueOf(response.getStatus()));
return accessTokenHttpResponseConverter.read(OAuth2AccessTokenResponse.class, httpResponse);
}
private static byte[] getClientRegistrationRequestContent(OidcClientRegistration clientRegistration)
throws Exception {
MockHttpOutputMessage httpRequest = new MockHttpOutputMessage();
clientRegistrationHttpMessageConverter.write(clientRegistration, null, httpRequest);
return httpRequest.getBodyAsBytes();
}
private static OidcClientRegistration readClientRegistrationResponse(MockHttpServletResponse response)
throws Exception {
MockClientHttpResponse httpResponse = new MockClientHttpResponse(response.getContentAsByteArray(),
HttpStatus.valueOf(response.getStatus()));
return clientRegistrationHttpMessageConverter.read(OidcClientRegistration.class, httpResponse);
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class CustomClientRegistrationConfiguration extends AuthorizationServerConfiguration {
// @formatter:off
@Bean
@Override
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
OAuth2AuthorizationServerConfigurer.authorizationServer();
http
.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
.with(authorizationServerConfigurer, (authorizationServer) ->
authorizationServer
.oidc((oidc) ->
oidc
.clientRegistrationEndpoint((clientRegistration) ->
clientRegistration
.clientRegistrationRequestConverter(authenticationConverter)
.clientRegistrationRequestConverters(authenticationConvertersConsumer)
.authenticationProvider(authenticationProvider)
.authenticationProviders(authenticationProvidersConsumer)
.clientRegistrationResponseHandler(authenticationSuccessHandler)
.errorResponseHandler(authenticationFailureHandler)
)
)
)
.authorizeHttpRequests((authorize) ->
authorize.anyRequest().authenticated()
);
return http.build();
}
// @formatter:on
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class CustomClientMetadataConfiguration extends AuthorizationServerConfiguration {
// @formatter:off
@Bean
@Override
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
OAuth2AuthorizationServerConfigurer.authorizationServer();
http
.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
.with(authorizationServerConfigurer, (authorizationServer) ->
authorizationServer
.oidc((oidc) ->
oidc
.clientRegistrationEndpoint((clientRegistration) ->
clientRegistration
.authenticationProviders(configureClientRegistrationConverters())
)
)
)
.authorizeHttpRequests((authorize) ->
authorize.anyRequest().authenticated()
);
return http.build();
}
// @formatter:on
private Consumer<List<AuthenticationProvider>> configureClientRegistrationConverters() {
// @formatter:off
return (authenticationProviders) ->
authenticationProviders.forEach((authenticationProvider) -> {
List<String> supportedCustomClientMetadata = List.of("custom-metadata-name-1", "custom-metadata-name-2");
if (authenticationProvider instanceof OidcClientRegistrationAuthenticationProvider provider) {
provider.setRegisteredClientConverter(new CustomRegisteredClientConverter(supportedCustomClientMetadata));
provider.setClientRegistrationConverter(new CustomClientRegistrationConverter(supportedCustomClientMetadata));
}
});
// @formatter:on
}
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class ClientSecretExpirationConfiguration extends AuthorizationServerConfiguration {
// @formatter:off
@Bean
@Override
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
OAuth2AuthorizationServerConfigurer.authorizationServer();
http
.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
.with(authorizationServerConfigurer, (authorizationServer) ->
authorizationServer
.oidc((oidc) ->
oidc
.clientRegistrationEndpoint((clientRegistration) ->
clientRegistration
.authenticationProviders(configureClientRegistrationConverters())
)
)
)
.authorizeHttpRequests((authorize) ->
authorize.anyRequest().authenticated()
);
return http.build();
}
// @formatter:on
private Consumer<List<AuthenticationProvider>> configureClientRegistrationConverters() {
// @formatter:off
return (authenticationProviders) ->
authenticationProviders.forEach((authenticationProvider) -> {
if (authenticationProvider instanceof OidcClientRegistrationAuthenticationProvider provider) {
provider.setRegisteredClientConverter(new ClientSecretExpirationRegisteredClientConverter());
}
});
// @formatter:on
}
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class AuthorizationServerConfiguration {
// @formatter:off
@Bean
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
OAuth2AuthorizationServerConfigurer.authorizationServer();
http
.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
.with(authorizationServerConfigurer, (authorizationServer) ->
authorizationServer
.oidc((oidc) ->
oidc
.clientRegistrationEndpoint(Customizer.withDefaults())
)
)
.authorizeHttpRequests((authorize) ->
authorize.anyRequest().authenticated()
);
return http.build();
}
// @formatter:on
@Bean
RegisteredClientRepository registeredClientRepository(JdbcOperations jdbcOperations) {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
RegisteredClientParametersMapper registeredClientParametersMapper = new RegisteredClientParametersMapper();
JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(
jdbcOperations);
registeredClientRepository.setRegisteredClientParametersMapper(registeredClientParametersMapper);
registeredClientRepository.save(registeredClient);
return registeredClientRepository;
}
@Bean
OAuth2AuthorizationService authorizationService(JdbcOperations jdbcOperations,
RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationService(jdbcOperations, registeredClientRepository);
}
@Bean
JdbcOperations jdbcOperations() {
return new JdbcTemplate(db);
}
@Bean
JWKSource<SecurityContext> jwkSource() {
return jwkSource;
}
@Bean
JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
@Bean
AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().multipleIssuersAllowed(true).build();
}
@Bean
PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
private static final class CustomRegisteredClientConverter
implements Converter<OidcClientRegistration, RegisteredClient> {
private final OidcClientRegistrationRegisteredClientConverter delegate = new OidcClientRegistrationRegisteredClientConverter();
private final List<String> supportedCustomClientMetadata;
private CustomRegisteredClientConverter(List<String> supportedCustomClientMetadata) {
this.supportedCustomClientMetadata = supportedCustomClientMetadata;
}
@Override
public RegisteredClient convert(OidcClientRegistration clientRegistration) {
RegisteredClient registeredClient = this.delegate.convert(clientRegistration);
ClientSettings.Builder clientSettingsBuilder = ClientSettings
.withSettings(registeredClient.getClientSettings().getSettings());
if (!CollectionUtils.isEmpty(this.supportedCustomClientMetadata)) {
clientRegistration.getClaims().forEach((claim, value) -> {
if (this.supportedCustomClientMetadata.contains(claim)) {
clientSettingsBuilder.setting(claim, value);
}
});
}
return RegisteredClient.from(registeredClient).clientSettings(clientSettingsBuilder.build()).build();
}
}
private static final class CustomClientRegistrationConverter
implements Converter<RegisteredClient, OidcClientRegistration> {
private final RegisteredClientOidcClientRegistrationConverter delegate = new RegisteredClientOidcClientRegistrationConverter();
private final List<String> supportedCustomClientMetadata;
private CustomClientRegistrationConverter(List<String> supportedCustomClientMetadata) {
this.supportedCustomClientMetadata = supportedCustomClientMetadata;
}
@Override
public OidcClientRegistration convert(RegisteredClient registeredClient) {
OidcClientRegistration clientRegistration = this.delegate.convert(registeredClient);
Map<String, Object> clientMetadata = new HashMap<>(clientRegistration.getClaims());
if (!CollectionUtils.isEmpty(this.supportedCustomClientMetadata)) {
Map<String, Object> clientSettings = registeredClient.getClientSettings().getSettings();
this.supportedCustomClientMetadata.forEach((customClaim) -> {
if (clientSettings.containsKey(customClaim)) {
clientMetadata.put(customClaim, clientSettings.get(customClaim));
}
});
}
return OidcClientRegistration.withClaims(clientMetadata).build();
}
}
/**
* This customization adds client secret expiration time by setting
* {@code RegisteredClient.clientSecretExpiresAt} during
* {@code OidcClientRegistration} -> {@code RegisteredClient} conversion
*/
private static final class ClientSecretExpirationRegisteredClientConverter
implements Converter<OidcClientRegistration, RegisteredClient> {
private static final OidcClientRegistrationRegisteredClientConverter delegate = new OidcClientRegistrationRegisteredClientConverter();
@Override
public RegisteredClient convert(OidcClientRegistration clientRegistration) {
RegisteredClient registeredClient = delegate.convert(clientRegistration);
RegisteredClient.Builder registeredClientBuilder = RegisteredClient.from(registeredClient);
Instant clientSecretExpiresAt = Instant.now().plus(Duration.ofHours(24));
registeredClientBuilder.clientSecretExpiresAt(clientSecretExpiresAt);
return registeredClientBuilder.build();
}
}
}
@@ -0,0 +1,413 @@
/*
* Copyright 2004-present 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.config.annotation.web.configurers.oauth2.server.authorization;
import java.util.function.Consumer;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.jose.TestJwks;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationServerMetadataClaimNames;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import org.springframework.security.oauth2.server.authorization.oidc.OidcProviderConfiguration;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultMatcher;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.hamcrest.CoreMatchers.hasItems;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Integration tests for the OpenID Connect 1.0 Provider Configuration endpoint.
*
* @author Sahariar Alam Khandoker
* @author Joe Grandja
* @author Daniel Garnier-Moiroux
*/
@ExtendWith(SpringTestContextExtension.class)
public class OidcProviderConfigurationTests {
private static final String DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI = "/.well-known/openid-configuration";
private static final String ISSUER = "https://example.com";
public final SpringTestContext spring = new SpringTestContext(this);
@Autowired
private AuthorizationServerSettings authorizationServerSettings;
@Autowired
private MockMvc mvc;
@Test
public void requestWhenConfigurationRequestAndIssuerSetThenReturnDefaultConfigurationResponse() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
this.mvc.perform(get(ISSUER.concat(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI)))
.andExpect(status().is2xxSuccessful())
.andExpectAll(defaultConfigurationMatchers(ISSUER));
}
@Test
public void requestWhenConfigurationRequestIncludesIssuerPathThenConfigurationResponseHasIssuerPath()
throws Exception {
this.spring.register(AuthorizationServerConfigurationWithMultipleIssuersAllowed.class).autowire();
String issuer = "https://example.com:8443/issuer1";
this.mvc.perform(get(issuer.concat(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI)))
.andExpect(status().is2xxSuccessful())
.andExpectAll(defaultConfigurationMatchers(issuer));
issuer = "https://example.com:8443/path1/issuer2";
this.mvc.perform(get(issuer.concat(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI)))
.andExpect(status().is2xxSuccessful())
.andExpectAll(defaultConfigurationMatchers(issuer));
issuer = "https://example.com:8443/path1/path2/issuer3";
this.mvc.perform(get(issuer.concat(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI)))
.andExpect(status().is2xxSuccessful())
.andExpectAll(defaultConfigurationMatchers(issuer));
}
// gh-632
@Test
public void requestWhenConfigurationRequestAndUserAuthenticatedThenReturnConfigurationResponse() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
this.mvc.perform(get(ISSUER.concat(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI)).with(user("user")))
.andExpect(status().is2xxSuccessful())
.andExpectAll(defaultConfigurationMatchers(ISSUER));
}
// gh-616
@Test
public void requestWhenConfigurationRequestAndConfigurationCustomizerSetThenReturnCustomConfigurationResponse()
throws Exception {
this.spring.register(AuthorizationServerConfigurationWithProviderConfigurationCustomizer.class).autowire();
this.mvc.perform(get(ISSUER.concat(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI)))
.andExpect(status().is2xxSuccessful())
.andExpect(jsonPath(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED,
hasItems(OidcScopes.OPENID, OidcScopes.PROFILE, OidcScopes.EMAIL)));
}
@Test
public void requestWhenConfigurationRequestAndClientRegistrationEnabledThenConfigurationResponseIncludesRegistrationEndpoint()
throws Exception {
this.spring.register(AuthorizationServerConfigurationWithClientRegistrationEnabled.class).autowire();
this.mvc.perform(get(ISSUER.concat(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI)))
.andExpect(status().is2xxSuccessful())
.andExpectAll(defaultConfigurationMatchers(ISSUER))
.andExpect(jsonPath("$.registration_endpoint")
.value(ISSUER.concat(this.authorizationServerSettings.getOidcClientRegistrationEndpoint())));
}
private ResultMatcher[] defaultConfigurationMatchers(String issuer) {
// @formatter:off
return new ResultMatcher[] {
jsonPath("issuer").value(issuer),
jsonPath("authorization_endpoint").value(issuer.concat(this.authorizationServerSettings.getAuthorizationEndpoint())),
jsonPath("token_endpoint").value(issuer.concat(this.authorizationServerSettings.getTokenEndpoint())),
jsonPath("$.token_endpoint_auth_methods_supported[0]").value(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()),
jsonPath("$.token_endpoint_auth_methods_supported[1]").value(ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue()),
jsonPath("$.token_endpoint_auth_methods_supported[2]").value(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue()),
jsonPath("$.token_endpoint_auth_methods_supported[3]").value(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue()),
jsonPath("jwks_uri").value(issuer.concat(this.authorizationServerSettings.getJwkSetEndpoint())),
jsonPath("userinfo_endpoint").value(issuer.concat(this.authorizationServerSettings.getOidcUserInfoEndpoint())),
jsonPath("end_session_endpoint").value(issuer.concat(this.authorizationServerSettings.getOidcLogoutEndpoint())),
jsonPath("response_types_supported").value(OAuth2AuthorizationResponseType.CODE.getValue()),
jsonPath("$.grant_types_supported[0]").value(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()),
jsonPath("$.grant_types_supported[1]").value(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()),
jsonPath("$.grant_types_supported[2]").value(AuthorizationGrantType.REFRESH_TOKEN.getValue()),
jsonPath("revocation_endpoint").value(issuer.concat(this.authorizationServerSettings.getTokenRevocationEndpoint())),
jsonPath("$.revocation_endpoint_auth_methods_supported[0]").value(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()),
jsonPath("$.revocation_endpoint_auth_methods_supported[1]").value(ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue()),
jsonPath("$.revocation_endpoint_auth_methods_supported[2]").value(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue()),
jsonPath("$.revocation_endpoint_auth_methods_supported[3]").value(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue()),
jsonPath("introspection_endpoint").value(issuer.concat(this.authorizationServerSettings.getTokenIntrospectionEndpoint())),
jsonPath("$.introspection_endpoint_auth_methods_supported[0]").value(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()),
jsonPath("$.introspection_endpoint_auth_methods_supported[1]").value(ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue()),
jsonPath("$.introspection_endpoint_auth_methods_supported[2]").value(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue()),
jsonPath("$.introspection_endpoint_auth_methods_supported[3]").value(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue()),
jsonPath("$.code_challenge_methods_supported[0]").value("S256"),
jsonPath("subject_types_supported").value("public"),
jsonPath("id_token_signing_alg_values_supported").value(SignatureAlgorithm.RS256.getName()),
jsonPath("scopes_supported").value(OidcScopes.OPENID)
};
// @formatter:on
}
@Test
public void loadContextWhenIssuerNotValidUrlThenThrowException() {
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(
() -> this.spring.register(AuthorizationServerConfigurationWithInvalidIssuerUrl.class).autowire());
}
@Test
public void loadContextWhenIssuerNotValidUriThenThrowException() {
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(
() -> this.spring.register(AuthorizationServerConfigurationWithInvalidIssuerUri.class).autowire());
}
@Test
public void loadContextWhenIssuerWithQueryThenThrowException() {
assertThatExceptionOfType(BeanCreationException.class)
.isThrownBy(() -> this.spring.register(AuthorizationServerConfigurationWithIssuerQuery.class).autowire());
}
@Test
public void loadContextWhenIssuerWithFragmentThenThrowException() {
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(
() -> this.spring.register(AuthorizationServerConfigurationWithIssuerFragment.class).autowire());
}
@Test
public void loadContextWhenIssuerWithQueryAndFragmentThenThrowException() {
assertThatExceptionOfType(BeanCreationException.class)
.isThrownBy(() -> this.spring.register(AuthorizationServerConfigurationWithIssuerQueryAndFragment.class)
.autowire());
}
@Test
public void loadContextWhenIssuerWithEmptyQueryThenThrowException() {
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(
() -> this.spring.register(AuthorizationServerConfigurationWithIssuerEmptyQuery.class).autowire());
}
@Test
public void loadContextWhenIssuerWithEmptyFragmentThenThrowException() {
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(
() -> this.spring.register(AuthorizationServerConfigurationWithIssuerEmptyFragment.class).autowire());
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class AuthorizationServerConfiguration {
@Bean
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = OAuth2AuthorizationServerConfigurer
.authorizationServer();
// @formatter:off
http
.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
.with(authorizationServerConfigurer, (authorizationServer) ->
authorizationServer
.oidc(Customizer.withDefaults()) // Enable OpenID Connect 1.0
);
// @formatter:on
return http.build();
}
@Bean
RegisteredClientRepository registeredClientRepository() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
return new InMemoryRegisteredClientRepository(registeredClient);
}
@Bean
JWKSource<SecurityContext> jwkSource() {
return new ImmutableJWKSet<>(new JWKSet(TestJwks.DEFAULT_RSA_JWK));
}
@Bean
JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
@Bean
AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().issuer(ISSUER).build();
}
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class AuthorizationServerConfigurationWithMultipleIssuersAllowed extends AuthorizationServerConfiguration {
@Bean
AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().multipleIssuersAllowed(true).build();
}
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class AuthorizationServerConfigurationWithProviderConfigurationCustomizer
extends AuthorizationServerConfiguration {
// @formatter:off
@Bean
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
OAuth2AuthorizationServerConfigurer.authorizationServer();
http
.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
.with(authorizationServerConfigurer, (authorizationServer) ->
authorizationServer
.oidc((oidc) ->
oidc.providerConfigurationEndpoint((providerConfigurationEndpoint) ->
providerConfigurationEndpoint
.providerConfigurationCustomizer(providerConfigurationCustomizer())))
)
.authorizeHttpRequests((authorize) ->
authorize.anyRequest().authenticated()
);
return http.build();
}
// @formatter:on
private Consumer<OidcProviderConfiguration.Builder> providerConfigurationCustomizer() {
return (providerConfiguration) -> providerConfiguration.scope(OidcScopes.PROFILE).scope(OidcScopes.EMAIL);
}
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class AuthorizationServerConfigurationWithClientRegistrationEnabled
extends AuthorizationServerConfiguration {
// @formatter:off
@Bean
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
OAuth2AuthorizationServerConfigurer.authorizationServer();
http
.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
.with(authorizationServerConfigurer, (authorizationServer) ->
authorizationServer
.oidc((oidc) ->
oidc.clientRegistrationEndpoint(Customizer.withDefaults())
)
);
return http.build();
}
// @formatter:on
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class AuthorizationServerConfigurationWithInvalidIssuerUrl extends AuthorizationServerConfiguration {
@Bean
AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().issuer("urn:example").build();
}
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class AuthorizationServerConfigurationWithInvalidIssuerUri extends AuthorizationServerConfiguration {
@Bean
AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().issuer("https://not a valid uri").build();
}
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class AuthorizationServerConfigurationWithIssuerQuery extends AuthorizationServerConfiguration {
@Bean
AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().issuer(ISSUER + "?param=value").build();
}
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class AuthorizationServerConfigurationWithIssuerFragment extends AuthorizationServerConfiguration {
@Bean
AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().issuer(ISSUER + "#fragment").build();
}
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class AuthorizationServerConfigurationWithIssuerQueryAndFragment extends AuthorizationServerConfiguration {
@Bean
AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().issuer(ISSUER + "?param=value#fragment").build();
}
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class AuthorizationServerConfigurationWithIssuerEmptyQuery extends AuthorizationServerConfiguration {
@Bean
AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().issuer(ISSUER + "?").build();
}
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class AuthorizationServerConfigurationWithIssuerEmptyFragment extends AuthorizationServerConfiguration {
@Bean
AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().issuer(ISSUER + "#").build();
}
}
}
@@ -0,0 +1,788 @@
/*
* Copyright 2004-present 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.config.annotation.web.configurers.oauth2.server.authorization;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.util.Base64;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.lang.Nullable;
import org.springframework.mock.http.client.MockClientHttpResponse;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
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.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.security.oauth2.jose.TestJwks;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository.RegisteredClientParametersMapper;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import org.springframework.security.oauth2.server.authorization.jackson2.TestingAuthenticationTokenMixin;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator;
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.JwtGenerator;
import org.springframework.security.oauth2.server.authorization.token.OAuth2RefreshTokenGenerator;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.CoreMatchers.containsString;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Integration tests for OpenID Connect 1.0.
*
* @author Daniel Garnier-Moiroux
* @author Joe Grandja
*/
@ExtendWith(SpringTestContextExtension.class)
public class OidcTests {
private static final String DEFAULT_AUTHORIZATION_ENDPOINT_URI = "/oauth2/authorize";
private static final String DEFAULT_TOKEN_ENDPOINT_URI = "/oauth2/token";
private static final String DEFAULT_OIDC_LOGOUT_ENDPOINT_URI = "/connect/logout";
private static final String AUTHORITIES_CLAIM = "authorities";
private static final OAuth2TokenType AUTHORIZATION_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.CODE);
private static EmbeddedDatabase db;
private static JWKSource<SecurityContext> jwkSource;
private static HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter = new OAuth2AccessTokenResponseHttpMessageConverter();
private static SessionRegistry sessionRegistry;
public final SpringTestContext spring = new SpringTestContext(this);
@Autowired
private MockMvc mvc;
@Autowired
private JdbcOperations jdbcOperations;
@Autowired
private RegisteredClientRepository registeredClientRepository;
@Autowired
private OAuth2AuthorizationService authorizationService;
@Autowired
private JwtDecoder jwtDecoder;
@Autowired(required = false)
private OAuth2TokenGenerator<?> tokenGenerator;
@BeforeAll
public static void init() {
JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
db = new EmbeddedDatabaseBuilder().generateUniqueName(true)
.setType(EmbeddedDatabaseType.HSQL)
.setScriptEncoding("UTF-8")
.addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-schema.sql")
.addScript(
"org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql")
.build();
sessionRegistry = spy(new SessionRegistryImpl());
}
@AfterEach
public void tearDown() {
if (this.jdbcOperations != null) {
this.jdbcOperations.update("truncate table oauth2_authorization");
this.jdbcOperations.update("truncate table oauth2_registered_client");
}
}
@AfterAll
public static void destroy() {
db.shutdown();
}
@Test
public void requestWhenAuthenticationRequestThenTokenResponseIncludesIdToken() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scope(OidcScopes.OPENID).build();
this.registeredClientRepository.save(registeredClient);
MultiValueMap<String, String> authorizationRequestParameters = getAuthorizationRequestParameters(
registeredClient);
MvcResult mvcResult = this.mvc
.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI).queryParams(authorizationRequestParameters)
.with(user("user").roles("A", "B")))
.andExpect(status().is3xxRedirection())
.andReturn();
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
String expectedRedirectUri = authorizationRequestParameters.getFirst(OAuth2ParameterNames.REDIRECT_URI);
assertThat(redirectedUrl).matches(expectedRedirectUri + "\\?code=.{15,}&state=state");
String authorizationCode = extractParameterFromRedirectUri(redirectedUrl, "code");
OAuth2Authorization authorization = this.authorizationService.findByToken(authorizationCode,
AUTHORIZATION_CODE_TOKEN_TYPE);
mvcResult = this.mvc
.perform(post(DEFAULT_TOKEN_ENDPOINT_URI).params(getTokenRequestParameters(registeredClient, authorization))
.header(HttpHeaders.AUTHORIZATION,
"Basic " + encodeBasicAuth(registeredClient.getClientId(), registeredClient.getClientSecret())))
.andExpect(status().isOk())
.andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store")))
.andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache")))
.andExpect(jsonPath("$.access_token").isNotEmpty())
.andExpect(jsonPath("$.token_type").isNotEmpty())
.andExpect(jsonPath("$.expires_in").isNotEmpty())
.andExpect(jsonPath("$.refresh_token").isNotEmpty())
.andExpect(jsonPath("$.scope").isNotEmpty())
.andExpect(jsonPath("$.id_token").isNotEmpty())
.andReturn();
MockHttpServletResponse servletResponse = mvcResult.getResponse();
MockClientHttpResponse httpResponse = new MockClientHttpResponse(servletResponse.getContentAsByteArray(),
HttpStatus.valueOf(servletResponse.getStatus()));
OAuth2AccessTokenResponse accessTokenResponse = accessTokenHttpResponseConverter
.read(OAuth2AccessTokenResponse.class, httpResponse);
Jwt idToken = this.jwtDecoder
.decode((String) accessTokenResponse.getAdditionalParameters().get(OidcParameterNames.ID_TOKEN));
// Assert user authorities was propagated as claim in ID Token
List<String> authoritiesClaim = idToken.getClaim(AUTHORITIES_CLAIM);
Authentication principal = authorization.getAttribute(Principal.class.getName());
Set<String> userAuthorities = new HashSet<>();
for (GrantedAuthority authority : principal.getAuthorities()) {
userAuthorities.add(authority.getAuthority());
}
assertThat(authoritiesClaim).containsExactlyInAnyOrderElementsOf(userAuthorities);
// Assert sid claim was added in ID Token
assertThat(idToken.<String>getClaim("sid")).isNotNull();
}
// gh-1224
@Test
public void requestWhenRefreshTokenRequestThenIdTokenContainsSidClaim() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scope(OidcScopes.OPENID).build();
this.registeredClientRepository.save(registeredClient);
MultiValueMap<String, String> authorizationRequestParameters = getAuthorizationRequestParameters(
registeredClient);
MvcResult mvcResult = this.mvc
.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI).queryParams(authorizationRequestParameters)
.with(user("user").roles("A", "B")))
.andExpect(status().is3xxRedirection())
.andReturn();
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
String expectedRedirectUri = authorizationRequestParameters.getFirst(OAuth2ParameterNames.REDIRECT_URI);
assertThat(redirectedUrl).matches(expectedRedirectUri + "\\?code=.{15,}&state=state");
String authorizationCode = extractParameterFromRedirectUri(redirectedUrl, "code");
OAuth2Authorization authorization = this.authorizationService.findByToken(authorizationCode,
AUTHORIZATION_CODE_TOKEN_TYPE);
mvcResult = this.mvc
.perform(post(DEFAULT_TOKEN_ENDPOINT_URI).params(getTokenRequestParameters(registeredClient, authorization))
.header(HttpHeaders.AUTHORIZATION,
"Basic " + encodeBasicAuth(registeredClient.getClientId(), registeredClient.getClientSecret())))
.andExpect(status().isOk())
.andReturn();
MockHttpServletResponse servletResponse = mvcResult.getResponse();
MockClientHttpResponse httpResponse = new MockClientHttpResponse(servletResponse.getContentAsByteArray(),
HttpStatus.valueOf(servletResponse.getStatus()));
OAuth2AccessTokenResponse accessTokenResponse = accessTokenHttpResponseConverter
.read(OAuth2AccessTokenResponse.class, httpResponse);
Jwt idToken = this.jwtDecoder
.decode((String) accessTokenResponse.getAdditionalParameters().get(OidcParameterNames.ID_TOKEN));
String sidClaim = idToken.getClaim("sid");
assertThat(sidClaim).isNotNull();
// Refresh access token
mvcResult = this.mvc
.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.REFRESH_TOKEN.getValue())
.param(OAuth2ParameterNames.REFRESH_TOKEN, accessTokenResponse.getRefreshToken().getTokenValue())
.header(HttpHeaders.AUTHORIZATION,
"Basic " + encodeBasicAuth(registeredClient.getClientId(), registeredClient.getClientSecret())))
.andExpect(status().isOk())
.andReturn();
servletResponse = mvcResult.getResponse();
httpResponse = new MockClientHttpResponse(servletResponse.getContentAsByteArray(),
HttpStatus.valueOf(servletResponse.getStatus()));
accessTokenResponse = accessTokenHttpResponseConverter.read(OAuth2AccessTokenResponse.class, httpResponse);
idToken = this.jwtDecoder
.decode((String) accessTokenResponse.getAdditionalParameters().get(OidcParameterNames.ID_TOKEN));
assertThat(idToken.<String>getClaim("sid")).isEqualTo(sidClaim);
}
@Test
public void requestWhenLogoutRequestThenLogout() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scope(OidcScopes.OPENID).build();
this.registeredClientRepository.save(registeredClient);
String issuer = "https://example.com:8443/issuer1";
// Login
MultiValueMap<String, String> authorizationRequestParameters = getAuthorizationRequestParameters(
registeredClient);
MvcResult mvcResult = this.mvc
.perform(get(issuer.concat(DEFAULT_AUTHORIZATION_ENDPOINT_URI)).queryParams(authorizationRequestParameters)
.with(user("user")))
.andExpect(status().is3xxRedirection())
.andReturn();
MockHttpSession session = (MockHttpSession) mvcResult.getRequest().getSession();
assertThat(session.isNew()).isTrue();
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
String authorizationCode = extractParameterFromRedirectUri(redirectedUrl, "code");
OAuth2Authorization authorization = this.authorizationService.findByToken(authorizationCode,
AUTHORIZATION_CODE_TOKEN_TYPE);
// Get ID Token
mvcResult = this.mvc
.perform(post(issuer.concat(DEFAULT_TOKEN_ENDPOINT_URI))
.params(getTokenRequestParameters(registeredClient, authorization))
.header(HttpHeaders.AUTHORIZATION,
"Basic " + encodeBasicAuth(registeredClient.getClientId(), registeredClient.getClientSecret())))
.andExpect(status().isOk())
.andReturn();
MockHttpServletResponse servletResponse = mvcResult.getResponse();
MockClientHttpResponse httpResponse = new MockClientHttpResponse(servletResponse.getContentAsByteArray(),
HttpStatus.valueOf(servletResponse.getStatus()));
OAuth2AccessTokenResponse accessTokenResponse = accessTokenHttpResponseConverter
.read(OAuth2AccessTokenResponse.class, httpResponse);
String idToken = (String) accessTokenResponse.getAdditionalParameters().get(OidcParameterNames.ID_TOKEN);
// Logout
mvcResult = this.mvc
.perform(post(issuer.concat(DEFAULT_OIDC_LOGOUT_ENDPOINT_URI)).param("id_token_hint", idToken)
.session(session))
.andExpect(status().is3xxRedirection())
.andReturn();
redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
assertThat(redirectedUrl).matches("/");
assertThat(session.isInvalid()).isTrue();
}
@Test
public void requestWhenLogoutRequestWithOtherUsersIdTokenThenNotLogout() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
// Login user1
RegisteredClient registeredClient1 = TestRegisteredClients.registeredClient().scope(OidcScopes.OPENID).build();
this.registeredClientRepository.save(registeredClient1);
MultiValueMap<String, String> authorizationRequestParameters = getAuthorizationRequestParameters(
registeredClient1);
MvcResult mvcResult = this.mvc
.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI).queryParams(authorizationRequestParameters)
.with(user("user1")))
.andExpect(status().is3xxRedirection())
.andReturn();
MockHttpSession user1Session = (MockHttpSession) mvcResult.getRequest().getSession();
assertThat(user1Session.isNew()).isTrue();
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
String authorizationCode = extractParameterFromRedirectUri(redirectedUrl, "code");
OAuth2Authorization user1Authorization = this.authorizationService.findByToken(authorizationCode,
AUTHORIZATION_CODE_TOKEN_TYPE);
mvcResult = this.mvc
.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
.params(getTokenRequestParameters(registeredClient1, user1Authorization))
.header(HttpHeaders.AUTHORIZATION,
"Basic " + encodeBasicAuth(registeredClient1.getClientId(),
registeredClient1.getClientSecret())))
.andExpect(status().isOk())
.andReturn();
MockHttpServletResponse servletResponse = mvcResult.getResponse();
MockClientHttpResponse httpResponse = new MockClientHttpResponse(servletResponse.getContentAsByteArray(),
HttpStatus.valueOf(servletResponse.getStatus()));
OAuth2AccessTokenResponse accessTokenResponse = accessTokenHttpResponseConverter
.read(OAuth2AccessTokenResponse.class, httpResponse);
String user1IdToken = (String) accessTokenResponse.getAdditionalParameters().get(OidcParameterNames.ID_TOKEN);
// Login user2
RegisteredClient registeredClient2 = TestRegisteredClients.registeredClient2().scope(OidcScopes.OPENID).build();
this.registeredClientRepository.save(registeredClient2);
authorizationRequestParameters = getAuthorizationRequestParameters(registeredClient2);
mvcResult = this.mvc
.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI).queryParams(authorizationRequestParameters)
.with(user("user2")))
.andExpect(status().is3xxRedirection())
.andReturn();
MockHttpSession user2Session = (MockHttpSession) mvcResult.getRequest().getSession();
assertThat(user2Session.isNew()).isTrue();
redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
authorizationCode = extractParameterFromRedirectUri(redirectedUrl, "code");
OAuth2Authorization user2Authorization = this.authorizationService.findByToken(authorizationCode,
AUTHORIZATION_CODE_TOKEN_TYPE);
mvcResult = this.mvc
.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
.params(getTokenRequestParameters(registeredClient2, user2Authorization))
.header(HttpHeaders.AUTHORIZATION,
"Basic " + encodeBasicAuth(registeredClient2.getClientId(),
registeredClient2.getClientSecret())))
.andExpect(status().isOk())
.andReturn();
servletResponse = mvcResult.getResponse();
httpResponse = new MockClientHttpResponse(servletResponse.getContentAsByteArray(),
HttpStatus.valueOf(servletResponse.getStatus()));
accessTokenResponse = accessTokenHttpResponseConverter.read(OAuth2AccessTokenResponse.class, httpResponse);
String user2IdToken = (String) accessTokenResponse.getAdditionalParameters().get(OidcParameterNames.ID_TOKEN);
// Attempt to log out user1 using user2's ID Token
mvcResult = this.mvc
.perform(post(DEFAULT_OIDC_LOGOUT_ENDPOINT_URI).param("id_token_hint", user2IdToken).session(user1Session))
.andExpect(status().isBadRequest())
.andExpect(status().reason("[invalid_token] OpenID Connect 1.0 Logout Request Parameter: sub"))
.andReturn();
assertThat(user1Session.isInvalid()).isFalse();
}
@Test
public void requestWhenCustomTokenGeneratorThenUsed() throws Exception {
this.spring.register(AuthorizationServerConfigurationWithTokenGenerator.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scope(OidcScopes.OPENID).build();
this.registeredClientRepository.save(registeredClient);
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build();
this.authorizationService.save(authorization);
this.mvc
.perform(post(DEFAULT_TOKEN_ENDPOINT_URI).params(getTokenRequestParameters(registeredClient, authorization))
.header(HttpHeaders.AUTHORIZATION,
"Basic " + encodeBasicAuth(registeredClient.getClientId(), registeredClient.getClientSecret())))
.andExpect(status().isOk());
verify(this.tokenGenerator, times(3)).generate(any());
}
// gh-1422
@Test
public void requestWhenAuthenticationRequestWithOfflineAccessScopeThenTokenResponseIncludesRefreshToken()
throws Exception {
this.spring.register(AuthorizationServerConfigurationWithCustomRefreshTokenGenerator.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.scope(OidcScopes.OPENID)
.scope("offline_access")
.build();
this.registeredClientRepository.save(registeredClient);
MultiValueMap<String, String> authorizationRequestParameters = getAuthorizationRequestParameters(
registeredClient);
MvcResult mvcResult = this.mvc
.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI).queryParams(authorizationRequestParameters)
.with(user("user")))
.andExpect(status().is3xxRedirection())
.andReturn();
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
String expectedRedirectUri = authorizationRequestParameters.getFirst(OAuth2ParameterNames.REDIRECT_URI);
assertThat(redirectedUrl).matches(expectedRedirectUri + "\\?code=.{15,}&state=state");
String authorizationCode = extractParameterFromRedirectUri(redirectedUrl, "code");
OAuth2Authorization authorization = this.authorizationService.findByToken(authorizationCode,
AUTHORIZATION_CODE_TOKEN_TYPE);
this.mvc
.perform(post(DEFAULT_TOKEN_ENDPOINT_URI).params(getTokenRequestParameters(registeredClient, authorization))
.header(HttpHeaders.AUTHORIZATION,
"Basic " + encodeBasicAuth(registeredClient.getClientId(), registeredClient.getClientSecret())))
.andExpect(status().isOk())
.andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store")))
.andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache")))
.andExpect(jsonPath("$.access_token").isNotEmpty())
.andExpect(jsonPath("$.token_type").isNotEmpty())
.andExpect(jsonPath("$.expires_in").isNotEmpty())
.andExpect(jsonPath("$.refresh_token").isNotEmpty())
.andExpect(jsonPath("$.scope").isNotEmpty())
.andExpect(jsonPath("$.id_token").isNotEmpty())
.andReturn();
}
// gh-1422
@Test
public void requestWhenAuthenticationRequestWithoutOfflineAccessScopeThenTokenResponseDoesNotIncludeRefreshToken()
throws Exception {
this.spring.register(AuthorizationServerConfigurationWithCustomRefreshTokenGenerator.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scope(OidcScopes.OPENID).build();
this.registeredClientRepository.save(registeredClient);
MultiValueMap<String, String> authorizationRequestParameters = getAuthorizationRequestParameters(
registeredClient);
MvcResult mvcResult = this.mvc
.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI).queryParams(authorizationRequestParameters)
.with(user("user")))
.andExpect(status().is3xxRedirection())
.andReturn();
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
String expectedRedirectUri = authorizationRequestParameters.getFirst(OAuth2ParameterNames.REDIRECT_URI);
assertThat(redirectedUrl).matches(expectedRedirectUri + "\\?code=.{15,}&state=state");
String authorizationCode = extractParameterFromRedirectUri(redirectedUrl, "code");
OAuth2Authorization authorization = this.authorizationService.findByToken(authorizationCode,
AUTHORIZATION_CODE_TOKEN_TYPE);
this.mvc
.perform(post(DEFAULT_TOKEN_ENDPOINT_URI).params(getTokenRequestParameters(registeredClient, authorization))
.header(HttpHeaders.AUTHORIZATION,
"Basic " + encodeBasicAuth(registeredClient.getClientId(), registeredClient.getClientSecret())))
.andExpect(status().isOk())
.andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store")))
.andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache")))
.andExpect(jsonPath("$.access_token").isNotEmpty())
.andExpect(jsonPath("$.token_type").isNotEmpty())
.andExpect(jsonPath("$.expires_in").isNotEmpty())
.andExpect(jsonPath("$.refresh_token").doesNotExist())
.andExpect(jsonPath("$.scope").isNotEmpty())
.andExpect(jsonPath("$.id_token").isNotEmpty())
.andReturn();
}
private static MultiValueMap<String, String> getAuthorizationRequestParameters(RegisteredClient registeredClient) {
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.set(OAuth2ParameterNames.RESPONSE_TYPE, OAuth2AuthorizationResponseType.CODE.getValue());
parameters.set(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
parameters.set(OAuth2ParameterNames.REDIRECT_URI, registeredClient.getRedirectUris().iterator().next());
parameters.set(OAuth2ParameterNames.SCOPE,
StringUtils.collectionToDelimitedString(registeredClient.getScopes(), " "));
parameters.set(OAuth2ParameterNames.STATE, "state");
return parameters;
}
private static MultiValueMap<String, String> getTokenRequestParameters(RegisteredClient registeredClient,
OAuth2Authorization authorization) {
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.set(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
parameters.set(OAuth2ParameterNames.CODE,
authorization.getToken(OAuth2AuthorizationCode.class).getToken().getTokenValue());
parameters.set(OAuth2ParameterNames.REDIRECT_URI, registeredClient.getRedirectUris().iterator().next());
return parameters;
}
private static String encodeBasicAuth(String clientId, String secret) throws Exception {
clientId = URLEncoder.encode(clientId, StandardCharsets.UTF_8.name());
secret = URLEncoder.encode(secret, StandardCharsets.UTF_8.name());
String credentialsString = clientId + ":" + secret;
byte[] encodedBytes = Base64.getEncoder().encode(credentialsString.getBytes(StandardCharsets.UTF_8));
return new String(encodedBytes, StandardCharsets.UTF_8);
}
private String extractParameterFromRedirectUri(String redirectUri, String param)
throws UnsupportedEncodingException {
String locationHeader = URLDecoder.decode(redirectUri, StandardCharsets.UTF_8.name());
UriComponents uriComponents = UriComponentsBuilder.fromUriString(locationHeader).build();
return uriComponents.getQueryParams().getFirst(param);
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class AuthorizationServerConfiguration {
@Bean
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
// @formatter:off
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
OAuth2AuthorizationServerConfigurer.authorizationServer();
http
.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
.with(authorizationServerConfigurer, (authorizationServer) ->
authorizationServer
.oidc(Customizer.withDefaults()) // Enable OpenID Connect 1.0
);
// @formatter:on
return http.build();
}
@Bean
OAuth2AuthorizationService authorizationService(JdbcOperations jdbcOperations,
RegisteredClientRepository registeredClientRepository) {
JdbcOAuth2AuthorizationService authorizationService = new JdbcOAuth2AuthorizationService(jdbcOperations,
registeredClientRepository);
authorizationService.setAuthorizationRowMapper(new RowMapper(registeredClientRepository));
authorizationService.setAuthorizationParametersMapper(new ParametersMapper());
return authorizationService;
}
@Bean
RegisteredClientRepository registeredClientRepository(JdbcOperations jdbcOperations) {
JdbcRegisteredClientRepository jdbcRegisteredClientRepository = new JdbcRegisteredClientRepository(
jdbcOperations);
RegisteredClientParametersMapper registeredClientParametersMapper = new RegisteredClientParametersMapper();
jdbcRegisteredClientRepository.setRegisteredClientParametersMapper(registeredClientParametersMapper);
return jdbcRegisteredClientRepository;
}
@Bean
JdbcOperations jdbcOperations() {
return new JdbcTemplate(db);
}
@Bean
JWKSource<SecurityContext> jwkSource() {
return jwkSource;
}
@Bean
JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
@Bean
OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
return (context) -> {
if (context.getTokenType().getValue().equals(OidcParameterNames.ID_TOKEN)) {
Authentication principal = context.getPrincipal();
Set<String> authorities = new HashSet<>();
for (GrantedAuthority authority : principal.getAuthorities()) {
authorities.add(authority.getAuthority());
}
context.getClaims().claim(AUTHORITIES_CLAIM, authorities);
}
};
}
@Bean
AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().multipleIssuersAllowed(true).build();
}
@Bean
PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Bean
SessionRegistry sessionRegistry() {
return sessionRegistry;
}
static class RowMapper extends JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper {
RowMapper(RegisteredClientRepository registeredClientRepository) {
super(registeredClientRepository);
getObjectMapper().addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class);
}
}
static class ParametersMapper extends JdbcOAuth2AuthorizationService.OAuth2AuthorizationParametersMapper {
ParametersMapper() {
super();
getObjectMapper().addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class);
}
}
}
@EnableWebSecurity
@Configuration
static class AuthorizationServerConfigurationWithTokenGenerator extends AuthorizationServerConfiguration {
// @formatter:off
@Bean
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
OAuth2AuthorizationServerConfigurer.authorizationServer();
http
.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
.with(authorizationServerConfigurer, (authorizationServer) ->
authorizationServer
.tokenGenerator(tokenGenerator())
.oidc(Customizer.withDefaults())
)
.authorizeHttpRequests((authorize) ->
authorize.anyRequest().authenticated()
);
return http.build();
}
// @formatter:on
@Bean
OAuth2TokenGenerator<?> tokenGenerator() {
JwtGenerator jwtGenerator = new JwtGenerator(new NimbusJwtEncoder(jwkSource()));
jwtGenerator.setJwtCustomizer(jwtCustomizer());
OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
OAuth2TokenGenerator<OAuth2Token> delegatingTokenGenerator = new DelegatingOAuth2TokenGenerator(
jwtGenerator, refreshTokenGenerator);
return spy(new OAuth2TokenGenerator<OAuth2Token>() {
@Override
public OAuth2Token generate(OAuth2TokenContext context) {
return delegatingTokenGenerator.generate(context);
}
});
}
}
@EnableWebSecurity
@Configuration
static class AuthorizationServerConfigurationWithCustomRefreshTokenGenerator
extends AuthorizationServerConfiguration {
// @formatter:off
@Bean
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
OAuth2AuthorizationServerConfigurer.authorizationServer();
http
.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
.with(authorizationServerConfigurer, (authorizationServer) ->
authorizationServer
.tokenGenerator(tokenGenerator())
.oidc(Customizer.withDefaults())
)
.authorizeHttpRequests((authorize) ->
authorize.anyRequest().authenticated()
);
return http.build();
}
// @formatter:on
@Bean
OAuth2TokenGenerator<?> tokenGenerator() {
JwtGenerator jwtGenerator = new JwtGenerator(new NimbusJwtEncoder(jwkSource()));
jwtGenerator.setJwtCustomizer(jwtCustomizer());
OAuth2TokenGenerator<OAuth2RefreshToken> refreshTokenGenerator = new CustomRefreshTokenGenerator();
return new DelegatingOAuth2TokenGenerator(jwtGenerator, refreshTokenGenerator);
}
private static final class CustomRefreshTokenGenerator implements OAuth2TokenGenerator<OAuth2RefreshToken> {
private final OAuth2RefreshTokenGenerator delegate = new OAuth2RefreshTokenGenerator();
@Nullable
@Override
public OAuth2RefreshToken generate(OAuth2TokenContext context) {
if (context.getAuthorizedScopes().contains(OidcScopes.OPENID)
&& !context.getAuthorizedScopes().contains("offline_access")) {
return null;
}
return this.delegate.generate(context);
}
}
}
}
@@ -0,0 +1,521 @@
/*
* Copyright 2004-present 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.config.annotation.web.configurers.oauth2.server.authorization;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.security.oauth2.jose.TestJwks;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.JwsHeader;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationContext;
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultMatcher;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Integration tests for the OpenID Connect 1.0 UserInfo endpoint.
*
* @author Steve Riesenberg
*/
@ExtendWith(SpringTestContextExtension.class)
public class OidcUserInfoTests {
private static final String DEFAULT_OIDC_USER_INFO_ENDPOINT_URI = "/userinfo";
private static SecurityContextRepository securityContextRepository;
public final SpringTestContext spring = new SpringTestContext(this);
@Autowired
private MockMvc mvc;
@Autowired
private JwtEncoder jwtEncoder;
@Autowired
private JwtDecoder jwtDecoder;
@Autowired
private OAuth2AuthorizationService authorizationService;
private static AuthenticationConverter authenticationConverter;
private static Consumer<List<AuthenticationConverter>> authenticationConvertersConsumer;
private static AuthenticationProvider authenticationProvider;
private static Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer;
private static AuthenticationSuccessHandler authenticationSuccessHandler;
private static AuthenticationFailureHandler authenticationFailureHandler;
private static Function<OidcUserInfoAuthenticationContext, OidcUserInfo> userInfoMapper;
@BeforeAll
public static void init() {
securityContextRepository = spy(new HttpSessionSecurityContextRepository());
authenticationConverter = mock(AuthenticationConverter.class);
authenticationConvertersConsumer = mock(Consumer.class);
authenticationProvider = mock(AuthenticationProvider.class);
authenticationProvidersConsumer = mock(Consumer.class);
authenticationSuccessHandler = mock(AuthenticationSuccessHandler.class);
authenticationFailureHandler = mock(AuthenticationFailureHandler.class);
userInfoMapper = mock(Function.class);
}
@BeforeEach
public void setup() {
reset(securityContextRepository);
reset(authenticationConverter);
reset(authenticationConvertersConsumer);
reset(authenticationProvider);
reset(authenticationProvidersConsumer);
reset(authenticationSuccessHandler);
reset(authenticationFailureHandler);
reset(userInfoMapper);
}
@Test
public void requestWhenUserInfoRequestGetThenUserInfoResponse() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
OAuth2Authorization authorization = createAuthorization();
this.authorizationService.save(authorization);
OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
// @formatter:off
this.mvc.perform(get(DEFAULT_OIDC_USER_INFO_ENDPOINT_URI)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken.getTokenValue()))
.andExpect(status().is2xxSuccessful())
.andExpectAll(userInfoResponse());
// @formatter:on
}
@Test
public void requestWhenUserInfoRequestPostThenUserInfoResponse() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
OAuth2Authorization authorization = createAuthorization();
this.authorizationService.save(authorization);
OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
// @formatter:off
this.mvc.perform(post(DEFAULT_OIDC_USER_INFO_ENDPOINT_URI)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken.getTokenValue()))
.andExpect(status().is2xxSuccessful())
.andExpectAll(userInfoResponse());
// @formatter:on
}
@Test
public void requestWhenUserInfoRequestIncludesIssuerPathThenUserInfoResponse() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
OAuth2Authorization authorization = createAuthorization();
this.authorizationService.save(authorization);
String issuer = "https://example.com:8443/issuer1";
OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
// @formatter:off
this.mvc.perform(get(issuer.concat(DEFAULT_OIDC_USER_INFO_ENDPOINT_URI))
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken.getTokenValue()))
.andExpect(status().is2xxSuccessful())
.andExpectAll(userInfoResponse());
// @formatter:on
}
@Test
public void requestWhenUserInfoEndpointCustomizedThenUsed() throws Exception {
this.spring.register(CustomUserInfoConfiguration.class).autowire();
OAuth2Authorization authorization = createAuthorization();
this.authorizationService.save(authorization);
given(userInfoMapper.apply(any())).willReturn(createUserInfo());
OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
// @formatter:off
this.mvc.perform(get(DEFAULT_OIDC_USER_INFO_ENDPOINT_URI)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken.getTokenValue()))
.andExpect(status().is2xxSuccessful());
// @formatter:on
verify(userInfoMapper).apply(any());
verify(authenticationConverter).convert(any());
verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), any());
verifyNoInteractions(authenticationFailureHandler);
ArgumentCaptor<List<AuthenticationProvider>> authenticationProvidersCaptor = ArgumentCaptor
.forClass(List.class);
verify(authenticationProvidersConsumer).accept(authenticationProvidersCaptor.capture());
List<AuthenticationProvider> authenticationProviders = authenticationProvidersCaptor.getValue();
assertThat(authenticationProviders).hasSize(2)
.allMatch((provider) -> provider == authenticationProvider
|| provider instanceof OidcUserInfoAuthenticationProvider);
ArgumentCaptor<List<AuthenticationConverter>> authenticationConvertersCaptor = ArgumentCaptor
.forClass(List.class);
verify(authenticationConvertersConsumer).accept(authenticationConvertersCaptor.capture());
List<AuthenticationConverter> authenticationConverters = authenticationConvertersCaptor.getValue();
assertThat(authenticationConverters).hasSize(2).allMatch(AuthenticationConverter.class::isInstance);
}
@Test
public void requestWhenUserInfoEndpointCustomizedWithAuthenticationProviderThenUsed() throws Exception {
this.spring.register(CustomUserInfoConfiguration.class).autowire();
OAuth2Authorization authorization = createAuthorization();
this.authorizationService.save(authorization);
given(authenticationProvider.supports(eq(OidcUserInfoAuthenticationToken.class))).willReturn(true);
String tokenValue = authorization.getAccessToken().getToken().getTokenValue();
Jwt jwt = this.jwtDecoder.decode(tokenValue);
OidcUserInfoAuthenticationToken oidcUserInfoAuthentication = new OidcUserInfoAuthenticationToken(
new JwtAuthenticationToken(jwt), createUserInfo());
given(authenticationProvider.authenticate(any())).willReturn(oidcUserInfoAuthentication);
OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
// @formatter:off
this.mvc.perform(get(DEFAULT_OIDC_USER_INFO_ENDPOINT_URI)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken.getTokenValue()))
.andExpect(status().is2xxSuccessful());
// @formatter:on
verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), any());
verify(authenticationProvider).authenticate(any());
verifyNoInteractions(authenticationFailureHandler);
verifyNoInteractions(userInfoMapper);
}
@Test
public void requestWhenUserInfoEndpointCustomizedWithAuthenticationFailureHandlerThenUsed() throws Exception {
this.spring.register(CustomUserInfoConfiguration.class).autowire();
given(userInfoMapper.apply(any())).willReturn(createUserInfo());
willAnswer((invocation) -> {
HttpServletResponse response = invocation.getArgument(1);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("unauthorized");
return null;
}).given(authenticationFailureHandler).onAuthenticationFailure(any(), any(), any());
OAuth2AccessToken accessToken = createAuthorization().getAccessToken().getToken();
// @formatter:off
this.mvc.perform(get(DEFAULT_OIDC_USER_INFO_ENDPOINT_URI)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken.getTokenValue()))
.andExpect(status().is4xxClientError());
// @formatter:on
verify(authenticationFailureHandler).onAuthenticationFailure(any(), any(), any());
verifyNoInteractions(authenticationSuccessHandler);
verifyNoInteractions(userInfoMapper);
}
// gh-482
@Test
public void requestWhenUserInfoRequestThenBearerTokenAuthenticationNotPersisted() throws Exception {
this.spring.register(AuthorizationServerConfigurationWithSecurityContextRepository.class).autowire();
OAuth2Authorization authorization = createAuthorization();
this.authorizationService.save(authorization);
OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
// @formatter:off
MvcResult mvcResult = this.mvc.perform(get(DEFAULT_OIDC_USER_INFO_ENDPOINT_URI)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken.getTokenValue()))
.andExpect(status().is2xxSuccessful())
.andExpectAll(userInfoResponse())
.andReturn();
// @formatter:on
org.springframework.security.core.context.SecurityContext securityContext = securityContextRepository
.loadDeferredContext(mvcResult.getRequest())
.get();
assertThat(securityContext.getAuthentication()).isNull();
}
private static ResultMatcher[] userInfoResponse() {
// @formatter:off
return new ResultMatcher[] {
jsonPath("sub").value("user1"),
jsonPath("name").value("First Last"),
jsonPath("given_name").value("First"),
jsonPath("family_name").value("Last"),
jsonPath("middle_name").value("Middle"),
jsonPath("nickname").value("User"),
jsonPath("preferred_username").value("user"),
jsonPath("profile").value("https://example.com/user1"),
jsonPath("picture").value("https://example.com/user1.jpg"),
jsonPath("website").value("https://example.com"),
jsonPath("email").value("user1@example.com"),
jsonPath("email_verified").value("true"),
jsonPath("gender").value("female"),
jsonPath("birthdate").value("1970-01-01"),
jsonPath("zoneinfo").value("Europe/Paris"),
jsonPath("locale").value("en-US"),
jsonPath("phone_number").value("+1 (604) 555-1234;ext=5678"),
jsonPath("phone_number_verified").value("false"),
jsonPath("address.formatted").value("Champ de Mars\n5 Av. Anatole France\n75007 Paris\nFrance"),
jsonPath("updated_at").value("1970-01-01T00:00:00Z")
};
// @formatter:on
}
private OAuth2Authorization createAuthorization() {
JwsHeader headers = JwsHeader.with(SignatureAlgorithm.RS256).build();
// @formatter:off
JwtClaimsSet claimSet = JwtClaimsSet.builder()
.claims((claims) -> claims.putAll(createUserInfo().getClaims()))
.build();
// @formatter:on
Jwt jwt = this.jwtEncoder.encode(JwtEncoderParameters.from(headers, claimSet));
Instant now = Instant.now();
Set<String> scopes = new HashSet<>(Arrays.asList(OidcScopes.OPENID, OidcScopes.ADDRESS, OidcScopes.EMAIL,
OidcScopes.PHONE, OidcScopes.PROFILE));
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, jwt.getTokenValue(),
now, now.plusSeconds(300), scopes);
OidcIdToken idToken = OidcIdToken.withTokenValue("id-token")
.claims((claims) -> claims.putAll(createUserInfo().getClaims()))
.build();
return TestOAuth2Authorizations.authorization().accessToken(accessToken).token(idToken).build();
}
private static OidcUserInfo createUserInfo() {
// @formatter:off
return OidcUserInfo.builder()
.subject("user1")
.name("First Last")
.givenName("First")
.familyName("Last")
.middleName("Middle")
.nickname("User")
.preferredUsername("user")
.profile("https://example.com/user1")
.picture("https://example.com/user1.jpg")
.website("https://example.com")
.email("user1@example.com")
.emailVerified(true)
.gender("female")
.birthdate("1970-01-01")
.zoneinfo("Europe/Paris")
.locale("en-US")
.phoneNumber("+1 (604) 555-1234;ext=5678")
.phoneNumberVerified(false)
.claim("address", Collections.singletonMap("formatted", "Champ de Mars\n5 Av. Anatole France\n75007 Paris\nFrance"))
.updatedAt("1970-01-01T00:00:00Z")
.build();
// @formatter:on
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class CustomUserInfoConfiguration extends AuthorizationServerConfiguration {
@Bean
@Override
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// @formatter:off
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
OAuth2AuthorizationServerConfigurer.authorizationServer();
http
.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
.with(authorizationServerConfigurer, (authorizationServer) ->
authorizationServer
.oidc((oidc) ->
oidc
.userInfoEndpoint((userInfo) ->
userInfo
.userInfoRequestConverter(authenticationConverter)
.userInfoRequestConverters(authenticationConvertersConsumer)
.authenticationProvider(authenticationProvider)
.authenticationProviders(authenticationProvidersConsumer)
.userInfoResponseHandler(authenticationSuccessHandler)
.errorResponseHandler(authenticationFailureHandler)
.userInfoMapper(userInfoMapper)))
)
.authorizeHttpRequests((authorize) ->
authorize.anyRequest().authenticated()
);
// @formatter:on
return http.build();
}
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class AuthorizationServerConfigurationWithSecurityContextRepository
extends AuthorizationServerConfiguration {
@Bean
@Override
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// @formatter:off
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
OAuth2AuthorizationServerConfigurer.authorizationServer();
http
.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
.with(authorizationServerConfigurer, (authorizationServer) ->
authorizationServer
.oidc(Customizer.withDefaults())
)
.authorizeHttpRequests((authorize) ->
authorize.anyRequest().authenticated()
)
.securityContext((securityContext) ->
securityContext.securityContextRepository(securityContextRepository));
// @formatter:on
return http.build();
}
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class AuthorizationServerConfiguration {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// @formatter:off
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
OAuth2AuthorizationServerConfigurer.authorizationServer();
http
.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
.with(authorizationServerConfigurer, (authorizationServer) ->
authorizationServer
.oidc(Customizer.withDefaults())
)
.authorizeHttpRequests((authorize) ->
authorize.anyRequest().authenticated()
);
// @formatter:on
return http.build();
}
@Bean
RegisteredClientRepository registeredClientRepository() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
return new InMemoryRegisteredClientRepository(registeredClient);
}
@Bean
OAuth2AuthorizationService authorizationService() {
return new InMemoryOAuth2AuthorizationService();
}
@Bean
JWKSource<SecurityContext> jwkSource() {
return new ImmutableJWKSet<>(new JWKSet(TestJwks.DEFAULT_RSA_JWK));
}
@Bean
JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
@Bean
JwtEncoder jwtEncoder(JWKSource<SecurityContext> jwkSource) {
return new NimbusJwtEncoder(jwkSource);
}
@Bean
AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().multipleIssuersAllowed(true).build();
}
}
}