Add self-signed certificate Mutual-TLS client authentication method
Issue gh-101 Closes gh-1559
This commit is contained in:
@@ -38,7 +38,8 @@ 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.
|
||||
* which authenticates the client {@code X509Certificate} received
|
||||
* when the {@code tls_client_auth} or {@code self_signed_tls_client_auth} authentication method is used.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 1.3
|
||||
@@ -51,10 +52,14 @@ public final class X509ClientCertificateAuthenticationProvider implements Authen
|
||||
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 static final ClientAuthenticationMethod SELF_SIGNED_TLS_CLIENT_AUTH_AUTHENTICATION_METHOD =
|
||||
new ClientAuthenticationMethod("self_signed_tls_client_auth");
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
private final RegisteredClientRepository registeredClientRepository;
|
||||
private final CodeVerifierAuthenticator codeVerifierAuthenticator;
|
||||
private Consumer<OAuth2ClientAuthenticationContext> certificateVerifier = this::verifyX509CertificateSubjectDN;
|
||||
private final Consumer<OAuth2ClientAuthenticationContext> selfSignedCertificateVerifier =
|
||||
new X509SelfSignedCertificateVerifier();
|
||||
private Consumer<OAuth2ClientAuthenticationContext> certificateVerifier = this::verifyX509Certificate;
|
||||
|
||||
/**
|
||||
* Constructs a {@code X509ClientCertificateAuthenticationProvider} using the provided parameters.
|
||||
@@ -75,7 +80,8 @@ public final class X509ClientCertificateAuthenticationProvider implements Authen
|
||||
OAuth2ClientAuthenticationToken clientAuthentication =
|
||||
(OAuth2ClientAuthenticationToken) authentication;
|
||||
|
||||
if (!TLS_CLIENT_AUTH_AUTHENTICATION_METHOD.equals(clientAuthentication.getClientAuthenticationMethod())) {
|
||||
if (!TLS_CLIENT_AUTH_AUTHENTICATION_METHOD.equals(clientAuthentication.getClientAuthenticationMethod()) &&
|
||||
!SELF_SIGNED_TLS_CLIENT_AUTH_AUTHENTICATION_METHOD.equals(clientAuthentication.getClientAuthenticationMethod())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -127,7 +133,8 @@ public final class X509ClientCertificateAuthenticationProvider implements Authen
|
||||
/**
|
||||
* 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}.
|
||||
* The default implementation for the {@code tls_client_auth} authentication method
|
||||
* verifies the {@link ClientSettings#getX509CertificateSubjectDN() expected subject distinguished name}.
|
||||
*
|
||||
* <p>
|
||||
* <b>NOTE:</b> If verification fails, an {@link OAuth2AuthenticationException} MUST be thrown.
|
||||
@@ -139,6 +146,15 @@ public final class X509ClientCertificateAuthenticationProvider implements Authen
|
||||
this.certificateVerifier = certificateVerifier;
|
||||
}
|
||||
|
||||
private void verifyX509Certificate(OAuth2ClientAuthenticationContext clientAuthenticationContext) {
|
||||
OAuth2ClientAuthenticationToken clientAuthentication = clientAuthenticationContext.getAuthentication();
|
||||
if (SELF_SIGNED_TLS_CLIENT_AUTH_AUTHENTICATION_METHOD.equals(clientAuthentication.getClientAuthenticationMethod())) {
|
||||
this.selfSignedCertificateVerifier.accept(clientAuthenticationContext);
|
||||
} else {
|
||||
verifyX509CertificateSubjectDN(clientAuthenticationContext);
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyX509CertificateSubjectDN(OAuth2ClientAuthenticationContext clientAuthenticationContext) {
|
||||
OAuth2ClientAuthenticationToken clientAuthentication = clientAuthenticationContext.getAuthentication();
|
||||
RegisteredClient registeredClient = clientAuthenticationContext.getRegisteredClient();
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* 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.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.text.ParseException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import javax.security.auth.x500.X500Principal;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWK;
|
||||
import com.nimbusds.jose.jwk.JWKMatcher;
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.RequestEntity;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
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.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestOperations;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* The default {@code X509Certificate} verifier for the {@code self_signed_tls_client_auth} authentication method.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 1.3
|
||||
* @see X509ClientCertificateAuthenticationProvider#setCertificateVerifier(Consumer)
|
||||
*/
|
||||
final class X509SelfSignedCertificateVerifier implements Consumer<OAuth2ClientAuthenticationContext> {
|
||||
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-3.2.1";
|
||||
private static final JWKMatcher HAS_X509_CERT_CHAIN_MATCHER = new JWKMatcher.Builder().hasX509CertChain(true).build();
|
||||
private final Function<RegisteredClient, JWKSet> jwkSetSupplier = new JwkSetSupplier();
|
||||
|
||||
@Override
|
||||
public void accept(OAuth2ClientAuthenticationContext clientAuthenticationContext) {
|
||||
OAuth2ClientAuthenticationToken clientAuthentication = clientAuthenticationContext.getAuthentication();
|
||||
RegisteredClient registeredClient = clientAuthenticationContext.getRegisteredClient();
|
||||
X509Certificate[] clientCertificateChain = (X509Certificate[]) clientAuthentication.getCredentials();
|
||||
X509Certificate clientCertificate = clientCertificateChain[0];
|
||||
|
||||
X500Principal issuer = clientCertificate.getIssuerX500Principal();
|
||||
X500Principal subject = clientCertificate.getSubjectX500Principal();
|
||||
if (issuer == null || !issuer.equals(subject)) {
|
||||
throwInvalidClient("x509_certificate_issuer");
|
||||
}
|
||||
|
||||
JWKSet jwkSet = this.jwkSetSupplier.apply(registeredClient);
|
||||
|
||||
boolean publicKeyMatches = false;
|
||||
for (JWK jwk : jwkSet.filter(HAS_X509_CERT_CHAIN_MATCHER).getKeys()) {
|
||||
X509Certificate x509Certificate = jwk.getParsedX509CertChain().get(0);
|
||||
PublicKey publicKey = x509Certificate.getPublicKey();
|
||||
if (Arrays.equals(clientCertificate.getPublicKey().getEncoded(), publicKey.getEncoded())) {
|
||||
publicKeyMatches = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!publicKeyMatches) {
|
||||
throwInvalidClient("x509_certificate");
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private static class JwkSetSupplier implements Function<RegisteredClient, JWKSet> {
|
||||
private static final MediaType APPLICATION_JWK_SET_JSON = new MediaType("application", "jwk-set+json");
|
||||
private final RestOperations restOperations;
|
||||
private final Map<String, Supplier<JWKSet>> jwkSets = new ConcurrentHashMap<>();
|
||||
|
||||
private JwkSetSupplier() {
|
||||
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
|
||||
requestFactory.setConnectTimeout(15_000);
|
||||
requestFactory.setReadTimeout(15_000);
|
||||
this.restOperations = new RestTemplate(requestFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JWKSet apply(RegisteredClient registeredClient) {
|
||||
Supplier<JWKSet> jwkSetSupplier = this.jwkSets.computeIfAbsent(
|
||||
registeredClient.getId(), (key) -> {
|
||||
if (!StringUtils.hasText(registeredClient.getClientSettings().getJwkSetUrl())) {
|
||||
throwInvalidClient("client_jwk_set_url");
|
||||
}
|
||||
return new JwkSetHolder(registeredClient.getClientSettings().getJwkSetUrl());
|
||||
});
|
||||
return jwkSetSupplier.get();
|
||||
}
|
||||
|
||||
private JWKSet retrieve(String jwkSetUrl) {
|
||||
URI jwkSetUri = null;
|
||||
try {
|
||||
jwkSetUri = new URI(jwkSetUrl);
|
||||
} catch (URISyntaxException ex) {
|
||||
throwInvalidClient("jwk_set_uri", ex);
|
||||
}
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON, APPLICATION_JWK_SET_JSON));
|
||||
RequestEntity<Void> request = new RequestEntity<>(headers, HttpMethod.GET, jwkSetUri);
|
||||
ResponseEntity<String> response = null;
|
||||
try {
|
||||
response = this.restOperations.exchange(request, String.class);
|
||||
} catch (Exception ex) {
|
||||
throwInvalidClient("jwk_set_response_error", ex);
|
||||
}
|
||||
if (response.getStatusCode().value() != 200) {
|
||||
throwInvalidClient("jwk_set_response_status");
|
||||
}
|
||||
|
||||
JWKSet jwkSet = null;
|
||||
try {
|
||||
jwkSet = JWKSet.parse(response.getBody());
|
||||
} catch (ParseException ex) {
|
||||
throwInvalidClient("jwk_set_response_body", ex);
|
||||
}
|
||||
|
||||
return jwkSet;
|
||||
}
|
||||
|
||||
private class JwkSetHolder implements Supplier<JWKSet> {
|
||||
private final String jwkSetUrl;
|
||||
private JWKSet jwkSet;
|
||||
|
||||
private JwkSetHolder(String jwkSetUrl) {
|
||||
this.jwkSetUrl = jwkSetUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JWKSet get() {
|
||||
if (this.jwkSet == null) {
|
||||
this.jwkSet = retrieve(this.jwkSetUrl);
|
||||
}
|
||||
return this.jwkSet;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -129,6 +129,7 @@ public final class OidcProviderConfigurationEndpointFilter extends OncePerReques
|
||||
authenticationMethods.add(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue());
|
||||
authenticationMethods.add(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue());
|
||||
authenticationMethods.add("tls_client_auth");
|
||||
authenticationMethods.add("self_signed_tls_client_auth");
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -122,6 +122,7 @@ public final class OAuth2AuthorizationServerMetadataEndpointFilter extends OnceP
|
||||
authenticationMethods.add(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue());
|
||||
authenticationMethods.add(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue());
|
||||
authenticationMethods.add("tls_client_auth");
|
||||
authenticationMethods.add("self_signed_tls_client_auth");
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ 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.
|
||||
* using the {@code tls_client_auth} or {@code self_signed_tls_client_auth} method.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 1.3
|
||||
@@ -46,13 +46,15 @@ import org.springframework.util.StringUtils;
|
||||
public final class X509ClientCertificateAuthenticationConverter implements AuthenticationConverter {
|
||||
private static final ClientAuthenticationMethod TLS_CLIENT_AUTH_AUTHENTICATION_METHOD =
|
||||
new ClientAuthenticationMethod("tls_client_auth");
|
||||
private static final ClientAuthenticationMethod SELF_SIGNED_TLS_CLIENT_AUTH_AUTHENTICATION_METHOD =
|
||||
new ClientAuthenticationMethod("self_signed_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) {
|
||||
if (clientCertificateChain == null || clientCertificateChain.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -68,7 +70,12 @@ public final class X509ClientCertificateAuthenticationConverter implements Authe
|
||||
Map<String, Object> additionalParameters = OAuth2EndpointUtils.getParametersIfMatchesAuthorizationCodeGrantRequest(
|
||||
request, OAuth2ParameterNames.CLIENT_ID);
|
||||
|
||||
return new OAuth2ClientAuthenticationToken(clientId, TLS_CLIENT_AUTH_AUTHENTICATION_METHOD,
|
||||
ClientAuthenticationMethod clientAuthenticationMethod =
|
||||
clientCertificateChain.length == 1 ?
|
||||
SELF_SIGNED_TLS_CLIENT_AUTH_AUTHENTICATION_METHOD :
|
||||
TLS_CLIENT_AUTH_AUTHENTICATION_METHOD;
|
||||
|
||||
return new OAuth2ClientAuthenticationToken(clientId, clientAuthenticationMethod,
|
||||
clientCertificateChain, additionalParameters);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,12 +15,27 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.KeyUse;
|
||||
import com.nimbusds.jose.jwk.RSAKey;
|
||||
import com.nimbusds.jose.util.Base64;
|
||||
import okhttp3.mockwebserver.Dispatcher;
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
import okhttp3.mockwebserver.RecordedRequest;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
@@ -59,18 +74,47 @@ public class X509ClientCertificateAuthenticationProviderTests {
|
||||
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 static final ClientAuthenticationMethod SELF_SIGNED_TLS_CLIENT_AUTH_AUTHENTICATION_METHOD =
|
||||
new ClientAuthenticationMethod("self_signed_tls_client_auth");
|
||||
private JWKSet selfSignedCertificateJwkSet;
|
||||
private MockWebServer server;
|
||||
private String clientJwkSetUrl;
|
||||
private RegisteredClientRepository registeredClientRepository;
|
||||
private OAuth2AuthorizationService authorizationService;
|
||||
private X509ClientCertificateAuthenticationProvider authenticationProvider;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
public void setUp() throws Exception {
|
||||
// @formatter:off
|
||||
X509Certificate selfSignedCertificate = TestX509Certificates.DEMO_CLIENT_SELF_SIGNED_CERTIFICATE[0];
|
||||
RSAKey selfSignedRSAKey = new RSAKey.Builder((RSAPublicKey) selfSignedCertificate.getPublicKey())
|
||||
.keyUse(KeyUse.SIGNATURE)
|
||||
.keyID(UUID.randomUUID().toString())
|
||||
.x509CertChain(Collections.singletonList(Base64.encode(selfSignedCertificate.getEncoded())))
|
||||
.build();
|
||||
// @formatter:on
|
||||
this.selfSignedCertificateJwkSet = new JWKSet(selfSignedRSAKey);
|
||||
this.server = new MockWebServer();
|
||||
this.server.start();
|
||||
this.clientJwkSetUrl = this.server.url("/jwks").toString();
|
||||
// @formatter:off
|
||||
MockResponse response = new MockResponse()
|
||||
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
|
||||
.setBody(this.selfSignedCertificateJwkSet.toString());
|
||||
// @formatter:on
|
||||
this.server.enqueue(response);
|
||||
|
||||
this.registeredClientRepository = mock(RegisteredClientRepository.class);
|
||||
this.authorizationService = mock(OAuth2AuthorizationService.class);
|
||||
this.authenticationProvider = new X509ClientCertificateAuthenticationProvider(
|
||||
this.registeredClientRepository, this.authorizationService);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() throws Exception {
|
||||
this.server.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenRegisteredClientRepositoryNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> new X509ClientCertificateAuthenticationProvider(null, this.authorizationService))
|
||||
@@ -159,7 +203,7 @@ public class X509ClientCertificateAuthenticationProviderTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenInvalidX509CertificateSubjectDNThenThrowOAuth2AuthenticationException() {
|
||||
public void authenticateWhenPKIX509CertificateInvalidSubjectDNThenThrowOAuth2AuthenticationException() {
|
||||
// @formatter:off
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||
.clientAuthenticationMethod(TLS_CLIENT_AUTH_AUTHENTICATION_METHOD)
|
||||
@@ -186,7 +230,7 @@ public class X509ClientCertificateAuthenticationProviderTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenValidX509CertificateThenAuthenticated() {
|
||||
public void authenticateWhenPKIX509CertificateValidThenAuthenticated() {
|
||||
// @formatter:off
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||
.clientAuthenticationMethod(TLS_CLIENT_AUTH_AUTHENTICATION_METHOD)
|
||||
@@ -214,6 +258,191 @@ public class X509ClientCertificateAuthenticationProviderTests {
|
||||
assertThat(authenticationResult.getClientAuthenticationMethod()).isEqualTo(TLS_CLIENT_AUTH_AUTHENTICATION_METHOD);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenSelfSignedX509CertificateInvalidIssuerThenThrowOAuth2AuthenticationException() {
|
||||
// @formatter:off
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||
.clientAuthenticationMethod(SELF_SIGNED_TLS_CLIENT_AUTH_AUTHENTICATION_METHOD)
|
||||
.clientSettings(
|
||||
ClientSettings.builder()
|
||||
.jwkSetUrl(this.clientJwkSetUrl)
|
||||
.build()
|
||||
)
|
||||
.build();
|
||||
// @formatter:on
|
||||
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||
.thenReturn(registeredClient);
|
||||
|
||||
OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
|
||||
registeredClient.getClientId(), SELF_SIGNED_TLS_CLIENT_AUTH_AUTHENTICATION_METHOD,
|
||||
TestX509Certificates.DEMO_CLIENT_PKI_CERTIFICATE, null); // PKI Certificate will have different issuer
|
||||
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_issuer");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenSelfSignedX509CertificateMissingClientJwkSetUrlThenThrowOAuth2AuthenticationException() {
|
||||
// @formatter:off
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||
.clientAuthenticationMethod(SELF_SIGNED_TLS_CLIENT_AUTH_AUTHENTICATION_METHOD)
|
||||
.build();
|
||||
// @formatter:on
|
||||
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||
.thenReturn(registeredClient);
|
||||
|
||||
OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
|
||||
registeredClient.getClientId(), SELF_SIGNED_TLS_CLIENT_AUTH_AUTHENTICATION_METHOD,
|
||||
TestX509Certificates.DEMO_CLIENT_SELF_SIGNED_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("client_jwk_set_url");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenSelfSignedX509CertificateInvalidClientJwkSetUrlThenThrowOAuth2AuthenticationException() {
|
||||
// @formatter:off
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||
.clientAuthenticationMethod(SELF_SIGNED_TLS_CLIENT_AUTH_AUTHENTICATION_METHOD)
|
||||
.clientSettings(
|
||||
ClientSettings.builder()
|
||||
.jwkSetUrl("https://this is an invalid URL")
|
||||
.build()
|
||||
)
|
||||
.build();
|
||||
// @formatter:on
|
||||
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||
.thenReturn(registeredClient);
|
||||
|
||||
OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
|
||||
registeredClient.getClientId(), SELF_SIGNED_TLS_CLIENT_AUTH_AUTHENTICATION_METHOD,
|
||||
TestX509Certificates.DEMO_CLIENT_SELF_SIGNED_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("jwk_set_uri");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenSelfSignedX509CertificateJwkSetResponseErrorStatusThenThrowOAuth2AuthenticationException() {
|
||||
MockResponse jwkSetResponse = new MockResponse().setResponseCode(400);
|
||||
authenticateWhenSelfSignedX509CertificateJwkSetResponseInvalidThenThrowOAuth2AuthenticationException(
|
||||
jwkSetResponse, "jwk_set_response_error");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenSelfSignedX509CertificateJwkSetResponseInvalidStatusThenThrowOAuth2AuthenticationException() {
|
||||
MockResponse jwkSetResponse = new MockResponse().setResponseCode(204);
|
||||
authenticateWhenSelfSignedX509CertificateJwkSetResponseInvalidThenThrowOAuth2AuthenticationException(
|
||||
jwkSetResponse, "jwk_set_response_status");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenSelfSignedX509CertificateJwkSetResponseInvalidContentThenThrowOAuth2AuthenticationException() {
|
||||
MockResponse jwkSetResponse = new MockResponse().setResponseCode(200).setBody("invalid-content");
|
||||
authenticateWhenSelfSignedX509CertificateJwkSetResponseInvalidThenThrowOAuth2AuthenticationException(
|
||||
jwkSetResponse, "jwk_set_response_body");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenSelfSignedX509CertificateJwkSetResponseNoMatchingKeysThenThrowOAuth2AuthenticationException() throws Exception {
|
||||
// @formatter:off
|
||||
X509Certificate pkiCertificate = TestX509Certificates.DEMO_CLIENT_PKI_CERTIFICATE[0];
|
||||
RSAKey pkiRSAKey = new RSAKey.Builder((RSAPublicKey) pkiCertificate.getPublicKey())
|
||||
.keyUse(KeyUse.SIGNATURE)
|
||||
.keyID(UUID.randomUUID().toString())
|
||||
.x509CertChain(Collections.singletonList(Base64.encode(pkiCertificate.getEncoded())))
|
||||
.build();
|
||||
// @formatter:on
|
||||
|
||||
// @formatter:off
|
||||
MockResponse jwkSetResponse = new MockResponse()
|
||||
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
|
||||
.setBody(new JWKSet(pkiRSAKey).toString());
|
||||
// @formatter:on
|
||||
|
||||
authenticateWhenSelfSignedX509CertificateJwkSetResponseInvalidThenThrowOAuth2AuthenticationException(
|
||||
jwkSetResponse, "x509_certificate");
|
||||
}
|
||||
|
||||
private void authenticateWhenSelfSignedX509CertificateJwkSetResponseInvalidThenThrowOAuth2AuthenticationException(
|
||||
final MockResponse jwkSetResponse, String expectedErrorDescription) {
|
||||
|
||||
// @formatter:off
|
||||
final Dispatcher dispatcher = new Dispatcher() {
|
||||
@Override
|
||||
public MockResponse dispatch(RecordedRequest request) {
|
||||
return jwkSetResponse;
|
||||
}
|
||||
};
|
||||
this.server.setDispatcher(dispatcher);
|
||||
// @formatter:on
|
||||
|
||||
// @formatter:off
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||
.clientAuthenticationMethod(SELF_SIGNED_TLS_CLIENT_AUTH_AUTHENTICATION_METHOD)
|
||||
.clientSettings(
|
||||
ClientSettings.builder()
|
||||
.jwkSetUrl(this.clientJwkSetUrl)
|
||||
.build()
|
||||
)
|
||||
.build();
|
||||
// @formatter:on
|
||||
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||
.thenReturn(registeredClient);
|
||||
|
||||
OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
|
||||
registeredClient.getClientId(), SELF_SIGNED_TLS_CLIENT_AUTH_AUTHENTICATION_METHOD,
|
||||
TestX509Certificates.DEMO_CLIENT_SELF_SIGNED_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(expectedErrorDescription);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenSelfSignedX509CertificateValidThenAuthenticated() {
|
||||
// @formatter:off
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||
.clientAuthenticationMethod(SELF_SIGNED_TLS_CLIENT_AUTH_AUTHENTICATION_METHOD)
|
||||
.clientSettings(
|
||||
ClientSettings.builder()
|
||||
.jwkSetUrl(this.clientJwkSetUrl)
|
||||
.build()
|
||||
)
|
||||
.build();
|
||||
// @formatter:on
|
||||
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||
.thenReturn(registeredClient);
|
||||
|
||||
OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
|
||||
registeredClient.getClientId(), SELF_SIGNED_TLS_CLIENT_AUTH_AUTHENTICATION_METHOD,
|
||||
TestX509Certificates.DEMO_CLIENT_SELF_SIGNED_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_SELF_SIGNED_CERTIFICATE);
|
||||
assertThat(authenticationResult.getRegisteredClient()).isEqualTo(registeredClient);
|
||||
assertThat(authenticationResult.getClientAuthenticationMethod()).isEqualTo(SELF_SIGNED_TLS_CLIENT_AUTH_AUTHENTICATION_METHOD);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenPkceAndValidCodeVerifierThenAuthenticated() {
|
||||
// @formatter:off
|
||||
|
||||
@@ -264,7 +264,7 @@ public class OAuth2ClientCredentialsGrantTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenTokenRequestWithX509ClientCertificateThenTokenResponse() throws Exception {
|
||||
public void requestWhenTokenRequestWithPKIX509ClientCertificateThenTokenResponse() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfiguration.class).autowire();
|
||||
|
||||
// @formatter:off
|
||||
|
||||
@@ -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\",\"tls_client_auth\"]");
|
||||
assertThat(providerConfigurationResponse).contains("\"revocation_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"client_secret_jwt\",\"private_key_jwt\",\"tls_client_auth\",\"self_signed_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\",\"tls_client_auth\"]");
|
||||
assertThat(providerConfigurationResponse).contains("\"introspection_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"client_secret_jwt\",\"private_key_jwt\",\"tls_client_auth\",\"self_signed_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\",\"tls_client_auth\"]");
|
||||
assertThat(providerConfigurationResponse).contains("\"token_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"client_secret_jwt\",\"private_key_jwt\",\"tls_client_auth\",\"self_signed_tls_client_auth\"]");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -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\",\"tls_client_auth\"]");
|
||||
assertThat(authorizationServerMetadataResponse).contains("\"token_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"client_secret_jwt\",\"private_key_jwt\",\"tls_client_auth\",\"self_signed_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\",\"tls_client_auth\"]");
|
||||
assertThat(authorizationServerMetadataResponse).contains("\"revocation_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"client_secret_jwt\",\"private_key_jwt\",\"tls_client_auth\",\"self_signed_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\",\"tls_client_auth\"]");
|
||||
assertThat(authorizationServerMetadataResponse).contains("\"introspection_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"client_secret_jwt\",\"private_key_jwt\",\"tls_client_auth\",\"self_signed_tls_client_auth\"]");
|
||||
assertThat(authorizationServerMetadataResponse).contains("\"code_challenge_methods_supported\":[\"S256\"]");
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.web.authentication;
|
||||
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
@@ -46,10 +48,10 @@ public class X509ClientCertificateAuthenticationConverterTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertWhenSelfSignedX509CertificateThenReturnNull() {
|
||||
public void convertWhenEmptyX509CertificateThenReturnNull() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setAttribute("jakarta.servlet.request.X509Certificate",
|
||||
TestX509Certificates.DEMO_CLIENT_SELF_SIGNED_CERTIFICATE);
|
||||
new X509Certificate[0]);
|
||||
Authentication authentication = this.converter.convert(request);
|
||||
assertThat(authentication).isNull();
|
||||
}
|
||||
@@ -102,4 +104,26 @@ public class X509ClientCertificateAuthenticationConverterTests {
|
||||
entry("custom-param-2", new String[] {"custom-value-1", "custom-value-2"}));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertWhenSelfSignedX509CertificateThenReturnClientAuthenticationToken() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setAttribute("jakarta.servlet.request.X509Certificate",
|
||||
TestX509Certificates.DEMO_CLIENT_SELF_SIGNED_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_SELF_SIGNED_CERTIFICATE);
|
||||
assertThat(authentication.getClientAuthenticationMethod().getValue()).isEqualTo("self_signed_tls_client_auth");
|
||||
assertThat(authentication.getAdditionalParameters())
|
||||
.containsOnly(
|
||||
entry(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue()),
|
||||
entry(OAuth2ParameterNames.CODE, "code"),
|
||||
entry("custom-param-1", "custom-value-1"),
|
||||
entry("custom-param-2", new String[] {"custom-value-1", "custom-value-2"}));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user