diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/JwsAlgorithms.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/JwsAlgorithms.java
index ed8976c22ff..7ef439ad64b 100644
--- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/JwsAlgorithms.java
+++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/JwsAlgorithms.java
@@ -22,6 +22,7 @@
* the JWS Protected Header and JWS Payload.
*
* @author Joe Grandja
+ * @author Andrey Litvitski
* @since 5.0
* @see JSON Web Algorithms
* (JWA)
@@ -93,6 +94,21 @@ public final class JwsAlgorithms {
*/
public static final String PS512 = "PS512";
+ /**
+ * EdDSA signature algorithms (optional).
+ */
+ public static final String EdDSA = "EdDSA";
+
+ /**
+ * EdDSA signature algorithms using Ed448 curve (optional).
+ */
+ public static final String ED448 = "Ed448";
+
+ /**
+ * EdDSA signature algorithms using Ed25519 curve (optional).
+ */
+ public static final String ED25519 = "Ed25519";
+
private JwsAlgorithms() {
}
diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/SignatureAlgorithm.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/SignatureAlgorithm.java
index 764601892c2..ba124085a0e 100644
--- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/SignatureAlgorithm.java
+++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/SignatureAlgorithm.java
@@ -79,7 +79,22 @@ public enum SignatureAlgorithm implements JwsAlgorithm {
/**
* RSASSA-PSS using SHA-512 and MGF1 with SHA-512 (Optional)
*/
- PS512(JwsAlgorithms.PS512);
+ PS512(JwsAlgorithms.PS512),
+
+ /**
+ * EdDSA signature algorithms (optional).
+ */
+ EdDSA(JwsAlgorithms.EdDSA),
+
+ /**
+ * EdDSA signature algorithms using Ed448 curve (optional).
+ */
+ ED448(JwsAlgorithms.ED448),
+
+ /**
+ * EdDSA signature algorithms using Ed25519 curve (optional).
+ */
+ ED25519(JwsAlgorithms.ED25519);
private final String name;
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
index a8da54e17ca..a1d41129ac9 100644
--- 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
@@ -32,6 +32,7 @@
import com.nimbusds.jose.jwk.ECKey;
import com.nimbusds.jose.jwk.KeyOperation;
import com.nimbusds.jose.jwk.KeyUse;
+import com.nimbusds.jose.jwk.OctetKeyPair;
import com.nimbusds.jose.jwk.OctetSequenceKey;
import com.nimbusds.jose.jwk.RSAKey;
@@ -84,4 +85,15 @@ static RSAKey.Builder signingWithRsa(RSAPublicKey pub, RSAPrivateKey key) throws
.notBeforeTime(issued);
}
+ static OctetKeyPair.Builder signingWithOkp(OctetKeyPair octetKeyPair) throws JOSEException {
+ Date issued = new Date();
+ return new OctetKeyPair.Builder(octetKeyPair.getCurve(), octetKeyPair.getX()).d(octetKeyPair.getD())
+ .keyOperations(Set.of(KeyOperation.SIGN))
+ .keyUse(KeyUse.SIGNATURE)
+ .algorithm(JWSAlgorithm.EdDSA)
+ .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 9f6ee0eba5e..32acbdaf0b4 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
@@ -50,6 +50,7 @@
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.KeyType;
import com.nimbusds.jose.jwk.KeyUse;
+import com.nimbusds.jose.jwk.OctetKeyPair;
import com.nimbusds.jose.jwk.OctetSequenceKey;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
@@ -84,6 +85,7 @@
* @author Joe Grandja
* @author Josh Cummings
* @author Suraj Bhadrike
+ * @author Andrey Litvitski
* @since 5.6
* @see JwtEncoder
* @see com.nimbusds.jose.jwk.source.JWKSource
@@ -250,6 +252,16 @@ else if (JWSAlgorithm.Family.HMAC_SHA.contains(jwsAlgorithm)) {
.build();
// @formatter:on
}
+ else if (JWSAlgorithm.Family.ED.contains(jwsAlgorithm)) {
+ // @formatter:off
+ return new JWKMatcher.Builder()
+ .keyType(KeyType.forAlgorithm(jwsAlgorithm))
+ .keyID(headers.getKeyId())
+ .keyUses(KeyUse.SIGNATURE, null)
+ .algorithms(jwsAlgorithm, null)
+ .build();
+ // @formatter:on
+ }
return null;
}
@@ -445,6 +457,16 @@ public static EcKeyPairJwtEncoderBuilder withKeyPair(ECPublicKey publicKey, ECPr
return new EcKeyPairJwtEncoderBuilder(publicKey, privateKey);
}
+ /**
+ * Creates a builder for constructing a {@link NimbusJwtEncoder} using the provided
+ * @param keyPair the {@link OctetKeyPair} to use for signing JWTs
+ * @return a {@link OctetKeyPairJwtEncoderBuilder}
+ * @since 7.0
+ */
+ public static OctetKeyPairJwtEncoderBuilder withKeyPair(OctetKeyPair keyPair) {
+ return new OctetKeyPairJwtEncoderBuilder(keyPair);
+ }
+
/**
* Creates a builder for constructing a {@link NimbusJwtEncoder} using the provided
* @param secretKey
@@ -625,4 +647,46 @@ public NimbusJwtEncoder build() {
}
+ /**
+ * A builder for creating {@link NimbusJwtEncoder} instances configured with a
+ * {@link OctetKeyPair}.
+ *
+ * This builder is used to create a {@link NimbusJwtEncoder}
+ *
+ * @since 7.0
+ */
+ public static final class OctetKeyPairJwtEncoderBuilder {
+
+ private static final ThrowingFunction defaultJwk = JWKS::signingWithOkp;
+
+ private final OctetKeyPair.Builder builder;
+
+ private OctetKeyPairJwtEncoderBuilder(OctetKeyPair keyPair) {
+ Assert.notNull(keyPair, "keyPair cannot be null");
+ Assert.isTrue(keyPair.isPrivate(), "keyPair must contain a private key");
+ this.builder = defaultJwk.apply(keyPair);
+ }
+
+ /**
+ * Post-process the {@link JWK} using the given {@link Consumer}. For example, you
+ * may use this to override the default {@code jwk}
+ * @param jwkPostProcessor the post-processor to use
+ * @return this builder instance for method chaining
+ */
+ public OctetKeyPairJwtEncoderBuilder 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/jose/TestJwks.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/TestJwks.java
index 2f75e4e8937..dc85a817c7a 100644
--- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/TestJwks.java
+++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/TestJwks.java
@@ -28,11 +28,13 @@
import com.nimbusds.jose.jwk.Curve;
import com.nimbusds.jose.jwk.ECKey;
+import com.nimbusds.jose.jwk.OctetKeyPair;
import com.nimbusds.jose.jwk.OctetSequenceKey;
import com.nimbusds.jose.jwk.RSAKey;
/**
* @author Joe Grandja
+ * @author Andrey Litvitski
*/
public final class TestJwks {
@@ -70,6 +72,13 @@ public final class TestJwks {
).build();
// @formatter:on
+ // @formatter:off
+ public static final OctetKeyPair DEFAULT_OKP_JWK =
+ jwk(
+ TestKeys.DEFAULT_OKP_KEY_PAIR
+ ).build();
+ // @formatter:on
+
private TestJwks() {
}
@@ -111,4 +120,12 @@ public static OctetSequenceKey.Builder jwk(SecretKey secretKey) {
// @formatter:on
}
+ public static OctetKeyPair.Builder jwk(OctetKeyPair octetKeyPair) {
+ // @formatter:off
+ return new OctetKeyPair.Builder(octetKeyPair.getCurve(), octetKeyPair.getX())
+ .d(octetKeyPair.getD())
+ .keyID("okp-jwk-kid");
+ // @formatter:on
+ }
+
}
diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/TestKeys.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/TestKeys.java
index ee8be9a696d..8402dbfdcda 100644
--- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/TestKeys.java
+++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/TestKeys.java
@@ -35,8 +35,14 @@
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.jwk.Curve;
+import com.nimbusds.jose.jwk.OctetKeyPair;
+import com.nimbusds.jose.jwk.gen.OctetKeyPairGenerator;
+
/**
* @author Joe Grandja
+ * @author Andrey Litvitski
* @since 5.2
*/
public final class TestKeys {
@@ -120,6 +126,8 @@ public final class TestKeys {
public static final KeyPair DEFAULT_EC_KEY_PAIR = generateEcKeyPair();
+ public static final OctetKeyPair DEFAULT_OKP_KEY_PAIR = generateOkpKeyPair();
+
static KeyPair generateEcKeyPair() {
EllipticCurve ellipticCurve = new EllipticCurve(
new ECFieldFp(new BigInteger(
@@ -144,6 +152,15 @@ static KeyPair generateEcKeyPair() {
return keyPair;
}
+ static OctetKeyPair generateOkpKeyPair() {
+ try {
+ return new OctetKeyPairGenerator(Curve.Ed25519).generate();
+ }
+ catch (JOSEException ex) {
+ throw new IllegalStateException(ex);
+ }
+ }
+
private TestKeys() {
}
diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/jws/SignatureAlgorithmTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/jws/SignatureAlgorithmTests.java
index 14f8341c044..69065939e84 100644
--- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/jws/SignatureAlgorithmTests.java
+++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/jws/SignatureAlgorithmTests.java
@@ -24,6 +24,7 @@
* Tests for {@link SignatureAlgorithm}
*
* @author Joe Grandja
+ * @author Andrey Litvitski
* @since 5.2
*/
public class SignatureAlgorithmTests {
@@ -39,6 +40,9 @@ public void fromWhenAlgorithmValidThenResolves() {
assertThat(SignatureAlgorithm.from(JwsAlgorithms.PS256)).isEqualTo(SignatureAlgorithm.PS256);
assertThat(SignatureAlgorithm.from(JwsAlgorithms.PS384)).isEqualTo(SignatureAlgorithm.PS384);
assertThat(SignatureAlgorithm.from(JwsAlgorithms.PS512)).isEqualTo(SignatureAlgorithm.PS512);
+ assertThat(SignatureAlgorithm.from(JwsAlgorithms.EdDSA)).isEqualTo(SignatureAlgorithm.EdDSA);
+ assertThat(SignatureAlgorithm.from(JwsAlgorithms.ED448)).isEqualTo(SignatureAlgorithm.ED448);
+ assertThat(SignatureAlgorithm.from(JwsAlgorithms.ED25519)).isEqualTo(SignatureAlgorithm.ED25519);
}
@Test
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 95e8a1e1c51..f44f979a5c1 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
@@ -39,9 +39,11 @@
import com.nimbusds.jose.jwk.JWKSelector;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.KeyUse;
+import com.nimbusds.jose.jwk.OctetKeyPair;
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.OctetKeyPairGenerator;
import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
@@ -72,6 +74,7 @@
* Tests for {@link NimbusJwtEncoder}.
*
* @author Joe Grandja
+ * @author Andrey Litvitski
*/
public class NimbusJwtEncoderTests {
@@ -109,6 +112,25 @@ public void encodeWhenClaimsNullThenThrowIllegalArgumentException() {
.withMessage("claims cannot be null");
}
+ @Test
+ void keyPairBuilderWithOkpWhenPublicKeyOnlyThenThrowIllegalArgumentException() throws JOSEException {
+ OctetKeyPair key = new OctetKeyPairGenerator(Curve.Ed25519).generate();
+
+ assertThatIllegalArgumentException().isThrownBy(() -> NimbusJwtEncoder.withKeyPair(key.toPublicJWK()))
+ .withMessage("keyPair must contain a private key");
+ }
+
+ @Test
+ void encodeWhenEdDsaAlgorithmThenSuccess() throws JOSEException {
+ OctetKeyPair key = new OctetKeyPairGenerator(Curve.Ed25519).generate();
+ this.jwkList.add(key);
+ JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.EdDSA).build();
+ JwtClaimsSet claims = buildClaims();
+ Jwt jwt = this.jwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, claims));
+ assertJwt(jwt);
+ assertThat(jwt.getHeaders()).containsEntry(JoseHeaderNames.ALG, SignatureAlgorithm.EdDSA);
+ }
+
@Test
public void encodeWhenJwkSelectFailedThenThrowJwtEncodingException() throws Exception {
this.jwkSource = mock(JWKSource.class);
@@ -389,6 +411,17 @@ void keyPairBuilderWithSecretKeyDefaultAlgorithm() {
assertThat(jwt.getHeaders()).containsKey(JoseHeaderNames.KID);
}
+ @Test
+ void keyPairBuilderWithOkpDefaultAlgorithm() throws JOSEException {
+ OctetKeyPair key = new OctetKeyPairGenerator(Curve.Ed25519).generate();
+ NimbusJwtEncoder jwtEncoder = NimbusJwtEncoder.withKeyPair(key).build();
+ JwtClaimsSet claims = buildClaims();
+ Jwt jwt = jwtEncoder.encode(JwtEncoderParameters.from(claims));
+ assertJwt(jwt);
+ assertThat(jwt.getHeaders()).containsEntry(JoseHeaderNames.ALG, SignatureAlgorithm.EdDSA);
+ assertThat(jwt.getHeaders()).containsKey(JoseHeaderNames.KID);
+ }
+
// With custom algorithm
@Test
void keyPairBuilderWithRsaWithAlgorithm() throws JOSEException {
@@ -490,6 +523,20 @@ void keyPairBuilderWithSecretKeyWithAlgorithmAndJwkSource() {
assertThat(jwt.getHeaders()).containsEntry(JoseHeaderNames.KID, keyId);
}
+ @Test
+ void keyPairBuilderWithOkpAlgorithmAndJwkSource() throws JOSEException {
+ OctetKeyPair key = new OctetKeyPairGenerator(Curve.Ed25519).generate();
+ String keyId = UUID.randomUUID().toString();
+ NimbusJwtEncoder jwtEncoder = NimbusJwtEncoder.withKeyPair(key)
+ .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.EdDSA);
+ assertThat(jwt.getHeaders()).containsEntry(JoseHeaderNames.KID, keyId);
+ }
+
private JwtClaimsSet buildClaims() {
Instant now = Instant.now();
return JwtClaimsSet.builder()
@@ -558,8 +605,11 @@ private void init() {
OctetSequenceKey secretJwk = TestJwks.jwk(TestKeys.DEFAULT_SECRET_KEY)
.keyID("secret-jwk-" + this.keyId++)
.build();
+ OctetKeyPair okpJwk = TestJwks.jwk(TestKeys.DEFAULT_OKP_KEY_PAIR)
+ .keyID("okp-jwk-" + this.keyId++)
+ .build();
// @formatter:on
- this.jwkSet = new JWKSet(Arrays.asList(rsaJwk, ecJwk, secretJwk));
+ this.jwkSet = new JWKSet(Arrays.asList(rsaJwk, ecJwk, secretJwk, okpJwk));
}
private void rotate() {