Browse Source

Add PKI Mutual-TLS client authentication method

Issue gh-101

Closes gh-1558
pull/1578/head
Joe Grandja 2 years ago
parent
commit
682c1f936e
  1. 2
      dependencies/spring-authorization-server-dependencies.gradle
  2. 2
      oauth2-authorization-server/spring-security-oauth2-authorization-server.gradle
  3. 107
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationContext.java
  4. 167
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/X509ClientCertificateAuthenticationProvider.java
  5. 7
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientAuthenticationConfigurer.java
  6. 1
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java
  7. 25
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/ClientSettings.java
  8. 9
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/ConfigurationSettingNames.java
  9. 1
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilter.java
  10. 7
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilter.java
  11. 75
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/X509ClientCertificateAuthenticationConverter.java
  12. 275
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/X509ClientCertificateAuthenticationProviderTests.java
  13. 42
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientCredentialsGrantTests.java
  14. 8
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilterTests.java
  15. 10
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/settings/ClientSettingsTests.java
  16. 69
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/util/TestX509Certificates.java
  17. 176
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/util/X509CertificateUtils.java
  18. 8
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilterTests.java
  19. 105
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/authentication/X509ClientCertificateAuthenticationConverterTests.java

2
dependencies/spring-authorization-server-dependencies.gradle vendored

@ -13,6 +13,8 @@ dependencies { @@ -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"

2
oauth2-authorization-server/spring-security-oauth2-authorization-server.gradle

@ -21,6 +21,8 @@ dependencies { @@ -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"

107
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationContext.java

@ -0,0 +1,107 @@ @@ -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());
}
}
}

167
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/X509ClientCertificateAuthenticationProvider.java

@ -0,0 +1,167 @@ @@ -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);
}
}

7
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientAuthenticationConfigurer.java

@ -34,6 +34,7 @@ import org.springframework.security.oauth2.server.authorization.authentication.C @@ -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 @@ -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 @@ -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 @@ -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);

1
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java

@ -129,6 +129,7 @@ public final class OidcProviderConfigurationEndpointFilter extends OncePerReques @@ -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");
};
}

25
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/ClientSettings.java

@ -1,5 +1,5 @@ @@ -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 { @@ -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 { @@ -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}.
*

9
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/ConfigurationSettingNames.java

@ -1,5 +1,5 @@ @@ -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 { @@ -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() {
}

1
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilter.java

@ -122,6 +122,7 @@ public final class OAuth2AuthorizationServerMetadataEndpointFilter extends OnceP @@ -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");
};
}

7
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilter.java

@ -1,5 +1,5 @@ @@ -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 @@ -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; @@ -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 @@ -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()));

75
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/X509ClientCertificateAuthenticationConverter.java

@ -0,0 +1,75 @@ @@ -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);
}
}

275
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/X509ClientCertificateAuthenticationProviderTests.java

@ -0,0 +1,275 @@ @@ -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;
}
}

42
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientCredentialsGrantTests.java

@ -24,12 +24,13 @@ import java.util.Base64; @@ -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 @@ -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 @@ -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 @@ -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; @@ -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 { @@ -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 { @@ -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 { @@ -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);

8
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilterTests.java

@ -1,5 +1,5 @@ @@ -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 { @@ -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

10
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/settings/ClientSettingsTests.java

@ -1,5 +1,5 @@ @@ -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 { @@ -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()

69
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/util/TestX509Certificates.java

@ -0,0 +1,69 @@ @@ -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() {
}
}

176
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/util/X509CertificateUtils.java

@ -0,0 +1,176 @@ @@ -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));
}
}

8
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilterTests.java

@ -1,5 +1,5 @@ @@ -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 { @@ -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\"]");
}

105
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/authentication/X509ClientCertificateAuthenticationConverterTests.java

@ -0,0 +1,105 @@ @@ -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"}));
}
}
Loading…
Cancel
Save