Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for the 'jwk' JWT header #772

Merged
merged 1 commit into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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());
MikeEdgar marked this conversation as resolved.
Show resolved Hide resolved

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
Loading