-
Notifications
You must be signed in to change notification settings - Fork 5.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Jwt validator for the X509Certificate thumbprint claim
Closes gh-10538
- Loading branch information
Showing
7 changed files
with
526 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
136 changes: 136 additions & 0 deletions
136
...main/java/org/springframework/security/oauth2/jwt/X509CertificateThumbprintValidator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
|
||
} | ||
|
||
} |
75 changes: 75 additions & 0 deletions
75
...th2-jose/src/test/java/org/springframework/security/oauth2/jose/TestX509Certificates.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() { | ||
} | ||
|
||
} |
Oops, something went wrong.