Introduce JwtEncoder

Closes gh-9208
This commit is contained in:
Joe Grandja
2020-11-16 19:56:45 -05:00
parent 339a05312e
commit 5830fda2fa
18 changed files with 1070 additions and 458 deletions

View File

@@ -1,390 +0,0 @@
/*
* Copyright 2002-2021 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.client.endpoint;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import org.springframework.security.oauth2.core.converter.ClaimConversionService;
import org.springframework.security.oauth2.jose.JwaAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.util.Assert;
/*
* NOTE:
* This originated in gh-9208 (JwtEncoder),
* which is required to realize the feature in gh-8175 (JWT Client Authentication).
* However, we decided not to merge gh-9208 as part of the 5.5.0 release
* and instead packaged it up privately with the gh-8175 feature.
* We MAY merge gh-9208 in a later release but that is yet to be determined.
*
* gh-9208 Introduce JwtEncoder
* https://github.com/spring-projects/spring-security/pull/9208
*
* gh-8175 Support JWT for Client Authentication
* https://github.com/spring-projects/spring-security/issues/8175
*/
/**
* The JOSE header is a JSON object representing the header parameters of a JSON Web
* Token, whether the JWT is a JWS or JWE, that describe the cryptographic operations
* applied to the JWT and optionally, additional properties of the JWT.
*
* @author Anoop Garlapati
* @author Joe Grandja
* @since 5.5
* @see Jwt
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519#section-5">JWT JOSE
* Header</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515#section-4">JWS JOSE
* Header</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7516#section-4">JWE JOSE
* Header</a>
*/
final class JoseHeader {
private final Map<String, Object> headers;
private JoseHeader(Map<String, Object> headers) {
this.headers = Collections.unmodifiableMap(new HashMap<>(headers));
}
/**
* Returns the {@link JwaAlgorithm JWA algorithm} used to digitally sign the JWS or
* encrypt the JWE.
* @return the {@link JwaAlgorithm}
*/
@SuppressWarnings("unchecked")
<T extends JwaAlgorithm> T getAlgorithm() {
return (T) getHeader(JoseHeaderNames.ALG);
}
/**
* Returns the JWK Set URL that refers to the resource of a set of JSON-encoded public
* keys, one of which corresponds to the key used to digitally sign the JWS or encrypt
* the JWE.
* @return the JWK Set URL
*/
URL getJwkSetUrl() {
return getHeader(JoseHeaderNames.JKU);
}
/**
* Returns the JSON Web Key which is the public key that corresponds to the key used
* to digitally sign the JWS or encrypt the JWE.
* @return the JSON Web Key
*/
Map<String, Object> getJwk() {
return getHeader(JoseHeaderNames.JWK);
}
/**
* Returns the key ID that is a hint indicating which key was used to secure the JWS
* or JWE.
* @return the key ID
*/
String getKeyId() {
return getHeader(JoseHeaderNames.KID);
}
/**
* Returns the X.509 URL that refers to the resource for the X.509 public key
* certificate or certificate chain corresponding to the key used to digitally sign
* the JWS or encrypt the JWE.
* @return the X.509 URL
*/
URL getX509Url() {
return getHeader(JoseHeaderNames.X5U);
}
/**
* Returns the X.509 certificate chain that contains the X.509 public key certificate
* or certificate chain corresponding to the key used to digitally sign the JWS or
* encrypt the JWE. The certificate or certificate chain is represented as a
* {@code List} of certificate value {@code String}s. Each {@code String} in the
* {@code List} is a Base64-encoded DER PKIX certificate value.
* @return the X.509 certificate chain
*/
List<String> getX509CertificateChain() {
return getHeader(JoseHeaderNames.X5C);
}
/**
* Returns the X.509 certificate SHA-1 thumbprint that is a base64url-encoded SHA-1
* thumbprint (a.k.a. digest) of the DER encoding of the X.509 certificate
* corresponding to the key used to digitally sign the JWS or encrypt the JWE.
* @return the X.509 certificate SHA-1 thumbprint
*/
String getX509SHA1Thumbprint() {
return getHeader(JoseHeaderNames.X5T);
}
/**
* Returns the X.509 certificate SHA-256 thumbprint that is a base64url-encoded
* SHA-256 thumbprint (a.k.a. digest) of the DER encoding of the X.509 certificate
* corresponding to the key used to digitally sign the JWS or encrypt the JWE.
* @return the X.509 certificate SHA-256 thumbprint
*/
String getX509SHA256Thumbprint() {
return getHeader(JoseHeaderNames.X5T_S256);
}
/**
* Returns the type header that declares the media type of the JWS/JWE.
* @return the type header
*/
String getType() {
return getHeader(JoseHeaderNames.TYP);
}
/**
* Returns the content type header that declares the media type of the secured content
* (the payload).
* @return the content type header
*/
String getContentType() {
return getHeader(JoseHeaderNames.CTY);
}
/**
* Returns the critical headers that indicates which extensions to the JWS/JWE/JWA
* specifications are being used that MUST be understood and processed.
* @return the critical headers
*/
Set<String> getCritical() {
return getHeader(JoseHeaderNames.CRIT);
}
/**
* Returns the headers.
* @return the headers
*/
Map<String, Object> getHeaders() {
return this.headers;
}
/**
* Returns the header value.
* @param name the header name
* @param <T> the type of the header value
* @return the header value
*/
@SuppressWarnings("unchecked")
<T> T getHeader(String name) {
Assert.hasText(name, "name cannot be empty");
return (T) getHeaders().get(name);
}
/**
* Returns a new {@link Builder}, initialized with the provided {@link JwaAlgorithm}.
* @param jwaAlgorithm the {@link JwaAlgorithm}
* @return the {@link Builder}
*/
static Builder withAlgorithm(JwaAlgorithm jwaAlgorithm) {
return new Builder(jwaAlgorithm);
}
/**
* Returns a new {@link Builder}, initialized with the provided {@code headers}.
* @param headers the headers
* @return the {@link Builder}
*/
static Builder from(JoseHeader headers) {
return new Builder(headers);
}
/**
* A builder for {@link JoseHeader}.
*/
static final class Builder {
final Map<String, Object> headers = new HashMap<>();
private Builder(JwaAlgorithm jwaAlgorithm) {
algorithm(jwaAlgorithm);
}
private Builder(JoseHeader headers) {
Assert.notNull(headers, "headers cannot be null");
this.headers.putAll(headers.getHeaders());
}
/**
* Sets the {@link JwaAlgorithm JWA algorithm} used to digitally sign the JWS or
* encrypt the JWE.
* @param jwaAlgorithm the {@link JwaAlgorithm}
* @return the {@link Builder}
*/
Builder algorithm(JwaAlgorithm jwaAlgorithm) {
Assert.notNull(jwaAlgorithm, "jwaAlgorithm cannot be null");
return header(JoseHeaderNames.ALG, jwaAlgorithm);
}
/**
* Sets the JWK Set URL that refers to the resource of a set of JSON-encoded
* public keys, one of which corresponds to the key used to digitally sign the JWS
* or encrypt the JWE.
* @param jwkSetUrl the JWK Set URL
* @return the {@link Builder}
*/
Builder jwkSetUrl(String jwkSetUrl) {
return header(JoseHeaderNames.JKU, convertAsURL(JoseHeaderNames.JKU, jwkSetUrl));
}
/**
* Sets the JSON Web Key which is the public key that corresponds to the key used
* to digitally sign the JWS or encrypt the JWE.
* @param jwk the JSON Web Key
* @return the {@link Builder}
*/
Builder jwk(Map<String, Object> jwk) {
return header(JoseHeaderNames.JWK, jwk);
}
/**
* Sets the key ID that is a hint indicating which key was used to secure the JWS
* or JWE.
* @param keyId the key ID
* @return the {@link Builder}
*/
Builder keyId(String keyId) {
return header(JoseHeaderNames.KID, keyId);
}
/**
* Sets the X.509 URL that refers to the resource for the X.509 public key
* certificate or certificate chain corresponding to the key used to digitally
* sign the JWS or encrypt the JWE.
* @param x509Url the X.509 URL
* @return the {@link Builder}
*/
Builder x509Url(String x509Url) {
return header(JoseHeaderNames.X5U, convertAsURL(JoseHeaderNames.X5U, x509Url));
}
/**
* Sets the X.509 certificate chain that contains the X.509 public key certificate
* or certificate chain corresponding to the key used to digitally sign the JWS or
* encrypt the JWE. The certificate or certificate chain is represented as a
* {@code List} of certificate value {@code String}s. Each {@code String} in the
* {@code List} is a Base64-encoded DER PKIX certificate value.
* @param x509CertificateChain the X.509 certificate chain
* @return the {@link Builder}
*/
Builder x509CertificateChain(List<String> x509CertificateChain) {
return header(JoseHeaderNames.X5C, x509CertificateChain);
}
/**
* Sets the X.509 certificate SHA-1 thumbprint that is a base64url-encoded SHA-1
* thumbprint (a.k.a. digest) of the DER encoding of the X.509 certificate
* corresponding to the key used to digitally sign the JWS or encrypt the JWE.
* @param x509SHA1Thumbprint the X.509 certificate SHA-1 thumbprint
* @return the {@link Builder}
*/
Builder x509SHA1Thumbprint(String x509SHA1Thumbprint) {
return header(JoseHeaderNames.X5T, x509SHA1Thumbprint);
}
/**
* Sets the X.509 certificate SHA-256 thumbprint that is a base64url-encoded
* SHA-256 thumbprint (a.k.a. digest) of the DER encoding of the X.509 certificate
* corresponding to the key used to digitally sign the JWS or encrypt the JWE.
* @param x509SHA256Thumbprint the X.509 certificate SHA-256 thumbprint
* @return the {@link Builder}
*/
Builder x509SHA256Thumbprint(String x509SHA256Thumbprint) {
return header(JoseHeaderNames.X5T_S256, x509SHA256Thumbprint);
}
/**
* Sets the type header that declares the media type of the JWS/JWE.
* @param type the type header
* @return the {@link Builder}
*/
Builder type(String type) {
return header(JoseHeaderNames.TYP, type);
}
/**
* Sets the content type header that declares the media type of the secured
* content (the payload).
* @param contentType the content type header
* @return the {@link Builder}
*/
Builder contentType(String contentType) {
return header(JoseHeaderNames.CTY, contentType);
}
/**
* Sets the critical headers that indicates which extensions to the JWS/JWE/JWA
* specifications are being used that MUST be understood and processed.
* @param headerNames the critical header names
* @return the {@link Builder}
*/
Builder critical(Set<String> headerNames) {
return header(JoseHeaderNames.CRIT, headerNames);
}
/**
* Sets the header.
* @param name the header name
* @param value the header value
* @return the {@link Builder}
*/
Builder header(String name, Object value) {
Assert.hasText(name, "name cannot be empty");
Assert.notNull(value, "value cannot be null");
this.headers.put(name, value);
return this;
}
/**
* A {@code Consumer} to be provided access to the headers allowing the ability to
* add, replace, or remove.
* @param headersConsumer a {@code Consumer} of the headers
* @return the {@link Builder}
*/
Builder headers(Consumer<Map<String, Object>> headersConsumer) {
headersConsumer.accept(this.headers);
return this;
}
/**
* Builds a new {@link JoseHeader}.
* @return a {@link JoseHeader}
*/
JoseHeader build() {
Assert.notEmpty(this.headers, "headers cannot be empty");
return new JoseHeader(this.headers);
}
private static URL convertAsURL(String header, String value) {
URL convertedValue = ClaimConversionService.getSharedInstance().convert(value, URL.class);
Assert.isTrue(convertedValue != null,
() -> "Unable to convert header '" + header + "' of type '" + value.getClass() + "' to URL.");
return convertedValue;
}
}
}

