diff --git a/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/Jwt.java b/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/Jwt.java index 7a110d42..b8ebc2b3 100644 --- a/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/Jwt.java +++ b/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/Jwt.java @@ -1,5 +1,6 @@ package io.smallrye.jwt.build; +import java.security.PublicKey; import java.time.Instant; import java.util.Collection; import java.util.Map; @@ -102,12 +103,16 @@ public static JwtClaimsBuilder claims(JsonWebToken jwt) { /** * Creates a new instance of {@link JwtClaimsBuilder} with a specified claim. * - * Simple claim value are converted to {@link String} unless it is an instance of {@link Boolean}, {@link Number} or - * {@link Instant}. {@link Instant} values have their number of seconds from the epoch converted to long. - * + * Simple claim value are converted to {@link String} unless it is an instance of {@link Boolean}, {@link Number}, + * {@link Instant} or {@link PublicKey}. + *

+ * {@link Instant} values have their number of seconds from the epoch converted to long. + *

+ * {@link PublicKey} values are converted to JSON Web Key (JWK) representations. + *

* Array claims can be set as {@link Collection} or {@link JsonArray}, complex claims can be set as {@link Map} or * {@link JsonObject}. The members of the array claims can be complex claims. - * + *

* Types of the claims directly supported by this builder are enforced. * The 'iss' (issuer), 'sub' (subject), 'upn', 'preferred_username' and 'jti' (token identifier) claims must be of * {@link String} type. @@ -126,12 +131,16 @@ public static JwtClaimsBuilder claim(Claims name, Object value) { /** * Creates a new instance of {@link JwtClaimsBuilder} with a specified claim. * - * Simple claim value are converted to {@link String} unless it is an instance of {@link Boolean}, {@link Number} or - * {@link Instant}. {@link Instant} values have their number of seconds from the epoch converted to long. - * + * Simple claim value are converted to {@link String} unless it is an instance of {@link Boolean}, {@link Number}, + * {@link Instant} or {@linkplain PublicKey}. + *

+ * {@link Instant} values have their number of seconds from the epoch converted to long. + *

+ * {@link PublicKey} values are converted to JSON Web Key (JWK) representations. + *

* Array claims can be set as {@link Collection} or {@link JsonArray}, complex claims can be set as {@link Map} or * {@link JsonObject}. The members of the array claims can be complex claims. - * + *

* Types of the claims directly supported by this builder are enforced. * The 'iss' (issuer), 'sub' (subject), 'upn', 'preferred_username' and 'jti' (token identifier) claims must be of * {@link String} type. diff --git a/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/JwtClaimsBuilder.java b/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/JwtClaimsBuilder.java index 18326071..f5083edb 100644 --- a/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/JwtClaimsBuilder.java +++ b/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/JwtClaimsBuilder.java @@ -1,5 +1,6 @@ package io.smallrye.jwt.build; +import java.security.PublicKey; import java.time.Duration; import java.time.Instant; import java.util.Collection; @@ -182,12 +183,16 @@ default JwtClaimsBuilder scope(String scope) { /** * Set a claim. * - * Simple claim value are converted to {@link String} unless it is an instance of {@link Boolean}, {@link Number} or - * {@link Instant}. {@link Instant} values have their number of seconds from the epoch converted to long. - * - * Array claims can be set as {@link Collection} or {@link JsonArray} and complex claims can be set as {@link Map} or + * Simple claim value are converted to {@link String} unless it is an instance of {@link Boolean}, {@link Number}, + * {@link Instant} or {@link PublicKey}. + *

+ * {@link Instant} values have their number of seconds from the epoch converted to long. + *

+ * {@link PublicKey} values are converted to JSON Web Key (JWK) representations. + *

+ * Array claims can be set as {@link Collection} or {@link JsonArray}, complex claims can be set as {@link Map} or * {@link JsonObject}. The members of the array claims can be complex claims. - * + *

* Types of claims directly supported by this builder are enforced. * The 'iss' (issuer), 'sub' (subject), 'upn', 'preferred_username' and 'jti' (token identifier) claims must be of * {@link String} type. @@ -206,12 +211,16 @@ default JwtClaimsBuilder claim(Claims name, Object value) { /** * Set a claim. * - * Simple claim value are converted to {@link String} unless it is an instance of {@link Boolean}, {@link Number} or - * {@link Instant}. {@link Instant} values have their number of seconds from the epoch converted to long. - * + * Simple claim value are converted to {@link String} unless it is an instance of {@link Boolean}, {@link Number}, + * {@link Instant} or {@link PublicKey}. + *

+ * {@link Instant} values have their number of seconds from the epoch converted to long. + *

+ * {@link PublicKey} values are converted to JSON Web Key (JWK) representations. + *

* Array claims can be set as {@link Collection} or {@link JsonArray}, complex claims can be set as {@link Map} or * {@link JsonObject}. The members of the array claims can be complex claims. - * + *

* Types of the claims directly supported by this builder are enforced. * The 'iss' (issuer), 'sub' (subject), 'upn', 'preferred_username' and 'jti' (token identifier) claims must be of * {@link String} type. diff --git a/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/JwtSignatureBuilder.java b/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/JwtSignatureBuilder.java index 86fca2c5..6b296153 100644 --- a/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/JwtSignatureBuilder.java +++ b/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/JwtSignatureBuilder.java @@ -1,5 +1,6 @@ package io.smallrye.jwt.build; +import java.security.PublicKey; import java.security.cert.X509Certificate; import java.util.List; @@ -100,6 +101,14 @@ default JwtSignatureBuilder chain(X509Certificate cert) { */ JwtSignatureBuilder chain(List chain); + /** + * Set JSON Web Key 'jwk' key. + * + * @param key the public key + * @return JwtSignatureBuilder + */ + JwtSignatureBuilder jwk(PublicKey key); + /** * Custom JWT signature header. * diff --git a/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/impl/JwtClaimsBuilderImpl.java b/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/impl/JwtClaimsBuilderImpl.java index 768fe8e5..151f7903 100644 --- a/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/impl/JwtClaimsBuilderImpl.java +++ b/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/impl/JwtClaimsBuilderImpl.java @@ -1,6 +1,7 @@ package io.smallrye.jwt.build.impl; import java.security.PrivateKey; +import java.security.PublicKey; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import java.time.Instant; @@ -23,10 +24,13 @@ import org.eclipse.microprofile.jwt.Claims; import org.jose4j.base64url.Base64; +import org.jose4j.jwk.JsonWebKey.OutputControlLevel; +import org.jose4j.jwk.PublicJsonWebKey; import org.jose4j.jwt.JwtClaims; import org.jose4j.jwt.NumericDate; import org.jose4j.jwx.HeaderParameterNames; import org.jose4j.keys.X509Util; +import org.jose4j.lang.JoseException; import io.smallrye.jwt.algorithm.SignatureAlgorithm; import io.smallrye.jwt.build.JwtClaimsBuilder; @@ -259,6 +263,15 @@ public JwtSignatureBuilder chain(List chain) { return this; } + /** + * {@inheritDoc} + */ + @Override + public JwtSignatureBuilder jwk(PublicKey key) { + headers.put(HeaderParameterNames.JWK, convertPublicKeyToJwk(key)); + return this; + } + /** * {@inheritDoc} */ @@ -323,6 +336,10 @@ private static Object prepareValue(Object value) { return ((Instant) value).getEpochSecond(); } + if (value instanceof PublicKey) { + return convertPublicKeyToJwk((PublicKey) value); + } + return value.toString(); } @@ -404,6 +421,14 @@ public Object verify(String name, Object value) { } } + static Map convertPublicKeyToJwk(PublicKey key) { + try { + return PublicJsonWebKey.Factory.newPublicJwk(key).toParams(OutputControlLevel.PUBLIC_ONLY); + } catch (JoseException ex) { + throw ImplMessages.msg.signatureException(ex); + } + } + @Override public JwtClaimsBuilder remove(String name) { claims.unsetClaim(name); diff --git a/implementation/jwt-build/src/test/java/io/smallrye/jwt/build/JwtSignTest.java b/implementation/jwt-build/src/test/java/io/smallrye/jwt/build/JwtSignTest.java index d9af4e0b..8fb8c0ff 100644 --- a/implementation/jwt-build/src/test/java/io/smallrye/jwt/build/JwtSignTest.java +++ b/implementation/jwt-build/src/test/java/io/smallrye/jwt/build/JwtSignTest.java @@ -593,19 +593,34 @@ private void doTestSignedExistingClaims(String jwt) throws Exception { @Test void signClaimsEllipticCurve() throws Exception { - EllipticCurveJsonWebKey jwk = createECJwk(); + EllipticCurveJsonWebKey ecJwk = createECJwk(); String jwt = Jwt.claims() .claim("customClaim", "custom-value") - .sign(jwk.getEcPrivateKey()); + .claim("evidence", ecJwk.getECPublicKey()) + .jws().jwk(ecJwk.getECPublicKey()) + .sign(ecJwk.getEcPrivateKey()); - JsonWebSignature jws = getVerifiedJws(jwt, jwk.getECPublicKey()); + JsonWebSignature jws = getVerifiedJws(jwt, ecJwk.getECPublicKey()); JwtClaims claims = JwtClaims.parse(jws.getPayload()); + assertEquals(5, claims.getClaimsMap().size()); - assertEquals(4, claims.getClaimsMap().size()); - checkDefaultClaimsAndHeaders(getJwsHeaders(jwt, 2), claims, "ES256", 300); + Map headers = getJwsHeaders(jwt, 3); + checkDefaultClaimsAndHeaders(headers, claims, "ES256", 300); assertEquals("custom-value", claims.getClaimValue("customClaim")); + + @SuppressWarnings("unchecked") + Map jwk = (Map) headers.get("jwk"); + assertEquals(4, jwk.size()); + assertEquals("EC", jwk.get("kty")); + assertEquals("P-256", jwk.get("crv")); + assertNotNull(jwk.get("x")); + assertNotNull(jwk.get("y")); + + @SuppressWarnings("unchecked") + Map evidence = (Map) claims.getClaimValue("evidence"); + assertEquals(evidence, jwk); } @Test @@ -617,17 +632,25 @@ void signClaimsEd25519() throws Exception { String jwt = Jwt.claims() .claim("customClaim", "custom-value") + .jws().jwk(keyPairEd25519.getPublic()) .sign(keyPairEd25519.getPrivate()); JsonWebSignature jws = getVerifiedJws(jwt, keyPairEd25519.getPublic()); JwtClaims claims = JwtClaims.parse(jws.getPayload()); assertEquals(4, claims.getClaimsMap().size()); - Map headers = getJwsHeaders(jwt, 2); + Map headers = getJwsHeaders(jwt, 3); checkDefaultClaimsAndHeaders(headers, claims, "EdDSA", 300); assertEquals("custom-value", claims.getClaimValue("customClaim")); + @SuppressWarnings("unchecked") + Map jwk = (Map) headers.get("jwk"); + assertEquals(3, jwk.size()); + assertEquals("OKP", jwk.get("kty")); + assertEquals("Ed25519", jwk.get("crv")); + assertNotNull(jwk.get("x")); + JwtConsumerBuilder builder = new JwtConsumerBuilder(); builder.setVerificationKey(keyPairEd448.getPublic()); builder.setJwsAlgorithmConstraints( @@ -733,19 +756,28 @@ void signWithKeyStore() throws Exception { configSource.setUseKeyStore(true); configSource.setSigningKeyLocation("/keystore.p12"); try { + KeyStore keyStore = KeyUtils.loadKeyStore("keystore.p12", "password", Optional.of("PKCS12"), Optional.empty()); + PublicKey verificationKey = keyStore.getCertificate("server").getPublicKey(); + String jwt = Jwt.claims() .claim("customClaim", "custom-value") + .jws().jwk(verificationKey) .sign(); - KeyStore keyStore = KeyUtils.loadKeyStore("keystore.p12", "password", Optional.of("PKCS12"), Optional.empty()); - PublicKey verificationKey = keyStore.getCertificate("server").getPublicKey(); - JsonWebSignature jws = getVerifiedJws(jwt, verificationKey); JwtClaims claims = JwtClaims.parse(jws.getPayload()); assertEquals(4, claims.getClaimsMap().size()); - checkDefaultClaimsAndHeaders(getJwsHeaders(jwt, 2), claims, "RS256", 300); + Map headers = getJwsHeaders(jwt, 3); + checkDefaultClaimsAndHeaders(headers, claims, "RS256", 300); assertEquals("custom-value", claims.getClaimValue("customClaim")); + + @SuppressWarnings("unchecked") + Map jwk = (Map) headers.get("jwk"); + assertEquals(3, jwk.size()); + assertEquals("RSA", jwk.get("kty")); + assertNotNull(jwk.get("n")); + assertNotNull(jwk.get("e")); } finally { configSource.setUseKeyStore(false); configSource.setSigningKeyLocation("/privateKey.pem");