Skip to content

Commit 644cfa9

Browse files
committed
Add Jwt validator for the X509Certificate thumbprint claim
Closes gh-10538
1 parent 2d24e09 commit 644cfa9

File tree

7 files changed

+526
-2
lines changed

7 files changed

+526
-2
lines changed

oauth2/oauth2-jose/spring-security-oauth2-jose.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ dependencies {
1010
optional 'io.projectreactor:reactor-core'
1111
optional 'org.springframework:spring-webflux'
1212

13+
testImplementation "org.bouncycastle:bcpkix-jdk15on"
14+
testImplementation "org.bouncycastle:bcprov-jdk15on"
15+
testImplementation "jakarta.servlet:jakarta.servlet-api"
1316
testImplementation 'com.squareup.okhttp3:mockwebserver'
1417
testImplementation 'io.projectreactor.netty:reactor-netty'
1518
testImplementation 'com.fasterxml.jackson.core:jackson-databind'

oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtValidators.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@ public static OAuth2TokenValidator<Jwt> createDefaultWithIssuer(String issuer) {
6868
* supplied
6969
*/
7070
public static OAuth2TokenValidator<Jwt> createDefault() {
71-
return new DelegatingOAuth2TokenValidator<>(Arrays.asList(new JwtTimestampValidator()));
71+
return new DelegatingOAuth2TokenValidator<>(
72+
Arrays.asList(new JwtTimestampValidator(), new X509CertificateThumbprintValidator(
73+
X509CertificateThumbprintValidator.DEFAULT_X509_CERTIFICATE_SUPPLIER)));
7274
}
7375

7476
/**
@@ -84,6 +86,12 @@ public static OAuth2TokenValidator<Jwt> createDefault() {
8486
public static OAuth2TokenValidator<Jwt> createDefaultWithValidators(List<OAuth2TokenValidator<Jwt>> validators) {
8587
Assert.notEmpty(validators, "validators cannot be null or empty");
8688
List<OAuth2TokenValidator<Jwt>> tokenValidators = new ArrayList<>(validators);
89+
X509CertificateThumbprintValidator x509CertificateThumbprintValidator = CollectionUtils
90+
.findValueOfType(tokenValidators, X509CertificateThumbprintValidator.class);
91+
if (x509CertificateThumbprintValidator == null) {
92+
tokenValidators.add(0, new X509CertificateThumbprintValidator(
93+
X509CertificateThumbprintValidator.DEFAULT_X509_CERTIFICATE_SUPPLIER));
94+
}
8795
JwtTimestampValidator jwtTimestampValidator = CollectionUtils.findValueOfType(tokenValidators,
8896
JwtTimestampValidator.class);
8997
if (jwtTimestampValidator == null) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.oauth2.jwt;
18+
19+
import java.security.MessageDigest;
20+
import java.security.cert.X509Certificate;
21+
import java.util.Base64;
22+
import java.util.Map;
23+
import java.util.function.Supplier;
24+
25+
import org.apache.commons.logging.Log;
26+
import org.apache.commons.logging.LogFactory;
27+
28+
import org.springframework.security.oauth2.core.OAuth2Error;
29+
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
30+
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
31+
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
32+
import org.springframework.util.Assert;
33+
import org.springframework.util.CollectionUtils;
34+
import org.springframework.web.context.request.RequestAttributes;
35+
import org.springframework.web.context.request.RequestContextHolder;
36+
37+
/**
38+
* An {@link OAuth2TokenValidator} responsible for validating the {@code x5t#S256} claim
39+
* (if available) in the {@link Jwt} against the SHA-256 Thumbprint of the supplied
40+
* {@code X509Certificate}.
41+
*
42+
* @author Joe Grandja
43+
* @since 6.3
44+
* @see OAuth2TokenValidator
45+
* @see Jwt
46+
* @see <a target="_blank" href=
47+
* "https://datatracker.ietf.org/doc/html/rfc8705#section-3">3. Mutual-TLS Client
48+
* Certificate-Bound Access Tokens</a>
49+
* @see <a target="_blank" href=
50+
* "https://datatracker.ietf.org/doc/html/rfc8705#section-3.1">3.1. JWT Certificate
51+
* Thumbprint Confirmation Method</a>
52+
*/
53+
final class X509CertificateThumbprintValidator implements OAuth2TokenValidator<Jwt> {
54+
55+
static final Supplier<X509Certificate> DEFAULT_X509_CERTIFICATE_SUPPLIER = new DefaultX509CertificateSupplier();
56+
57+
private final Log logger = LogFactory.getLog(getClass());
58+
59+
private final Supplier<X509Certificate> x509CertificateSupplier;
60+
61+
X509CertificateThumbprintValidator(Supplier<X509Certificate> x509CertificateSupplier) {
62+
Assert.notNull(x509CertificateSupplier, "x509CertificateSupplier cannot be null");
63+
this.x509CertificateSupplier = x509CertificateSupplier;
64+
}
65+
66+
@Override
67+
public OAuth2TokenValidatorResult validate(Jwt jwt) {
68+
Map<String, Object> confirmationMethodClaim = jwt.getClaim("cnf");
69+
String x509CertificateThumbprintClaim = null;
70+
if (!CollectionUtils.isEmpty(confirmationMethodClaim) && confirmationMethodClaim.containsKey("x5t#S256")) {
71+
x509CertificateThumbprintClaim = (String) confirmationMethodClaim.get("x5t#S256");
72+
}
73+
if (x509CertificateThumbprintClaim == null) {
74+
return OAuth2TokenValidatorResult.success();
75+
}
76+
77+
X509Certificate x509Certificate = this.x509CertificateSupplier.get();
78+
if (x509Certificate == null) {
79+
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN,
80+
"Unable to obtain X509Certificate from current request.", null);
81+
if (this.logger.isDebugEnabled()) {
82+
this.logger.debug(error.toString());
83+
}
84+
return OAuth2TokenValidatorResult.failure(error);
85+
}
86+
87+
String x509CertificateThumbprint;
88+
try {
89+
x509CertificateThumbprint = computeSHA256Thumbprint(x509Certificate);
90+
}
91+
catch (Exception ex) {
92+
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN,
93+
"Failed to compute SHA-256 Thumbprint for X509Certificate.", null);
94+
if (this.logger.isDebugEnabled()) {
95+
this.logger.debug(error.toString());
96+
}
97+
return OAuth2TokenValidatorResult.failure(error);
98+
}
99+
100+
if (!x509CertificateThumbprint.equals(x509CertificateThumbprintClaim)) {
101+
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN,
102+
"Invalid SHA-256 Thumbprint for X509Certificate.", null);
103+
if (this.logger.isDebugEnabled()) {
104+
this.logger.debug(error.toString());
105+
}
106+
return OAuth2TokenValidatorResult.failure(error);
107+
}
108+
109+
return OAuth2TokenValidatorResult.success();
110+
}
111+
112+
static String computeSHA256Thumbprint(X509Certificate x509Certificate) throws Exception {
113+
MessageDigest md = MessageDigest.getInstance("SHA-256");
114+
byte[] digest = md.digest(x509Certificate.getEncoded());
115+
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
116+
}
117+
118+
private static final class DefaultX509CertificateSupplier implements Supplier<X509Certificate> {
119+
120+
@Override
121+
public X509Certificate get() {
122+
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
123+
if (requestAttributes == null) {
124+
return null;
125+
}
126+
127+
X509Certificate[] clientCertificateChain = (X509Certificate[]) requestAttributes
128+
.getAttribute("jakarta.servlet.request.X509Certificate", RequestAttributes.SCOPE_REQUEST);
129+
130+
return (clientCertificateChain != null && clientCertificateChain.length > 0) ? clientCertificateChain[0]
131+
: null;
132+
}
133+
134+
}
135+
136+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.oauth2.jose;
18+
19+
import java.security.KeyPair;
20+
import java.security.cert.X509Certificate;
21+
22+
/**
23+
* @author Joe Grandja
24+
* @since 6.3
25+
*/
26+
public final class TestX509Certificates {
27+
28+
public static final X509Certificate[] DEFAULT_PKI_CERTIFICATE;
29+
static {
30+
try {
31+
// Generate the Root certificate (Trust Anchor or most-trusted CA)
32+
KeyPair rootKeyPair = X509CertificateUtils.generateRSAKeyPair();
33+
String distinguishedName = "CN=spring-samples-trusted-ca, OU=Spring Samples, O=Spring, C=US";
34+
X509Certificate rootCertificate = X509CertificateUtils.createTrustAnchorCertificate(rootKeyPair,
35+
distinguishedName);
36+
37+
// Generate the CA (intermediary) certificate
38+
KeyPair caKeyPair = X509CertificateUtils.generateRSAKeyPair();
39+
distinguishedName = "CN=spring-samples-ca, OU=Spring Samples, O=Spring, C=US";
40+
X509Certificate caCertificate = X509CertificateUtils.createCACertificate(rootCertificate,
41+
rootKeyPair.getPrivate(), caKeyPair.getPublic(), distinguishedName);
42+
43+
// Generate certificate for subject1
44+
KeyPair subject1KeyPair = X509CertificateUtils.generateRSAKeyPair();
45+
distinguishedName = "CN=subject1, OU=Spring Samples, O=Spring, C=US";
46+
X509Certificate subject1Certificate = X509CertificateUtils.createEndEntityCertificate(caCertificate,
47+
caKeyPair.getPrivate(), subject1KeyPair.getPublic(), distinguishedName);
48+
49+
DEFAULT_PKI_CERTIFICATE = new X509Certificate[] { subject1Certificate, caCertificate, rootCertificate };
50+
}
51+
catch (Exception ex) {
52+
throw new IllegalStateException(ex);
53+
}
54+
}
55+
56+
public static final X509Certificate[] DEFAULT_SELF_SIGNED_CERTIFICATE;
57+
static {
58+
try {
59+
// Generate self-signed certificate for subject1
60+
KeyPair keyPair = X509CertificateUtils.generateRSAKeyPair();
61+
String distinguishedName = "CN=subject1, OU=Spring Samples, O=Spring, C=US";
62+
X509Certificate subject1SelfSignedCertificate = X509CertificateUtils.createTrustAnchorCertificate(keyPair,
63+
distinguishedName);
64+
65+
DEFAULT_SELF_SIGNED_CERTIFICATE = new X509Certificate[] { subject1SelfSignedCertificate };
66+
}
67+
catch (Exception ex) {
68+
throw new IllegalStateException(ex);
69+
}
70+
}
71+
72+
private TestX509Certificates() {
73+
}
74+
75+
}

0 commit comments

Comments
 (0)