View File

@@ -1,127 +0,0 @@
/*
* Copyright 2002-2021 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.client.endpoint;
/*
* NOTE:
* This originated in gh-9208 (JwtEncoder),
* which is required to realize the feature in gh-8175 (JWT Client Authentication).
* However, we decided not to merge gh-9208 as part of the 5.5.0 release
* and instead packaged it up privately with the gh-8175 feature.
* We MAY merge gh-9208 in a later release but that is yet to be determined.
*
* gh-9208 Introduce JwtEncoder
* https://github.com/spring-projects/spring-security/pull/9208
*
* gh-8175 Support JWT for Client Authentication
* https://github.com/spring-projects/spring-security/issues/8175
*/
/**
* The Registered Header Parameter Names defined by the JSON Web Token (JWT), JSON Web
* Signature (JWS) and JSON Web Encryption (JWE) specifications that may be contained in
* the JOSE Header of a JWT.
*
* @author Anoop Garlapati
* @author Joe Grandja
* @since 5.5
* @see JoseHeader
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519#section-5">JWT JOSE
* Header</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515#section-4">JWS JOSE
* Header</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7516#section-4">JWE JOSE
* Header</a>
*/
final class JoseHeaderNames {
/**
* {@code alg} - the algorithm header identifies the cryptographic algorithm used to
* secure a JWS or JWE
*/
static final String ALG = "alg";
/**
* {@code jku} - the JWK Set URL header is a URI that refers to a resource for a set
* of JSON-encoded public keys, one of which corresponds to the key used to digitally
* sign a JWS or encrypt a JWE
*/
static final String JKU = "jku";
/**
* {@code jwk} - the JSON Web Key header is the public key that corresponds to the key
* used to digitally sign a JWS or encrypt a JWE
*/
static final String JWK = "jwk";
/**
* {@code kid} - the key ID header is a hint indicating which key was used to secure a
* JWS or JWE
*/
static final String KID = "kid";
/**
* {@code x5u} - the X.509 URL header is a URI that refers to a resource for the X.509
* public key certificate or certificate chain corresponding to the key used to
* digitally sign a JWS or encrypt a JWE
*/
static final String X5U = "x5u";
/**
* {@code x5c} - the X.509 certificate chain header contains the X.509 public key
* certificate or certificate chain corresponding to the key used to digitally sign a
* JWS or encrypt a JWE
*/
static final String X5C = "x5c";
/**
* {@code x5t} - the X.509 certificate SHA-1 thumbprint header is a base64url-encoded
* SHA-1 thumbprint (a.k.a. digest) of the DER encoding of the X.509 certificate
* corresponding to the key used to digitally sign a JWS or encrypt a JWE
*/
static final String X5T = "x5t";
/**
* {@code x5t#S256} - the X.509 certificate SHA-256 thumbprint header is a
* base64url-encoded SHA-256 thumbprint (a.k.a. digest) of the DER encoding of the
* X.509 certificate corresponding to the key used to digitally sign a JWS or encrypt
* a JWE
*/
static final String X5T_S256 = "x5t#S256";
/**
* {@code typ} - the type header is used by JWS/JWE applications to declare the media
* type of a JWS/JWE
*/
static final String TYP = "typ";
/**
* {@code cty} - the content type header is used by JWS/JWE applications to declare
* the media type of the secured content (the payload)
*/
static final String CTY = "cty";
/**
* {@code crit} - the critical header indicates that extensions to the JWS/JWE/JWA
* specifications are being used that MUST be understood and processed
*/
static final String CRIT = "crit";
private JoseHeaderNames() {
}
}

