Add PKI Mutual-TLS client authentication method
Issue gh-101 Closes gh-1558
This commit is contained in:
@@ -13,6 +13,8 @@ dependencies {
|
||||
constraints {
|
||||
api "com.nimbusds:nimbus-jose-jwt:9.37.3"
|
||||
api "jakarta.servlet:jakarta.servlet-api:6.0.0"
|
||||
api "org.bouncycastle:bcpkix-jdk18on:1.77"
|
||||
api "org.bouncycastle:bcprov-jdk18on:1.77"
|
||||
api "org.junit.jupiter:junit-jupiter:5.10.1"
|
||||
api "org.assertj:assertj-core:3.25.1"
|
||||
api "org.mockito:mockito-core:4.11.0"
|
||||
|
||||
@@ -21,6 +21,8 @@ dependencies {
|
||||
|
||||
testImplementation "org.springframework.security:spring-security-test"
|
||||
testImplementation "org.springframework:spring-webmvc"
|
||||
testImplementation "org.bouncycastle:bcpkix-jdk18on"
|
||||
testImplementation "org.bouncycastle:bcprov-jdk18on"
|
||||
testImplementation "org.junit.jupiter:junit-jupiter"
|
||||
testImplementation "org.assertj:assertj-core"
|
||||
testImplementation "org.mockito:mockito-core"
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright 2020-2024 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.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An {@link OAuth2AuthenticationContext} that holds an {@link OAuth2ClientAuthenticationToken} and additional information
|
||||
* and is used when validating an OAuth 2.0 Client Authentication.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 1.3
|
||||
* @see OAuth2AuthenticationContext
|
||||
* @see OAuth2ClientAuthenticationToken
|
||||
* @see X509ClientCertificateAuthenticationProvider#setCertificateVerifier(Consumer)
|
||||
*/
|
||||
public final class OAuth2ClientAuthenticationContext implements OAuth2AuthenticationContext {
|
||||
private final Map<Object, Object> context;
|
||||
|
||||
private OAuth2ClientAuthenticationContext(Map<Object, Object> context) {
|
||||
this.context = Collections.unmodifiableMap(new HashMap<>(context));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Nullable
|
||||
@Override
|
||||
public <V> V get(Object key) {
|
||||
return hasKey(key) ? (V) this.context.get(key) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasKey(Object key) {
|
||||
Assert.notNull(key, "key cannot be null");
|
||||
return this.context.containsKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link RegisteredClient registered client}.
|
||||
*
|
||||
* @return the {@link RegisteredClient}
|
||||
*/
|
||||
public RegisteredClient getRegisteredClient() {
|
||||
return get(RegisteredClient.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link Builder} with the provided {@link OAuth2ClientAuthenticationToken}.
|
||||
*
|
||||
* @param authentication the {@link OAuth2ClientAuthenticationToken}
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public static Builder with(OAuth2ClientAuthenticationToken authentication) {
|
||||
return new Builder(authentication);
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link OAuth2ClientAuthenticationContext}.
|
||||
*/
|
||||
public static final class Builder extends AbstractBuilder<OAuth2ClientAuthenticationContext, Builder> {
|
||||
|
||||
private Builder(OAuth2ClientAuthenticationToken authentication) {
|
||||
super(authentication);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link RegisteredClient registered client}.
|
||||
*
|
||||
* @param registeredClient the {@link RegisteredClient}
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder registeredClient(RegisteredClient registeredClient) {
|
||||
return put(RegisteredClient.class, registeredClient);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a new {@link OAuth2ClientAuthenticationContext}.
|
||||
*
|
||||
* @return the {@link OAuth2ClientAuthenticationContext}
|
||||
*/
|
||||
public OAuth2ClientAuthenticationContext build() {
|
||||
Assert.notNull(get(RegisteredClient.class), "registeredClient cannot be null");
|
||||
return new OAuth2ClientAuthenticationContext(getContext());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Copyright 2020-2024 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.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
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.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
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.settings.ClientSettings;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* An {@link AuthenticationProvider} implementation used for OAuth 2.0 Client Authentication,
|
||||
* which authenticates the client {@code X509Certificate} received when the {@code tls_client_auth} authentication method is used.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 1.3
|
||||
* @see AuthenticationProvider
|
||||
* @see OAuth2ClientAuthenticationToken
|
||||
* @see RegisteredClientRepository
|
||||
* @see OAuth2AuthorizationService
|
||||
*/
|
||||
public final class X509ClientCertificateAuthenticationProvider implements AuthenticationProvider {
|
||||
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-3.2.1";
|
||||
private static final ClientAuthenticationMethod TLS_CLIENT_AUTH_AUTHENTICATION_METHOD =
|
||||
new ClientAuthenticationMethod("tls_client_auth");
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
private final RegisteredClientRepository registeredClientRepository;
|
||||
private final CodeVerifierAuthenticator codeVerifierAuthenticator;
|
||||
private Consumer<OAuth2ClientAuthenticationContext> certificateVerifier = this::verifyX509CertificateSubjectDN;
|
||||
|
||||
/**
|
||||
* Constructs a {@code X509ClientCertificateAuthenticationProvider} using the provided parameters.
|
||||
*
|
||||
* @param registeredClientRepository the repository of registered clients
|
||||
* @param authorizationService the authorization service
|
||||
*/
|
||||
public X509ClientCertificateAuthenticationProvider(RegisteredClientRepository registeredClientRepository,
|
||||
OAuth2AuthorizationService authorizationService) {
|
||||
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
|
||||
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||
this.registeredClientRepository = registeredClientRepository;
|
||||
this.codeVerifierAuthenticator = new CodeVerifierAuthenticator(authorizationService);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
OAuth2ClientAuthenticationToken clientAuthentication =
|
||||
(OAuth2ClientAuthenticationToken) authentication;
|
||||
|
||||
if (!TLS_CLIENT_AUTH_AUTHENTICATION_METHOD.equals(clientAuthentication.getClientAuthenticationMethod())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String clientId = clientAuthentication.getPrincipal().toString();
|
||||
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
|
||||
if (registeredClient == null) {
|
||||
throwInvalidClient(OAuth2ParameterNames.CLIENT_ID);
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Retrieved registered client");
|
||||
}
|
||||
|
||||
if (!registeredClient.getClientAuthenticationMethods().contains(
|
||||
clientAuthentication.getClientAuthenticationMethod())) {
|
||||
throwInvalidClient("authentication_method");
|
||||
}
|
||||
|
||||
if (!(clientAuthentication.getCredentials() instanceof X509Certificate[])) {
|
||||
throwInvalidClient("credentials");
|
||||
}
|
||||
|
||||
OAuth2ClientAuthenticationContext authenticationContext =
|
||||
OAuth2ClientAuthenticationContext.with(clientAuthentication)
|
||||
.registeredClient(registeredClient)
|
||||
.build();
|
||||
this.certificateVerifier.accept(authenticationContext);
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Validated client authentication parameters");
|
||||
}
|
||||
|
||||
// Validate the "code_verifier" parameter for the confidential client, if available
|
||||
this.codeVerifierAuthenticator.authenticateIfAvailable(clientAuthentication, registeredClient);
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Authenticated client X509Certificate");
|
||||
}
|
||||
|
||||
return new OAuth2ClientAuthenticationToken(registeredClient,
|
||||
clientAuthentication.getClientAuthenticationMethod(), clientAuthentication.getCredentials());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code Consumer} providing access to the {@link OAuth2ClientAuthenticationContext}
|
||||
* and is responsible for verifying the client {@code X509Certificate} associated in the {@link OAuth2ClientAuthenticationToken}.
|
||||
* The default implementation verifies the {@link ClientSettings#getX509CertificateSubjectDN() expected subject distinguished name}.
|
||||
*
|
||||
* <p>
|
||||
* <b>NOTE:</b> If verification fails, an {@link OAuth2AuthenticationException} MUST be thrown.
|
||||
*
|
||||
* @param certificateVerifier the {@code Consumer} providing access to the {@link OAuth2ClientAuthenticationContext} and is responsible for verifying the client {@code X509Certificate}
|
||||
*/
|
||||
public void setCertificateVerifier(Consumer<OAuth2ClientAuthenticationContext> certificateVerifier) {
|
||||
Assert.notNull(certificateVerifier, "certificateVerifier cannot be null");
|
||||
this.certificateVerifier = certificateVerifier;
|
||||
}
|
||||
|
||||
private void verifyX509CertificateSubjectDN(OAuth2ClientAuthenticationContext clientAuthenticationContext) {
|
||||
OAuth2ClientAuthenticationToken clientAuthentication = clientAuthenticationContext.getAuthentication();
|
||||
RegisteredClient registeredClient = clientAuthenticationContext.getRegisteredClient();
|
||||
X509Certificate[] clientCertificateChain = (X509Certificate[]) clientAuthentication.getCredentials();
|
||||
X509Certificate clientCertificate = clientCertificateChain[0];
|
||||
String expectedSubjectDN = registeredClient.getClientSettings().getX509CertificateSubjectDN();
|
||||
if (!StringUtils.hasText(expectedSubjectDN) ||
|
||||
!clientCertificate.getSubjectX500Principal().getName().equals(expectedSubjectDN)) {
|
||||
throwInvalidClient("x509_certificate_subject_dn");
|
||||
}
|
||||
}
|
||||
|
||||
private static void throwInvalidClient(String parameterName) {
|
||||
throwInvalidClient(parameterName, null);
|
||||
}
|
||||
|
||||
private static void throwInvalidClient(String parameterName, Throwable cause) {
|
||||
OAuth2Error error = new OAuth2Error(
|
||||
OAuth2ErrorCodes.INVALID_CLIENT,
|
||||
"Client authentication failed: " + parameterName,
|
||||
ERROR_URI
|
||||
);
|
||||
throw new OAuth2AuthenticationException(error, error.toString(), cause);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -34,6 +34,7 @@ import org.springframework.security.oauth2.server.authorization.authentication.C
|
||||
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;
|
||||
@@ -42,6 +43,7 @@ import org.springframework.security.oauth2.server.authorization.web.authenticati
|
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter;
|
||||
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;
|
||||
@@ -214,6 +216,7 @@ public final class OAuth2ClientAuthenticationConfigurer extends AbstractOAuth2Co
|
||||
List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
|
||||
|
||||
authenticationConverters.add(new JwtClientAssertionAuthenticationConverter());
|
||||
authenticationConverters.add(new X509ClientCertificateAuthenticationConverter());
|
||||
authenticationConverters.add(new ClientSecretBasicAuthenticationConverter());
|
||||
authenticationConverters.add(new ClientSecretPostAuthenticationConverter());
|
||||
authenticationConverters.add(new PublicClientAuthenticationConverter());
|
||||
@@ -231,6 +234,10 @@ public final class OAuth2ClientAuthenticationConfigurer extends AbstractOAuth2Co
|
||||
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);
|
||||
|
||||
@@ -129,6 +129,7 @@ public final class OidcProviderConfigurationEndpointFilter extends OncePerReques
|
||||
authenticationMethods.add(ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue());
|
||||
authenticationMethods.add(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue());
|
||||
authenticationMethods.add(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue());
|
||||
authenticationMethods.add("tls_client_auth");
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
* Copyright 2020-2024 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.
|
||||
@@ -78,6 +78,17 @@ public final class ClientSettings extends AbstractSettings {
|
||||
return getSetting(ConfigurationSettingNames.Client.TOKEN_ENDPOINT_AUTHENTICATION_SIGNING_ALGORITHM);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the expected subject distinguished name associated to the client {@code X509Certificate}
|
||||
* received during client authentication when using the {@code tls_client_auth} method.
|
||||
*
|
||||
* @return the expected subject distinguished name associated to the client {@code X509Certificate} received during client authentication
|
||||
* @since 1.3
|
||||
*/
|
||||
public String getX509CertificateSubjectDN() {
|
||||
return getSetting(ConfigurationSettingNames.Client.X509_CERTIFICATE_SUBJECT_DN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link Builder} with the default settings.
|
||||
*
|
||||
@@ -156,6 +167,18 @@ public final class ClientSettings extends AbstractSettings {
|
||||
return setting(ConfigurationSettingNames.Client.TOKEN_ENDPOINT_AUTHENTICATION_SIGNING_ALGORITHM, authenticationSigningAlgorithm);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the expected subject distinguished name associated to the client {@code X509Certificate}
|
||||
* received during client authentication when using the {@code tls_client_auth} method.
|
||||
*
|
||||
* @param x509CertificateSubjectDN the expected subject distinguished name associated to the client {@code X509Certificate} received during client authentication * @return the {@link Builder} for further configuration
|
||||
* @return the {@link Builder} for further configuration
|
||||
* @since 1.3
|
||||
*/
|
||||
public Builder x509CertificateSubjectDN(String x509CertificateSubjectDN) {
|
||||
return setting(ConfigurationSettingNames.Client.X509_CERTIFICATE_SUBJECT_DN, x509CertificateSubjectDN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the {@link ClientSettings}.
|
||||
*
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2023 the original author or authors.
|
||||
* Copyright 2020-2024 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.
|
||||
@@ -65,6 +65,13 @@ public final class ConfigurationSettingNames {
|
||||
*/
|
||||
public static final String TOKEN_ENDPOINT_AUTHENTICATION_SIGNING_ALGORITHM = CLIENT_SETTINGS_NAMESPACE.concat("token-endpoint-authentication-signing-algorithm");
|
||||
|
||||
/**
|
||||
* Set the expected subject distinguished name associated to the client {@code X509Certificate}
|
||||
* received during client authentication when using the {@code tls_client_auth} method.
|
||||
* @since 1.3
|
||||
*/
|
||||
public static final String X509_CERTIFICATE_SUBJECT_DN = CLIENT_SETTINGS_NAMESPACE.concat("x509-certificate-subject-dn");
|
||||
|
||||
private Client() {
|
||||
}
|
||||
|
||||
|
||||
@@ -122,6 +122,7 @@ public final class OAuth2AuthorizationServerMetadataEndpointFilter extends OnceP
|
||||
authenticationMethods.add(ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue());
|
||||
authenticationMethods.add(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue());
|
||||
authenticationMethods.add(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue());
|
||||
authenticationMethods.add("tls_client_auth");
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
* Copyright 2020-2024 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.
|
||||
@@ -42,11 +42,13 @@ import org.springframework.security.oauth2.server.authorization.authentication.C
|
||||
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.web.authentication.ClientSecretBasicAuthenticationConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.ClientSecretPostAuthenticationConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter;
|
||||
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;
|
||||
@@ -64,6 +66,8 @@ import org.springframework.web.filter.OncePerRequestFilter;
|
||||
* @see AuthenticationManager
|
||||
* @see JwtClientAssertionAuthenticationConverter
|
||||
* @see JwtClientAssertionAuthenticationProvider
|
||||
* @see X509ClientCertificateAuthenticationConverter
|
||||
* @see X509ClientCertificateAuthenticationProvider
|
||||
* @see ClientSecretBasicAuthenticationConverter
|
||||
* @see ClientSecretPostAuthenticationConverter
|
||||
* @see ClientSecretAuthenticationProvider
|
||||
@@ -97,6 +101,7 @@ public final class OAuth2ClientAuthenticationFilter extends OncePerRequestFilter
|
||||
this.authenticationConverter = new DelegatingAuthenticationConverter(
|
||||
Arrays.asList(
|
||||
new JwtClientAssertionAuthenticationConverter(),
|
||||
new X509ClientCertificateAuthenticationConverter(),
|
||||
new ClientSecretBasicAuthenticationConverter(),
|
||||
new ClientSecretPostAuthenticationConverter(),
|
||||
new PublicClientAuthenticationConverter()));
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2020-2024 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.oauth2.server.authorization.web.authentication;
|
||||
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2ClientAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Attempts to extract a client {@code X509Certificate} chain from {@link HttpServletRequest}
|
||||
* and then converts to an {@link OAuth2ClientAuthenticationToken} used for authenticating the client
|
||||
* using the {@code tls_client_auth} method.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 1.3
|
||||
* @see AuthenticationConverter
|
||||
* @see OAuth2ClientAuthenticationToken
|
||||
* @see OAuth2ClientAuthenticationFilter
|
||||
*/
|
||||
public final class X509ClientCertificateAuthenticationConverter implements AuthenticationConverter {
|
||||
private static final ClientAuthenticationMethod TLS_CLIENT_AUTH_AUTHENTICATION_METHOD =
|
||||
new ClientAuthenticationMethod("tls_client_auth");
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Authentication convert(HttpServletRequest request) {
|
||||
X509Certificate[] clientCertificateChain =
|
||||
(X509Certificate[]) request.getAttribute("jakarta.servlet.request.X509Certificate");
|
||||
if (clientCertificateChain == null || clientCertificateChain.length <= 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getFormParameters(request);
|
||||
|
||||
// client_id (REQUIRED)
|
||||
String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID);
|
||||
if (!StringUtils.hasText(clientId) ||
|
||||
parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
|
||||
}
|
||||
|
||||
Map<String, Object> additionalParameters = OAuth2EndpointUtils.getParametersIfMatchesAuthorizationCodeGrantRequest(
|
||||
request, OAuth2ParameterNames.CLIENT_ID);
|
||||
|
||||
return new OAuth2ClientAuthenticationToken(clientId, TLS_CLIENT_AUTH_AUTHENTICATION_METHOD,
|
||||
clientCertificateChain, additionalParameters);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
/*
|
||||
* Copyright 2020-2024 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.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
|
||||
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.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.ClientSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.util.TestX509Certificates;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Tests for {@link X509ClientCertificateAuthenticationProvider}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
public class X509ClientCertificateAuthenticationProviderTests {
|
||||
// See RFC 7636: Appendix B. Example for the S256 code_challenge_method
|
||||
// https://tools.ietf.org/html/rfc7636#appendix-B
|
||||
private static final String S256_CODE_VERIFIER = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
|
||||
private static final String S256_CODE_CHALLENGE = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM";
|
||||
|
||||
private static final String AUTHORIZATION_CODE = "code";
|
||||
private static final OAuth2TokenType AUTHORIZATION_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.CODE);
|
||||
private static final ClientAuthenticationMethod TLS_CLIENT_AUTH_AUTHENTICATION_METHOD =
|
||||
new ClientAuthenticationMethod("tls_client_auth");
|
||||
private RegisteredClientRepository registeredClientRepository;
|
||||
private OAuth2AuthorizationService authorizationService;
|
||||
private X509ClientCertificateAuthenticationProvider authenticationProvider;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
this.registeredClientRepository = mock(RegisteredClientRepository.class);
|
||||
this.authorizationService = mock(OAuth2AuthorizationService.class);
|
||||
this.authenticationProvider = new X509ClientCertificateAuthenticationProvider(
|
||||
this.registeredClientRepository, this.authorizationService);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenRegisteredClientRepositoryNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> new X509ClientCertificateAuthenticationProvider(null, this.authorizationService))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("registeredClientRepository cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenAuthorizationServiceNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> new X509ClientCertificateAuthenticationProvider(this.registeredClientRepository, null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("authorizationService cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setCertificateVerifierWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> this.authenticationProvider.setCertificateVerifier(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("certificateVerifier cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void supportsWhenTypeOAuth2ClientAuthenticationTokenThenReturnTrue() {
|
||||
assertThat(this.authenticationProvider.supports(OAuth2ClientAuthenticationToken.class)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenInvalidClientIdThenThrowOAuth2AuthenticationException() {
|
||||
// @formatter:off
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||
.clientAuthenticationMethod(TLS_CLIENT_AUTH_AUTHENTICATION_METHOD)
|
||||
.build();
|
||||
// @formatter:on
|
||||
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||
.thenReturn(registeredClient);
|
||||
|
||||
OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
|
||||
registeredClient.getClientId() + "-invalid", TLS_CLIENT_AUTH_AUTHENTICATION_METHOD,
|
||||
TestX509Certificates.DEMO_CLIENT_PKI_CERTIFICATE, null);
|
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
||||
.satisfies(error -> {
|
||||
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||
assertThat(error.getDescription()).contains(OAuth2ParameterNames.CLIENT_ID);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenUnsupportedClientAuthenticationMethodThenThrowOAuth2AuthenticationException() {
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||
.thenReturn(registeredClient);
|
||||
|
||||
OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
|
||||
registeredClient.getClientId(), TLS_CLIENT_AUTH_AUTHENTICATION_METHOD,
|
||||
TestX509Certificates.DEMO_CLIENT_PKI_CERTIFICATE, null);
|
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
||||
.satisfies(error -> {
|
||||
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||
assertThat(error.getDescription()).contains("authentication_method");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenX509CertificateNotProvidedThenThrowOAuth2AuthenticationException() {
|
||||
// @formatter:off
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||
.clientAuthenticationMethod(TLS_CLIENT_AUTH_AUTHENTICATION_METHOD)
|
||||
.build();
|
||||
// @formatter:on
|
||||
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||
.thenReturn(registeredClient);
|
||||
|
||||
OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
|
||||
registeredClient.getClientId(), TLS_CLIENT_AUTH_AUTHENTICATION_METHOD, null, null);
|
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
||||
.satisfies(error -> {
|
||||
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||
assertThat(error.getDescription()).contains("credentials");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenInvalidX509CertificateSubjectDNThenThrowOAuth2AuthenticationException() {
|
||||
// @formatter:off
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||
.clientAuthenticationMethod(TLS_CLIENT_AUTH_AUTHENTICATION_METHOD)
|
||||
.clientSettings(
|
||||
ClientSettings.builder()
|
||||
.x509CertificateSubjectDN("CN=demo-client-sample-2,OU=Spring Samples,O=Spring,C=US")
|
||||
.build()
|
||||
)
|
||||
.build();
|
||||
// @formatter:on
|
||||
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||
.thenReturn(registeredClient);
|
||||
|
||||
OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
|
||||
registeredClient.getClientId(), TLS_CLIENT_AUTH_AUTHENTICATION_METHOD,
|
||||
TestX509Certificates.DEMO_CLIENT_PKI_CERTIFICATE, null);
|
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
||||
.satisfies(error -> {
|
||||
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||
assertThat(error.getDescription()).contains("x509_certificate_subject_dn");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenValidX509CertificateThenAuthenticated() {
|
||||
// @formatter:off
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||
.clientAuthenticationMethod(TLS_CLIENT_AUTH_AUTHENTICATION_METHOD)
|
||||
.clientSettings(
|
||||
ClientSettings.builder()
|
||||
.x509CertificateSubjectDN(TestX509Certificates.DEMO_CLIENT_PKI_CERTIFICATE[0].getSubjectX500Principal().getName())
|
||||
.build()
|
||||
)
|
||||
.build();
|
||||
// @formatter:on
|
||||
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||
.thenReturn(registeredClient);
|
||||
|
||||
OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
|
||||
registeredClient.getClientId(), TLS_CLIENT_AUTH_AUTHENTICATION_METHOD,
|
||||
TestX509Certificates.DEMO_CLIENT_PKI_CERTIFICATE, null);
|
||||
|
||||
OAuth2ClientAuthenticationToken authenticationResult =
|
||||
(OAuth2ClientAuthenticationToken) this.authenticationProvider.authenticate(authentication);
|
||||
|
||||
assertThat(authenticationResult.isAuthenticated()).isTrue();
|
||||
assertThat(authenticationResult.getPrincipal().toString()).isEqualTo(registeredClient.getClientId());
|
||||
assertThat(authenticationResult.getCredentials()).isEqualTo(TestX509Certificates.DEMO_CLIENT_PKI_CERTIFICATE);
|
||||
assertThat(authenticationResult.getRegisteredClient()).isEqualTo(registeredClient);
|
||||
assertThat(authenticationResult.getClientAuthenticationMethod()).isEqualTo(TLS_CLIENT_AUTH_AUTHENTICATION_METHOD);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenPkceAndValidCodeVerifierThenAuthenticated() {
|
||||
// @formatter:off
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||
.clientAuthenticationMethod(TLS_CLIENT_AUTH_AUTHENTICATION_METHOD)
|
||||
.clientSettings(
|
||||
ClientSettings.builder()
|
||||
.x509CertificateSubjectDN(TestX509Certificates.DEMO_CLIENT_PKI_CERTIFICATE[0].getSubjectX500Principal().getName())
|
||||
.build()
|
||||
)
|
||||
.build();
|
||||
// @formatter:on
|
||||
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||
.thenReturn(registeredClient);
|
||||
|
||||
OAuth2Authorization authorization = TestOAuth2Authorizations
|
||||
.authorization(registeredClient, createPkceAuthorizationParametersS256())
|
||||
.build();
|
||||
when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(AUTHORIZATION_CODE_TOKEN_TYPE)))
|
||||
.thenReturn(authorization);
|
||||
|
||||
Map<String, Object> parameters = createPkceTokenParameters(S256_CODE_VERIFIER);
|
||||
|
||||
OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
|
||||
registeredClient.getClientId(), TLS_CLIENT_AUTH_AUTHENTICATION_METHOD,
|
||||
TestX509Certificates.DEMO_CLIENT_PKI_CERTIFICATE, parameters);
|
||||
|
||||
OAuth2ClientAuthenticationToken authenticationResult =
|
||||
(OAuth2ClientAuthenticationToken) this.authenticationProvider.authenticate(authentication);
|
||||
|
||||
verify(this.authorizationService).findByToken(eq(AUTHORIZATION_CODE), eq(AUTHORIZATION_CODE_TOKEN_TYPE));
|
||||
assertThat(authenticationResult.isAuthenticated()).isTrue();
|
||||
assertThat(authenticationResult.getPrincipal().toString()).isEqualTo(registeredClient.getClientId());
|
||||
assertThat(authenticationResult.getCredentials()).isEqualTo(TestX509Certificates.DEMO_CLIENT_PKI_CERTIFICATE);
|
||||
assertThat(authenticationResult.getRegisteredClient()).isEqualTo(registeredClient);
|
||||
assertThat(authenticationResult.getClientAuthenticationMethod()).isEqualTo(TLS_CLIENT_AUTH_AUTHENTICATION_METHOD);
|
||||
}
|
||||
|
||||
private static Map<String, Object> createPkceAuthorizationParametersS256() {
|
||||
Map<String, Object> parameters = new HashMap<>();
|
||||
parameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
|
||||
parameters.put(PkceParameterNames.CODE_CHALLENGE, S256_CODE_CHALLENGE);
|
||||
return parameters;
|
||||
}
|
||||
|
||||
private static Map<String, Object> createPkceTokenParameters(String codeVerifier) {
|
||||
Map<String, Object> parameters = createAuthorizationCodeTokenParameters();
|
||||
parameters.put(PkceParameterNames.CODE_VERIFIER, codeVerifier);
|
||||
return parameters;
|
||||
}
|
||||
|
||||
private static Map<String, Object> createAuthorizationCodeTokenParameters() {
|
||||
Map<String, Object> parameters = new HashMap<>();
|
||||
parameters.put(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
|
||||
parameters.put(OAuth2ParameterNames.CODE, AUTHORIZATION_CODE);
|
||||
return parameters;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -24,12 +24,13 @@ 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 jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
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;
|
||||
@@ -75,6 +76,7 @@ import org.springframework.security.oauth2.server.authorization.authentication.O
|
||||
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;
|
||||
@@ -82,10 +84,12 @@ import org.springframework.security.oauth2.server.authorization.client.Registere
|
||||
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
|
||||
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
|
||||
import org.springframework.security.oauth2.server.authorization.jackson2.TestingAuthenticationTokenMixin;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.test.SpringTestContext;
|
||||
import org.springframework.security.oauth2.server.authorization.test.SpringTestContextExtension;
|
||||
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;
|
||||
@@ -95,6 +99,7 @@ import org.springframework.security.oauth2.server.authorization.web.authenticati
|
||||
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.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
@@ -111,6 +116,7 @@ import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.x509;
|
||||
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;
|
||||
@@ -257,6 +263,34 @@ public class OAuth2ClientCredentialsGrantTests {
|
||||
assertThat(updatedRegisteredClient.getClientSecret()).startsWith("{bcrypt}");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenTokenRequestWithX509ClientCertificateThenTokenResponse() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfiguration.class).autowire();
|
||||
|
||||
// @formatter:off
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient2()
|
||||
.clientAuthenticationMethod(new 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(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());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenTokenEndpointCustomizedThenUsed() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfigurationCustomTokenEndpoint.class).autowire();
|
||||
@@ -341,6 +375,7 @@ public class OAuth2ClientCredentialsGrantTests {
|
||||
assertThat(authenticationConverters).allMatch((converter) ->
|
||||
converter == authenticationConverter ||
|
||||
converter instanceof JwtClientAssertionAuthenticationConverter ||
|
||||
converter instanceof X509ClientCertificateAuthenticationConverter ||
|
||||
converter instanceof ClientSecretBasicAuthenticationConverter ||
|
||||
converter instanceof ClientSecretPostAuthenticationConverter ||
|
||||
converter instanceof PublicClientAuthenticationConverter);
|
||||
@@ -354,6 +389,7 @@ public class OAuth2ClientCredentialsGrantTests {
|
||||
assertThat(authenticationProviders).allMatch((provider) ->
|
||||
provider == authenticationProvider ||
|
||||
provider instanceof JwtClientAssertionAuthenticationProvider ||
|
||||
provider instanceof X509ClientCertificateAuthenticationProvider ||
|
||||
provider instanceof ClientSecretAuthenticationProvider ||
|
||||
provider instanceof PublicClientAuthenticationProvider);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2023 the original author or authors.
|
||||
* Copyright 2020-2024 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.
|
||||
@@ -127,15 +127,15 @@ public class OidcProviderConfigurationEndpointFilterTests {
|
||||
assertThat(providerConfigurationResponse).contains("\"response_types_supported\":[\"code\"]");
|
||||
assertThat(providerConfigurationResponse).contains("\"grant_types_supported\":[\"authorization_code\",\"client_credentials\",\"refresh_token\",\"urn:ietf:params:oauth:grant-type:device_code\",\"urn:ietf:params:oauth:grant-type:token-exchange\"]");
|
||||
assertThat(providerConfigurationResponse).contains("\"revocation_endpoint\":\"https://example.com/oauth2/v1/revoke\"");
|
||||
assertThat(providerConfigurationResponse).contains("\"revocation_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"client_secret_jwt\",\"private_key_jwt\"]");
|
||||
assertThat(providerConfigurationResponse).contains("\"revocation_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"client_secret_jwt\",\"private_key_jwt\",\"tls_client_auth\"]");
|
||||
assertThat(providerConfigurationResponse).contains("\"introspection_endpoint\":\"https://example.com/oauth2/v1/introspect\"");
|
||||
assertThat(providerConfigurationResponse).contains("\"introspection_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"client_secret_jwt\",\"private_key_jwt\"]");
|
||||
assertThat(providerConfigurationResponse).contains("\"introspection_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"client_secret_jwt\",\"private_key_jwt\",\"tls_client_auth\"]");
|
||||
assertThat(providerConfigurationResponse).contains("\"code_challenge_methods_supported\":[\"S256\"]");
|
||||
assertThat(providerConfigurationResponse).contains("\"subject_types_supported\":[\"public\"]");
|
||||
assertThat(providerConfigurationResponse).contains("\"id_token_signing_alg_values_supported\":[\"RS256\"]");
|
||||
assertThat(providerConfigurationResponse).contains("\"userinfo_endpoint\":\"https://example.com/userinfo\"");
|
||||
assertThat(providerConfigurationResponse).contains("\"end_session_endpoint\":\"https://example.com/connect/logout\"");
|
||||
assertThat(providerConfigurationResponse).contains("\"token_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"client_secret_jwt\",\"private_key_jwt\"]");
|
||||
assertThat(providerConfigurationResponse).contains("\"token_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"client_secret_jwt\",\"private_key_jwt\",\"tls_client_auth\"]");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
* Copyright 2020-2024 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.
|
||||
@@ -68,6 +68,14 @@ public class ClientSettingsTests {
|
||||
assertThat(clientSettings.getJwkSetUrl()).isEqualTo("https://client.example.com/jwks");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void x509CertificateSubjectDNWhenProvidedThenSet() {
|
||||
ClientSettings clientSettings = ClientSettings.builder()
|
||||
.x509CertificateSubjectDN("CN=demo-client-sample, OU=Spring Samples, O=Spring, C=US")
|
||||
.build();
|
||||
assertThat(clientSettings.getX509CertificateSubjectDN()).isEqualTo("CN=demo-client-sample, OU=Spring Samples, O=Spring, C=US");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void settingWhenCustomThenSet() {
|
||||
ClientSettings clientSettings = ClientSettings.builder()
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2020-2024 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.oauth2.server.authorization.util;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
public final class TestX509Certificates {
|
||||
|
||||
public static final X509Certificate[] DEMO_CLIENT_PKI_CERTIFICATE;
|
||||
static {
|
||||
try {
|
||||
// Generate the Root certificate (Trust Anchor or most-trusted CA)
|
||||
KeyPair rootKeyPair = X509CertificateUtils.generateRSAKeyPair();
|
||||
String distinguishedName = "CN=spring-samples-trusted-ca, OU=Spring Samples, O=Spring, C=US";
|
||||
X509Certificate rootCertificate = X509CertificateUtils.createTrustAnchorCertificate(rootKeyPair, distinguishedName);
|
||||
|
||||
// Generate the CA (intermediary) certificate
|
||||
KeyPair caKeyPair = X509CertificateUtils.generateRSAKeyPair();
|
||||
distinguishedName = "CN=spring-samples-ca, OU=Spring Samples, O=Spring, C=US";
|
||||
X509Certificate caCertificate = X509CertificateUtils.createCACertificate(
|
||||
rootCertificate, rootKeyPair.getPrivate(), caKeyPair.getPublic(), distinguishedName);
|
||||
|
||||
// Generate certificate for demo-client-sample
|
||||
KeyPair demoClientKeyPair = X509CertificateUtils.generateRSAKeyPair();
|
||||
distinguishedName = "CN=demo-client-sample, OU=Spring Samples, O=Spring, C=US";
|
||||
X509Certificate demoClientCertificate = X509CertificateUtils.createEndEntityCertificate(
|
||||
caCertificate, caKeyPair.getPrivate(), demoClientKeyPair.getPublic(), distinguishedName);
|
||||
|
||||
DEMO_CLIENT_PKI_CERTIFICATE = new X509Certificate[] { demoClientCertificate, caCertificate, rootCertificate };
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static final X509Certificate[] DEMO_CLIENT_SELF_SIGNED_CERTIFICATE;
|
||||
static {
|
||||
try {
|
||||
// Generate self-signed certificate for demo-client-sample
|
||||
KeyPair keyPair = X509CertificateUtils.generateRSAKeyPair();
|
||||
String distinguishedName = "CN=demo-client-sample, OU=Spring Samples, O=Spring, C=US";
|
||||
X509Certificate demoClientSelfSignedCertificate = X509CertificateUtils.createTrustAnchorCertificate(keyPair, distinguishedName);
|
||||
|
||||
DEMO_CLIENT_SELF_SIGNED_CERTIFICATE = new X509Certificate[] { demoClientSelfSignedCertificate };
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private TestX509Certificates() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* Copyright 2020-2024 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.oauth2.server.authorization.util;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.Security;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.spec.RSAKeyGenParameterSpec;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
|
||||
import javax.security.auth.x500.X500Principal;
|
||||
|
||||
import org.bouncycastle.asn1.x509.BasicConstraints;
|
||||
import org.bouncycastle.asn1.x509.Extension;
|
||||
import org.bouncycastle.asn1.x509.KeyUsage;
|
||||
import org.bouncycastle.cert.X509v3CertificateBuilder;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.operator.ContentSigner;
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
public final class X509CertificateUtils {
|
||||
private static final String BC_PROVIDER = "BC";
|
||||
private static final String SHA256_RSA_SIGNATURE_ALGORITHM = "SHA256withRSA";
|
||||
private static final Date DEFAULT_START_DATE;
|
||||
private static final Date DEFAULT_END_DATE;
|
||||
|
||||
static {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
|
||||
// Setup default certificate start date to yesterday and end date for 1 year validity
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.add(Calendar.DATE, -1);
|
||||
DEFAULT_START_DATE = calendar.getTime();
|
||||
calendar.add(Calendar.YEAR, 1);
|
||||
DEFAULT_END_DATE = calendar.getTime();
|
||||
}
|
||||
|
||||
private X509CertificateUtils() {
|
||||
}
|
||||
|
||||
public static KeyPair generateRSAKeyPair() {
|
||||
KeyPair keyPair;
|
||||
try {
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", BC_PROVIDER);
|
||||
keyPairGenerator.initialize(new RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4));
|
||||
keyPair = keyPairGenerator.generateKeyPair();
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
return keyPair;
|
||||
}
|
||||
|
||||
public static X509Certificate createTrustAnchorCertificate(KeyPair keyPair, String distinguishedName) throws Exception {
|
||||
X500Principal subject = new X500Principal(distinguishedName);
|
||||
BigInteger serialNum = new BigInteger(Long.toString(new SecureRandom().nextLong()));
|
||||
|
||||
X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
|
||||
subject,
|
||||
serialNum,
|
||||
DEFAULT_START_DATE,
|
||||
DEFAULT_END_DATE,
|
||||
subject,
|
||||
keyPair.getPublic());
|
||||
|
||||
// Add Extensions
|
||||
JcaX509ExtensionUtils extensionUtils = new JcaX509ExtensionUtils();
|
||||
certBuilder
|
||||
// A BasicConstraints to mark root certificate as CA certificate
|
||||
.addExtension(Extension.basicConstraints, true, new BasicConstraints(true))
|
||||
.addExtension(Extension.subjectKeyIdentifier, false,
|
||||
extensionUtils.createSubjectKeyIdentifier(keyPair.getPublic()));
|
||||
|
||||
ContentSigner signer = new JcaContentSignerBuilder(SHA256_RSA_SIGNATURE_ALGORITHM)
|
||||
.setProvider(BC_PROVIDER).build(keyPair.getPrivate());
|
||||
|
||||
JcaX509CertificateConverter converter = new JcaX509CertificateConverter().setProvider(BC_PROVIDER);
|
||||
|
||||
return converter.getCertificate(certBuilder.build(signer));
|
||||
}
|
||||
|
||||
public static X509Certificate createCACertificate(X509Certificate signerCert, PrivateKey signerKey,
|
||||
PublicKey certKey, String distinguishedName) throws Exception {
|
||||
|
||||
X500Principal subject = new X500Principal(distinguishedName);
|
||||
BigInteger serialNum = new BigInteger(Long.toString(new SecureRandom().nextLong()));
|
||||
|
||||
X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
|
||||
signerCert.getSubjectX500Principal(),
|
||||
serialNum,
|
||||
DEFAULT_START_DATE,
|
||||
DEFAULT_END_DATE,
|
||||
subject,
|
||||
certKey);
|
||||
|
||||
// Add Extensions
|
||||
JcaX509ExtensionUtils extensionUtils = new JcaX509ExtensionUtils();
|
||||
certBuilder
|
||||
// A BasicConstraints to mark as CA certificate and how many CA certificates can follow it in the chain
|
||||
// (with 0 meaning the chain ends with the next certificate in the chain).
|
||||
.addExtension(Extension.basicConstraints, true, new BasicConstraints(0))
|
||||
// KeyUsage specifies what the public key in the certificate can be used for.
|
||||
// In this case, it can be used for signing other certificates and/or
|
||||
// signing Certificate Revocation Lists (CRLs).
|
||||
.addExtension(Extension.keyUsage, true,
|
||||
new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign))
|
||||
.addExtension(Extension.authorityKeyIdentifier, false,
|
||||
extensionUtils.createAuthorityKeyIdentifier(signerCert))
|
||||
.addExtension(Extension.subjectKeyIdentifier, false,
|
||||
extensionUtils.createSubjectKeyIdentifier(certKey));
|
||||
|
||||
ContentSigner signer = new JcaContentSignerBuilder(SHA256_RSA_SIGNATURE_ALGORITHM)
|
||||
.setProvider(BC_PROVIDER).build(signerKey);
|
||||
|
||||
JcaX509CertificateConverter converter = new JcaX509CertificateConverter().setProvider(BC_PROVIDER);
|
||||
|
||||
return converter.getCertificate(certBuilder.build(signer));
|
||||
}
|
||||
|
||||
public static X509Certificate createEndEntityCertificate(X509Certificate signerCert, PrivateKey signerKey,
|
||||
PublicKey certKey, String distinguishedName) throws Exception {
|
||||
|
||||
X500Principal subject = new X500Principal(distinguishedName);
|
||||
BigInteger serialNum = new BigInteger(Long.toString(new SecureRandom().nextLong()));
|
||||
|
||||
X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
|
||||
signerCert.getSubjectX500Principal(),
|
||||
serialNum,
|
||||
DEFAULT_START_DATE,
|
||||
DEFAULT_END_DATE,
|
||||
subject,
|
||||
certKey);
|
||||
|
||||
JcaX509ExtensionUtils extensionUtils = new JcaX509ExtensionUtils();
|
||||
certBuilder
|
||||
.addExtension(Extension.basicConstraints, true, new BasicConstraints(false))
|
||||
.addExtension(Extension.keyUsage, true,
|
||||
new KeyUsage(KeyUsage.digitalSignature))
|
||||
.addExtension(Extension.authorityKeyIdentifier, false,
|
||||
extensionUtils.createAuthorityKeyIdentifier(signerCert))
|
||||
.addExtension(Extension.subjectKeyIdentifier, false,
|
||||
extensionUtils.createSubjectKeyIdentifier(certKey));
|
||||
|
||||
ContentSigner signer = new JcaContentSignerBuilder(SHA256_RSA_SIGNATURE_ALGORITHM)
|
||||
.setProvider(BC_PROVIDER).build(signerKey);
|
||||
|
||||
JcaX509CertificateConverter converter = new JcaX509CertificateConverter().setProvider(BC_PROVIDER);
|
||||
|
||||
return converter.getCertificate(certBuilder.build(signer));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2023 the original author or authors.
|
||||
* Copyright 2020-2024 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.
|
||||
@@ -118,14 +118,14 @@ public class OAuth2AuthorizationServerMetadataEndpointFilterTests {
|
||||
assertThat(authorizationServerMetadataResponse).contains("\"issuer\":\"https://example.com\"");
|
||||
assertThat(authorizationServerMetadataResponse).contains("\"authorization_endpoint\":\"https://example.com/oauth2/v1/authorize\"");
|
||||
assertThat(authorizationServerMetadataResponse).contains("\"token_endpoint\":\"https://example.com/oauth2/v1/token\"");
|
||||
assertThat(authorizationServerMetadataResponse).contains("\"token_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"client_secret_jwt\",\"private_key_jwt\"]");
|
||||
assertThat(authorizationServerMetadataResponse).contains("\"token_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"client_secret_jwt\",\"private_key_jwt\",\"tls_client_auth\"]");
|
||||
assertThat(authorizationServerMetadataResponse).contains("\"jwks_uri\":\"https://example.com/oauth2/v1/jwks\"");
|
||||
assertThat(authorizationServerMetadataResponse).contains("\"response_types_supported\":[\"code\"]");
|
||||
assertThat(authorizationServerMetadataResponse).contains("\"grant_types_supported\":[\"authorization_code\",\"client_credentials\",\"refresh_token\",\"urn:ietf:params:oauth:grant-type:device_code\",\"urn:ietf:params:oauth:grant-type:token-exchange\"]");
|
||||
assertThat(authorizationServerMetadataResponse).contains("\"revocation_endpoint\":\"https://example.com/oauth2/v1/revoke\"");
|
||||
assertThat(authorizationServerMetadataResponse).contains("\"revocation_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"client_secret_jwt\",\"private_key_jwt\"]");
|
||||
assertThat(authorizationServerMetadataResponse).contains("\"revocation_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"client_secret_jwt\",\"private_key_jwt\",\"tls_client_auth\"]");
|
||||
assertThat(authorizationServerMetadataResponse).contains("\"introspection_endpoint\":\"https://example.com/oauth2/v1/introspect\"");
|
||||
assertThat(authorizationServerMetadataResponse).contains("\"introspection_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"client_secret_jwt\",\"private_key_jwt\"]");
|
||||
assertThat(authorizationServerMetadataResponse).contains("\"introspection_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"client_secret_jwt\",\"private_key_jwt\",\"tls_client_auth\"]");
|
||||
assertThat(authorizationServerMetadataResponse).contains("\"code_challenge_methods_supported\":[\"S256\"]");
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright 2020-2024 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.oauth2.server.authorization.web.authentication;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.util.TestX509Certificates;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.assertj.core.api.Assertions.entry;
|
||||
|
||||
/**
|
||||
* Tests for {@link X509ClientCertificateAuthenticationConverter}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
public class X509ClientCertificateAuthenticationConverterTests {
|
||||
private final X509ClientCertificateAuthenticationConverter converter = new X509ClientCertificateAuthenticationConverter();
|
||||
|
||||
@Test
|
||||
public void convertWhenMissingX509CertificateThenReturnNull() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
Authentication authentication = this.converter.convert(request);
|
||||
assertThat(authentication).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertWhenSelfSignedX509CertificateThenReturnNull() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setAttribute("jakarta.servlet.request.X509Certificate",
|
||||
TestX509Certificates.DEMO_CLIENT_SELF_SIGNED_CERTIFICATE);
|
||||
Authentication authentication = this.converter.convert(request);
|
||||
assertThat(authentication).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertWhenMissingClientIdThenInvalidRequestError() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setAttribute("jakarta.servlet.request.X509Certificate",
|
||||
TestX509Certificates.DEMO_CLIENT_PKI_CERTIFICATE);
|
||||
assertThatThrownBy(() -> this.converter.convert(request))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
||||
.extracting("errorCode")
|
||||
.isEqualTo(OAuth2ErrorCodes.INVALID_REQUEST);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertWhenMultipleClientIdThenInvalidRequestError() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setAttribute("jakarta.servlet.request.X509Certificate",
|
||||
TestX509Certificates.DEMO_CLIENT_PKI_CERTIFICATE);
|
||||
request.addParameter(OAuth2ParameterNames.CLIENT_ID, "client-1");
|
||||
request.addParameter(OAuth2ParameterNames.CLIENT_ID, "client-2");
|
||||
assertThatThrownBy(() -> this.converter.convert(request))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
||||
.extracting("errorCode")
|
||||
.isEqualTo(OAuth2ErrorCodes.INVALID_REQUEST);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertWhenPkiX509CertificateThenReturnClientAuthenticationToken() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setAttribute("jakarta.servlet.request.X509Certificate",
|
||||
TestX509Certificates.DEMO_CLIENT_PKI_CERTIFICATE);
|
||||
request.addParameter(OAuth2ParameterNames.CLIENT_ID, "client-1");
|
||||
request.addParameter(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
|
||||
request.addParameter(OAuth2ParameterNames.CODE, "code");
|
||||
request.addParameter("custom-param-1", "custom-value-1");
|
||||
request.addParameter("custom-param-2", "custom-value-1", "custom-value-2");
|
||||
OAuth2ClientAuthenticationToken authentication = (OAuth2ClientAuthenticationToken) this.converter.convert(request);
|
||||
assertThat(authentication.getPrincipal()).isEqualTo("client-1");
|
||||
assertThat(authentication.getCredentials()).isEqualTo(TestX509Certificates.DEMO_CLIENT_PKI_CERTIFICATE);
|
||||
assertThat(authentication.getClientAuthenticationMethod().getValue()).isEqualTo("tls_client_auth");
|
||||
assertThat(authentication.getAdditionalParameters())
|
||||
.containsOnly(
|
||||
entry(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue()),
|
||||
entry(OAuth2ParameterNames.CODE, "code"),
|
||||
entry("custom-param-1", "custom-value-1"),
|
||||
entry("custom-param-2", new String[] {"custom-value-1", "custom-value-2"}));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user