Move OAuth2AuthorizationServerConfigurer and OAuth2AuthorizationServerConfiguration
Issue gh-17880
This commit is contained in:
@@ -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')
|
||||
|
||||
+91
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+75
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+51
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+156
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+148
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+326
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+499
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+120
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+287
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+247
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+274
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+326
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+267
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+279
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+250
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+249
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+269
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+167
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+238
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+118
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+281
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+46
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+112
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+138
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+243
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
+200
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+1510
File diff suppressed because it is too large
Load Diff
+230
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+691
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+696
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+646
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+472
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+610
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+413
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+919
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+413
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+788
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+521
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user