View File

@@ -1,222 +0,0 @@
/*
* Copyright 2002-2021 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.client.endpoint;
import java.net.URL;
import java.time.Instant;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import org.springframework.security.oauth2.core.converter.ClaimConversionService;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimAccessor;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.util.Assert;
/*
* NOTE:
* This originated in gh-9208 (JwtEncoder),
* which is required to realize the feature in gh-8175 (JWT Client Authentication).
* However, we decided not to merge gh-9208 as part of the 5.5.0 release
* and instead packaged it up privately with the gh-8175 feature.
* We MAY merge gh-9208 in a later release but that is yet to be determined.
*
* gh-9208 Introduce JwtEncoder
* https://github.com/spring-projects/spring-security/pull/9208
*
* gh-8175 Support JWT for Client Authentication
* https://github.com/spring-projects/spring-security/issues/8175
*/
/**
* The {@link Jwt JWT} Claims Set is a JSON object representing the claims conveyed by a
* JSON Web Token.
*
* @author Anoop Garlapati
* @author Joe Grandja
* @since 5.5
* @see Jwt
* @see JwtClaimAccessor
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519#section-4">JWT Claims
* Set</a>
*/
final class JwtClaimsSet implements JwtClaimAccessor {
private final Map<String, Object> claims;
private JwtClaimsSet(Map<String, Object> claims) {
this.claims = Collections.unmodifiableMap(new HashMap<>(claims));
}
@Override
public Map<String, Object> getClaims() {
return this.claims;
}
/**
* Returns a new {@link Builder}.
* @return the {@link Builder}
*/
static Builder builder() {
return new Builder();
}
/**
* Returns a new {@link Builder}, initialized with the provided {@code claims}.
* @param claims a JWT claims set
* @return the {@link Builder}
*/
static Builder from(JwtClaimsSet claims) {
return new Builder(claims);
}
/**
* A builder for {@link JwtClaimsSet}.
*/
static final class Builder {
final Map<String, Object> claims = new HashMap<>();
private Builder() {
}
private Builder(JwtClaimsSet claims) {
Assert.notNull(claims, "claims cannot be null");
this.claims.putAll(claims.getClaims());
}
/**
* Sets the issuer {@code (iss)} claim, which identifies the principal that issued
* the JWT.
* @param issuer the issuer identifier
* @return the {@link Builder}
*/
Builder issuer(String issuer) {
return claim(JwtClaimNames.ISS, issuer);
}
/**
* Sets the subject {@code (sub)} claim, which identifies the principal that is
* the subject of the JWT.
* @param subject the subject identifier
* @return the {@link Builder}
*/
Builder subject(String subject) {
return claim(JwtClaimNames.SUB, subject);
}
/**
* Sets the audience {@code (aud)} claim, which identifies the recipient(s) that
* the JWT is intended for.
* @param audience the audience that this JWT is intended for
* @return the {@link Builder}
*/
Builder audience(List<String> audience) {
return claim(JwtClaimNames.AUD, audience);
}
/**
* Sets the expiration time {@code (exp)} claim, which identifies the time on or
* after which the JWT MUST NOT be accepted for processing.
* @param expiresAt the time on or after which the JWT MUST NOT be accepted for
* processing
* @return the {@link Builder}
*/
Builder expiresAt(Instant expiresAt) {
return claim(JwtClaimNames.EXP, expiresAt);
}
/**
* Sets the not before {@code (nbf)} claim, which identifies the time before which
* the JWT MUST NOT be accepted for processing.
* @param notBefore the time before which the JWT MUST NOT be accepted for
* processing
* @return the {@link Builder}
*/
Builder notBefore(Instant notBefore) {
return claim(JwtClaimNames.NBF, notBefore);
}
/**
* Sets the issued at {@code (iat)} claim, which identifies the time at which the
* JWT was issued.
* @param issuedAt the time at which the JWT was issued
* @return the {@link Builder}
*/
Builder issuedAt(Instant issuedAt) {
return claim(JwtClaimNames.IAT, issuedAt);
}
/**
* Sets the JWT ID {@code (jti)} claim, which provides a unique identifier for the
* JWT.
* @param jti the unique identifier for the JWT
* @return the {@link Builder}
*/
Builder id(String jti) {
return claim(JwtClaimNames.JTI, jti);
}
/**
* Sets the claim.
* @param name the claim name
* @param value the claim value
* @return the {@link Builder}
*/
Builder claim(String name, Object value) {
Assert.hasText(name, "name cannot be empty");
Assert.notNull(value, "value cannot be null");
this.claims.put(name, value);
return this;
}
/**
* A {@code Consumer} to be provided access to the claims allowing the ability to
* add, replace, or remove.
* @param claimsConsumer a {@code Consumer} of the claims
*/
Builder claims(Consumer<Map<String, Object>> claimsConsumer) {
claimsConsumer.accept(this.claims);
return this;
}
/**
* Builds a new {@link JwtClaimsSet}.
* @return a {@link JwtClaimsSet}
*/
JwtClaimsSet build() {
Assert.notEmpty(this.claims, "claims cannot be empty");
// The value of the 'iss' claim is a String or URL (StringOrURI).
// Attempt to convert to URL.
Object issuer = this.claims.get(JwtClaimNames.ISS);
if (issuer != null) {
URL convertedValue = ClaimConversionService.getSharedInstance().convert(issuer, URL.class);
if (convertedValue != null) {
this.claims.put(JwtClaimNames.ISS, convertedValue);
}
}
return new JwtClaimsSet(this.claims);
}
}
}

