Skip to content

Commit

Permalink
Add Jwt validator for the X509Certificate thumbprint claim
Browse files Browse the repository at this point in the history
Closes gh-10538
  • Loading branch information
jgrandja committed Apr 11, 2024
1 parent 2d24e09 commit 644cfa9
Show file tree
Hide file tree
Showing 7 changed files with 526 additions and 2 deletions.
3 changes: 3 additions & 0 deletions oauth2/oauth2-jose/spring-security-oauth2-jose.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ public static OAuth2TokenValidator<Jwt> createDefaultWithIssuer(String issuer) {
* 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)));
}

/**
Expand All @@ -84,6 +86,12 @@ public static OAuth2TokenValidator<Jwt> createDefault() {
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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}

}

}
Original file line number Diff line number Diff line change
@@ -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() {
}

}
Loading

0 comments on commit 644cfa9

Please sign in to comment.