mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-05-31 09:12:14 +00:00
Add Jwt validator for the X509Certificate thumbprint claim
Closes gh-10538
This commit is contained in:
parent
2d24e09665
commit
644cfa9f87
@ -10,6 +10,9 @@ dependencies {
|
||||
optional 'io.projectreactor:reactor-core'
|
||||
optional 'org.springframework:spring-webflux'
|
||||
|
||||
testImplementation "org.bouncycastle:bcpkix-jdk15on"
|
||||
testImplementation "org.bouncycastle:bcprov-jdk15on"
|
||||
testImplementation "jakarta.servlet:jakarta.servlet-api"
|
||||
testImplementation 'com.squareup.okhttp3:mockwebserver'
|
||||
testImplementation 'io.projectreactor.netty:reactor-netty'
|
||||
testImplementation 'com.fasterxml.jackson.core:jackson-databind'
|
||||
|
@ -68,7 +68,9 @@ public final class JwtValidators {
|
||||
* supplied
|
||||
*/
|
||||
public static OAuth2TokenValidator<Jwt> createDefault() {
|
||||
return new DelegatingOAuth2TokenValidator<>(Arrays.asList(new JwtTimestampValidator()));
|
||||
return new DelegatingOAuth2TokenValidator<>(
|
||||
Arrays.asList(new JwtTimestampValidator(), new X509CertificateThumbprintValidator(
|
||||
X509CertificateThumbprintValidator.DEFAULT_X509_CERTIFICATE_SUPPLIER)));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -84,6 +86,12 @@ public final class JwtValidators {
|
||||
public static OAuth2TokenValidator<Jwt> createDefaultWithValidators(List<OAuth2TokenValidator<Jwt>> validators) {
|
||||
Assert.notEmpty(validators, "validators cannot be null or empty");
|
||||
List<OAuth2TokenValidator<Jwt>> tokenValidators = new ArrayList<>(validators);
|
||||
X509CertificateThumbprintValidator x509CertificateThumbprintValidator = CollectionUtils
|
||||
.findValueOfType(tokenValidators, X509CertificateThumbprintValidator.class);
|
||||
if (x509CertificateThumbprintValidator == null) {
|
||||
tokenValidators.add(0, new X509CertificateThumbprintValidator(
|
||||
X509CertificateThumbprintValidator.DEFAULT_X509_CERTIFICATE_SUPPLIER));
|
||||
}
|
||||
JwtTimestampValidator jwtTimestampValidator = CollectionUtils.findValueOfType(tokenValidators,
|
||||
JwtTimestampValidator.class);
|
||||
if (jwtTimestampValidator == null) {
|
||||
|
@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright 2002-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.jwt;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
|
||||
/**
|
||||
* An {@link OAuth2TokenValidator} responsible for validating the {@code x5t#S256} claim
|
||||
* (if available) in the {@link Jwt} against the SHA-256 Thumbprint of the supplied
|
||||
* {@code X509Certificate}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 6.3
|
||||
* @see OAuth2TokenValidator
|
||||
* @see Jwt
|
||||
* @see <a target="_blank" href=
|
||||
* "https://datatracker.ietf.org/doc/html/rfc8705#section-3">3. Mutual-TLS Client
|
||||
* Certificate-Bound Access Tokens</a>
|
||||
* @see <a target="_blank" href=
|
||||
* "https://datatracker.ietf.org/doc/html/rfc8705#section-3.1">3.1. JWT Certificate
|
||||
* Thumbprint Confirmation Method</a>
|
||||
*/
|
||||
final class X509CertificateThumbprintValidator implements OAuth2TokenValidator<Jwt> {
|
||||
|
||||
static final Supplier<X509Certificate> DEFAULT_X509_CERTIFICATE_SUPPLIER = new DefaultX509CertificateSupplier();
|
||||
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private final Supplier<X509Certificate> x509CertificateSupplier;
|
||||
|
||||
X509CertificateThumbprintValidator(Supplier<X509Certificate> x509CertificateSupplier) {
|
||||
Assert.notNull(x509CertificateSupplier, "x509CertificateSupplier cannot be null");
|
||||
this.x509CertificateSupplier = x509CertificateSupplier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2TokenValidatorResult validate(Jwt jwt) {
|
||||
Map<String, Object> confirmationMethodClaim = jwt.getClaim("cnf");
|
||||
String x509CertificateThumbprintClaim = null;
|
||||
if (!CollectionUtils.isEmpty(confirmationMethodClaim) && confirmationMethodClaim.containsKey("x5t#S256")) {
|
||||
x509CertificateThumbprintClaim = (String) confirmationMethodClaim.get("x5t#S256");
|
||||
}
|
||||
if (x509CertificateThumbprintClaim == null) {
|
||||
return OAuth2TokenValidatorResult.success();
|
||||
}
|
||||
|
||||
X509Certificate x509Certificate = this.x509CertificateSupplier.get();
|
||||
if (x509Certificate == null) {
|
||||
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN,
|
||||
"Unable to obtain X509Certificate from current request.", null);
|
||||
if (this.logger.isDebugEnabled()) {
|
||||
this.logger.debug(error.toString());
|
||||
}
|
||||
return OAuth2TokenValidatorResult.failure(error);
|
||||
}
|
||||
|
||||
String x509CertificateThumbprint;
|
||||
try {
|
||||
x509CertificateThumbprint = computeSHA256Thumbprint(x509Certificate);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN,
|
||||
"Failed to compute SHA-256 Thumbprint for X509Certificate.", null);
|
||||
if (this.logger.isDebugEnabled()) {
|
||||
this.logger.debug(error.toString());
|
||||
}
|
||||
return OAuth2TokenValidatorResult.failure(error);
|
||||
}
|
||||
|
||||
if (!x509CertificateThumbprint.equals(x509CertificateThumbprintClaim)) {
|
||||
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN,
|
||||
"Invalid SHA-256 Thumbprint for X509Certificate.", null);
|
||||
if (this.logger.isDebugEnabled()) {
|
||||
this.logger.debug(error.toString());
|
||||
}
|
||||
return OAuth2TokenValidatorResult.failure(error);
|
||||
}
|
||||
|
||||
return OAuth2TokenValidatorResult.success();
|
||||
}
|
||||
|
||||
static String computeSHA256Thumbprint(X509Certificate x509Certificate) throws Exception {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
byte[] digest = md.digest(x509Certificate.getEncoded());
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
|
||||
}
|
||||
|
||||
private static final class DefaultX509CertificateSupplier implements Supplier<X509Certificate> {
|
||||
|
||||
@Override
|
||||
public X509Certificate get() {
|
||||
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
|
||||
if (requestAttributes == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
X509Certificate[] clientCertificateChain = (X509Certificate[]) requestAttributes
|
||||
.getAttribute("jakarta.servlet.request.X509Certificate", RequestAttributes.SCOPE_REQUEST);
|
||||
|
||||
return (clientCertificateChain != null && clientCertificateChain.length > 0) ? clientCertificateChain[0]
|
||||
: null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2002-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.jose;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
* @since 6.3
|
||||
*/
|
||||
public final class TestX509Certificates {
|
||||
|
||||
public static final X509Certificate[] DEFAULT_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 subject1
|
||||
KeyPair subject1KeyPair = X509CertificateUtils.generateRSAKeyPair();
|
||||
distinguishedName = "CN=subject1, OU=Spring Samples, O=Spring, C=US";
|
||||
X509Certificate subject1Certificate = X509CertificateUtils.createEndEntityCertificate(caCertificate,
|
||||
caKeyPair.getPrivate(), subject1KeyPair.getPublic(), distinguishedName);
|
||||
|
||||
DEFAULT_PKI_CERTIFICATE = new X509Certificate[] { subject1Certificate, caCertificate, rootCertificate };
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static final X509Certificate[] DEFAULT_SELF_SIGNED_CERTIFICATE;
|
||||
static {
|
||||
try {
|
||||
// Generate self-signed certificate for subject1
|
||||
KeyPair keyPair = X509CertificateUtils.generateRSAKeyPair();
|
||||
String distinguishedName = "CN=subject1, OU=Spring Samples, O=Spring, C=US";
|
||||
X509Certificate subject1SelfSignedCertificate = X509CertificateUtils.createTrustAnchorCertificate(keyPair,
|
||||
distinguishedName);
|
||||
|
||||
DEFAULT_SELF_SIGNED_CERTIFICATE = new X509Certificate[] { subject1SelfSignedCertificate };
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private TestX509Certificates() {
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,165 @@
|
||||
/*
|
||||
* Copyright 2002-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.jose;
|
||||
|
||||
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
|
||||
* @since 6.3
|
||||
*/
|
||||
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() {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
}
|
@ -46,6 +46,7 @@ public class JwtValidatorsTests {
|
||||
|
||||
assertThat(containsByType(validator, JwtIssuerValidator.class)).isTrue();
|
||||
assertThat(containsByType(validator, JwtTimestampValidator.class)).isTrue();
|
||||
assertThat(containsByType(validator, X509CertificateThumbprintValidator.class)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -58,7 +59,8 @@ public class JwtValidatorsTests {
|
||||
.getField(delegatingOAuth2TokenValidator, "tokenValidators");
|
||||
|
||||
assertThat(containsByType(validator, JwtTimestampValidator.class)).isTrue();
|
||||
assertThat(Objects.requireNonNull(tokenValidators).size()).isEqualTo(1);
|
||||
assertThat(containsByType(validator, X509CertificateThumbprintValidator.class)).isTrue();
|
||||
assertThat(Objects.requireNonNull(tokenValidators).size()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright 2002-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.jwt;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.jose.TestX509Certificates;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
* @since 6.3
|
||||
*/
|
||||
class X509CertificateThumbprintValidatorTests {
|
||||
|
||||
private final X509CertificateThumbprintValidator validator = new X509CertificateThumbprintValidator(
|
||||
X509CertificateThumbprintValidator.DEFAULT_X509_CERTIFICATE_SUPPLIER);
|
||||
|
||||
@AfterEach
|
||||
void cleanup() {
|
||||
RequestContextHolder.resetRequestAttributes();
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructorWhenX509CertificateSupplierNullThenThrowIllegalArgumentException() {
|
||||
// @formatter:off
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new X509CertificateThumbprintValidator(null)).withMessage("x509CertificateSupplier cannot be null");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateWhenCnfClaimNotAvailableThenSuccess() {
|
||||
Jwt jwt = TestJwts.jwt().build();
|
||||
assertThat(this.validator.validate(jwt).hasErrors()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateWhenX5tClaimNotAvailableThenSuccess() {
|
||||
// @formatter:off
|
||||
Jwt jwt = TestJwts.jwt()
|
||||
.claim("cnf", Collections.emptyMap())
|
||||
.build();
|
||||
// @formatter:on
|
||||
assertThat(this.validator.validate(jwt).hasErrors()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateWhenX509CertificateMissingThenHasErrors() throws Exception {
|
||||
String sha256Thumbprint = X509CertificateThumbprintValidator
|
||||
.computeSHA256Thumbprint(TestX509Certificates.DEFAULT_PKI_CERTIFICATE[0]);
|
||||
// @formatter:off
|
||||
Jwt jwt = TestJwts.jwt()
|
||||
.claim("cnf", Collections.singletonMap("x5t#S256", sha256Thumbprint))
|
||||
.build();
|
||||
// @formatter:on
|
||||
|
||||
// @formatter:off
|
||||
assertThat(this.validator.validate(jwt).getErrors())
|
||||
.hasSize(1)
|
||||
.first()
|
||||
.satisfies((error) -> {
|
||||
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
|
||||
assertThat(error.getDescription()).isEqualTo("Unable to obtain X509Certificate from current request.");
|
||||
});
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateWhenX509CertificateThumbprintInvalidThenHasErrors() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setAttribute("jakarta.servlet.request.X509Certificate",
|
||||
TestX509Certificates.DEFAULT_SELF_SIGNED_CERTIFICATE);
|
||||
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request, null));
|
||||
|
||||
String sha256Thumbprint = X509CertificateThumbprintValidator
|
||||
.computeSHA256Thumbprint(TestX509Certificates.DEFAULT_PKI_CERTIFICATE[0]);
|
||||
// @formatter:off
|
||||
Jwt jwt = TestJwts.jwt()
|
||||
.claim("cnf", Collections.singletonMap("x5t#S256", sha256Thumbprint))
|
||||
.build();
|
||||
// @formatter:on
|
||||
|
||||
// @formatter:off
|
||||
assertThat(this.validator.validate(jwt).getErrors())
|
||||
.hasSize(1)
|
||||
.first()
|
||||
.satisfies((error) -> {
|
||||
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
|
||||
assertThat(error.getDescription()).isEqualTo("Invalid SHA-256 Thumbprint for X509Certificate.");
|
||||
});
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateWhenX509CertificateThumbprintValidThenSuccess() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setAttribute("jakarta.servlet.request.X509Certificate", TestX509Certificates.DEFAULT_PKI_CERTIFICATE);
|
||||
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request, null));
|
||||
|
||||
String sha256Thumbprint = X509CertificateThumbprintValidator
|
||||
.computeSHA256Thumbprint(TestX509Certificates.DEFAULT_PKI_CERTIFICATE[0]);
|
||||
// @formatter:off
|
||||
Jwt jwt = TestJwts.jwt()
|
||||
.claim("cnf", Collections.singletonMap("x5t#S256", sha256Thumbprint))
|
||||
.build();
|
||||
// @formatter:on
|
||||
|
||||
assertThat(this.validator.validate(jwt).hasErrors()).isFalse();
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user