View File

@@ -1,62 +0,0 @@
/*
* Copyright 2002-2021 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.client.endpoint;
import org.springframework.security.oauth2.jwt.JwtException;
/*
* NOTE:
* This originated in gh-9208 (JwtEncoder),
* which is required to realize the feature in gh-8175 (JWT Client Authentication).
* However, we decided not to merge gh-9208 as part of the 5.5.0 release
* and instead packaged it up privately with the gh-8175 feature.
* We MAY merge gh-9208 in a later release but that is yet to be determined.
*
* gh-9208 Introduce JwtEncoder
* https://github.com/spring-projects/spring-security/pull/9208
*
* gh-8175 Support JWT for Client Authentication
* https://github.com/spring-projects/spring-security/issues/8175
*/
/**
* This exception is thrown when an error occurs while attempting to encode a JSON Web
* Token (JWT).
*
* @author Joe Grandja
* @since 5.5
*/
class JwtEncodingException extends JwtException {
/**
* Constructs a {@code JwtEncodingException} using the provided parameters.
* @param message the detail message
*/
JwtEncodingException(String message) {
super(message);
}
/**
* Constructs a {@code JwtEncodingException} using the provided parameters.
* @param message the detail message
* @param cause the root cause
*/
JwtEncodingException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -1,359 +0,0 @@
/*
* Copyright 2002-2021 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.client.endpoint;
import java.net.URI;
import java.net.URL;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JOSEObjectType;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.crypto.factories.DefaultJWSSignerFactory;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKMatcher;
import com.nimbusds.jose.jwk.JWKSelector;
import com.nimbusds.jose.jwk.KeyType;
import com.nimbusds.jose.jwk.KeyUse;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jose.produce.JWSSignerFactory;
import com.nimbusds.jose.util.Base64;
import com.nimbusds.jose.util.Base64URL;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/*
* NOTE:
* This originated in gh-9208 (JwtEncoder),
* which is required to realize the feature in gh-8175 (JWT Client Authentication).
* However, we decided not to merge gh-9208 as part of the 5.5.0 release
* and instead packaged it up privately with the gh-8175 feature.
* We MAY merge gh-9208 in a later release but that is yet to be determined.
*
* gh-9208 Introduce JwtEncoder
* https://github.com/spring-projects/spring-security/pull/9208
*
* gh-8175 Support JWT for Client Authentication
* https://github.com/spring-projects/spring-security/issues/8175
*/
/**
* A JWT encoder that encodes a JSON Web Token (JWT) using the JSON Web Signature (JWS)
* Compact Serialization format. The private/secret key used for signing the JWS is
* supplied by the {@code com.nimbusds.jose.jwk.source.JWKSource} provided via the
* constructor.
*
* <p>
* <b>NOTE:</b> This implementation uses the Nimbus JOSE + JWT SDK.
*
* @author Joe Grandja
* @since 5.5
* @see com.nimbusds.jose.jwk.source.JWKSource
* @see com.nimbusds.jose.jwk.JWK
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519">JSON Web Token
* (JWT)</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515">JSON Web Signature
* (JWS)</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515#section-3.1">JWS
* Compact Serialization</a>
* @see <a target="_blank" href="https://connect2id.com/products/nimbus-jose-jwt">Nimbus
* JOSE + JWT SDK</a>
*/
final class NimbusJwsEncoder {
private static final String ENCODING_ERROR_MESSAGE_TEMPLATE = "An error occurred while attempting to encode the Jwt: %s";
private static final JWSSignerFactory JWS_SIGNER_FACTORY = new DefaultJWSSignerFactory();
private final Map<JWK, JWSSigner> jwsSigners = new ConcurrentHashMap<>();
private final JWKSource<SecurityContext> jwkSource;
/**
* Constructs a {@code NimbusJwsEncoder} using the provided parameters.
* @param jwkSource the {@code com.nimbusds.jose.jwk.source.JWKSource}
*/
NimbusJwsEncoder(JWKSource<SecurityContext> jwkSource) {
Assert.notNull(jwkSource, "jwkSource cannot be null");
this.jwkSource = jwkSource;
}
Jwt encode(JoseHeader headers, JwtClaimsSet claims) throws JwtEncodingException {
Assert.notNull(headers, "headers cannot be null");
Assert.notNull(claims, "claims cannot be null");
JWK jwk = selectJwk(headers);
headers = addKeyIdentifierHeadersIfNecessary(headers, jwk);
String jws = serialize(headers, claims, jwk);
return new Jwt(jws, claims.getIssuedAt(), claims.getExpiresAt(), headers.getHeaders(), claims.getClaims());
}
private JWK selectJwk(JoseHeader headers) {
List<JWK> jwks;
try {
JWKSelector jwkSelector = new JWKSelector(createJwkMatcher(headers));
jwks = this.jwkSource.get(jwkSelector, null);
}
catch (Exception ex) {
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
"Failed to select a JWK signing key -> " + ex.getMessage()), ex);
}
if (jwks.size() > 1) {
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
"Found multiple JWK signing keys for algorithm '" + headers.getAlgorithm().getName() + "'"));
}
if (jwks.isEmpty()) {
throw new JwtEncodingException(
String.format(ENCODING_ERROR_MESSAGE_TEMPLATE, "Failed to select a JWK signing key"));
}
return jwks.get(0);
}
private String serialize(JoseHeader headers, JwtClaimsSet claims, JWK jwk) {
JWSHeader jwsHeader = convert(headers);
JWTClaimsSet jwtClaimsSet = convert(claims);
JWSSigner jwsSigner = this.jwsSigners.computeIfAbsent(jwk, NimbusJwsEncoder::createSigner);
SignedJWT signedJwt = new SignedJWT(jwsHeader, jwtClaimsSet);
try {
signedJwt.sign(jwsSigner);
}
catch (JOSEException ex) {
throw new JwtEncodingException(
String.format(ENCODING_ERROR_MESSAGE_TEMPLATE, "Failed to sign the JWT -> " + ex.getMessage()), ex);
}
return signedJwt.serialize();
}
private static JWKMatcher createJwkMatcher(JoseHeader headers) {
JWSAlgorithm jwsAlgorithm = JWSAlgorithm.parse(headers.getAlgorithm().getName());
if (JWSAlgorithm.Family.RSA.contains(jwsAlgorithm) || JWSAlgorithm.Family.EC.contains(jwsAlgorithm)) {
// @formatter:off
return new JWKMatcher.Builder()
.keyType(KeyType.forAlgorithm(jwsAlgorithm))
.keyID(headers.getKeyId())
.keyUses(KeyUse.SIGNATURE, null)
.algorithms(jwsAlgorithm, null)
.x509CertSHA256Thumbprint(Base64URL.from(headers.getX509SHA256Thumbprint()))
.build();
// @formatter:on
}
else if (JWSAlgorithm.Family.HMAC_SHA.contains(jwsAlgorithm)) {
// @formatter:off
return new JWKMatcher.Builder()
.keyType(KeyType.forAlgorithm(jwsAlgorithm))
.keyID(headers.getKeyId())
.privateOnly(true)
.algorithms(jwsAlgorithm, null)
.build();
// @formatter:on
}
return null;
}
private static JoseHeader addKeyIdentifierHeadersIfNecessary(JoseHeader headers, JWK jwk) {
// Check if headers have already been added
if (StringUtils.hasText(headers.getKeyId()) && StringUtils.hasText(headers.getX509SHA256Thumbprint())) {
return headers;
}
// Check if headers can be added from JWK
if (!StringUtils.hasText(jwk.getKeyID()) && jwk.getX509CertSHA256Thumbprint() == null) {
return headers;
}
JoseHeader.Builder headersBuilder = JoseHeader.from(headers);
if (!StringUtils.hasText(headers.getKeyId()) && StringUtils.hasText(jwk.getKeyID())) {
headersBuilder.keyId(jwk.getKeyID());
}
if (!StringUtils.hasText(headers.getX509SHA256Thumbprint()) && jwk.getX509CertSHA256Thumbprint() != null) {
headersBuilder.x509SHA256Thumbprint(jwk.getX509CertSHA256Thumbprint().toString());
}
return headersBuilder.build();
}
private static JWSSigner createSigner(JWK jwk) {
try {
return JWS_SIGNER_FACTORY.createJWSSigner(jwk);
}
catch (JOSEException ex) {
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
"Failed to create a JWS Signer -> " + ex.getMessage()), ex);
}
}
private static JWSHeader convert(JoseHeader headers) {
JWSHeader.Builder builder = new JWSHeader.Builder(JWSAlgorithm.parse(headers.getAlgorithm().getName()));
if (headers.getJwkSetUrl() != null) {
builder.jwkURL(convertAsURI(JoseHeaderNames.JKU, headers.getJwkSetUrl()));
}
Map<String, Object> jwk = headers.getJwk();
if (!CollectionUtils.isEmpty(jwk)) {
try {
builder.jwk(JWK.parse(jwk));
}
catch (Exception ex) {
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
"Unable to convert '" + JoseHeaderNames.JWK + "' JOSE header"), ex);
}
}
String keyId = headers.getKeyId();
if (StringUtils.hasText(keyId)) {
builder.keyID(keyId);
}
if (headers.getX509Url() != null) {
builder.x509CertURL(convertAsURI(JoseHeaderNames.X5U, headers.getX509Url()));
}
List<String> x509CertificateChain = headers.getX509CertificateChain();
if (!CollectionUtils.isEmpty(x509CertificateChain)) {
List<Base64> x5cList = new ArrayList<>();
x509CertificateChain.forEach((x5c) -> x5cList.add(new Base64(x5c)));
if (!x5cList.isEmpty()) {
builder.x509CertChain(x5cList);
}
}
String x509SHA1Thumbprint = headers.getX509SHA1Thumbprint();
if (StringUtils.hasText(x509SHA1Thumbprint)) {
builder.x509CertThumbprint(new Base64URL(x509SHA1Thumbprint));
}
String x509SHA256Thumbprint = headers.getX509SHA256Thumbprint();
if (StringUtils.hasText(x509SHA256Thumbprint)) {
builder.x509CertSHA256Thumbprint(new Base64URL(x509SHA256Thumbprint));
}
String type = headers.getType();
if (StringUtils.hasText(type)) {
builder.type(new JOSEObjectType(type));
}
String contentType = headers.getContentType();
if (StringUtils.hasText(contentType)) {
builder.contentType(contentType);
}
Set<String> critical = headers.getCritical();
if (!CollectionUtils.isEmpty(critical)) {
builder.criticalParams(critical);
}
Map<String, Object> customHeaders = new HashMap<>();
headers.getHeaders().forEach((name, value) -> {
if (!JWSHeader.getRegisteredParameterNames().contains(name)) {
customHeaders.put(name, value);
}
});
if (!customHeaders.isEmpty()) {
builder.customParams(customHeaders);
}
return builder.build();
}
private static JWTClaimsSet convert(JwtClaimsSet claims) {
JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder();
// NOTE: The value of the 'iss' claim is a String or URL (StringOrURI).
Object issuer = claims.getClaim(JwtClaimNames.ISS);
if (issuer != null) {
builder.issuer(issuer.toString());
}
String subject = claims.getSubject();
if (StringUtils.hasText(subject)) {
builder.subject(subject);
}
List<String> audience = claims.getAudience();
if (!CollectionUtils.isEmpty(audience)) {
builder.audience(audience);
}
Instant expiresAt = claims.getExpiresAt();
if (expiresAt != null) {
builder.expirationTime(Date.from(expiresAt));
}
Instant notBefore = claims.getNotBefore();
if (notBefore != null) {
builder.notBeforeTime(Date.from(notBefore));
}
Instant issuedAt = claims.getIssuedAt();
if (issuedAt != null) {
builder.issueTime(Date.from(issuedAt));
}
String jwtId = claims.getId();
if (StringUtils.hasText(jwtId)) {
builder.jwtID(jwtId);
}
Map<String, Object> customClaims = new HashMap<>();
claims.getClaims().forEach((name, value) -> {
if (!JWTClaimsSet.getRegisteredNames().contains(name)) {
customClaims.put(name, value);
}
});
if (!customClaims.isEmpty()) {
customClaims.forEach(builder::claim);
}
return builder.build();
}
private static URI convertAsURI(String header, URL url) {
try {
return url.toURI();
}
catch (Exception ex) {
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
"Unable to convert '" + header + "' JOSE header to a URI"), ex);
}
}
}

