diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JWKS.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JWKS.java new file mode 100644 index 00000000000..8596749bc29 --- /dev/null +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JWKS.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2025 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.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.Date; +import java.util.Set; + +import javax.crypto.SecretKey; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.crypto.impl.ECDSA; +import com.nimbusds.jose.jwk.Curve; +import com.nimbusds.jose.jwk.ECKey; +import com.nimbusds.jose.jwk.KeyOperation; +import com.nimbusds.jose.jwk.KeyUse; +import com.nimbusds.jose.jwk.OctetSequenceKey; +import com.nimbusds.jose.jwk.RSAKey; + +final class JWKS { + + private JWKS() { + + } + + static OctetSequenceKey.Builder signing(SecretKey key) throws JOSEException { + Date issued = new Date(); + return new OctetSequenceKey.Builder(key).keyOperations(Set.of(KeyOperation.SIGN)) + .keyUse(KeyUse.SIGNATURE) + .algorithm(JWSAlgorithm.HS256) + .keyIDFromThumbprint() + .issueTime(issued) + .notBeforeTime(issued); + } + + static ECKey.Builder signingWithEc(ECPublicKey pub, ECPrivateKey key) throws JOSEException { + Date issued = new Date(); + Curve curve = Curve.forECParameterSpec(pub.getParams()); + JWSAlgorithm algorithm = computeAlgorithm(curve); + return new ECKey.Builder(curve, pub).privateKey(key) + .keyOperations(Set.of(KeyOperation.SIGN)) + .keyUse(KeyUse.SIGNATURE) + .algorithm(algorithm) + .keyIDFromThumbprint() + .issueTime(issued) + .notBeforeTime(issued); + } + + private static JWSAlgorithm computeAlgorithm(Curve curve) { + try { + return ECDSA.resolveAlgorithm(curve); + } + catch (JOSEException ex) { + throw new IllegalArgumentException(ex); + } + } + + static RSAKey.Builder signingWithRsa(RSAPublicKey pub, RSAPrivateKey key) throws JOSEException { + Date issued = new Date(); + return new RSAKey.Builder(pub).privateKey(key) + .keyUse(KeyUse.SIGNATURE) + .keyOperations(Set.of(KeyOperation.SIGN)) + .algorithm(JWSAlgorithm.RS256) + .keyIDFromThumbprint() + .issueTime(issued) + .notBeforeTime(issued); + } + +} diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoder.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoder.java index fb0468fa9bc..8fd1ada518e 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoder.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoder.java @@ -18,6 +18,11 @@ import java.net.URI; import java.net.URL; +import java.security.KeyPair; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; import java.time.Instant; import java.util.ArrayList; import java.util.Date; @@ -26,18 +31,28 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; + +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.MACSigner; 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,10 +62,14 @@ import com.nimbusds.jwt.SignedJWT; import org.springframework.core.convert.converter.Converter; +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.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; +import org.springframework.util.function.ThrowingBiFunction; +import org.springframework.util.function.ThrowingFunction; /** * An implementation of a {@link JwtEncoder} that encodes a JSON Web Token (JWT) using the @@ -62,6 +81,8 @@ * NOTE: This implementation uses the Nimbus JOSE + JWT SDK. * * @author Joe Grandja + * @author Josh Cummings + * @author Suraj Bhadrike * @since 5.6 * @see JwtEncoder * @see com.nimbusds.jose.jwk.source.JWKSource @@ -83,6 +104,8 @@ public final class NimbusJwtEncoder implements JwtEncoder { private static final JWSSignerFactory JWS_SIGNER_FACTORY = new DefaultJWSSignerFactory(); + private final JwsHeader defaultJwsHeader; + private final Map jwsSigners = new ConcurrentHashMap<>(); private final JWKSource jwkSource; @@ -100,10 +123,35 @@ public final class NimbusJwtEncoder implements JwtEncoder { * @param jwkSource the {@code com.nimbusds.jose.jwk.source.JWKSource} */ public NimbusJwtEncoder(JWKSource jwkSource) { + this.defaultJwsHeader = DEFAULT_JWS_HEADER; Assert.notNull(jwkSource, "jwkSource cannot be null"); this.jwkSource = jwkSource; } + private NimbusJwtEncoder(JWK jwk) { + Assert.notNull(jwk, "jwk cannot be null"); + this.jwkSource = new ImmutableJWKSet<>(new JWKSet(jwk)); + JwsAlgorithm algorithm = SignatureAlgorithm.from(jwk.getAlgorithm().getName()); + if (algorithm == null) { + algorithm = MacAlgorithm.from(jwk.getAlgorithm().getName()); + } + Assert.notNull(algorithm, "Failed to derive supported algorithm from " + jwk.getAlgorithm()); + JwsHeader.Builder builder = JwsHeader.with(algorithm).type(jwk.getKeyType().getValue()).keyId(jwk.getKeyID()); + URI x509Url = jwk.getX509CertURL(); + if (x509Url != null) { + builder.x509Url(jwk.getX509CertURL().toASCIIString()); + } + List 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;