19 changed files with 1081 additions and 15 deletions
@ -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()); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -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); |
||||
} |
||||
|
||||
} |
||||
@ -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); |
||||
} |
||||
|
||||
} |
||||
@ -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; |
||||
} |
||||
|
||||
} |
||||
@ -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() { |
||||
} |
||||
|
||||
} |
||||
@ -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)); |
||||
} |
||||
|
||||
} |
||||
@ -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…
Reference in new issue