View File

@@ -40,7 +40,12 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.JwsHeader;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@@ -122,7 +127,7 @@ public final class NimbusJwtClientAuthenticationParametersConverter<T extends Ab
throw new OAuth2AuthorizationException(oauth2Error);
}
JoseHeader.Builder headersBuilder = JoseHeader.withAlgorithm(jwsAlgorithm);
JwsHeader.Builder headersBuilder = JwsHeader.with(jwsAlgorithm);
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(Duration.ofSeconds(60));
@@ -137,7 +142,7 @@ public final class NimbusJwtClientAuthenticationParametersConverter<T extends Ab
.expiresAt(expiresAt);
// @formatter:on
JoseHeader joseHeader = headersBuilder.build();
JwsHeader jwsHeader = headersBuilder.build();
JwtClaimsSet jwtClaimsSet = claimsBuilder.build();
JwsEncoderHolder jwsEncoderHolder = this.jwsEncoders.compute(clientRegistration.getRegistrationId(),
@@ -146,11 +151,11 @@ public final class NimbusJwtClientAuthenticationParametersConverter<T extends Ab
return currentJwsEncoderHolder;
}
JWKSource<SecurityContext> jwkSource = new ImmutableJWKSet<>(new JWKSet(jwk));
return new JwsEncoderHolder(new NimbusJwsEncoder(jwkSource), jwk);
return new JwsEncoderHolder(new NimbusJwtEncoder(jwkSource), jwk);
});
NimbusJwsEncoder jwsEncoder = jwsEncoderHolder.getJwsEncoder();
Jwt jws = jwsEncoder.encode(joseHeader, jwtClaimsSet);
JwtEncoder jwsEncoder = jwsEncoderHolder.getJwsEncoder();
Jwt jws = jwsEncoder.encode(JwtEncoderParameters.from(jwsHeader, jwtClaimsSet));
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.set(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, CLIENT_ASSERTION_TYPE_VALUE);
@@ -186,16 +191,16 @@ public final class NimbusJwtClientAuthenticationParametersConverter<T extends Ab
private static final class JwsEncoderHolder {
private final NimbusJwsEncoder jwsEncoder;
private final JwtEncoder jwsEncoder;
private final JWK jwk;
private JwsEncoderHolder(NimbusJwsEncoder jwsEncoder, JWK jwk) {
private JwsEncoderHolder(JwtEncoder jwsEncoder, JWK jwk) {
this.jwsEncoder = jwsEncoder;
this.jwk = jwk;
}
private NimbusJwsEncoder getJwsEncoder() {
private JwtEncoder getJwsEncoder() {
return this.jwsEncoder;
}