certs = jwk.getX509CertChain();
+ if (certs != null) {
+ builder.x509CertificateChain(certs.stream().map(Base64::toString).toList());
+ }
+ Base64URL thumbprint = jwk.getX509CertSHA256Thumbprint();
+ if (thumbprint != null) {
+ builder.x509SHA256Thumbprint(thumbprint.toString());
+ }
+ this.defaultJwsHeader = builder.build();
+ }
+
/**
* Use this strategy to reduce the list of matching JWKs when there is more than one.
*
@@ -125,8 +173,9 @@ public Jwt encode(JwtEncoderParameters parameters) throws JwtEncodingException {
JwsHeader headers = parameters.getJwsHeader();
if (headers == null) {
- headers = DEFAULT_JWS_HEADER;
+ headers = this.defaultJwsHeader;
}
+
JwtClaimsSet claims = parameters.getClaims();
JWK jwk = selectJwk(headers);
@@ -369,4 +418,206 @@ private static URI convertAsURI(String header, URL url) {
}
}
+ /**
+ * Creates a builder for constructing a {@link NimbusJwtEncoder} using the provided
+ * @param publicKey the {@link RSAPublicKey} and @Param privateKey the
+ * {@link RSAPrivateKey} to use for signing JWTs
+ * @return a {@link RsaKeyPairJwtEncoderBuilder}
+ * @since 7.0
+ */
+ public static RsaKeyPairJwtEncoderBuilder withKeyPair(RSAPublicKey publicKey, RSAPrivateKey privateKey) {
+ return new RsaKeyPairJwtEncoderBuilder(publicKey, privateKey);
+ }
+
+ /**
+ * Creates a builder for constructing a {@link NimbusJwtEncoder} using the provided
+ * @param publicKey the {@link ECPublicKey} and @param privateKey the
+ * {@link ECPrivateKey} to use for signing JWTs
+ * @return a {@link EcKeyPairJwtEncoderBuilder}
+ * @since 7.0
+ */
+ public static EcKeyPairJwtEncoderBuilder withKeyPair(ECPublicKey publicKey, ECPrivateKey privateKey) {
+ return new EcKeyPairJwtEncoderBuilder(publicKey, privateKey);
+ }
+
+ /**
+ * Creates a builder for constructing a {@link NimbusJwtEncoder} using the provided
+ * @param secretKey
+ * @return a {@link SecretKeyJwtEncoderBuilder} for configuring the {@link JWK}
+ * @since 7.0
+ */
+ public static SecretKeyJwtEncoderBuilder withSecretKey(SecretKey secretKey) {
+ return new SecretKeyJwtEncoderBuilder(secretKey);
+ }
+
+ /**
+ * A builder for creating {@link NimbusJwtEncoder} instances configured with a
+ * {@link SecretKey}.
+ *
+ * @since 7.0
+ */
+ public static final class SecretKeyJwtEncoderBuilder {
+
+ private static final ThrowingFunction defaultJwk = JWKS::signing;
+
+ private final OctetSequenceKey.Builder builder;
+
+ private final Set allowedAlgorithms;
+
+ private SecretKeyJwtEncoderBuilder(SecretKey secretKey) {
+ Assert.notNull(secretKey, "secretKey cannot be null");
+ Set allowedAlgorithms = computeAllowedAlgorithms(secretKey);
+ Assert.notEmpty(allowedAlgorithms,
+ "This key is too small for any standard JWK symmetric signing algorithm");
+ this.allowedAlgorithms = allowedAlgorithms;
+ this.builder = defaultJwk.apply(secretKey, IllegalArgumentException::new)
+ .algorithm(this.allowedAlgorithms.iterator().next());
+ }
+
+ private Set computeAllowedAlgorithms(SecretKey secretKey) {
+ try {
+ return new MACSigner(secretKey).supportedJWSAlgorithms();
+ }
+ catch (JOSEException ex) {
+ throw new IllegalArgumentException(ex);
+ }
+ }
+
+ /**
+ * 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 algorithm(MacAlgorithm macAlgorithm) {
+ Assert.notNull(macAlgorithm, "macAlgorithm cannot be null");
+ JWSAlgorithm jws = JWSAlgorithm.parse(macAlgorithm.getName());
+ Assert.isTrue(this.allowedAlgorithms.contains(jws), String
+ .format("This key can only support " + "the following algorithms: [%s]", this.allowedAlgorithms));
+ this.builder.algorithm(JWSAlgorithm.parse(macAlgorithm.getName()));
+ return this;
+ }
+
+ /**
+ * Post-process the {@link JWK} using the given {@link Consumer}. For example, you
+ * may use this to override the default {@code kid}
+ * @param jwkPostProcessor the post-processor to use
+ * @return this builder instance for method chaining
+ */
+ public SecretKeyJwtEncoderBuilder jwkPostProcessor(Consumer jwkPostProcessor) {
+ Assert.notNull(jwkPostProcessor, "jwkPostProcessor cannot be null");
+ jwkPostProcessor.accept(this.builder);
+ 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() {
+ return new NimbusJwtEncoder(this.builder.build());
+ }
+
+ }
+
+ /**
+ * A builder for creating {@link NimbusJwtEncoder} instances configured with a
+ * {@link KeyPair}.
+ *
+ * @since 7.0
+ */
+ public static final class RsaKeyPairJwtEncoderBuilder {
+
+ private static final ThrowingBiFunction defaultKid = JWKS::signingWithRsa;
+
+ private final RSAKey.Builder builder;
+
+ private RsaKeyPairJwtEncoderBuilder(RSAPublicKey publicKey, RSAPrivateKey privateKey) {
+ Assert.notNull(publicKey, "publicKey cannot be null");
+ Assert.notNull(privateKey, "privateKey cannot be null");
+ this.builder = defaultKid.apply(publicKey, privateKey);
+ }
+
+ /**
+ * Sets the JWS algorithm to use for signing. Defaults to
+ * {@link SignatureAlgorithm#RS256}. Must be an RSA-based algorithm
+ * @param signatureAlgorithm the {@link SignatureAlgorithm} to use
+ * @return this builder instance for method chaining
+ */
+ public RsaKeyPairJwtEncoderBuilder algorithm(SignatureAlgorithm signatureAlgorithm) {
+ Assert.notNull(signatureAlgorithm, "signatureAlgorithm cannot be null");
+ this.builder.algorithm(JWSAlgorithm.parse(signatureAlgorithm.getName()));
+ return this;
+ }
+
+ /**
+ * Add commentMore actions Post-process the {@link JWK} using the given
+ * {@link Consumer}. For example, you may use this to override the default
+ * {@code kid}
+ * @param jwkPostProcessor the post-processor to use
+ * @return this builder instance for method chaining
+ */
+ public RsaKeyPairJwtEncoderBuilder jwkPostProcessor(Consumer jwkPostProcessor) {
+ Assert.notNull(jwkPostProcessor, "jwkPostProcessor cannot be null");
+ jwkPostProcessor.accept(this.builder);
+ return this;
+ }
+
+ /**
+ * Builds the {@link NimbusJwtEncoder} instance.
+ * @return the configured {@link NimbusJwtEncoder}
+ */
+ public NimbusJwtEncoder build() {
+ return new NimbusJwtEncoder(this.builder.build());
+ }
+
+ }
+
+ /**
+ * A builder for creating {@link NimbusJwtEncoder} instances configured with a
+ * {@link ECPublicKey} and {@link ECPrivateKey}.
+ *
+ * This builder is used to create a {@link NimbusJwtEncoder}
+ *
+ * @since 7.0
+ */
+ public static final class EcKeyPairJwtEncoderBuilder {
+
+ private static final ThrowingBiFunction defaultKid = JWKS::signingWithEc;
+
+ private final ECKey.Builder builder;
+
+ private EcKeyPairJwtEncoderBuilder(ECPublicKey publicKey, ECPrivateKey privateKey) {
+ Assert.notNull(publicKey, "publicKey cannot be null");
+ Assert.notNull(privateKey, "privateKey cannot be null");
+ Curve curve = Curve.forECParameterSpec(publicKey.getParams());
+ Assert.notNull(curve, "Unable to determine Curve for EC public key.");
+ this.builder = defaultKid.apply(publicKey, privateKey);
+ }
+
+ /**
+ * Post-process the {@link JWK} using the given {@link Consumer}. For example, you
+ * may use this to override the default {@code kid}
+ * @param jwkPostProcessor the post-processor to use
+ * @return this builder instance for method chaining
+ */
+ public EcKeyPairJwtEncoderBuilder jwkPostProcessor(Consumer jwkPostProcessor) {
+ Assert.notNull(jwkPostProcessor, "jwkPostProcessor cannot be null");
+ jwkPostProcessor.accept(this.builder);
+ return this;
+ }
+
+ /**
+ * Builds the {@link NimbusJwtEncoder} instance.
+ * @return the configured {@link NimbusJwtEncoder}
+ */
+ public NimbusJwtEncoder build() {
+ return new NimbusJwtEncoder(this.builder.build());
+ }
+
+ }
+
}
diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoderTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoderTests.java
index ab17156eacb..840b9cdcca4 100644
--- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoderTests.java
+++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoderTests.java
@@ -18,13 +18,22 @@
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.UUID;
+import java.util.function.Consumer;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.KeySourceException;
+import com.nimbusds.jose.jwk.Curve;
import com.nimbusds.jose.jwk.ECKey;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSelector;
@@ -32,6 +41,8 @@
import com.nimbusds.jose.jwk.KeyUse;
import com.nimbusds.jose.jwk.OctetSequenceKey;
import com.nimbusds.jose.jwk.RSAKey;
+import com.nimbusds.jose.jwk.gen.ECKeyGenerator;
+import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jose.util.Base64URL;
@@ -43,6 +54,7 @@
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.oauth2.jose.TestJwks;
import org.springframework.security.oauth2.jose.TestKeys;
+import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import static org.assertj.core.api.Assertions.assertThat;
@@ -344,6 +356,165 @@ public void encodeWhenNoKeysThenJwkSelectorIsNotUsed() throws Exception {
verifyNoInteractions(selector);
}
+ // Default algorithm
+ @Test
+ void keyPairBuilderWithRsaDefaultAlgorithm() throws JOSEException {
+ RSAKeyGenerator generator = new RSAKeyGenerator(2048);
+ RSAKey key = generator.generate();
+ NimbusJwtEncoder jwtEncoder = NimbusJwtEncoder.withKeyPair(key.toRSAPublicKey(), key.toRSAPrivateKey()).build();
+ JwtClaimsSet claims = buildClaims();
+ Jwt jwt = jwtEncoder.encode(JwtEncoderParameters.from(claims));
+ assertJwt(jwt);
+ assertThat(jwt.getHeaders()).containsKey(JoseHeaderNames.KID);
+ }
+
+ @Test
+ void keyPairBuilderWithEcDefaultAlgorithm() throws JOSEException {
+ ECKeyGenerator generator = new ECKeyGenerator(Curve.P_256);
+ ECKey key = generator.generate();
+ NimbusJwtEncoder jwtEncoder = NimbusJwtEncoder.withKeyPair(key.toECPublicKey(), key.toECPrivateKey()).build();
+ JwtClaimsSet claims = buildClaims();
+ Jwt jwt = jwtEncoder.encode(JwtEncoderParameters.from(claims));
+ assertJwt(jwt);
+ assertThat(jwt.getHeaders()).containsKey(JoseHeaderNames.KID);
+ }
+
+ @Test
+ void keyPairBuilderWithSecretKeyDefaultAlgorithm() {
+ SecretKey key = TestKeys.DEFAULT_SECRET_KEY;
+ NimbusJwtEncoder jwtEncoder = NimbusJwtEncoder.withSecretKey(key).build();
+ JwtClaimsSet claims = buildClaims();
+ Jwt jwt = jwtEncoder.encode(JwtEncoderParameters.from(claims));
+ assertJwt(jwt);
+ assertThat(jwt.getHeaders()).containsKey(JoseHeaderNames.KID);
+ }
+
+ // With custom algorithm
+ @Test
+ void keyPairBuilderWithRsaWithAlgorithm() throws JOSEException {
+ RSAKeyGenerator generator = new RSAKeyGenerator(2048);
+ RSAKey key = generator.generate();
+ NimbusJwtEncoder jwtEncoder = NimbusJwtEncoder.withKeyPair(key.toRSAPublicKey(), key.toRSAPrivateKey())
+ .algorithm(SignatureAlgorithm.RS384)
+ .build();
+ JwtClaimsSet claims = buildClaims();
+ Jwt jwt = jwtEncoder.encode(JwtEncoderParameters.from(claims));
+ assertJwt(jwt);
+ assertThat(jwt.getHeaders()).containsEntry(JoseHeaderNames.ALG, SignatureAlgorithm.RS384);
+ assertThat(jwt.getHeaders()).containsKey(JoseHeaderNames.KID);
+ }
+
+ @Test
+ void keyPairBuilderWithEcWithAlgorithm() throws JOSEException {
+ ECKeyGenerator generator = new ECKeyGenerator(Curve.P_384);
+ ECKey key = generator.generate();
+ NimbusJwtEncoder jwtEncoder = NimbusJwtEncoder.withKeyPair(key.toECPublicKey(), key.toECPrivateKey()).build();
+ JwtClaimsSet claims = buildClaims();
+ Jwt jwt = jwtEncoder.encode(JwtEncoderParameters.from(claims));
+ assertJwt(jwt);
+ assertThat(jwt.getHeaders()).containsEntry(JoseHeaderNames.ALG, SignatureAlgorithm.ES384);
+ assertThat(jwt.getHeaders()).containsKey(JoseHeaderNames.KID);
+ }
+
+ @Test
+ void keyPairBuilderWithSecretKeyWithAlgorithm() {
+ String keyStr = UUID.randomUUID().toString();
+ keyStr += keyStr;
+ SecretKey Key = new SecretKeySpec(keyStr.getBytes(), "AES");
+ NimbusJwtEncoder jwtEncoder = NimbusJwtEncoder.withSecretKey(Key).algorithm(MacAlgorithm.HS512).build();
+ JwtClaimsSet claims = buildClaims();
+ Jwt jwt = jwtEncoder.encode(JwtEncoderParameters.from(claims));
+ assertJwt(jwt);
+ assertThat(jwt.getHeaders()).containsEntry(JoseHeaderNames.ALG, MacAlgorithm.HS512);
+ assertThat(jwt.getHeaders()).containsKey(JoseHeaderNames.KID);
+ }
+
+ @Test
+ void keyPairBuilderWhenShortSecretThenHigherAlgorithmNotSupported() {
+ String keyStr = UUID.randomUUID().toString();
+ SecretKey Key = new SecretKeySpec(keyStr.getBytes(), "AES");
+ assertThatExceptionOfType(IllegalArgumentException.class)
+ .isThrownBy(() -> NimbusJwtEncoder.withSecretKey(Key).algorithm(MacAlgorithm.HS512).build());
+ }
+
+ @Test
+ void keyPairBuilderWhenTooShortSecretThenException() {
+ SecretKey Key = new SecretKeySpec("key".getBytes(), "AES");
+ assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> NimbusJwtEncoder.withSecretKey(Key));
+ }
+
+ // with custom jwkPostProcessor
+ @Test
+ void keyPairBuilderWithRsaWithAlgorithmAndJwkSource() throws JOSEException {
+ RSAKeyGenerator generator = new RSAKeyGenerator(2048);
+ RSAKey key = generator.generate();
+ String keyId = UUID.randomUUID().toString();
+ NimbusJwtEncoder jwtEncoder = NimbusJwtEncoder.withKeyPair(key.toRSAPublicKey(), key.toRSAPrivateKey())
+ .algorithm(SignatureAlgorithm.RS384)
+ .jwkPostProcessor((builder) -> builder.keyID(keyId))
+ .build();
+ JwtClaimsSet claims = buildClaims();
+ Jwt jwt = jwtEncoder.encode(JwtEncoderParameters.from(claims));
+ assertJwt(jwt);
+ assertThat(jwt.getHeaders()).containsEntry(JoseHeaderNames.ALG, SignatureAlgorithm.RS384);
+ assertThat(jwt.getHeaders()).containsEntry(JoseHeaderNames.KID, keyId);
+ }
+
+ @Test
+ void keyPairBuilderWithEcWithAlgorithmAndJwkSource() throws JOSEException {
+ ECKeyGenerator generator = new ECKeyGenerator(Curve.P_256);
+ ECKey key = generator.generate();
+ String keyId = UUID.randomUUID().toString();
+ Consumer jwkPostProcessor = (builder) -> builder.keyID(keyId);
+ NimbusJwtEncoder jwtEncoder = NimbusJwtEncoder.withKeyPair(key.toECPublicKey(), key.toECPrivateKey())
+ .jwkPostProcessor(jwkPostProcessor)
+ .build();
+ JwtClaimsSet claims = buildClaims();
+ Jwt jwt = jwtEncoder.encode(JwtEncoderParameters.from(claims));
+ assertJwt(jwt);
+ assertThat(jwt.getHeaders()).containsEntry(JoseHeaderNames.ALG, SignatureAlgorithm.ES256);
+ assertThat(jwt.getHeaders()).containsEntry(JoseHeaderNames.KID, keyId);
+ }
+
+ @Test
+ void keyPairBuilderWithSecretKeyWithAlgorithmAndJwkSource() {
+ final String keyStr = UUID.randomUUID().toString();
+ SecretKey key = new SecretKeySpec(keyStr.getBytes(), "HS256");
+ String keyId = UUID.randomUUID().toString();
+ Consumer jwkPostProcessor = (builder) -> builder.keyID(keyId);
+ NimbusJwtEncoder jwtEncoder = NimbusJwtEncoder.withSecretKey(key).jwkPostProcessor(jwkPostProcessor).build();
+ JwtClaimsSet claims = buildClaims();
+ Jwt jwt = jwtEncoder.encode(JwtEncoderParameters.from(claims));
+ assertJwt(jwt);
+ assertThat(jwt.getHeaders()).containsEntry(JoseHeaderNames.ALG, MacAlgorithm.HS256);
+ assertThat(jwt.getHeaders()).containsEntry(JoseHeaderNames.KID, keyId);
+ }
+
+ private JwtClaimsSet buildClaims() {
+ Instant now = Instant.now();
+ return JwtClaimsSet.builder()
+ .issuer("https://example.com")
+ .subject("subject")
+ .audience(Collections.singletonList("audience"))
+ .issuedAt(now)
+ .notBefore(now)
+ .expiresAt(now.plus(1, ChronoUnit.HOURS))
+ .id(UUID.randomUUID().toString())
+ .claim("custom", "value")
+ .build();
+ }
+
+ private static void assertJwt(Jwt jwt) {
+ assertThat(jwt.getIssuer().toString()).isEqualTo("https://example.com");
+ assertThat(jwt.getSubject()).isEqualTo("subject");
+ assertThat(jwt.getAudience()).containsExactly("audience");
+ assertThat(jwt.getIssuedAt()).isNotNull();
+ assertThat(jwt.getNotBefore()).isNotNull();
+ assertThat(jwt.getExpiresAt()).isNotNull();
+ assertThat(jwt.getId()).isNotNull();
+ assertThat(jwt.getClaim("custom").toString()).isEqualTo("value");
+ }
+
private static final class JwkListResultCaptor implements Answer> {
private List result;