Skip to content

Commit

Permalink
Support for the 'jwk' JWT header (#772)
Browse files Browse the repository at this point in the history
  • Loading branch information
sberyozkin authored Mar 21, 2024
1 parent 1abd4ca commit 05c2346
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 27 deletions.
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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}.
* <p/>
* {@link Instant} values have their number of seconds from the epoch converted to long.
* <p/>
* {@link PublicKey} values are converted to JSON Web Key (JWK) representations.
* <p/>
* 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.
*
* <p/>
* 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.
Expand All @@ -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}.
* <p/>
* {@link Instant} values have their number of seconds from the epoch converted to long.
* <p/>
* {@link PublicKey} values are converted to JSON Web Key (JWK) representations.
* <p/>
* 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.
*
* <p/>
* 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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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}.
* <p/>
* {@link Instant} values have their number of seconds from the epoch converted to long.
* <p/>
* {@link PublicKey} values are converted to JSON Web Key (JWK) representations.
* <p/>
* 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.
*
* <p/>
* 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.
Expand All @@ -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}.
* <p/>
* {@link Instant} values have their number of seconds from the epoch converted to long.
* <p/>
* {@link PublicKey} values are converted to JSON Web Key (JWK) representations.
* <p/>
* 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.
*
* <p/>
* 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.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.smallrye.jwt.build;

import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.util.List;

Expand Down Expand Up @@ -100,6 +101,14 @@ default JwtSignatureBuilder chain(X509Certificate cert) {
*/
JwtSignatureBuilder chain(List<X509Certificate> chain);

/**
* Set JSON Web Key 'jwk' key.
*
* @param key the public key
* @return JwtSignatureBuilder
*/
JwtSignatureBuilder jwk(PublicKey key);

/**
* Custom JWT signature header.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -259,6 +263,15 @@ public JwtSignatureBuilder chain(List<X509Certificate> chain) {
return this;
}

/**
* {@inheritDoc}
*/
@Override
public JwtSignatureBuilder jwk(PublicKey key) {
headers.put(HeaderParameterNames.JWK, convertPublicKeyToJwk(key));
return this;
}

/**
* {@inheritDoc}
*/
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -404,6 +421,14 @@ public Object verify(String name, Object value) {
}
}

static Map<String, Object> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Object> headers = getJwsHeaders(jwt, 3);
checkDefaultClaimsAndHeaders(headers, claims, "ES256", 300);

assertEquals("custom-value", claims.getClaimValue("customClaim"));

@SuppressWarnings("unchecked")
Map<String, Object> jwk = (Map<String, Object>) 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<String, Object> evidence = (Map<String, Object>) claims.getClaimValue("evidence");
assertEquals(evidence, jwk);
}

@Test
Expand All @@ -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<String, Object> headers = getJwsHeaders(jwt, 2);
Map<String, Object> headers = getJwsHeaders(jwt, 3);
checkDefaultClaimsAndHeaders(headers, claims, "EdDSA", 300);

assertEquals("custom-value", claims.getClaimValue("customClaim"));

@SuppressWarnings("unchecked")
Map<String, Object> jwk = (Map<String, Object>) 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(
Expand Down Expand Up @@ -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<String, Object> headers = getJwsHeaders(jwt, 3);
checkDefaultClaimsAndHeaders(headers, claims, "RS256", 300);
assertEquals("custom-value", claims.getClaimValue("customClaim"));

@SuppressWarnings("unchecked")
Map<String, Object> jwk = (Map<String, Object>) 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");
Expand Down

0 comments on commit 05c2346

Please sign in to comment.