|
@@ -18,6 +18,8 @@ package org.springframework.security.oauth2.jwt;
|
|
|
|
|
|
import java.net.URI;
|
|
|
import java.net.URL;
|
|
|
+import java.security.KeyPair;
|
|
|
+import java.security.interfaces.ECPublicKey;
|
|
|
import java.time.Instant;
|
|
|
import java.util.ArrayList;
|
|
|
import java.util.Date;
|
|
@@ -25,19 +27,28 @@ import java.util.HashMap;
|
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
|
import java.util.Set;
|
|
|
+import java.util.UUID;
|
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
|
|
|
|
+import javax.crypto.SecretKey;
|
|
|
+
|
|
|
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.Curve;
|
|
|
+import com.nimbusds.jose.jwk.ECKey;
|
|
|
import com.nimbusds.jose.jwk.JWK;
|
|
|
import com.nimbusds.jose.jwk.JWKMatcher;
|
|
|
import com.nimbusds.jose.jwk.JWKSelector;
|
|
|
+import com.nimbusds.jose.jwk.JWKSet;
|
|
|
import com.nimbusds.jose.jwk.KeyType;
|
|
|
import com.nimbusds.jose.jwk.KeyUse;
|
|
|
+import com.nimbusds.jose.jwk.OctetSequenceKey;
|
|
|
+import com.nimbusds.jose.jwk.RSAKey;
|
|
|
+import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
|
|
|
import com.nimbusds.jose.jwk.source.JWKSource;
|
|
|
import com.nimbusds.jose.proc.SecurityContext;
|
|
|
import com.nimbusds.jose.produce.JWSSignerFactory;
|
|
@@ -47,6 +58,7 @@ import com.nimbusds.jwt.JWTClaimsSet;
|
|
|
import com.nimbusds.jwt.SignedJWT;
|
|
|
|
|
|
import org.springframework.core.convert.converter.Converter;
|
|
|
+import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
|
|
|
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
|
|
import org.springframework.util.Assert;
|
|
|
import org.springframework.util.CollectionUtils;
|
|
@@ -83,6 +95,8 @@ public final class NimbusJwtEncoder implements JwtEncoder {
|
|
|
|
|
|
private static final JWSSignerFactory JWS_SIGNER_FACTORY = new DefaultJWSSignerFactory();
|
|
|
|
|
|
+ private JwsHeader jwsHeader;
|
|
|
+
|
|
|
private final Map<JWK, JWSSigner> jwsSigners = new ConcurrentHashMap<>();
|
|
|
|
|
|
private final JWKSource<SecurityContext> jwkSource;
|
|
@@ -119,14 +133,16 @@ public final class NimbusJwtEncoder implements JwtEncoder {
|
|
|
this.jwkSelector = jwkSelector;
|
|
|
}
|
|
|
|
|
|
+ public void setJwsHeader(JwsHeader jwsHeader) {
|
|
|
+ this.jwsHeader = jwsHeader;
|
|
|
+ }
|
|
|
+
|
|
|
@Override
|
|
|
public Jwt encode(JwtEncoderParameters parameters) throws JwtEncodingException {
|
|
|
Assert.notNull(parameters, "parameters cannot be null");
|
|
|
|
|
|
JwsHeader headers = parameters.getJwsHeader();
|
|
|
- if (headers == null) {
|
|
|
- headers = DEFAULT_JWS_HEADER;
|
|
|
- }
|
|
|
+ headers = (headers != null) ? headers : (this.jwsHeader != null) ? this.jwsHeader : DEFAULT_JWS_HEADER;
|
|
|
JwtClaimsSet claims = parameters.getClaims();
|
|
|
|
|
|
JWK jwk = selectJwk(headers);
|
|
@@ -369,4 +385,249 @@ public final class NimbusJwtEncoder implements JwtEncoder {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Creates a builder for constructing a {@link NimbusJwtEncoder} using the provided
|
|
|
+ * {@link SecretKey}.
|
|
|
+ * @param secretKey the {@link SecretKey} to use for signing JWTs
|
|
|
+ * @return a {@link SecretKeyJwtEncoderBuilder} for further configuration
|
|
|
+ * @since 7.0
|
|
|
+ */
|
|
|
+ public static SecretKeyJwtEncoderBuilder withSecretKey(SecretKey secretKey) {
|
|
|
+ Assert.notNull(secretKey, "secretKey cannot be null");
|
|
|
+ return new SecretKeyJwtEncoderBuilder(secretKey);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Creates a builder for constructing a {@link NimbusJwtEncoder} using the provided
|
|
|
+ * {@link KeyPair}. The key pair must contain either an {@link RSAKey} or an
|
|
|
+ * {@link ECKey}.
|
|
|
+ * @param keyPair the {@link KeyPair} to use for signing JWTs
|
|
|
+ * @return a {@link KeyPairJwtEncoderBuilder} for further configuration
|
|
|
+ * @since 7.0
|
|
|
+ */
|
|
|
+ public static KeyPairJwtEncoderBuilder withKeyPair(KeyPair keyPair) {
|
|
|
+ Assert.isTrue(keyPair != null && keyPair.getPrivate() != null && keyPair.getPublic() != null,
|
|
|
+ "keyPair, its private key, and public key must not be null");
|
|
|
+ Assert.isTrue(
|
|
|
+ keyPair.getPrivate() instanceof java.security.interfaces.RSAKey
|
|
|
+ || keyPair.getPrivate() instanceof java.security.interfaces.ECKey,
|
|
|
+ "keyPair must be an RSAKey or an ECKey");
|
|
|
+ if (keyPair.getPrivate() instanceof java.security.interfaces.RSAKey) {
|
|
|
+ return new RsaKeyPairJwtEncoderBuilder(keyPair);
|
|
|
+ }
|
|
|
+ if (keyPair.getPrivate() instanceof java.security.interfaces.ECKey) {
|
|
|
+ return new EcKeyPairJwtEncoderBuilder(keyPair);
|
|
|
+ }
|
|
|
+ throw new IllegalArgumentException("keyPair must be an RSAKey or an ECKey");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * A builder for creating {@link NimbusJwtEncoder} instances configured with a
|
|
|
+ * {@link SecretKey}.
|
|
|
+ *
|
|
|
+ * @since 7.0
|
|
|
+ */
|
|
|
+ public static final class SecretKeyJwtEncoderBuilder {
|
|
|
+
|
|
|
+ private final SecretKey secretKey;
|
|
|
+
|
|
|
+ private String keyId;
|
|
|
+
|
|
|
+ private JWSAlgorithm jwsAlgorithm = JWSAlgorithm.HS256;
|
|
|
+
|
|
|
+ private SecretKeyJwtEncoderBuilder(SecretKey secretKey) {
|
|
|
+ this.secretKey = secretKey;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Sets the JWS algorithm to use for signing. Defaults to
|
|
|
+ * {@link JWSAlgorithm#HS256}. Must be an HMAC-based algorithm (HS256, HS384, or
|
|
|
+ * HS512).
|
|
|
+ * @param macAlgorithm the {@link MacAlgorithm} to use
|
|
|
+ * @return this builder instance for method chaining
|
|
|
+ */
|
|
|
+ public SecretKeyJwtEncoderBuilder macAlgorithm(MacAlgorithm macAlgorithm) {
|
|
|
+ Assert.notNull(macAlgorithm, "macAlgorithm cannot be null");
|
|
|
+ Assert.state(JWSAlgorithm.Family.HMAC_SHA.contains(this.jwsAlgorithm),
|
|
|
+ () -> "The algorithm '" + this.jwsAlgorithm + "' is not compatible with a SecretKey. "
|
|
|
+ + "Please use one of the HS256, HS384, or HS512 algorithms.");
|
|
|
+
|
|
|
+ this.jwsAlgorithm = JWSAlgorithm.parse(macAlgorithm.getName());
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Sets the key ID ({@code kid}) to be included in the JWK and potentially the JWS
|
|
|
+ * header.
|
|
|
+ * @param keyId the key identifier
|
|
|
+ * @return this builder instance for method chaining
|
|
|
+ */
|
|
|
+ public SecretKeyJwtEncoderBuilder keyId(String keyId) {
|
|
|
+ this.keyId = keyId;
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Builds the {@link NimbusJwtEncoder} instance.
|
|
|
+ * @return the configured {@link NimbusJwtEncoder}
|
|
|
+ * @throws IllegalStateException if the configured JWS algorithm is not compatible
|
|
|
+ * with a {@link SecretKey}.
|
|
|
+ */
|
|
|
+ public NimbusJwtEncoder build() {
|
|
|
+ this.jwsAlgorithm = (this.jwsAlgorithm != null) ? this.jwsAlgorithm : JWSAlgorithm.HS256;
|
|
|
+
|
|
|
+ OctetSequenceKey.Builder builder = new OctetSequenceKey.Builder(this.secretKey).keyUse(KeyUse.SIGNATURE)
|
|
|
+ .algorithm(this.jwsAlgorithm)
|
|
|
+ .keyID(this.keyId);
|
|
|
+
|
|
|
+ OctetSequenceKey jwk = builder.build();
|
|
|
+ JWKSource<SecurityContext> jwkSource = new ImmutableJWKSet<>(new JWKSet(jwk));
|
|
|
+ NimbusJwtEncoder encoder = new NimbusJwtEncoder(jwkSource);
|
|
|
+ encoder.setJwsHeader(JwsHeader.with(MacAlgorithm.from(this.jwsAlgorithm.getName())).build());
|
|
|
+ return encoder;
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * A builder for creating {@link NimbusJwtEncoder} instances configured with a
|
|
|
+ * {@link KeyPair}.
|
|
|
+ *
|
|
|
+ * @since 7.0
|
|
|
+ */
|
|
|
+ public abstract static class KeyPairJwtEncoderBuilder {
|
|
|
+
|
|
|
+ private final KeyPair keyPair;
|
|
|
+
|
|
|
+ private String keyId;
|
|
|
+
|
|
|
+ private JWSAlgorithm jwsAlgorithm;
|
|
|
+
|
|
|
+ private KeyPairJwtEncoderBuilder(KeyPair keyPair) {
|
|
|
+ this.keyPair = keyPair;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Sets the JWS algorithm to use for signing. Must be compatible with the key type
|
|
|
+ * (RSA or EC). If not set, a default algorithm will be chosen based on the key
|
|
|
+ * type (e.g., RS256 for RSA, ES256 for EC).
|
|
|
+ * @param signatureAlgorithm the {@link SignatureAlgorithm} to use
|
|
|
+ * @return this builder instance for method chaining
|
|
|
+ */
|
|
|
+ public KeyPairJwtEncoderBuilder signatureAlgorithm(SignatureAlgorithm signatureAlgorithm) {
|
|
|
+ Assert.notNull(signatureAlgorithm, "signatureAlgorithm cannot be null");
|
|
|
+ this.jwsAlgorithm = JWSAlgorithm.parse(signatureAlgorithm.getName());
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Sets the key ID ({@code kid}) to be included in the JWK and potentially the JWS
|
|
|
+ * header.
|
|
|
+ * @param keyId the key identifier
|
|
|
+ * @return this builder instance for method chaining
|
|
|
+ */
|
|
|
+ public KeyPairJwtEncoderBuilder keyId(String keyId) {
|
|
|
+ this.keyId = keyId;
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Builds the {@link NimbusJwtEncoder} instance.
|
|
|
+ * @return the configured {@link NimbusJwtEncoder}
|
|
|
+ * @throws IllegalStateException if the key type is unsupported or the configured
|
|
|
+ * JWS algorithm is not compatible with the key type.
|
|
|
+ * @throws JwtEncodingException if the key is invalid (e.g., EC key with unknown
|
|
|
+ * curve)
|
|
|
+ */
|
|
|
+ public NimbusJwtEncoder build() {
|
|
|
+ this.keyId = (this.keyId != null) ? this.keyId : UUID.randomUUID().toString();
|
|
|
+ JWK jwk = buildJwk();
|
|
|
+ JWKSource<SecurityContext> jwkSource = new ImmutableJWKSet<>(new JWKSet(jwk));
|
|
|
+ NimbusJwtEncoder encoder = new NimbusJwtEncoder(jwkSource);
|
|
|
+ JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.from(this.jwsAlgorithm.getName()))
|
|
|
+ .keyId(jwk.getKeyID())
|
|
|
+ .build();
|
|
|
+ encoder.setJwsHeader(jwsHeader);
|
|
|
+ return encoder;
|
|
|
+ }
|
|
|
+
|
|
|
+ protected abstract JWK buildJwk();
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * A builder for creating {@link NimbusJwtEncoder} instances configured with a
|
|
|
+ * {@link KeyPair}.
|
|
|
+ *
|
|
|
+ * @since 7.0
|
|
|
+ */
|
|
|
+ public static final class RsaKeyPairJwtEncoderBuilder extends KeyPairJwtEncoderBuilder {
|
|
|
+
|
|
|
+ private RsaKeyPairJwtEncoderBuilder(KeyPair keyPair) {
|
|
|
+ super(keyPair);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ protected JWK buildJwk() {
|
|
|
+ if (super.jwsAlgorithm == null) {
|
|
|
+ super.jwsAlgorithm = JWSAlgorithm.RS256;
|
|
|
+ }
|
|
|
+ Assert.state(JWSAlgorithm.Family.RSA.contains(super.jwsAlgorithm),
|
|
|
+ () -> "The algorithm '" + super.jwsAlgorithm + "' is not compatible with an RSAKey. "
|
|
|
+ + "Please use one of the RS256, RS384, RS512, PS256, PS384, or PS512 algorithms.");
|
|
|
+
|
|
|
+ RSAKey.Builder builder = new RSAKey.Builder(
|
|
|
+ (java.security.interfaces.RSAPublicKey) super.keyPair.getPublic())
|
|
|
+ .privateKey(super.keyPair.getPrivate())
|
|
|
+ .keyID(super.keyId)
|
|
|
+ .keyUse(KeyUse.SIGNATURE)
|
|
|
+ .algorithm(super.jwsAlgorithm);
|
|
|
+ return builder.build();
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * A builder for creating {@link NimbusJwtEncoder} instances configured with a
|
|
|
+ * {@link KeyPair}.
|
|
|
+ *
|
|
|
+ * @since 7.0
|
|
|
+ */
|
|
|
+ public static final class EcKeyPairJwtEncoderBuilder extends KeyPairJwtEncoderBuilder {
|
|
|
+
|
|
|
+ private EcKeyPairJwtEncoderBuilder(KeyPair keyPair) {
|
|
|
+ super(keyPair);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ protected JWK buildJwk() {
|
|
|
+ if (super.jwsAlgorithm == null) {
|
|
|
+ super.jwsAlgorithm = JWSAlgorithm.ES256;
|
|
|
+ }
|
|
|
+ Assert.state(JWSAlgorithm.Family.EC.contains(super.jwsAlgorithm),
|
|
|
+ () -> "The algorithm '" + super.jwsAlgorithm + "' is not compatible with an ECKey. "
|
|
|
+ + "Please use one of the ES256, ES384, or ES512 algorithms.");
|
|
|
+
|
|
|
+ ECPublicKey publicKey = (ECPublicKey) super.keyPair.getPublic();
|
|
|
+ Curve curve = Curve.forECParameterSpec(publicKey.getParams());
|
|
|
+ if (curve == null) {
|
|
|
+ throw new JwtEncodingException("Unable to determine Curve for EC public key.");
|
|
|
+ }
|
|
|
+
|
|
|
+ com.nimbusds.jose.jwk.ECKey.Builder builder = new com.nimbusds.jose.jwk.ECKey.Builder(curve, publicKey)
|
|
|
+ .privateKey(super.keyPair.getPrivate())
|
|
|
+ .keyUse(KeyUse.SIGNATURE)
|
|
|
+ .keyID(super.keyId)
|
|
|
+ .algorithm(super.jwsAlgorithm);
|
|
|
+
|
|
|
+ try {
|
|
|
+ return builder.build();
|
|
|
+ }
|
|
|
+ catch (IllegalStateException ex) {
|
|
|
+ throw new IllegalArgumentException("Failed to build ECKey: " + ex.getMessage(), ex);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
}
|