From 406f2f39dfb36d9e26dbb3f3013d89c7203ba896 Mon Sep 17 00:00:00 2001 From: lhazlewood <121180+lhazlewood@users.noreply.github.com> Date: Thu, 11 Jan 2024 13:34:34 -0800 Subject: [PATCH 01/17] Ensured a single string `aud` (Audience) claim would be retained (without converting it to a `Set`) when copying/applying a source Claims instance to a destination Claims builder. Updated CHANGELOG.md accordingly. (#891) Fixes #890. --- CHANGELOG.md | 9 ++++++ .../impl/DelegatingClaimsMutator.java | 26 ++++++++++++++++ .../groovy/io/jsonwebtoken/JwtsTest.groovy | 31 +++++++++++++++++++ 3 files changed, 66 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7133667d..4f1b9f7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ ## Release Notes +### 0.12.4 + +This patch release: + +* Ensures Android environments and older `org.json` library usages can parse JSON from a `JwtBuilder`-provided + `java.io.Reader` instance. [Issue 882](https://github.com/jwtk/jjwt/issues/882). +* Ensures a single string `aud` (Audience) claim is retained (without converting it to a `Set`) when copying/applying a + source Claims instance to a destination Claims builder. [Issue 890](https://github.com/jwtk/jjwt/issues/890). + ### 0.12.3 This patch release: diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DelegatingClaimsMutator.java b/impl/src/main/java/io/jsonwebtoken/impl/DelegatingClaimsMutator.java index 8636b535..7740fe27 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DelegatingClaimsMutator.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DelegatingClaimsMutator.java @@ -24,6 +24,7 @@ import io.jsonwebtoken.lang.MapMutator; import io.jsonwebtoken.lang.Strings; import java.util.Date; +import java.util.Map; import java.util.Set; /** @@ -46,6 +47,31 @@ public class DelegatingClaimsMutator & C return self(); } + @Override + public Object put(String key, Object value) { + if (AUDIENCE_STRING.getId().equals(key)) { // https://github.com/jwtk/jjwt/issues/890 + if (value instanceof String) { + Object existing = get(key); + //noinspection deprecation + audience().single((String) value); + return existing; + } + // otherwise ensure that the Parameter type is the RFC-default data type (JSON Array of Strings): + getAudience(); + } + // otherwise retain expected behavior: + return super.put(key, value); + } + + @Override + public void putAll(Map m) { + if (m == null) return; + for (Map.Entry entry : m.entrySet()) { + String s = entry.getKey(); + put(s, entry.getValue()); // ensure local put is called per https://github.com/jwtk/jjwt/issues/890 + } + } + F get(Parameter param) { return this.DELEGATE.get(param); } diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index 5ded8038..95e2e8e0 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -23,6 +23,7 @@ import io.jsonwebtoken.impl.lang.Bytes import io.jsonwebtoken.impl.lang.Services import io.jsonwebtoken.impl.security.* import io.jsonwebtoken.io.Decoders +import io.jsonwebtoken.io.Deserializer import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.io.Serializer import io.jsonwebtoken.lang.Strings @@ -1167,6 +1168,36 @@ class JwtsTest { .build().parseSignedClaims(jws) } + /** + * Asserts that if a {@link Jwts#claims()} builder is used to set a single string Audience value, and the + * resulting constructed {@link Claims} instance is used on a {@link Jwts#builder()}, that the resulting JWT + * retains a single-string Audience value (and it is not automatically coerced to a {@code Set}). + * + * @since 0.12.4 + * @see JJWT Issue 890 + */ + @Test + void testClaimsBuilderSingleStringAudienceThenJwtBuilder() { + + def key = TestKeys.HS256 + def aud = 'foo' + def claims = Jwts.claims().audience().single(aud).build() + def jws = Jwts.builder().claims(claims).signWith(key).compact() + + // we can't use a JwtParser here because that will automatically normalize a single String value as a + // Set for app developer convenience. So we assert that the JWT looks as expected by simple + // json parsing and map inspection + + int i = jws.indexOf('.') + int j = jws.lastIndexOf('.') + def b64 = jws.substring(i, j) + def json = Strings.utf8(Decoders.BASE64URL.decode(b64)) + def deser = Services.loadFirst(Deserializer) + def m = deser.deserialize(new StringReader(json)) as Map + + assertEquals aud, m.get('aud') // single string value + } + //Asserts correct/expected behavior discussed in https://github.com/jwtk/jjwt/issues/20 @Test void testForgedTokenWithSwappedHeaderUsingNoneAlgorithm() { From d8784044348eca2a22213b8a1c87b11e05bdd0e5 Mon Sep 17 00:00:00 2001 From: lhazlewood <121180+lhazlewood@users.noreply.github.com> Date: Wed, 17 Jan 2024 13:35:20 -0800 Subject: [PATCH 02/17] Thread-safe ServiceLoader usage Blend of pre-0.11.0 behavior that cached implementation instances and post-0.11.0 behavior using the JDK ServiceLoader to find/create instances of an SPI interface. This change: - Reinstates the <= 0.10.x behavior of caching application singleton service implementation instances in a thread-safe reference (previously an AtomicReference, but in this change, a ConcurrentMap). If an app singleton instance is cached and found, it is returned to be (re)used immediately when requested. This is ok for JJWT's purposes because all service implementations instances must be thread-safe application singletons by API contract/design, so caching them for repeated use is fine. - Ensures that only if a service implementation instance is not in the app singleton cache, a new instance is located/created using a new JDK ServiceLoader instance, which doesn't require thread-safe considerations since it is used only in a single-threaded model for the short time it is used to discover a service implementation. This PR/change removes the post-0.11.0 concurrent cache of ServiceLoader instances since they themselves are not designed to be thread-safe. - Ensures that if a ServiceLoader discovers an implementation and returns a new instance, that instance is then cached as an application singleton in the aforementioned ConcurrentMap for continued reuse. - Renames Services#loadFirst to Services#get to more accurately reflect calling expectations: The fact that any 'loading' via the ServiceLoader may occur is not important for Services callers, and the previous method name was unnecessarily exposing internal implementation concepts. This is safe to do in a point release (0.12.3 -> 0.12.4) because the Services class and its methods, while public, are in the `impl` module, only to be used internally for JJWT's purpose and never intended to be used by application developers. - Updates all test methods to use the renamed method accordingly. Fixes #873 --- .../jsonwebtoken/impl/DefaultJwtBuilder.java | 2 +- .../impl/DefaultJwtParserBuilder.java | 2 +- .../impl/io/AbstractParserBuilder.java | 2 +- .../io/jsonwebtoken/impl/lang/Services.java | 104 +++++++----------- .../impl/security/JwksBridge.java | 2 +- .../groovy/io/jsonwebtoken/JwtsTest.groovy | 4 +- .../jsonwebtoken/RFC7515AppendixETest.groovy | 4 +- .../groovy/io/jsonwebtoken/RFC7797Test.groovy | 4 +- .../impl/DefaultJwtBuilderTest.groovy | 6 +- .../impl/DefaultJwtParserTest.groovy | 2 +- .../io/jsonwebtoken/impl/RfcTests.groovy | 2 +- .../impl/lang/ServicesTest.groovy | 25 ++--- .../impl/security/RFC7518AppendixCTest.groovy | 2 +- 13 files changed, 59 insertions(+), 102 deletions(-) diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java index b6b178c4..ef41c7ae 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java @@ -508,7 +508,7 @@ public class DefaultJwtBuilder implements JwtBuilder { if (this.serializer == null) { // try to find one based on the services available //noinspection unchecked - json(Services.loadFirst(Serializer.class)); + json(Services.get(Serializer.class)); } if (!Collections.isEmpty(claims)) { // normalize so we have one object to deal with: diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java index 9b57af0c..19290480 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java @@ -370,7 +370,7 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder { if (this.deserializer == null) { //noinspection unchecked - json(Services.loadFirst(Deserializer.class)); + json(Services.get(Deserializer.class)); } if (this.signingKeyResolver != null && this.signatureVerificationKey != null) { String msg = "Both a 'signingKeyResolver and a 'verifyWith' key cannot be configured. " + diff --git a/impl/src/main/java/io/jsonwebtoken/impl/io/AbstractParserBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/io/AbstractParserBuilder.java index b7d8eb78..b5549d59 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/io/AbstractParserBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/io/AbstractParserBuilder.java @@ -50,7 +50,7 @@ public abstract class AbstractParserBuilder> im public final Parser build() { if (this.deserializer == null) { //noinspection unchecked - this.deserializer = Services.loadFirst(Deserializer.class); + this.deserializer = Services.get(Deserializer.class); } return doBuild(); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Services.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Services.java index cd9c2f11..6125ebc0 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/Services.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Services.java @@ -15,25 +15,24 @@ */ package io.jsonwebtoken.impl.lang; +import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Assert; -import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import java.util.ServiceLoader; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import static io.jsonwebtoken.lang.Collections.arrayToList; - /** * Helper class for loading services from the classpath, using a {@link ServiceLoader}. Decouples loading logic for * better separation of concerns and testability. */ public final class Services { - private static ConcurrentMap, ServiceLoader> SERVICE_CACHE = new ConcurrentHashMap<>(); + private static final ConcurrentMap, Object> SERVICES = new ConcurrentHashMap<>(); - private static final List CLASS_LOADER_ACCESSORS = arrayToList(new ClassLoaderAccessor[] { + private static final List CLASS_LOADER_ACCESSORS = Arrays.asList(new ClassLoaderAccessor[]{ new ClassLoaderAccessor() { @Override public ClassLoader getClassLoader() { @@ -54,86 +53,57 @@ public final class Services { } }); - private Services() {} - - /** - * Loads and instantiates all service implementation of the given SPI class and returns them as a List. - * - * @param spi The class of the Service Provider Interface - * @param The type of the SPI - * @return An unmodifiable list with an instance of all available implementations of the SPI. No guarantee is given - * on the order of implementations, if more than one. - */ - public static List loadAll(Class spi) { - Assert.notNull(spi, "Parameter 'spi' must not be null."); - - ServiceLoader serviceLoader = serviceLoader(spi); - if (serviceLoader != null) { - - List implementations = new ArrayList<>(); - for (T implementation : serviceLoader) { - implementations.add(implementation); - } - return implementations; - } - - throw new UnavailableImplementationException(spi); + private Services() { } /** - * Loads the first available implementation the given SPI class from the classpath. Uses the {@link ServiceLoader} - * to find implementations. When multiple implementations are available it will return the first one that it - * encounters. There is no guarantee with regard to ordering. + * Returns the first available implementation for the given SPI class, checking an internal thread-safe cache first, + * and, if not found, using a {@link ServiceLoader} to find implementations. When multiple implementations are + * available it will return the first one that it encounters. There is no guarantee with regard to ordering. * * @param spi The class of the Service Provider Interface * @param The type of the SPI - * @return A new instance of the service. - * @throws UnavailableImplementationException When no implementation the SPI is available on the classpath. + * @return The first available instance of the service. + * @throws UnavailableImplementationException When no implementation of the SPI class can be found. */ - public static T loadFirst(Class spi) { - Assert.notNull(spi, "Parameter 'spi' must not be null."); - - ServiceLoader serviceLoader = serviceLoader(spi); - if (serviceLoader != null) { - return serviceLoader.iterator().next(); + public static T get(Class spi) { + // TODO: JDK8, replace this find/putIfAbsent logic with ConcurrentMap.computeIfAbsent + T instance = findCached(spi); + if (instance == null) { + instance = loadFirst(spi); // throws UnavailableImplementationException if not found, which is what we want + SERVICES.putIfAbsent(spi, instance); // cache if not already cached } - - throw new UnavailableImplementationException(spi); + return instance; } - /** - * Returns a ServiceLoader for spi class, checking multiple classloaders. The ServiceLoader - * will be cached if it contains at least one implementation of the spi class.
- * - * NOTE: Only the first Serviceloader will be cached. - * @param spi The interface or abstract class representing the service loader. - * @return A service loader, or null if no implementations are found - * @param The type of the SPI. - */ - private static ServiceLoader serviceLoader(Class spi) { - // TODO: JDK8, replace this get/putIfAbsent logic with ConcurrentMap.computeIfAbsent - ServiceLoader serviceLoader = (ServiceLoader) SERVICE_CACHE.get(spi); - if (serviceLoader != null) { - return serviceLoader; + private static T findCached(Class spi) { + Assert.notNull(spi, "Service interface cannot be null."); + Object obj = SERVICES.get(spi); + if (obj != null) { + return Assert.isInstanceOf(spi, obj, "Unexpected cached service implementation type."); } - - for (ClassLoaderAccessor classLoaderAccessor : CLASS_LOADER_ACCESSORS) { - serviceLoader = ServiceLoader.load(spi, classLoaderAccessor.getClassLoader()); - if (serviceLoader.iterator().hasNext()) { - SERVICE_CACHE.putIfAbsent(spi, serviceLoader); - return serviceLoader; - } - } - return null; } + private static T loadFirst(Class spi) { + for (ClassLoaderAccessor accessor : CLASS_LOADER_ACCESSORS) { + ServiceLoader loader = ServiceLoader.load(spi, accessor.getClassLoader()); + Assert.stateNotNull(loader, "JDK ServiceLoader#load should never return null."); + Iterator i = loader.iterator(); + Assert.stateNotNull(i, "JDK ServiceLoader#iterator() should never return null."); + if (i.hasNext()) { + return i.next(); + } + } + throw new UnavailableImplementationException(spi); + } + /** - * Clears internal cache of ServiceLoaders. This is useful when testing, or for applications that dynamically + * Clears internal cache of service singletons. This is useful when testing, or for applications that dynamically * change classloaders. */ public static void reload() { - SERVICE_CACHE.clear(); + SERVICES.clear(); } private interface ClassLoaderAccessor { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/JwksBridge.java b/impl/src/main/java/io/jsonwebtoken/impl/security/JwksBridge.java index 8b5de6a8..4ab494c0 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/JwksBridge.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/JwksBridge.java @@ -32,7 +32,7 @@ public final class JwksBridge { @SuppressWarnings({"unchecked", "unused"}) // used via reflection by io.jsonwebtoken.security.Jwks public static String UNSAFE_JSON(Jwk jwk) { - Serializer> serializer = Services.loadFirst(Serializer.class); + Serializer> serializer = Services.get(Serializer.class); Assert.stateNotNull(serializer, "Serializer lookup failed. Ensure JSON impl .jar is in the runtime classpath."); NamedSerializer ser = new NamedSerializer("JWK", serializer); ByteArrayOutputStream out = new ByteArrayOutputStream(512); diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index 95e2e8e0..2f1c432c 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -75,7 +75,7 @@ class JwtsTest { } static def toJson(def o) { - def serializer = Services.loadFirst(Serializer) + def serializer = Services.get(Serializer) def out = new ByteArrayOutputStream() serializer.serialize(o, out) return Strings.utf8(out.toByteArray()) @@ -1192,7 +1192,7 @@ class JwtsTest { int j = jws.lastIndexOf('.') def b64 = jws.substring(i, j) def json = Strings.utf8(Decoders.BASE64URL.decode(b64)) - def deser = Services.loadFirst(Deserializer) + def deser = Services.get(Deserializer) def m = deser.deserialize(new StringReader(json)) as Map assertEquals aud, m.get('aud') // single string value diff --git a/impl/src/test/groovy/io/jsonwebtoken/RFC7515AppendixETest.groovy b/impl/src/test/groovy/io/jsonwebtoken/RFC7515AppendixETest.groovy index 52d1e4a2..847a8022 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/RFC7515AppendixETest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/RFC7515AppendixETest.groovy @@ -29,8 +29,8 @@ import static org.junit.Assert.fail class RFC7515AppendixETest { - static final Serializer> serializer = Services.loadFirst(Serializer) - static final Deserializer> deserializer = Services.loadFirst(Deserializer) + static final Serializer> serializer = Services.get(Serializer) + static final Deserializer> deserializer = Services.get(Deserializer) static byte[] ser(def value) { ByteArrayOutputStream baos = new ByteArrayOutputStream(512) diff --git a/impl/src/test/groovy/io/jsonwebtoken/RFC7797Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/RFC7797Test.groovy index deb24973..b32036f8 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/RFC7797Test.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/RFC7797Test.groovy @@ -100,11 +100,9 @@ class RFC7797Test { def claims = Jwts.claims().subject('me').build() ByteArrayOutputStream out = new ByteArrayOutputStream() - Services.loadFirst(Serializer).serialize(claims, out) + Services.get(Serializer).serialize(claims, out) byte[] content = out.toByteArray() - //byte[] content = Services.loadFirst(Serializer).serialize(claims) - String s = Jwts.builder().signWith(key).content(content).encodePayload(false).compact() // But verify with 3 types of sources: string, byte array, and two different kinds of InputStreams: diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy index 3d0dfa94..591b4b07 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy @@ -45,7 +45,7 @@ class DefaultJwtBuilderTest { private DefaultJwtBuilder builder private static byte[] serialize(Map map) { - def serializer = Services.loadFirst(Serializer) + def serializer = Services.get(Serializer) ByteArrayOutputStream out = new ByteArrayOutputStream(512) serializer.serialize(map, out) return out.toByteArray() @@ -53,7 +53,7 @@ class DefaultJwtBuilderTest { private static Map deser(byte[] data) { def reader = Streams.reader(data) - Map m = Services.loadFirst(Deserializer).deserialize(reader) as Map + Map m = Services.get(Deserializer).deserialize(reader) as Map return m } @@ -749,7 +749,7 @@ class DefaultJwtBuilderTest { // so we need to check the raw payload: def encoded = new JwtTokenizer().tokenize(Streams.reader(jwt)).getPayload() byte[] bytes = Decoders.BASE64URL.decode(encoded) - def claims = Services.loadFirst(Deserializer).deserialize(Streams.reader(bytes)) + def claims = Services.get(Deserializer).deserialize(Streams.reader(bytes)) assertEquals two, claims.aud } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy index 8084b32f..4c41142c 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy @@ -54,7 +54,7 @@ class DefaultJwtParserTest { } private static byte[] serialize(Map map) { - def serializer = Services.loadFirst(Serializer) + def serializer = Services.get(Serializer) ByteArrayOutputStream out = new ByteArrayOutputStream(512) serializer.serialize(map, out) return out.toByteArray() diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/RfcTests.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/RfcTests.groovy index 548cf408..99cf1dd1 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/RfcTests.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/RfcTests.groovy @@ -38,7 +38,7 @@ class RfcTests { static final Map jsonToMap(String json) { Reader r = new CharSequenceReader(json) - Map m = Services.loadFirst(Deserializer).deserialize(r) as Map + Map m = Services.get(Deserializer).deserialize(r) as Map return m } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/ServicesTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/ServicesTest.groovy index 8af9300e..0f880d2b 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/ServicesTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/ServicesTest.groovy @@ -20,32 +20,21 @@ import io.jsonwebtoken.impl.DefaultStubService import org.junit.After import org.junit.Test -import static org.junit.Assert.* +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertNotNull class ServicesTest { @Test void testSuccessfulLoading() { - def factory = Services.loadFirst(StubService) - assertNotNull factory - assertEquals(DefaultStubService, factory.class) + def service = Services.get(StubService) + assertNotNull service + assertEquals(DefaultStubService, service.class) } @Test(expected = UnavailableImplementationException) - void testLoadFirstUnavailable() { - Services.loadFirst(NoService.class) - } - - @Test - void testLoadAllAvailable() { - def list = Services.loadAll(StubService.class) - assertEquals 1, list.size() - assertTrue list[0] instanceof StubService - } - - @Test(expected = UnavailableImplementationException) - void testLoadAllUnavailable() { - Services.loadAll(NoService.class) + void testLoadUnavailable() { + Services.get(NoService.class) } @Test diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixCTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixCTest.groovy index abdb1a94..b1d068c7 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixCTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixCTest.groovy @@ -43,7 +43,7 @@ class RFC7518AppendixCTest { } private static final Map fromJson(String s) { - return Services.loadFirst(Deserializer).deserialize(new StringReader(s)) as Map + return Services.get(Deserializer).deserialize(new StringReader(s)) as Map } private static EcPrivateJwk readJwk(String json) { From 86e06559bc84091c0c94158f44b89af298a0cb6b Mon Sep 17 00:00:00 2001 From: lhazlewood <121180+lhazlewood@users.noreply.github.com> Date: Wed, 17 Jan 2024 16:45:30 -0800 Subject: [PATCH 03/17] - Ensures that Jackson duplicate property detection/rejection is enabled by default. (#895) Fixes #877 --- .../jackson/io/JacksonDeserializer.java | 50 ++++++++++++++++--- .../jackson/io/JacksonSerializer.java | 18 ++++++- .../jackson/io/JacksonDeserializerTest.groovy | 26 ++++++++++ 3 files changed, 85 insertions(+), 9 deletions(-) diff --git a/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonDeserializer.java b/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonDeserializer.java index 8314f1ef..afb15291 100644 --- a/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonDeserializer.java +++ b/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonDeserializer.java @@ -72,14 +72,8 @@ public class JacksonDeserializer extends AbstractDeserializer { * @param claimTypeMap The claim name-to-class map used to deserialize claims into the given type */ public JacksonDeserializer(Map> claimTypeMap) { - // DO NOT reuse JacksonSerializer.DEFAULT_OBJECT_MAPPER as this could result in sharing the custom deserializer - // between instances - this(new ObjectMapper()); - Assert.notNull(claimTypeMap, "Claim type map cannot be null."); - // register a new Deserializer - SimpleModule module = new SimpleModule(); - module.addDeserializer(Object.class, new MappedTypeDeserializer(Collections.unmodifiableMap(claimTypeMap))); - objectMapper.registerModule(module); + // DO NOT specify JacksonSerializer.DEFAULT_OBJECT_MAPPER here as that would modify the shared instance + this(JacksonSerializer.newObjectMapper(), claimTypeMap); } /** @@ -92,6 +86,46 @@ public class JacksonDeserializer extends AbstractDeserializer { this(objectMapper, (Class) Object.class); } + /** + * Creates a new JacksonDeserializer where the values of the claims can be parsed into given types by registering + * a type-converting {@link com.fasterxml.jackson.databind.Module Module} on the specified {@link ObjectMapper}. + * A common usage example is to parse custom User object out of a claim, for example the claims: + *
{@code
+     * {
+     *     "issuer": "https://issuer.example.com",
+     *     "user": {
+     *         "firstName": "Jill",
+     *         "lastName": "Coder"
+     *     }
+     * }}
+ * Passing a map of {@code ["user": User.class]} to this constructor would result in the {@code user} claim being + * transformed to an instance of your custom {@code User} class, instead of the default of {@code Map}. + *

+ * Because custom type parsing requires modifying the state of a Jackson {@code ObjectMapper}, this + * constructor modifies the specified {@code objectMapper} argument and customizes it to support the + * specified {@code claimTypeMap}. + *

+ * If you do not want your {@code ObjectMapper} instance modified, but also want to support custom types for + * JWT {@code Claims}, you will need to first customize your {@code ObjectMapper} instance by registering + * your custom types separately and then use the {@link #JacksonDeserializer(ObjectMapper)} constructor instead + * (which does not modify the {@code objectMapper} argument). + * + * @param objectMapper the objectMapper to modify by registering a custom type-converting + * {@link com.fasterxml.jackson.databind.Module Module} + * @param claimTypeMap The claim name-to-class map used to deserialize claims into the given type + * @since 0.12.4 + */ + //TODO: Make this public on a minor release + // (cannot do that on a point release as that would violate semver) + private JacksonDeserializer(ObjectMapper objectMapper, Map> claimTypeMap) { + this(objectMapper); + Assert.notNull(claimTypeMap, "Claim type map cannot be null."); + // register a new Deserializer on the ObjectMapper instance: + SimpleModule module = new SimpleModule(); + module.addDeserializer(Object.class, new MappedTypeDeserializer(Collections.unmodifiableMap(claimTypeMap))); + objectMapper.registerModule(module); + } + private JacksonDeserializer(ObjectMapper objectMapper, Class returnType) { Assert.notNull(objectMapper, "ObjectMapper cannot be null."); Assert.notNull(returnType, "Return type cannot be null."); diff --git a/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonSerializer.java b/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonSerializer.java index 31c5ae00..2582da50 100644 --- a/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonSerializer.java +++ b/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonSerializer.java @@ -16,6 +16,7 @@ package io.jsonwebtoken.jackson.io; import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; @@ -41,7 +42,22 @@ public class JacksonSerializer extends AbstractSerializer { MODULE = module; } - static final ObjectMapper DEFAULT_OBJECT_MAPPER = new ObjectMapper().registerModule(MODULE); + static final ObjectMapper DEFAULT_OBJECT_MAPPER = newObjectMapper(); + + /** + * Creates and returns a new ObjectMapper with the {@code jjwt-jackson} module registered and + * {@code JsonParser.Feature.STRICT_DUPLICATE_DETECTION} enabled (set to true). + * + * @return and returns a new ObjectMapper with the {@code jjwt-jackson} module registered and + * {@code JsonParser.Feature.STRICT_DUPLICATE_DETECTION} enabled (set to true). + * @since 0.12.4 + */ + // package protected on purpose, do not expose to the public API + static ObjectMapper newObjectMapper() { + return new ObjectMapper() + .registerModule(MODULE) + .configure(JsonParser.Feature.STRICT_DUPLICATE_DETECTION, true); // https://github.com/jwtk/jjwt/issues/877 + } protected final ObjectMapper objectMapper; diff --git a/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonDeserializerTest.groovy b/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonDeserializerTest.groovy index 25e27a36..62f253ec 100644 --- a/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonDeserializerTest.groovy +++ b/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonDeserializerTest.groovy @@ -16,6 +16,7 @@ //file:noinspection GrDeprecatedAPIUsage package io.jsonwebtoken.jackson.io +import com.fasterxml.jackson.core.JsonParseException import com.fasterxml.jackson.databind.ObjectMapper import io.jsonwebtoken.io.DeserializationException import io.jsonwebtoken.io.Deserializer @@ -120,6 +121,31 @@ class JacksonDeserializerTest { assertEquals expected, result } + /** + * Asserts https://github.com/jwtk/jjwt/issues/877 + */ + @Test + void testStrictDuplicateDetection() { + // 'bKey' is repeated twice: + String json = """ + { + "aKey":"oneValue", + "bKey": 15, + "bKey": "hello" + } + """ + try { + new JacksonDeserializer<>().deserialize(new StringReader(json)) + fail() + } catch (DeserializationException expected) { + String causeMsg = "Duplicate field 'bKey'\n at [Source: (StringReader); line: 5, column: 23]" + String msg = "Unable to deserialize: $causeMsg" + assertEquals msg, expected.getMessage() + assertTrue expected.getCause() instanceof JsonParseException + assertEquals causeMsg, expected.getCause().getMessage() + } + } + /** * For: https://github.com/jwtk/jjwt/issues/564 */ From 3e8f8a84f50c2a9ac496eff3616c2fe7394ff314 Mon Sep 17 00:00:00 2001 From: Philzen Date: Tue, 23 Jan 2024 21:47:28 +0100 Subject: [PATCH 04/17] Fix broken links in "Learn more" section (#898) All of Stormpath.com consistently responds with 404 ATM, therefore this commit replaces the links with archive.org versions that will hopefully stay available for good. --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 79bc6cc1..c5211588 100644 --- a/README.md +++ b/README.md @@ -3767,11 +3767,11 @@ assert privJwk.equals(parsed); ## Learn More -- [JSON Web Token for Java and Android](https://stormpath.com/blog/jjwt-how-it-works-why/) -- [How to Create and Verify JWTs in Java](https://stormpath.com/blog/jwt-java-create-verify/) -- [Where to Store Your JWTs - Cookies vs HTML5 Web Storage](https://stormpath.com/blog/where-to-store-your-jwts-cookies-vs-html5-web-storage/) -- [Use JWT the Right Way!](https://stormpath.com/blog/jwt-the-right-way/) -- [Token Authentication for Java Applications](https://stormpath.com/blog/token-auth-for-java/) +- [JSON Web Token for Java and Android](https://web.archive.org/web/20230427122653/https://stormpath.com/blog/jjwt-how-it-works-why) +- [How to Create and Verify JWTs in Java](https://web.archive.org/web/20230426235608/https://stormpath.com/blog/jwt-java-create-verify) +- [Where to Store Your JWTs - Cookies vs HTML5 Web Storage](https://web.archive.org/web/20230428094039/https://stormpath.com/blog/where-to-store-your-jwts-cookies-vs-html5-web-storage) +- [Use JWT the Right Way!](https://web.archive.org/web/20230428184004/https://stormpath.com/blog/jwt-the-right-way) +- [Token Authentication for Java Applications](https://web.archive.org/web/20230427151310/https://stormpath.com/blog/token-auth-for-java) - [JJWT Changelog](CHANGELOG.md) ## Author From 07631914c39f5852fd24c0e2868e32fd47fa3128 Mon Sep 17 00:00:00 2001 From: lhazlewood <121180+lhazlewood@users.noreply.github.com> Date: Thu, 25 Jan 2024 21:31:36 -0800 Subject: [PATCH 05/17] NIST Elliptic Curve JWKs: field element byte array padding (#903) * Ensured NIST Elliptic Curve JWKs pre-pad their X, Y and D byte arrays with zero bytes before Base64URL-encoding if necessary per length requirements defined in: - https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.1.2 - https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.1.3 - https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.2.1 Fixes #901. --- CHANGELOG.md | 6 ++ .../java/io/jsonwebtoken/impl/lang/Bytes.java | 21 ++++++ .../impl/security/AbstractEcJwkFactory.java | 17 +++-- .../impl/security/DefaultEcPrivateJwk.java | 5 +- .../impl/security/DefaultEcPublicJwk.java | 8 ++- .../impl/security/EcPrivateJwkFactory.java | 6 +- .../impl/security/EcPublicJwkFactory.java | 5 +- .../impl/security/FieldElementConverter.java | 69 +++++++++++++++++++ .../security/AbstractEcJwkFactoryTest.groovy | 40 ++++++++++- .../security/DispatchingJwkFactoryTest.groovy | 6 +- .../security/FieldElementConverterTest.groovy | 41 +++++++++++ .../security/RFC7517AppendixA2Test.groovy | 13 ++-- 12 files changed, 209 insertions(+), 28 deletions(-) create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/FieldElementConverter.java create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/FieldElementConverterTest.groovy diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f1b9f7d..d637784f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ This patch release: `java.io.Reader` instance. [Issue 882](https://github.com/jwtk/jjwt/issues/882). * Ensures a single string `aud` (Audience) claim is retained (without converting it to a `Set`) when copying/applying a source Claims instance to a destination Claims builder. [Issue 890](https://github.com/jwtk/jjwt/issues/890). +* Ensures P-256, P-384 and P-521 Elliptic Curve JWKs zero-pad their field element (`x`, `y`, and `d`) byte array values + if necessary before Base64Url-encoding per [RFC 7518](https://datatracker.ietf.org/doc/html/rfc7518), Sections + [6.2.1.2](https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.1.2), + [6.2.1.3](https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.1.3), and + [6.2.2.1](https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.2.1), respectively. + [Issue 901](https://github.com/jwtk/jjwt/issues/901). ### 0.12.3 diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Bytes.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Bytes.java index 3cb49c6c..7f1c84ee 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/Bytes.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Bytes.java @@ -240,4 +240,25 @@ public final class Bytes { } } } + + /** + * Pads the front of the specified byte array with zeros if necessary, returning a new padded result, or the + * original array unmodified if padding isn't necessary. Padding is only performed if {@code length} is greater + * than {@code bytes.length}. + * + * @param bytes the byte array to pre-pad with zeros if necessary + * @param length the length of the required output array + * @return the potentially pre-padded byte array, or the existing {@code bytes} array if padding wasn't necessary. + * @since 0.12.4 + */ + public static byte[] prepad(byte[] bytes, int length) { + Assert.notNull(bytes, "byte array cannot be null."); + Assert.gt(length, 0, "length must be positive (> 0)."); + if (bytes.length < length) { // need to pad with leading zero(es): + byte[] padded = new byte[length]; + System.arraycopy(bytes, 0, padded, length - bytes.length, bytes.length); + bytes = padded; + } + return bytes; + } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java index dc2c9ca0..46b844dd 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java @@ -15,6 +15,7 @@ */ package io.jsonwebtoken.impl.security; +import io.jsonwebtoken.impl.lang.Bytes; import io.jsonwebtoken.impl.lang.Converters; import io.jsonwebtoken.impl.lang.Parameter; import io.jsonwebtoken.io.Encoders; @@ -24,6 +25,7 @@ import io.jsonwebtoken.security.UnsupportedKeyException; import java.math.BigInteger; import java.security.Key; import java.security.interfaces.ECKey; +import java.security.spec.EllipticCurve; import java.util.Set; abstract class AbstractEcJwkFactory> extends AbstractFamilyJwkFactory { @@ -41,19 +43,16 @@ abstract class AbstractEcJwkFactory> ext * https://tools.ietf.org/html/rfc7518#section-6.2.1.2 indicates that this algorithm logic is defined in * http://www.secg.org/sec1-v2.pdf Section 2.3.5. * - * @param fieldSize EC field size - * @param coordinate EC point coordinate (e.g. x or y) + * @param curve EllipticCurve + * @param coordinate EC point coordinate (e.g. x or y) on the {@code curve} * @return A base64Url-encoded String representing the EC field element per the RFC format */ // Algorithm defined in http://www.secg.org/sec1-v2.pdf Section 2.3.5 - static String toOctetString(int fieldSize, BigInteger coordinate) { + static String toOctetString(EllipticCurve curve, BigInteger coordinate) { byte[] bytes = Converters.BIGINT_UBYTES.applyTo(coordinate); - int mlen = (int) Math.ceil(fieldSize / 8d); - if (mlen > bytes.length) { - byte[] m = new byte[mlen]; - System.arraycopy(bytes, 0, m, mlen - bytes.length, bytes.length); - bytes = m; - } + int fieldSizeInBits = curve.getField().getFieldSize(); + int mlen = Bytes.length(fieldSizeInBits); + bytes = Bytes.prepad(bytes, mlen); return Encoders.BASE64URL.encode(bytes); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPrivateJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPrivateJwk.java index c8f6c6e7..61f56c13 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPrivateJwk.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPrivateJwk.java @@ -31,7 +31,10 @@ import static io.jsonwebtoken.impl.security.DefaultEcPublicJwk.equalsPublic; class DefaultEcPrivateJwk extends AbstractPrivateJwk implements EcPrivateJwk { - static final Parameter D = Parameters.secretBigInt("d", "ECC Private Key"); + static final Parameter D = Parameters.bigInt("d", "ECC Private Key") + .setConverter(FieldElementConverter.B64URL_CONVERTER) + .setSecret(true) // important! + .build(); static final Set> PARAMS = Collections.concat(DefaultEcPublicJwk.PARAMS, D); DefaultEcPrivateJwk(JwkContext ctx, EcPublicJwk pubJwk) { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPublicJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPublicJwk.java index b60864f3..add89f69 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPublicJwk.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPublicJwk.java @@ -30,9 +30,12 @@ import java.util.Set; class DefaultEcPublicJwk extends AbstractPublicJwk implements EcPublicJwk { static final String TYPE_VALUE = "EC"; + static final Parameter CRV = Parameters.string("crv", "Curve"); - static final Parameter X = Parameters.bigInt("x", "X Coordinate").build(); - static final Parameter Y = Parameters.bigInt("y", "Y Coordinate").build(); + static final Parameter X = Parameters.bigInt("x", "X Coordinate") + .setConverter(FieldElementConverter.B64URL_CONVERTER).build(); + static final Parameter Y = Parameters.bigInt("y", "Y Coordinate") + .setConverter(FieldElementConverter.B64URL_CONVERTER).build(); static final Set> PARAMS = Collections.concat(AbstractAsymmetricJwk.PARAMS, CRV, X, Y); // https://www.rfc-editor.org/rfc/rfc7638#section-3.2 @@ -52,4 +55,5 @@ class DefaultEcPublicJwk extends AbstractPublicJwk implements EcPub protected boolean equals(PublicJwk jwk) { return jwk instanceof EcPublicJwk && equalsPublic(this, jwk); } + } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EcPrivateJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EcPrivateJwkFactory.java index 941cdc20..b37c2242 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/EcPrivateJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EcPrivateJwkFactory.java @@ -35,7 +35,8 @@ import java.security.spec.InvalidKeySpecException; class EcPrivateJwkFactory extends AbstractEcJwkFactory { - private static final String ECPUBKEY_ERR_MSG = "JwkContext publicKey must be an " + ECPublicKey.class.getName() + " instance."; + private static final String ECPUBKEY_ERR_MSG = "JwkContext publicKey must be an " + ECPublicKey.class.getName() + + " instance."; private static final EcPublicJwkFactory PUB_FACTORY = EcPublicJwkFactory.INSTANCE; @@ -96,8 +97,7 @@ class EcPrivateJwkFactory extends AbstractEcJwkFactory ctx.put(DefaultEcPublicJwk.CRV.getId(), curveId); - int fieldSize = curve.getField().getFieldSize(); - String x = toOctetString(fieldSize, point.getAffineX()); + String x = toOctetString(curve, point.getAffineX()); ctx.put(DefaultEcPublicJwk.X.getId(), x); - String y = toOctetString(fieldSize, point.getAffineY()); + String y = toOctetString(curve, point.getAffineY()); ctx.put(DefaultEcPublicJwk.Y.getId(), y); return new DefaultEcPublicJwk(ctx); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/FieldElementConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/security/FieldElementConverter.java new file mode 100644 index 00000000..7c07ca43 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/FieldElementConverter.java @@ -0,0 +1,69 @@ +/* + * Copyright © 2024 jsonwebtoken.io + * + * 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 + * + * http://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 io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.impl.io.Codec; +import io.jsonwebtoken.impl.lang.Bytes; +import io.jsonwebtoken.impl.lang.Converter; +import io.jsonwebtoken.impl.lang.Converters; + +import java.math.BigInteger; + +/** + * Hotfix for JJWT Issue 901. This is currently hard-coded + * expecting field elements for NIST P-256, P-384, or P-521 curves. Ideally this should be refactored to work for + * any curve based on its field size, not just for these NIST curves. However, the + * {@link EcPublicJwkFactory} and {@link EcPrivateJwkFactory} implementations only work with JWA NIST curves, + * so this implementation is acceptable until (and if) different Weierstrass elliptic curves (ever) need to be + * supported. + * + * @since 0.12.4 + */ +final class FieldElementConverter implements Converter { + + static final FieldElementConverter INSTANCE = new FieldElementConverter(); + + static final Converter B64URL_CONVERTER = Converters.forEncoded(BigInteger.class, + Converters.compound(INSTANCE, Codec.BASE64URL)); + + private static int bytelen(ECCurve curve) { + return Bytes.length(curve.toParameterSpec().getCurve().getField().getFieldSize()); + } + + private static final int P256_BYTE_LEN = bytelen(ECCurve.P256); + private static final int P384_BYTE_LEN = bytelen(ECCurve.P384); + private static final int P521_BYTE_LEN = bytelen(ECCurve.P521); + + @Override + public byte[] applyTo(BigInteger bigInteger) { + byte[] bytes = Converters.BIGINT_UBYTES.applyTo(bigInteger); + int len = bytes.length; + if (len == P256_BYTE_LEN || len == P384_BYTE_LEN || len == P521_BYTE_LEN) return bytes; + if (len < P256_BYTE_LEN) { + bytes = Bytes.prepad(bytes, P256_BYTE_LEN); + } else if (len < P384_BYTE_LEN) { + bytes = Bytes.prepad(bytes, P384_BYTE_LEN); + } else { // > P-384, so must be P-521: + bytes = Bytes.prepad(bytes, P521_BYTE_LEN); + } + return bytes; + } + + @Override + public BigInteger applyFrom(byte[] bytes) { + return Converters.BIGINT_UBYTES.applyFrom(bytes); + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkFactoryTest.groovy index 78fb0a18..42fb889e 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkFactoryTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkFactoryTest.groovy @@ -15,10 +15,16 @@ */ package io.jsonwebtoken.impl.security - +import io.jsonwebtoken.impl.lang.Bytes +import io.jsonwebtoken.impl.lang.Services +import io.jsonwebtoken.io.Decoders +import io.jsonwebtoken.io.Deserializer +import io.jsonwebtoken.security.Jwks import io.jsonwebtoken.security.UnsupportedKeyException import org.junit.Test +import java.security.interfaces.ECPrivateKey + import static org.junit.Assert.assertEquals import static org.junit.Assert.fail @@ -35,4 +41,36 @@ class AbstractEcJwkFactoryTest { assertEquals msg, e.getMessage() } } + + /** + * Asserts correct behavior per https://github.com/jwtk/jjwt/issues/901 + */ + @Test + void fieldElementByteArrayLength() { + + EcSignatureAlgorithmTest.algs().each { alg -> + + def key = alg.keyPair().build().getPrivate() as ECPrivateKey + def jwk = Jwks.builder().key(key).build() + + def json = Jwks.UNSAFE_JSON(jwk) + def map = Services.get(Deserializer).deserialize(new StringReader(json)) as Map + def xs = map.get("x") as String + def ys = map.get("y") as String + def ds = map.get("d") as String + + def x = Decoders.BASE64URL.decode(xs) + def y = Decoders.BASE64URL.decode(ys) + def d = Decoders.BASE64URL.decode(ds) + + // most important part of the test: the decoded byte arrays must have a length equal to the curve + // field size (in bytes): + int fieldSizeInBits = key.getParams().getCurve().getField().getFieldSize() + int fieldSizeInBytes = Bytes.length(fieldSizeInBits) + + assertEquals fieldSizeInBytes, x.length + assertEquals fieldSizeInBytes, y.length + assertEquals fieldSizeInBytes, d.length + } + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DispatchingJwkFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DispatchingJwkFactoryTest.groovy index 612b75fa..126ce186 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DispatchingJwkFactoryTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DispatchingJwkFactoryTest.groovy @@ -109,9 +109,9 @@ class DispatchingJwkFactoryTest { assertTrue jwk instanceof EcPrivateJwk def key = jwk.toKey() assertTrue key instanceof ECPrivateKey - String x = AbstractEcJwkFactory.toOctetString(key.params.curve.field.fieldSize, jwk.toPublicJwk().toKey().w.affineX) - String y = AbstractEcJwkFactory.toOctetString(key.params.curve.field.fieldSize, jwk.toPublicJwk().toKey().w.affineY) - String d = AbstractEcJwkFactory.toOctetString(key.params.curve.field.fieldSize, key.s) + String x = AbstractEcJwkFactory.toOctetString(key.params.curve, jwk.toPublicJwk().toKey().w.affineX) + String y = AbstractEcJwkFactory.toOctetString(key.params.curve, jwk.toPublicJwk().toKey().w.affineY) + String d = AbstractEcJwkFactory.toOctetString(key.params.curve, key.s) assertEquals jwk.d.get(), d //remove the 'd' mapping to represent only a public key: diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/FieldElementConverterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/FieldElementConverterTest.groovy new file mode 100644 index 00000000..e8a1ea16 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/FieldElementConverterTest.groovy @@ -0,0 +1,41 @@ +/* + * Copyright © 2024 jsonwebtoken.io + * + * 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 + * + * http://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 io.jsonwebtoken.impl.security + +import io.jsonwebtoken.impl.lang.Bytes +import org.junit.Test + +import static org.junit.Assert.assertEquals + +/** + * @since 0.12.4 + */ +class FieldElementConverterTest { + + static FieldElementConverter converter = FieldElementConverter.INSTANCE + + @Test + void p384CoordinateNeedsPadding() { + def requiredByteLen = 48 + def coordBytes = Bytes.random(requiredByteLen - 1) // one less to see if padding is applied + def coord = new BigInteger(1, coordBytes) + byte[] result = converter.applyTo(coord) + assertEquals requiredByteLen, result.length + assertEquals 0x00 as byte, result[0] + //ensure roundtrip works: + assertEquals coord, converter.applyFrom(result) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixA2Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixA2Test.groovy index b3321944..d7e9fa0c 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixA2Test.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixA2Test.groovy @@ -23,6 +23,7 @@ import org.junit.Test import java.security.interfaces.ECPrivateKey import java.security.interfaces.RSAPrivateCrtKey +import java.security.spec.EllipticCurve import static org.junit.Assert.* @@ -31,8 +32,8 @@ import static org.junit.Assert.* */ class RFC7517AppendixA2Test { - private static final String ecEncode(int fieldSize, BigInteger coord) { - return AbstractEcJwkFactory.toOctetString(fieldSize, coord) + private static final String ecEncode(EllipticCurve curve, BigInteger coord) { + return AbstractEcJwkFactory.toOctetString(curve, coord) } private static final String rsaEncode(BigInteger i) { @@ -90,17 +91,17 @@ class RFC7517AppendixA2Test { def m = keys[0] def jwk = Jwks.builder().add(m).build() as EcPrivateJwk def key = jwk.toKey() - int fieldSize = key.params.curve.field.fieldSize + def curve = key.params.curve assertTrue key instanceof ECPrivateKey assertEquals m.size(), jwk.size() assertEquals m.kty, jwk.getType() assertEquals m.crv, jwk.get('crv') assertEquals m.x, jwk.get('x') - assertEquals m.x, ecEncode(fieldSize, jwk.toPublicJwk().toKey().w.affineX) + assertEquals m.x, ecEncode(curve, jwk.toPublicJwk().toKey().w.affineX) assertEquals m.y, jwk.get('y') - assertEquals m.y, ecEncode(fieldSize, jwk.toPublicJwk().toKey().w.affineY) + assertEquals m.y, ecEncode(curve, jwk.toPublicJwk().toKey().w.affineY) assertEquals m.d, jwk.get('d').get() - assertEquals m.d, ecEncode(fieldSize, key.s) + assertEquals m.d, ecEncode(curve, key.s) assertEquals m.use, jwk.getPublicKeyUse() assertEquals m.kid, jwk.getId() From fd619e0a4229e01cbd3ab1bd0a7a4f6cab21d784 Mon Sep 17 00:00:00 2001 From: Ahmad Amiri Date: Sat, 27 Jan 2024 03:42:34 +0330 Subject: [PATCH 06/17] disable FAIL_ON_UNKNOWN_PROPERTIES deserialization feature of Jackson by default (#896) --- .../jackson/io/JacksonSerializer.java | 13 +++-- .../jackson/io/JacksonDeserializerTest.groovy | 58 +++++++++++++++++++ 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonSerializer.java b/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonSerializer.java index 2582da50..a00541b6 100644 --- a/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonSerializer.java +++ b/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonSerializer.java @@ -17,6 +17,7 @@ package io.jsonwebtoken.jackson.io; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; @@ -46,17 +47,21 @@ public class JacksonSerializer extends AbstractSerializer { /** * Creates and returns a new ObjectMapper with the {@code jjwt-jackson} module registered and - * {@code JsonParser.Feature.STRICT_DUPLICATE_DETECTION} enabled (set to true). + * {@code JsonParser.Feature.STRICT_DUPLICATE_DETECTION} enabled (set to true) and + * {@code DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES} disabled (set to false). + * + * @return a new ObjectMapper with the {@code jjwt-jackson} module registered and + * {@code JsonParser.Feature.STRICT_DUPLICATE_DETECTION} enabled (set to true) and + * {@code DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES} disabled (set to false). * - * @return and returns a new ObjectMapper with the {@code jjwt-jackson} module registered and - * {@code JsonParser.Feature.STRICT_DUPLICATE_DETECTION} enabled (set to true). * @since 0.12.4 */ // package protected on purpose, do not expose to the public API static ObjectMapper newObjectMapper() { return new ObjectMapper() .registerModule(MODULE) - .configure(JsonParser.Feature.STRICT_DUPLICATE_DETECTION, true); // https://github.com/jwtk/jjwt/issues/877 + .configure(JsonParser.Feature.STRICT_DUPLICATE_DETECTION, true) // https://github.com/jwtk/jjwt/issues/877 + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); // https://github.com/jwtk/jjwt/issues/893 } protected final ObjectMapper objectMapper; diff --git a/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonDeserializerTest.groovy b/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonDeserializerTest.groovy index 62f253ec..2363057d 100644 --- a/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonDeserializerTest.groovy +++ b/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonDeserializerTest.groovy @@ -146,6 +146,64 @@ class JacksonDeserializerTest { } } + /** + * Asserts https://github.com/jwtk/jjwt/issues/893 + */ + @Test + void testIgnoreUnknownPropertiesWhenDeserializeWithCustomObject() { + + long currentTime = System.currentTimeMillis() + + String json = """ + { + "oneKey":"oneValue", + "custom": { + "stringValue": "s-value", + "intValue": "11", + "dateValue": ${currentTime}, + "shortValue": 22, + "longValue": 33, + "byteValue": 15, + "byteArrayValue": "${base64('bytes')}", + "unknown": "unknown", + "nestedValue": { + "stringValue": "nested-value", + "intValue": "111", + "dateValue": ${currentTime + 1}, + "shortValue": 222, + "longValue": 333, + "byteValue": 10, + "byteArrayValue": "${base64('bytes2')}", + "unknown": "unknown" + } + } + } + """ + + CustomBean expectedCustomBean = new CustomBean() + .setByteArrayValue("bytes".getBytes("UTF-8")) + .setByteValue(0xF as byte) + .setDateValue(new Date(currentTime)) + .setIntValue(11) + .setShortValue(22 as short) + .setLongValue(33L) + .setStringValue("s-value") + .setNestedValue(new CustomBean() + .setByteArrayValue("bytes2".getBytes("UTF-8")) + .setByteValue(0xA as byte) + .setDateValue(new Date(currentTime + 1)) + .setIntValue(111) + .setShortValue(222 as short) + .setLongValue(333L) + .setStringValue("nested-value") + ) + + def expected = [oneKey: "oneValue", custom: expectedCustomBean] + def result = new JacksonDeserializer(Maps.of("custom", CustomBean).build()) + .deserialize(new StringReader(json)) + assertEquals expected, result + } + /** * For: https://github.com/jwtk/jjwt/issues/564 */ From f61cfa875d1bfd9164c16069ab62bf7b74fcc831 Mon Sep 17 00:00:00 2001 From: lhazlewood <121180+lhazlewood@users.noreply.github.com> Date: Fri, 26 Jan 2024 17:54:51 -0800 Subject: [PATCH 07/17] Test case change to reflect accurate assertion for Elliptic Curve 'd' values against the curve order (not the field size) per https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.2.1 (#906) --- .../impl/security/AbstractEcJwkFactoryTest.groovy | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkFactoryTest.groovy index 42fb889e..fbbd144d 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkFactoryTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkFactoryTest.groovy @@ -63,14 +63,18 @@ class AbstractEcJwkFactoryTest { def y = Decoders.BASE64URL.decode(ys) def d = Decoders.BASE64URL.decode(ds) - // most important part of the test: the decoded byte arrays must have a length equal to the curve - // field size (in bytes): + // most important part of the test: 'x' and 'y' decoded byte arrays must have a length equal to the curve + // field size (in bytes) per https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.1.2 and + // https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.1.3 int fieldSizeInBits = key.getParams().getCurve().getField().getFieldSize() int fieldSizeInBytes = Bytes.length(fieldSizeInBits) - assertEquals fieldSizeInBytes, x.length assertEquals fieldSizeInBytes, y.length - assertEquals fieldSizeInBytes, d.length + + // and 'd' must have a length equal to the curve order size in bytes per + // https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.2.1 + int orderSizeInBytes = Bytes.length(key.params.order.bitLength()) + assertEquals orderSizeInBytes, d.length } } } From 26f5dc3dbbb6070735498e4ea497f174b0a3850f Mon Sep 17 00:00:00 2001 From: lhazlewood <121180+lhazlewood@users.noreply.github.com> Date: Fri, 26 Jan 2024 21:23:18 -0800 Subject: [PATCH 08/17] Updating changelog with more information/clarity for the 0.12.4 release (#907) --- CHANGELOG.md | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d637784f..5934880e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,59 @@ ### 0.12.4 -This patch release: +This patch release includes various changes listed below. +#### Jackson Default Parsing Behavior + +This release makes two behavioral changes to JJWT's default Jackson `ObjectMapper` parsing settings: + +1. In the interest of having stronger standards to reject potentially malformed/malicious/accidental JSON that could + have undesirable effects on an application, JJWT's default `ObjectMapper `is now configured to explicitly reject/fail + parsing JSON (JWT headers and/or Claims) if/when that JSON contains duplicate JSON member names. + + For example, now the following JSON, if parsed, would fail (be rejected) by default: + ```json + { + "hello": "world", + "thisWillFail": 42, + "thisWillFail": "test" + } + ``` + + Technically, the JWT RFCs _do allow_ duplicate named fields as long as the last parsed member is the one used + (see [JWS RFC 7515, Section 4](https://datatracker.ietf.org/doc/html/rfc7515#section-4)), so this is allowed. + However, because JWTs often reflect security concepts, it's usually better to be defensive and reject these + unexpected scenarios by default. The RFC later supports this position/preference in + [Section 10.12](https://datatracker.ietf.org/doc/html/rfc7515#section-10.12): + + Ambiguous and potentially exploitable situations + could arise if the JSON parser used does not enforce the uniqueness + of member names or returns an unpredictable value for duplicate + member names. + + Finally, this is just a default, and the RFC does indeed allow duplicate member names if the last value is used, + so applications that require duplicates to be allowed can simply configure their own `ObjectMapper` and use + that with JJWT instead of assuming this (new) JJWT default. See + [Issue #877](https://github.com/jwtk/jjwt/issues/877) for more. +2. If using JJWT's support to use Jackson to parse + [Custom Claim Types](https://github.com/jwtk/jjwt#json-jackson-custom-types) (for example, a Claim that should be + unmarshalled into a POJO), and the JSON for that POJO contained a member that is not represented in the specified + class, Jackson would fail parsing by default. Because POJOs and JSON data models can sometimes be out of sync + due to different class versions, the default behavior has been changed to ignore these unknown JSON members instead + of failing (i.e. the `ObjectMapper`'s `DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES` is now set to `false`) + by default. + + Again, if you prefer the stricter behavior of rejecting JSON with extra or unknown properties, you can configure + `true` on your own `ObjectMapper` instance and use that instance with the `Jwts.parser()` builder. + +#### Additional Changes + +This release also: + +* Fixes a thread-safety issue when using `java.util.ServiceLoader` to dynamically lookup/instantiate pluggable + implementations of JJWT interfaces (e.g. JSON parsers, etc). See + [Issue #873](https://github.com/jwtk/jjwt/issues/873) and its documented fix in + [PR #893](https://github.com/jwtk/jjwt/pull/892). * Ensures Android environments and older `org.json` library usages can parse JSON from a `JwtBuilder`-provided `java.io.Reader` instance. [Issue 882](https://github.com/jwtk/jjwt/issues/882). * Ensures a single string `aud` (Audience) claim is retained (without converting it to a `Set`) when copying/applying a @@ -14,6 +65,7 @@ This patch release: [6.2.1.3](https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.1.3), and [6.2.2.1](https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.2.1), respectively. [Issue 901](https://github.com/jwtk/jjwt/issues/901). +* Fixes various typos in documentation and JavaDoc. Thanks to those contributing pull requests for these! ### 0.12.3 From b12dabf100bbe8749d9bce49628d70b9f73af729 Mon Sep 17 00:00:00 2001 From: Renato Lochetti Date: Sat, 27 Jan 2024 16:45:09 -0300 Subject: [PATCH 09/17] Fix small typos (#908) --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index c5211588..0a111b63 100644 --- a/README.md +++ b/README.md @@ -321,7 +321,7 @@ If you would like to help, but don't know where to start, please visit the ones there, and we'll be happy to discuss and answer questions in the issue comments. If any of those don't appeal to you, no worries! Any help you would like to offer would be -appreciated based on the above caveats concerning [contributing pull reqeuests](#contributing-pull-requests). Feel free +appreciated based on the above caveats concerning [contributing pull requests](#contributing-pull-requests). Feel free to [discuss or ask questions first](https://github.com/jwtk/jjwt/discussions) if you're not sure. :) @@ -2282,7 +2282,7 @@ During JWE creation, these algorithms: * Obtain the Content Encryption Key (CEK) used to encrypt the JWE payload as follows: * Inspect the JWE recipient's Elliptic Curve public key and determine its Curve. - * Generate a new secure-random ephemeral Ellipic Curve public/private key pair on this same Curve. + * Generate a new secure-random ephemeral Elliptic Curve public/private key pair on this same Curve. * Add the ephemeral EC public key to the JWE [epk header](https://www.rfc-editor.org/rfc/rfc7518.html#section-4.6.1.1) for inclusion in the final JWE. * Produce an ECDH shared secret with the ECDH Key Agreement algorithm using the JWE recipient's EC public key @@ -2628,7 +2628,7 @@ OctetPrivateJwk edEcPrivJwk = Jwks.builder().octetKeyPair(edEcKeyPair).build(); ``` Note that: -* An exception will thrown when calling `rsaKeyPair` if the specified `KeyPair` instance does not contain +* An exception will be thrown when calling `rsaKeyPair` if the specified `KeyPair` instance does not contain `RSAPublicKey` and `RSAPrivateKey` instances. * Similarly, an exception will be thrown when calling `ecKeyPair` if the `KeyPair` instance does not contain `ECPublicKey` and `ECPrivateKey` instances. @@ -2766,7 +2766,7 @@ For example, consider the following Secret JWK JSON example from } ``` -The `k` value (`AyAyM1SysPpby...`) reflects secure key material and should never be accidentially +The `k` value (`AyAyM1SysPpby...`) reflects secure key material and should never be accidentally exposed. If you were to parse this JSON as a `Jwk`, calling `toString()` will _NOT_ print this value. It will @@ -2992,7 +2992,7 @@ Jwts.parser() #### Parsing of Custom Claim Types -By default JJWT will only convert simple claim types: String, Date, Long, Integer, Short and Byte. If you need to +By default, JJWT will only convert simple claim types: String, Date, Long, Integer, Short and Byte. If you need to deserialize other types you can configure the `JacksonDeserializer` by passing a `Map` of claim names to types in through a constructor. For example: @@ -3211,7 +3211,7 @@ characters at the end of a Base64 string may not work and can often result in an ##### Adding Invalid Characters JJWT's default Base64/Base64URL decoders automatically ignore illegal Base64 characters located in the beginning and -end of an encoded string. Therefore prepending or appending invalid characters like `{` or `]` or similar will also +end of an encoded string. Therefore, prepending or appending invalid characters like `{` or `]` or similar will also not fail JJWT's signature checks either. Why? Because such edits - whether changing a trailing character or two, or appending invalid characters - do not actually @@ -3524,7 +3524,7 @@ assert "me".equals(issuer); ### JWT Encrypted with ECDH-ES -This is an example showing how to encrypt and decrypt a JWT using Elliptic Curve Diffie-Hellman Ephmeral Static +This is an example showing how to encrypt and decrypt a JWT using Elliptic Curve Diffie-Hellman Ephemeral Static Key Agreement (ECDH-ES) algorithms. These algorithms use ECDH-ES to encrypt and decrypt a secure-random key, and that From 628bd6f4e8b885be2f9cfbd8cbf0767ce616003a Mon Sep 17 00:00:00 2001 From: lhazlewood <121180+lhazlewood@users.noreply.github.com> Date: Sat, 27 Jan 2024 19:54:40 -0800 Subject: [PATCH 10/17] Secret JWK `k` values larger than HMAC-SHA minimums (#909) - Ensured Secret JWK 'k' byte arrays for HMAC-SHA algorithms can be larger than the identified HS* algorithm. This is allowed per https://datatracker.ietf.org/doc/html/rfc7518#section-3.2: "A key of the same size as the hash output ... _or larger_ MUST be used with this algorithm" - Ensured that, when using the JwkBuilder, Secret JWK 'alg' values would automatically be set to 'HS256', 'HS384', or 'HS512' if the specified Java SecretKey algorithm name equals a JCA standard name (HmacSHA256, HmacSHA384, etc) or JCA standard HMAC-SHA OID. - Updated CHANGELOG.md accordingly. Fixes #905 --- CHANGELOG.md | 2 + .../impl/security/AesAlgorithm.java | 16 ++- .../impl/security/SecretJwkFactory.java | 96 ++++++++++----- .../security/AbstractJwkBuilderTest.groovy | 4 +- .../impl/security/JwkSerializationTest.groovy | 2 +- .../impl/security/JwksTest.groovy | 4 +- .../impl/security/SecretJwkFactoryTest.groovy | 111 +++++++++++++++--- .../impl/security/TestKeys.groovy | 7 ++ 8 files changed, 190 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5934880e..8230a11e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,8 @@ This release also: [6.2.1.3](https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.1.3), and [6.2.2.1](https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.2.1), respectively. [Issue 901](https://github.com/jwtk/jjwt/issues/901). +* Ensures that Secret JWKs for HMAC-SHA algorithms with `k` sizes larger than the algorithm minimum can + be parsed/used as expected. See [Issue #905](https://github.com/jwtk/jjwt/issues/905) * Fixes various typos in documentation and JavaDoc. Thanks to those contributing pull requests for these! ### 0.12.3 diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AesAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AesAlgorithm.java index 0747f8fc..a7d68117 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AesAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AesAlgorithm.java @@ -30,6 +30,7 @@ import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; import java.io.InputStream; import java.io.OutputStream; import java.security.SecureRandom; @@ -54,9 +55,22 @@ abstract class AesAlgorithm extends CryptoAlgorithm implements KeyBuilderSupplie protected final int tagBitLength; protected final boolean gcm; + static void assertKeyBitLength(int keyBitLength) { + if (keyBitLength == 128 || keyBitLength == 192 || keyBitLength == 256) return; // valid + String msg = "Invalid AES key length: " + Bytes.bitsMsg(keyBitLength) + ". AES only supports " + + "128, 192, or 256 bit keys."; + throw new IllegalArgumentException(msg); + } + + static SecretKey keyFor(byte[] bytes) { + int bitlen = (int) Bytes.bitLength(bytes); + assertKeyBitLength(bitlen); + return new SecretKeySpec(bytes, KEY_ALG_NAME); + } + AesAlgorithm(String id, final String jcaTransformation, int keyBitLength) { super(id, jcaTransformation); - Assert.isTrue(keyBitLength == 128 || keyBitLength == 192 || keyBitLength == 256, "Invalid AES key length: it must equal 128, 192, or 256."); + assertKeyBitLength(keyBitLength); this.keyBitLength = keyBitLength; this.gcm = jcaTransformation.startsWith("AES/GCM"); this.ivBitLength = jcaTransformation.equals("AESWrap") ? 0 : (this.gcm ? GCM_IV_SIZE : BLOCK_SIZE); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/SecretJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/SecretJwkFactory.java index e099e32c..36238cc4 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/SecretJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/SecretJwkFactory.java @@ -15,6 +15,7 @@ */ package io.jsonwebtoken.impl.security; +import io.jsonwebtoken.Identifiable; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.impl.lang.Bytes; import io.jsonwebtoken.impl.lang.ParameterReadable; @@ -22,11 +23,14 @@ import io.jsonwebtoken.impl.lang.RequiredParameterReader; import io.jsonwebtoken.io.Encoders; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.AeadAlgorithm; import io.jsonwebtoken.security.InvalidKeyException; +import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.MacAlgorithm; import io.jsonwebtoken.security.MalformedKeyException; import io.jsonwebtoken.security.SecretJwk; -import io.jsonwebtoken.security.SecureDigestAlgorithm; +import io.jsonwebtoken.security.SecretKeyAlgorithm; +import io.jsonwebtoken.security.WeakKeyException; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; @@ -44,61 +48,97 @@ class SecretJwkFactory extends AbstractFamilyJwkFactory { protected SecretJwk createJwkFromKey(JwkContext ctx) { SecretKey key = Assert.notNull(ctx.getKey(), "JwkContext key cannot be null."); String k; + byte[] encoded = null; try { - byte[] encoded = KeysBridge.getEncoded(key); + encoded = KeysBridge.getEncoded(key); k = Encoders.BASE64URL.encode(encoded); Assert.hasText(k, "k value cannot be null or empty."); } catch (Throwable t) { String msg = "Unable to encode SecretKey to JWK: " + t.getMessage(); throw new InvalidKeyException(msg, t); + } finally { + Bytes.clear(encoded); + } + + MacAlgorithm mac = DefaultMacAlgorithm.findByKey(key); + if (mac != null) { + ctx.put(AbstractJwk.ALG.getId(), mac.getId()); } ctx.put(DefaultSecretJwk.K.getId(), k); - return new DefaultSecretJwk(ctx); + return createJwkFromValues(ctx); } private static void assertKeyBitLength(byte[] bytes, MacAlgorithm alg) { long bitLen = Bytes.bitLength(bytes); long requiredBitLen = alg.getKeyBitLength(); - if (bitLen != requiredBitLen) { + if (bitLen < requiredBitLen) { // Implementors note: Don't print out any information about the `bytes` value itself - size, // content, etc., as it is considered secret material: String msg = "Secret JWK " + AbstractJwk.ALG + " value is '" + alg.getId() + - "', but the " + DefaultSecretJwk.K + " length does not equal the '" + alg.getId() + - "' length requirement of " + Bytes.bitsMsg(requiredBitLen) + - ". This discrepancy could be the result of an algorithm " + - "substitution attack or simply an erroneously constructed JWK. In either case, it is likely " + - "to result in unexpected or undesired security consequences."; - throw new MalformedKeyException(msg); + "', but the " + DefaultSecretJwk.K + " length is smaller than the " + alg.getId() + + " minimum length of " + Bytes.bitsMsg(requiredBitLen) + + " required by " + + "[JWA RFC 7518, Section 3.2](https://www.rfc-editor.org/rfc/rfc7518.html#section-3.2), " + + "2nd paragraph: 'A key of the same size as the hash output or larger MUST be used with this " + + "algorithm.'"; + throw new WeakKeyException(msg); } } + private static void assertSymmetric(Identifiable alg) { + if (alg instanceof MacAlgorithm || alg instanceof SecretKeyAlgorithm || alg instanceof AeadAlgorithm) + return; // valid + String msg = "Invalid Secret JWK " + AbstractJwk.ALG + " value '" + alg.getId() + "'. Secret JWKs " + + "may only be used with symmetric (secret) key algorithms."; + throw new MalformedKeyException(msg); + } + @Override protected SecretJwk createJwkFromValues(JwkContext ctx) { ParameterReadable reader = new RequiredParameterReader(ctx); - byte[] bytes = reader.get(DefaultSecretJwk.K); - String jcaName = null; + final byte[] bytes = reader.get(DefaultSecretJwk.K); + SecretKey key; - String id = ctx.getAlgorithm(); - if (Strings.hasText(id)) { - SecureDigestAlgorithm alg = Jwts.SIG.get().get(id); - if (alg instanceof MacAlgorithm) { - jcaName = ((CryptoAlgorithm) alg).getJcaName(); // valid for all JJWT alg implementations - Assert.hasText(jcaName, "Algorithm jcaName cannot be null or empty."); - assertKeyBitLength(bytes, (MacAlgorithm) alg); - } - } - if (!Strings.hasText(jcaName)) { - if (ctx.isSigUse()) { + String algId = ctx.getAlgorithm(); + if (!Strings.hasText(algId)) { // optional per https://www.rfc-editor.org/rfc/rfc7517.html#section-4.4 + + // Here we try to infer the best type of key to create based on siguse and/or key length. + // + // AES requires 128, 192, or 256 bits, so anything larger than 256 cannot be AES, so we'll need to assume + // HMAC. + // + // Also, 256 bits works for either HMAC or AES, so we just have to choose one as there is no other + // RFC-based criteria for determining. Historically, we've chosen AES due to the larger number of + // KeyAlgorithm and AeadAlgorithm use cases, so that's our default. + int kBitLen = (int) Bytes.bitLength(bytes); + + if (ctx.isSigUse() || kBitLen > Jwts.SIG.HS256.getKeyBitLength()) { // The only JWA SecretKey signature algorithms are HS256, HS384, HS512, so choose based on bit length: - jcaName = "HmacSHA" + Bytes.bitLength(bytes); - } else { // not an HS* algorithm, and all standard AeadAlgorithms use AES keys: - jcaName = AesAlgorithm.KEY_ALG_NAME; + key = Keys.hmacShaKeyFor(bytes); + } else { + key = AesAlgorithm.keyFor(bytes); } + ctx.setKey(key); + return new DefaultSecretJwk(ctx); + } + + //otherwise 'alg' was specified, ensure it's valid for secret key use: + Identifiable alg = Jwts.SIG.get().get(algId); + if (alg == null) alg = Jwts.KEY.get().get(algId); + if (alg == null) alg = Jwts.ENC.get().get(algId); + if (alg != null) assertSymmetric(alg); // if we found a standard alg, it must be a symmetric key algorithm + + if (alg instanceof MacAlgorithm) { + assertKeyBitLength(bytes, ((MacAlgorithm) alg)); + String jcaName = ((CryptoAlgorithm) alg).getJcaName(); + Assert.hasText(jcaName, "Algorithm jcaName cannot be null or empty."); + key = new SecretKeySpec(bytes, jcaName); + } else { + // all other remaining JWA-standard symmetric algs use AES: + key = AesAlgorithm.keyFor(bytes); } - Assert.stateNotNull(jcaName, "jcaName cannot be null (invariant)"); - SecretKey key = new SecretKeySpec(bytes, jcaName); ctx.setKey(key); return new DefaultSecretJwk(ctx); } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy index d54ab088..ce73f8ac 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy @@ -29,10 +29,8 @@ import static org.junit.Assert.* class AbstractJwkBuilderTest { - private static final SecretKey SKEY = TestKeys.A256GCM - private static AbstractJwkBuilder builder() { - return (AbstractJwkBuilder) Jwks.builder().key(SKEY) + return (AbstractJwkBuilder) Jwks.builder().key(TestKeys.NA256) } @Test diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwkSerializationTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwkSerializationTest.groovy index 5ae4212c..9f95fbc2 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwkSerializationTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwkSerializationTest.groovy @@ -97,7 +97,7 @@ class JwkSerializationTest { static void testSecretJwk(Serializer ser, Deserializer des) { - def key = TestKeys.A128GCM + def key = TestKeys.NA256 def jwk = Jwks.builder().key(key).id('id').build() assertWrapped(jwk, ['k']) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy index 16c99f57..30d8d43e 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy @@ -40,7 +40,7 @@ import static org.junit.Assert.* class JwksTest { - private static final SecretKey SKEY = Jwts.SIG.HS256.key().build() + private static final SecretKey SKEY = TestKeys.NA256 private static final java.security.KeyPair EC_PAIR = Jwts.SIG.ES256.keyPair().build() private static String srandom() { @@ -172,7 +172,7 @@ class JwksTest { @Test void testOperations() { def val = [Jwks.OP.SIGN, Jwks.OP.VERIFY] as Set - def jwk = Jwks.builder().key(TestKeys.A128GCM).operations().add(val).and().build() + def jwk = Jwks.builder().key(TestKeys.NA256).operations().add(val).and().build() assertEquals val, jwk.getOperations() } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/SecretJwkFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/SecretJwkFactoryTest.groovy index 336b40c1..6af9c138 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/SecretJwkFactoryTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/SecretJwkFactoryTest.groovy @@ -15,9 +15,10 @@ */ package io.jsonwebtoken.impl.security -import io.jsonwebtoken.security.Jwks -import io.jsonwebtoken.security.MalformedKeyException -import io.jsonwebtoken.security.SecretJwk +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.impl.lang.Bytes +import io.jsonwebtoken.io.Encoders +import io.jsonwebtoken.security.* import org.junit.Test import static org.junit.Assert.* @@ -30,10 +31,14 @@ import static org.junit.Assert.* */ class SecretJwkFactoryTest { + private static Set macAlgs() { + return Jwts.SIG.get().values().findAll({ it -> it instanceof MacAlgorithm }) as Collection + } + @Test // if a jwk does not have an 'alg' or 'use' param, we default to an AES key void testNoAlgNoSigJcaName() { - SecretJwk jwk = Jwks.builder().key(TestKeys.HS256).delete('alg').build() + SecretJwk jwk = Jwks.builder().key(TestKeys.NA256).build() SecretJwk result = Jwks.builder().add(jwk).build() as SecretJwk assertEquals 'AES', result.toKey().getAlgorithm() } @@ -47,7 +52,7 @@ class SecretJwkFactoryTest { @Test void testSignOpSetsKeyHmacSHA256() { - SecretJwk jwk = Jwks.builder().key(TestKeys.HS256).delete('alg').build() + SecretJwk jwk = Jwks.builder().key(TestKeys.NA256).build() SecretJwk result = Jwks.builder().add(jwk).operations().add(Jwks.OP.SIGN).and().build() as SecretJwk assertNull result.getAlgorithm() assertNull result.get('use') @@ -63,7 +68,7 @@ class SecretJwkFactoryTest { @Test void testSignOpSetsKeyHmacSHA384() { - SecretJwk jwk = Jwks.builder().key(TestKeys.HS384).delete('alg').build() + SecretJwk jwk = Jwks.builder().key(TestKeys.NA384).build() SecretJwk result = Jwks.builder().add(jwk).operations().add(Jwks.OP.SIGN).and().build() as SecretJwk assertNull result.getAlgorithm() assertNull result.get('use') @@ -79,7 +84,7 @@ class SecretJwkFactoryTest { @Test void testSignOpSetsKeyHmacSHA512() { - SecretJwk jwk = Jwks.builder().key(TestKeys.HS512).delete('alg').build() + SecretJwk jwk = Jwks.builder().key(TestKeys.NA512).build() SecretJwk result = Jwks.builder().add(jwk).operations().add(Jwks.OP.SIGN).and().build() as SecretJwk assertNull result.getAlgorithm() assertNull result.get('use') @@ -89,7 +94,7 @@ class SecretJwkFactoryTest { @Test // no 'alg' jwk property, but 'use' is 'sig', so forces jcaName to be HmacSHA256 void testNoAlgAndSigUseForHS256() { - SecretJwk jwk = Jwks.builder().key(TestKeys.HS256).delete('alg').build() + SecretJwk jwk = Jwks.builder().key(TestKeys.NA256).build() assertFalse jwk.containsKey('alg') assertFalse jwk.containsKey('use') SecretJwk result = Jwks.builder().add(jwk).add('use', 'sig').build() as SecretJwk @@ -99,7 +104,7 @@ class SecretJwkFactoryTest { @Test // no 'alg' jwk property, but 'use' is 'sig', so forces jcaName to be HmacSHA384 void testNoAlgAndSigUseForHS384() { - SecretJwk jwk = Jwks.builder().key(TestKeys.HS384).delete('alg').build() + SecretJwk jwk = Jwks.builder().key(TestKeys.NA384).build() assertFalse jwk.containsKey('alg') assertFalse jwk.containsKey('use') SecretJwk result = Jwks.builder().add(jwk).add('use', 'sig').build() as SecretJwk @@ -109,7 +114,7 @@ class SecretJwkFactoryTest { @Test // no 'alg' jwk property, but 'use' is 'sig', so forces jcaName to be HmacSHA512 void testNoAlgAndSigUseForHS512() { - SecretJwk jwk = Jwks.builder().key(TestKeys.HS512).delete('alg').build() + SecretJwk jwk = Jwks.builder().key(TestKeys.NA512).build() assertFalse jwk.containsKey('alg') assertFalse jwk.containsKey('use') SecretJwk result = Jwks.builder().add(jwk).add('use', 'sig').build() as SecretJwk @@ -119,20 +124,32 @@ class SecretJwkFactoryTest { @Test // no 'alg' jwk property, but 'use' is something other than 'sig', so jcaName should default to AES void testNoAlgAndNonSigUse() { - SecretJwk jwk = Jwks.builder().key(TestKeys.HS256).delete('alg').build() + SecretJwk jwk = Jwks.builder().key(TestKeys.NA256).build() assertFalse jwk.containsKey('alg') assertFalse jwk.containsKey('use') SecretJwk result = Jwks.builder().add(jwk).add('use', 'foo').build() as SecretJwk assertEquals 'AES', result.toKey().getAlgorithm() } + @Test + // 'oct' type, but 'alg' value is not a secret key algorithm (and therefore malformed) + void testMismatchedAlgorithm() { + try { + Jwks.builder().key(TestKeys.NA256).add('alg', Jwts.SIG.RS256.getId()).build() + fail() + } catch (MalformedKeyException expected) { + String msg = "Invalid Secret JWK ${AbstractJwk.ALG} value 'RS256'. Secret JWKs may only be used with " + + "symmetric (secret) key algorithms." + assertEquals msg, expected.message + } + } + /** * Test the case where a jwk `alg` value is present, but the key material doesn't match that algs key length * requirements. This would be a malformed key. */ @Test void testSizeMismatchedSecretJwk() { - //first get a valid HS256 JWK: SecretJwk validJwk = Jwks.builder().key(TestKeys.HS256).build() @@ -142,12 +159,72 @@ class SecretJwkFactoryTest { .add('alg', 'HS384') .build() fail() - } catch (MalformedKeyException expected) { - String msg = "Secret JWK 'alg' (Algorithm) value is 'HS384', but the 'k' (Key Value) length does " + - "not equal the 'HS384' length requirement of 384 bits (48 bytes). This discrepancy could " + - "be the result of an algorithm substitution attack or simply an erroneously constructed " + - "JWK. In either case, it is likely to result in unexpected or undesired security consequences." + } catch (WeakKeyException expected) { + String msg = "Secret JWK 'alg' (Algorithm) value is 'HS384', but the 'k' (Key Value) length is smaller " + + "than the HS384 minimum length of 384 bits (48 bytes) required by " + + "[JWA RFC 7518, Section 3.2](https://www.rfc-editor.org/rfc/rfc7518.html#section-3.2), 2nd " + + "paragraph: 'A key of the same size as the hash output or larger MUST be used with this " + + "algorithm.'" assertEquals msg, expected.getMessage() } } + + /** + * Test when a {@code k} size is smaller, equal to, and larger than the minimum required number of bits/bytes for + * a given HmacSHA* algorithm. The RFCs indicate smaller-than is not allowed, while equal-to and greater-than are + * allowed. + * + * This test asserts this allowed behavior per https://github.com/jwtk/jjwt/issues/905 + * @see JJWT Issue 905 + */ + @Test + void testAllowedKeyLengths() { + + def parser = Jwks.parser().build() + + for (MacAlgorithm alg : macAlgs()) { + + // 3 key length sizes for each alg to test: + // index 0: smaller than minimum required + // index 1: minimum required + // index 2: more than minimum required: + def sizes = [alg.keyBitLength - Byte.SIZE, alg.keyBitLength, alg.keyBitLength + Byte.SIZE] + + for (int i = 0; i < sizes.size(); i++) { + + def kBitLength = sizes.get(i) + def k = Bytes.random(Bytes.length(kBitLength)) + + def jwkJson = """ + { + "kid": "${UUID.randomUUID().toString()}", + "kty": "oct", + "alg": "${alg.getId()}", + "k": "${Encoders.BASE64URL.encode(k)}" + }""".toString() + + def jwk + try { + jwk = parser.parse(jwkJson) + } catch (WeakKeyException expected) { + assertEquals("Should only occur on index 0 with less-than-minimum key length", 0, i) + String msg = "Secret JWK 'alg' (Algorithm) value is '${alg.getId()}', but the 'k' (Key Value) " + + "length is smaller than the ${alg.getId()} minimum length of " + + "${Bytes.bitsMsg(alg.keyBitLength)} required by " + + "[JWA RFC 7518, Section 3.2](https://www.rfc-editor.org/rfc/rfc7518.html#section-3.2), " + + "2nd paragraph: 'A key of the same size as the hash output or larger MUST be used with " + + "this algorithm.'" + assertEquals msg, expected.getMessage() + continue // expected for index 0 (purposefully weak key), so let loop continue + } + + // otherwise not weak, sizes should reflect equal-to or greater-than alg bitlength sizes + assert jwk instanceof SecretJwk + assertEquals alg.getId(), jwk.getAlgorithm() + def bytes = jwk.toKey().getEncoded() + assertTrue Bytes.bitLength(bytes) >= alg.keyBitLength + assertEquals Bytes.length(kBitLength), jwk.toKey().getEncoded().length + } + } + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestKeys.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestKeys.groovy index 50113765..95226c9c 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestKeys.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestKeys.groovy @@ -21,6 +21,7 @@ import io.jsonwebtoken.lang.Collections import io.jsonwebtoken.security.Jwks import javax.crypto.SecretKey +import javax.crypto.spec.SecretKeySpec import java.security.KeyPair import java.security.PrivateKey import java.security.Provider @@ -42,6 +43,11 @@ class TestKeys { static SecretKey HS512 = Jwts.SIG.HS512.key().build() static Collection HS = Collections.setOf(HS256, HS384, HS512) + static SecretKey NA256 = new SecretKeySpec(HS256.encoded, "NONE") + static SecretKey NA384 = new SecretKeySpec(HS384.encoded, "NONE") + static SecretKey NA512 = new SecretKeySpec(HS512.encoded, "NONE") + static Collection NA = [NA256, NA384, NA512] + static SecretKey A128GCM, A192GCM, A256GCM, A128KW, A192KW, A256KW, A128GCMKW, A192GCMKW, A256GCMKW static Collection AGCM static { @@ -59,6 +65,7 @@ class TestKeys { static Collection SECRET = new LinkedHashSet<>() static { SECRET.addAll(HS) + SECRET.addAll(NA) SECRET.addAll(AGCM) SECRET.addAll(ACBC) } From 2884eb79529ec8b56ecdd7c9f7e7fbea5dfc4806 Mon Sep 17 00:00:00 2001 From: lhazlewood <121180+lhazlewood@users.noreply.github.com> Date: Sat, 27 Jan 2024 20:06:20 -0800 Subject: [PATCH 11/17] - Updating to GitHub latest actions/checkout and actions/setup-java script versions (#910) --- .github/workflows/ci.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b48bcb8..488dd5ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: runs-on: 'ubuntu-latest' name: jdk-${{ matrix.java }}-oracle steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up JDK uses: oracle-actions/setup-java@v1 with: @@ -42,9 +42,9 @@ jobs: runs-on: 'ubuntu-latest' name: jdk-${{ matrix.java }}-temurin steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: ${{ matrix.java }} distribution: 'temurin' @@ -72,9 +72,9 @@ jobs: JDK_MAJOR_VERSION: ${{ matrix.java }} name: jdk-${{ matrix.java }}-zulu steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: ${{ matrix.java }} distribution: 'zulu' @@ -99,11 +99,11 @@ jobs: license-check: runs-on: 'ubuntu-latest' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 # avoid license plugin history warnings (plus it needs full history) - name: Set up JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: '8' @@ -120,9 +120,9 @@ jobs: #needs: zulu # wait until others finish so a coverage failure doesn't cancel others accidentally runs-on: 'ubuntu-latest' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: '8' From 6335381c978ce83c9c15bd3c349f32d1bed72d4f Mon Sep 17 00:00:00 2001 From: lhazlewood <121180+lhazlewood@users.noreply.github.com> Date: Sun, 28 Jan 2024 13:17:53 -0800 Subject: [PATCH 12/17] PBES2 decryption maximum iterations (#911) Ensured there is an upper bound (maximum) iterations enforced for PBES2 decryption to help mitigate potential DoS attacks. Many thanks to Jingcheng Yang and Jianjun Chen from Sichuan University and Zhongguancun Lab for their work on this! --- CHANGELOG.md | 3 ++ .../impl/security/Pbes2HsAkwAlgorithm.java | 16 ++++++++- .../security/Pbes2HsAkwAlgorithmTest.groovy | 33 +++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8230a11e..33ce2fb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,9 @@ This release also: [Issue 901](https://github.com/jwtk/jjwt/issues/901). * Ensures that Secret JWKs for HMAC-SHA algorithms with `k` sizes larger than the algorithm minimum can be parsed/used as expected. See [Issue #905](https://github.com/jwtk/jjwt/issues/905) +* Ensures there is an upper bound (maximum) iterations enforced for PBES2 decryption to help mitigate potential DoS + attacks. Many thanks to Jingcheng Yang and Jianjun Chen from Sichuan University and Zhongguancun Lab for their + work on this. See [PR 911](https://github.com/jwtk/jjwt/pull/911). * Fixes various typos in documentation and JavaDoc. Thanks to those contributing pull requests for these! ### 0.12.3 diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithm.java index 064b545d..796c78c8 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithm.java @@ -16,9 +16,11 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.JweHeader; +import io.jsonwebtoken.UnsupportedJwtException; import io.jsonwebtoken.impl.DefaultJweHeader; import io.jsonwebtoken.impl.lang.Bytes; import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.impl.lang.Parameter; import io.jsonwebtoken.impl.lang.ParameterReadable; import io.jsonwebtoken.impl.lang.RequiredParameterReader; import io.jsonwebtoken.lang.Assert; @@ -50,11 +52,13 @@ public class Pbes2HsAkwAlgorithm extends CryptoAlgorithm implements KeyAlgorithm "[JWA RFC 7518, Section 4.8.1.2](https://www.rfc-editor.org/rfc/rfc7518.html#section-4.8.1.2) " + "recommends password-based-encryption iterations be greater than or equal to " + MIN_RECOMMENDED_ITERATIONS + ". Provided: "; + private static final double MAX_ITERATIONS_FACTOR = 2.5; private final int HASH_BYTE_LENGTH; private final int DERIVED_KEY_BIT_LENGTH; private final byte[] SALT_PREFIX; private final int DEFAULT_ITERATIONS; + private final int MAX_ITERATIONS; private final KeyAlgorithm wrapAlg; private static byte[] toRfcSaltPrefix(byte[] bytes) { @@ -106,6 +110,7 @@ public class Pbes2HsAkwAlgorithm extends CryptoAlgorithm implements KeyAlgorithm } else { DEFAULT_ITERATIONS = DEFAULT_SHA256_ITERATIONS; } + MAX_ITERATIONS = (int) (DEFAULT_ITERATIONS * MAX_ITERATIONS_FACTOR); // upper bound to help mitigate DoS attacks // https://www.rfc-editor.org/rfc/rfc7518.html#section-4.8, 2nd paragraph, last sentence: // "Their derived-key lengths respectively are 16, 24, and 32 octets." : @@ -184,7 +189,16 @@ public class Pbes2HsAkwAlgorithm extends CryptoAlgorithm implements KeyAlgorithm final Password key = Assert.notNull(request.getKey(), "Decryption Password cannot be null."); ParameterReadable reader = new RequiredParameterReader(header); final byte[] inputSalt = reader.get(DefaultJweHeader.P2S); - final int iterations = reader.get(DefaultJweHeader.P2C); + + Parameter param = DefaultJweHeader.P2C; + final int iterations = reader.get(param); + if (iterations > MAX_ITERATIONS) { + String msg = "JWE Header " + param + " value " + iterations + " exceeds " + getId() + " maximum " + + "allowed value " + MAX_ITERATIONS + ". The larger value is rejected to help mitigate " + + "potential Denial of Service attacks."; + throw new UnsupportedJwtException(msg); + } + final byte[] rfcSalt = Bytes.concat(SALT_PREFIX, inputSalt); final char[] password = key.toCharArray(); // password will be safely cleaned/zeroed in deriveKey next: final SecretKey derivedKek = deriveKey(request, password, rfcSalt, iterations); diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithmTest.groovy index 33513d5f..36142646 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithmTest.groovy @@ -16,8 +16,11 @@ package io.jsonwebtoken.impl.security import io.jsonwebtoken.Jwts +import io.jsonwebtoken.UnsupportedJwtException import io.jsonwebtoken.impl.DefaultJweHeaderMutator import io.jsonwebtoken.impl.DefaultMutableJweHeader +import io.jsonwebtoken.io.Encoders +import io.jsonwebtoken.lang.Strings import io.jsonwebtoken.security.KeyRequest import io.jsonwebtoken.security.Keys import io.jsonwebtoken.security.Password @@ -50,6 +53,36 @@ class Pbes2HsAkwAlgorithmTest { } } + @Test + void testExceedsMaxIterations() { + for (Pbes2HsAkwAlgorithm alg : ALGS) { + def password = Keys.password('correct horse battery staple'.toCharArray()) + def iterations = alg.MAX_ITERATIONS + 1 + // we make the JWE string directly from JSON here (instead of using Jwts.builder()) to avoid + // the computational time it would take to create such JWEs with excessive iterations as well as + // avoid the builder throwing any exceptions (and this is what a potential attacker would do anyway): + def headerJson = """ + { + "p2c": ${iterations}, + "p2s": "831BG_z_ZxkN7Rnt5v1iYm1A0bn6VEuxpW4gV7YBMoE", + "alg": "${alg.id}", + "enc": "A256GCM" + }""" + def jwe = Encoders.BASE64URL.encode(Strings.utf8(headerJson)) + + '.OSAhMk3FtaCeZ5v1c8bWBgssEVqx2mCPUEnJUsg4hwIQyrUP-LCYkg.' + + 'K4R_-zb4qaZ3R0W8.sGS4mcT_xBhZC1d7G-g.kWqd_4sEsaKrWE_hMZ5HmQ' + try { + Jwts.parser().decryptWith(password).build().parse(jwe) + } catch (UnsupportedJwtException expected) { + String msg = "JWE Header 'p2c' (PBES2 Count) value ${iterations} exceeds ${alg.id} maximum allowed " + + "value ${alg.MAX_ITERATIONS}. The larger value is rejected to help mitigate potential " + + "Denial of Service attacks." + //println msg + assertEquals msg, expected.message + } + } + } + // for manual/developer testing only. Takes a long time and there is no deterministic output to assert /* @Test From dd10b12b53b5bb26299c5435aa9d193e71b6b918 Mon Sep 17 00:00:00 2001 From: lhazlewood <121180+lhazlewood@users.noreply.github.com> Date: Sun, 28 Jan 2024 14:49:01 -0800 Subject: [PATCH 13/17] Added JWK Set documentation to README.mdJwkset doc (#912) Added JWK Set documentation to README.md --- README.md | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/README.md b/README.md index 0a111b63..374ea0b1 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,9 @@ JJWT is open source under the terms of the [Apache 2.0 License](http://www.apach * [JWK Thumbprint URI](#jwk-thumbprint-uri) * [JWK Security Considerations](#jwk-security) * [JWK `toString()` Safety](#jwk-tostring) +* [JWK Sets](#jwkset) + * [Create a JWK Set](#jwkset-create) + * [Read a JWK Set](#jwkset-read) * [Compression](#compression) * [Custom Compression Algorithm](#compression-custom) * [JSON Processor](#json) @@ -2788,6 +2791,91 @@ This code would print the following string literal to the System console: This is true for all secret or private key members in `SecretJwk` and `PrivateJwk` (e.g. `RsaPrivateJwk`, `EcPrivateJwk`, etc) instances. + +## JWK Sets + +The JWK specification specification also defines the concept of a +[JWK Set](https://datatracker.ietf.org/doc/html/rfc7517#section-5): + + A JWK Set is a JSON object that represents a set of JWKs. The JSON + object MUST have a "keys" member, with its value being an array of + JWKs. + +For example: + +```json +{ + "keys": [jwk1, jwk2, ...] +} +``` +Where `jwk1`, `jwk2`, etc., are each a single [JWK](#jwk) JSON Object. + +A JWK Set _may_ have other members that are peers to the `keys` member, but the JWK specification does not define any +others - any such additional members would be custom or unique based on an application's needs or preferences. + +A JWK Set can be useful for conveying multiple keys simultaneously. For example, an identity web service could expose +all of its RSA or Elliptic Curve public keys that might be used for various purposes or different algorithms to +3rd parties or API clients as a single JWK Set JSON Object or document. An API client can then parse the JWK Set +to obtain the keys that might be used to verify or decrypt JWTs sent by the web service. + +JWK Sets are (mostly) simple collections of JWKs, and they are easily supported by JJWT with parallel builder/parser +concepts we've seen above. + + +### Create a JWK Set + +You create a JWK Set as follows: + +1. Use the `Jwks.set()` method to create a `JwkSetBuilder` instance. +2. Call the `add(Jwk)` method any number of times to add one or more JWKs to the set. +3. Call builder methods to set any additional JSON members if desired, or the `operationPolicy(KeyOperationPolicy)` + builder method to control what key operations may be assigned to any given JWK added to the set. +4. Call the `build()` method to produce the resulting JWK Set. + +For example: + +```java +Jwk jwk = Jwks.builder()/* ... */.build(); +SecretJwk = Jwks.set() // 1 + .add(jwk) // 2, appends a key + //.add(aCollection) // append multiple keys + //.keys(allJwks) // sets/replaces all keys + //.add("aName", "aValue") // 3, optional + //.operationPolicy(Jwks.OP // 3, optional + // .policy() + // /* etc... */ + // .build()) + //.provider(aJcaProvider) // optional + .build(); // (4) +``` + +As shown, you can optionally configure the `.operationPolicy(KeyOperationPolicy)` method using a +`Jwts.OP.policy()` builder. A `KeyOperationPolicy` allows you control what operations are allowed for any JWK +before being added to the JWK Set; any JWK that does not match the policy will be rejected and not added to the set. +JJWT internally defaults to a standard RFC-compliant policy, but you can create a +policy to override the default if desired using the `Jwks.OP.policy()` builder method. + + +### Read a JWK Set + +You can read/parse a JWK Set by building a JWK Set `Parser` and parsing the JWK Set JSON with one of its various +`parse` methods: + +```java +JwkSet jwkSet = Jwks.setParser() + //.provider(aJcaProvider) // optional + //.deserializer(deserializer) // optional + //.policy(aKeyOperationPolicy) // optional + .build() // create the parser + .parse(json); // actually parse JSON String, InputStream, Reader, etc. + +jwkSet.forEach(jwk -> System.out.println(jwk)); +``` + +As shown above, you can specify a custom JCA Provider, [JSON deserializer](#json) or `KeyOperationPolicy` in the +same way as the `JwkSetBuilder`. Any JWK that does not match the default (or configured) policy will be +rejected. You can create a policy to override the default if desired using the `Jwks.OP.policy()` builder method. + ## Compression From afcd88983252b249204d53830dd03884050b41af Mon Sep 17 00:00:00 2001 From: lhazlewood <121180+lhazlewood@users.noreply.github.com> Date: Sun, 28 Jan 2024 16:52:21 -0800 Subject: [PATCH 14/17] 0.12.4 staging (#913) Released 0.12.4, with the following additional changes: - Added 0.12.4 release version references - Added CI 'workflow_dispatch' event trigger - Changed git url from ssh to https --- .github/workflows/ci.yml | 1 + README.md | 26 +++++++++---------- api/pom.xml | 2 +- extensions/gson/pom.xml | 2 +- extensions/jackson/pom.xml | 2 +- .../jackson/io/JacksonDeserializerTest.groovy | 1 + extensions/orgjson/pom.xml | 2 +- extensions/pom.xml | 2 +- impl/pom.xml | 2 +- .../impl/DelegatingClaimsMutator.java | 4 +-- .../io/jsonwebtoken/impl/lang/Services.java | 1 + .../impl/security/AesAlgorithm.java | 5 ++++ .../security/AbstractEcJwkFactoryTest.groovy | 1 + .../security/Pbes2HsAkwAlgorithmTest.groovy | 3 +++ .../impl/security/SecretJwkFactoryTest.groovy | 4 +++ pom.xml | 6 ++--- tdjar/pom.xml | 2 +- 17 files changed, 41 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 488dd5ed..4e83275d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,7 @@ name: CI on: + workflow_dispatch: pull_request: # all pull requests push: branches: diff --git a/README.md b/README.md index 374ea0b1..794f95b5 100644 --- a/README.md +++ b/README.md @@ -543,18 +543,18 @@ If you're building a (non-Android) JDK project, you will want to define the foll io.jsonwebtoken jjwt-api - 0.12.3 + 0.12.4 io.jsonwebtoken jjwt-impl - 0.12.3 + 0.12.4 runtime io.jsonwebtoken jjwt-jackson - 0.12.3 + 0.12.4 runtime ``` @@ -3049,7 +3049,7 @@ scope which is the typical JJWT default). That is: ```groovy dependencies { - implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3' + implementation 'io.jsonwebtoken:jjwt-jackson:0.12.4' } ``` @@ -3157,7 +3157,7 @@ scope which is the typical JJWT default). That is: io.jsonwebtoken jjwt-gson - 0.12.3 + 0.12.4 compile ``` @@ -3166,7 +3166,7 @@ scope which is the typical JJWT default). That is: ```groovy dependencies { - implementation 'io.jsonwebtoken:jjwt-gson:0.12.3' + implementation 'io.jsonwebtoken:jjwt-gson:0.12.4' } ``` diff --git a/api/pom.xml b/api/pom.xml index 80a49db9..6ee645b6 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -21,7 +21,7 @@ io.jsonwebtoken jjwt-root - 0.12.4-SNAPSHOT + 0.12.5-SNAPSHOT ../pom.xml diff --git a/extensions/gson/pom.xml b/extensions/gson/pom.xml index fb17afec..4b3a296e 100644 --- a/extensions/gson/pom.xml +++ b/extensions/gson/pom.xml @@ -21,7 +21,7 @@ io.jsonwebtoken jjwt-root - 0.12.4-SNAPSHOT + 0.12.5-SNAPSHOT ../../pom.xml diff --git a/extensions/jackson/pom.xml b/extensions/jackson/pom.xml index 658ebd67..00f09665 100644 --- a/extensions/jackson/pom.xml +++ b/extensions/jackson/pom.xml @@ -21,7 +21,7 @@ io.jsonwebtoken jjwt-root - 0.12.4-SNAPSHOT + 0.12.5-SNAPSHOT ../../pom.xml diff --git a/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonDeserializerTest.groovy b/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonDeserializerTest.groovy index 2363057d..b2166744 100644 --- a/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonDeserializerTest.groovy +++ b/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonDeserializerTest.groovy @@ -123,6 +123,7 @@ class JacksonDeserializerTest { /** * Asserts https://github.com/jwtk/jjwt/issues/877 + * @since 0.12.4 */ @Test void testStrictDuplicateDetection() { diff --git a/extensions/orgjson/pom.xml b/extensions/orgjson/pom.xml index 0936fe40..f37fe1b0 100644 --- a/extensions/orgjson/pom.xml +++ b/extensions/orgjson/pom.xml @@ -21,7 +21,7 @@ io.jsonwebtoken jjwt-root - 0.12.4-SNAPSHOT + 0.12.5-SNAPSHOT ../../pom.xml diff --git a/extensions/pom.xml b/extensions/pom.xml index e0495520..0ee8810b 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -21,7 +21,7 @@ io.jsonwebtoken jjwt-root - 0.12.4-SNAPSHOT + 0.12.5-SNAPSHOT ../pom.xml diff --git a/impl/pom.xml b/impl/pom.xml index bb7fb61a..7c1f137d 100644 --- a/impl/pom.xml +++ b/impl/pom.xml @@ -21,7 +21,7 @@ io.jsonwebtoken jjwt-root - 0.12.4-SNAPSHOT + 0.12.5-SNAPSHOT ../pom.xml diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DelegatingClaimsMutator.java b/impl/src/main/java/io/jsonwebtoken/impl/DelegatingClaimsMutator.java index 7740fe27..9c408a15 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DelegatingClaimsMutator.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DelegatingClaimsMutator.java @@ -47,7 +47,7 @@ public class DelegatingClaimsMutator & C return self(); } - @Override + @Override // override starting in 0.12.4 public Object put(String key, Object value) { if (AUDIENCE_STRING.getId().equals(key)) { // https://github.com/jwtk/jjwt/issues/890 if (value instanceof String) { @@ -63,7 +63,7 @@ public class DelegatingClaimsMutator & C return super.put(key, value); } - @Override + @Override // overridden starting in 0.12.4 public void putAll(Map m) { if (m == null) return; for (Map.Entry entry : m.entrySet()) { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Services.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Services.java index 6125ebc0..f0c05669 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/Services.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Services.java @@ -65,6 +65,7 @@ public final class Services { * @param The type of the SPI * @return The first available instance of the service. * @throws UnavailableImplementationException When no implementation of the SPI class can be found. + * @since 0.12.4 */ public static T get(Class spi) { // TODO: JDK8, replace this find/putIfAbsent logic with ConcurrentMap.computeIfAbsent diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AesAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AesAlgorithm.java index a7d68117..32355571 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AesAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AesAlgorithm.java @@ -55,6 +55,11 @@ abstract class AesAlgorithm extends CryptoAlgorithm implements KeyBuilderSupplie protected final int tagBitLength; protected final boolean gcm; + /** + * Ensures {@code keyBitLength is a valid AES key length} + * @param keyBitLength the key length (in bits) to check + * @since 0.12.4 + */ static void assertKeyBitLength(int keyBitLength) { if (keyBitLength == 128 || keyBitLength == 192 || keyBitLength == 256) return; // valid String msg = "Invalid AES key length: " + Bytes.bitsMsg(keyBitLength) + ". AES only supports " + diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkFactoryTest.groovy index fbbd144d..e11e0e84 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkFactoryTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkFactoryTest.groovy @@ -44,6 +44,7 @@ class AbstractEcJwkFactoryTest { /** * Asserts correct behavior per https://github.com/jwtk/jjwt/issues/901 + * @since 0.12.4 */ @Test void fieldElementByteArrayLength() { diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithmTest.groovy index 36142646..242308bf 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithmTest.groovy @@ -53,6 +53,9 @@ class Pbes2HsAkwAlgorithmTest { } } + /** + * @since 0.12.4 + */ @Test void testExceedsMaxIterations() { for (Pbes2HsAkwAlgorithm alg : ALGS) { diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/SecretJwkFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/SecretJwkFactoryTest.groovy index 6af9c138..4eeb8106 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/SecretJwkFactoryTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/SecretJwkFactoryTest.groovy @@ -131,6 +131,9 @@ class SecretJwkFactoryTest { assertEquals 'AES', result.toKey().getAlgorithm() } + /** + * @since 0.12.4 + */ @Test // 'oct' type, but 'alg' value is not a secret key algorithm (and therefore malformed) void testMismatchedAlgorithm() { @@ -176,6 +179,7 @@ class SecretJwkFactoryTest { * * This test asserts this allowed behavior per https://github.com/jwtk/jjwt/issues/905 * @see JJWT Issue 905 + * @since 0.12.4 */ @Test void testAllowedKeyLengths() { diff --git a/pom.xml b/pom.xml index 69e29a10..a49bb2a1 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ io.jsonwebtoken jjwt-root - 0.12.4-SNAPSHOT + 0.12.5-SNAPSHOT JJWT JSON Web Token support for the JVM and Android pom @@ -50,8 +50,8 @@ scm:git:https://github.com/jwtk/jjwt.git - scm:git:git@github.com:jwtk/jjwt.git - git@github.com:jwtk/jjwt.git + scm:git:https://github.com/jwtk/jjwt.git + https://github.com/jwtk/jjwt.git HEAD diff --git a/tdjar/pom.xml b/tdjar/pom.xml index 52cbbc2a..c22c9c55 100644 --- a/tdjar/pom.xml +++ b/tdjar/pom.xml @@ -21,7 +21,7 @@ io.jsonwebtoken jjwt-root - 0.12.4-SNAPSHOT + 0.12.5-SNAPSHOT ../pom.xml From a0a123e848fc25a7920bcbd84615f639c4cc098a Mon Sep 17 00:00:00 2001 From: lhazlewood <121180+lhazlewood@users.noreply.github.com> Date: Wed, 31 Jan 2024 17:48:38 -0800 Subject: [PATCH 15/17] PR #917 * Ensured `NestedCollection`s do not need their `.and()` method called to apply collection changes. Instead, changes are applied immediately as they occur (via `.add`, `.remove`, etc), and `.and()` is now purely for returning to the parent builder if necessary/desired. * Updated associated JavaDoc with code examples to make the `.and()` method's purpose a little clearer. * Updated CHANGELOG.md Closes #916 --- CHANGELOG.md | 26 +++++++ README.md | 2 +- .../java/io/jsonwebtoken/ClaimsMutator.java | 21 +++++ .../io/jsonwebtoken/JwtParserBuilder.java | 21 ++++- .../jsonwebtoken/ProtectedHeaderMutator.java | 6 +- .../jsonwebtoken/lang/NestedCollection.java | 7 +- .../io/jsonwebtoken/security/JwkBuilder.java | 4 +- .../impl/DefaultJweHeaderMutator.java | 3 +- .../impl/DefaultJwtParserBuilder.java | 15 ++-- .../impl/DelegatingClaimsMutator.java | 4 +- .../impl/lang/DefaultCollectionMutator.java | 28 +++++-- .../impl/security/AbstractJwkBuilder.java | 3 +- .../impl/DefaultJwtBuilderTest.groovy | 43 ++++++++++ .../impl/DefaultJwtHeaderBuilderTest.groovy | 16 ++++ .../impl/DefaultJwtParserBuilderTest.groovy | 78 +++++++++++++++++++ .../lang/DefaultCollectionMutatorTest.groovy | 34 +++++++- .../security/AbstractJwkBuilderTest.groovy | 16 +++- 17 files changed, 292 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33ce2fb9..70c53ede 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ ## Release Notes +### 0.12.5 + +This patch release: + +* Ensures that builders' `NestedCollection` changes are applied to the collection immediately as mutation methods are called, no longer + requiring application developers to call `.and()` to 'commit' or apply a change. For example, prior to this release, + the following code did not apply changes: + ```java + JwtBuilder builder = Jwts.builder(); + builder.audience().add("an-audience"); // no .and() call + builder.compact(); // would not keep 'an-audience' + ``` + Now this code works as expected and all other `NestedCollection` instances like it apply changes immediately (e.g. when calling + `.add(value)`). + + However, standard fluent builder chains are still recommended for readability when feasible, e.g. + + ```java + Jwts.builder() + .audience().add("an-audience").and() // allows fluent chaining + .subject("Joe") + // etc... + .compact() + ``` + See [Issue 916](https://github.com/jwtk/jjwt/issues/916). + ### 0.12.4 This patch release includes various changes listed below. diff --git a/README.md b/README.md index 794f95b5..797e307b 100644 --- a/README.md +++ b/README.md @@ -993,7 +993,7 @@ String jws = Jwts.builder() .issuer("me") .subject("Bob") - .audience("you") + .audience().add("you").and() .expiration(expiration) //a java.util.Date .notBefore(notBefore) //a java.util.Date .issuedAt(new Date()) // for example, now diff --git a/api/src/main/java/io/jsonwebtoken/ClaimsMutator.java b/api/src/main/java/io/jsonwebtoken/ClaimsMutator.java index 4db0950a..1fdca1e4 100644 --- a/api/src/main/java/io/jsonwebtoken/ClaimsMutator.java +++ b/api/src/main/java/io/jsonwebtoken/ClaimsMutator.java @@ -96,6 +96,17 @@ public interface ClaimsMutator> { * aud (audience) Claim * set, quietly ignoring any null, empty, whitespace-only, or existing value already in the set. * + *

When finished, the {@code audience} collection's {@link AudienceCollection#and() and()} method may be used + * to continue configuration. For example:

+ *
+     *  Jwts.builder() // or Jwts.claims()
+     *
+     *     .audience().add("anAudience").and() // return parent
+     *
+     *  .subject("Joe") // resume configuration...
+     *  // etc...
+     * 
+ * * @return the {@link AudienceCollection AudienceCollection} to use for {@code aud} configuration. * @see AudienceCollection AudienceCollection * @see AudienceCollection#single(String) AudienceCollection.single(String) @@ -221,6 +232,16 @@ public interface ClaimsMutator> { * A {@code NestedCollection} for setting {@link #audience()} values that also allows overriding the collection * to be a {@link #single(String) single string value} for legacy JWT recipients if necessary. * + *

Because this interface extends {@link NestedCollection}, the {@link #and()} method may be used to continue + * parent configuration. For example:

+ *
+     *  Jwts.builder() // or Jwts.claims()
+     *
+     *     .audience().add("anAudience").and() // return parent
+     *
+     *  .subject("Joe") // resume parent configuration...
+     *  // etc...
+ * * @param

the type of ClaimsMutator to return for method chaining. * @see #single(String) * @since 0.12.0 diff --git a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java index d1a4755d..79936695 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java @@ -101,12 +101,25 @@ public interface JwtParserBuilder extends Builder { * the parser encounters a Protected JWT that {@link ProtectedHeader#getCritical() requires} extensions, and * those extensions' header names are not specified via this method, the parser will reject that JWT. * + *

The collection's {@link Conjunctor#and() and()} method returns to the builder for continued parser + * configuration, for example:

+ *
+     * parserBuilder.critical().add("headerName").{@link Conjunctor#and() and()} // etc...
+ * *

Extension Behavior

* *

The {@code critical} collection only identifies header parameter names that are used in extensions supported * by the application. Application developers, not JJWT, MUST perform the associated extension behavior * using the parsed JWT.

* + *

Continued Parser Configuration

+ *

When finished, use the collection's + * {@link Conjunctor#and() and()} method to continue parser configuration, for example: + *

+     * Jwts.parser()
+     *     .critical().add("headerName").{@link Conjunctor#and() and()} // return parent
+     * // resume parser configuration...
+ * * @return the {@link NestedCollection} to use for {@code crit} configuration. * @see ProtectedHeader#getCritical() * @since 0.12.0 @@ -557,7 +570,7 @@ public interface JwtParserBuilder extends Builder { *

The collection's {@link Conjunctor#and() and()} method returns to the builder for continued parser * configuration, for example:

*
-     * parserBuilder.enc().add(anAeadAlgorithm).{@link Conjunctor#and() and()} // etc...
+ * parserBuilder.enc().add(anAeadAlgorithm).{@link Conjunctor#and() and()} // etc... * *

Standard Algorithms and Overrides

* @@ -597,7 +610,7 @@ public interface JwtParserBuilder extends Builder { *

The collection's {@link Conjunctor#and() and()} method returns to the builder for continued parser * configuration, for example:

*
-     * parserBuilder.key().add(aKeyAlgorithm).{@link Conjunctor#and() and()} // etc...
+ * parserBuilder.key().add(aKeyAlgorithm).{@link Conjunctor#and() and()} // etc... * *

Standard Algorithms and Overrides

* @@ -639,7 +652,7 @@ public interface JwtParserBuilder extends Builder { *

The collection's {@link Conjunctor#and() and()} method returns to the builder for continued parser * configuration, for example:

*
-     * parserBuilder.sig().add(aSignatureAlgorithm).{@link Conjunctor#and() and()} // etc...
+ * parserBuilder.sig().add(aSignatureAlgorithm).{@link Conjunctor#and() and()} // etc... * *

Standard Algorithms and Overrides

* @@ -680,7 +693,7 @@ public interface JwtParserBuilder extends Builder { *

The collection's {@link Conjunctor#and() and()} method returns to the builder for continued parser * configuration, for example:

*
-     * parserBuilder.zip().add(aCompressionAlgorithm).{@link Conjunctor#and() and()} // etc...
+ * parserBuilder.zip().add(aCompressionAlgorithm).{@link Conjunctor#and() and()} // etc... * *

Standard Algorithms and Overrides

* diff --git a/api/src/main/java/io/jsonwebtoken/ProtectedHeaderMutator.java b/api/src/main/java/io/jsonwebtoken/ProtectedHeaderMutator.java index dd38bcac..00225065 100644 --- a/api/src/main/java/io/jsonwebtoken/ProtectedHeaderMutator.java +++ b/api/src/main/java/io/jsonwebtoken/ProtectedHeaderMutator.java @@ -33,9 +33,11 @@ public interface ProtectedHeaderMutator> ext /** * Configures names of header parameters used by JWT or JWA specification extensions that MUST be * understood and supported by the JWT recipient. When finished, use the collection's - * {@link Conjunctor#and() and()} method to return to the header builder, for example: + * {@link Conjunctor#and() and()} method to continue header configuration, for example: *
-     * builder.critical().add("headerName").{@link Conjunctor#and() and()} // etc...
+ * headerBuilder + * .critical().add("headerName").{@link Conjunctor#and() and()} // return parent + * // resume header configuration... * * @return the {@link NestedCollection} to use for {@code crit} configuration. * @see JWS crit (Critical) Header Parameter diff --git a/api/src/main/java/io/jsonwebtoken/lang/NestedCollection.java b/api/src/main/java/io/jsonwebtoken/lang/NestedCollection.java index cf0118d4..2fac66c9 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/NestedCollection.java +++ b/api/src/main/java/io/jsonwebtoken/lang/NestedCollection.java @@ -17,7 +17,12 @@ package io.jsonwebtoken.lang; /** * A {@link CollectionMutator} that can return access to its parent via the {@link Conjunctor#and() and()} method for - * continued configuration. + * continued configuration. For example: + *
+ * builder
+ *     .aNestedCollection()// etc...
+ *     .and() // return parent
+ * // resume parent configuration...
* * @param the type of elements in the collection * @param

the parent to return diff --git a/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java index 2133925c..add7be04 100644 --- a/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java @@ -109,9 +109,9 @@ public interface JwkBuilder, T extends JwkBuilde * the key is intended to be used. When finished, use the collection's {@link Conjunctor#and() and()} method to * return to the JWK builder, for example: *

-     * jwkBuilder.operations().add(aKeyOperation).{@link Conjunctor#and() and()} // etc...
+ * jwkBuilder.operations().add(aKeyOperation).{@link Conjunctor#and() and()} // etc... * - *

The {@code and()} method will throw an {@link IllegalArgumentException} if any of the specified + *

The {@code add()} method(s) will throw an {@link IllegalArgumentException} if any of the specified * {@code KeyOperation}s are not permitted by the JWK's * {@link #operationPolicy(KeyOperationPolicy) operationPolicy}. See that documentation for more * information on security vulnerabilities when using the same key with multiple algorithms.

diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeaderMutator.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeaderMutator.java index b35806f4..fc7c1f11 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeaderMutator.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeaderMutator.java @@ -107,9 +107,8 @@ public class DefaultJweHeaderMutator> public NestedCollection critical() { return new DefaultNestedCollection(self(), this.DELEGATE.get(DefaultProtectedHeader.CRIT)) { @Override - public T and() { + protected void changed() { put(DefaultProtectedHeader.CRIT, Collections.asSet(getCollection())); - return super.and(); } }; } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java index 19290480..ddce12b2 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java @@ -220,9 +220,8 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder { public NestedCollection critical() { return new DefaultNestedCollection(this, this.critical) { @Override - public JwtParserBuilder and() { + protected void changed() { critical = Collections.asSet(getCollection()); - return super.and(); } }; } @@ -304,9 +303,8 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder { public NestedCollection zip() { return new DefaultNestedCollection(this, this.zipAlgs.values()) { @Override - public JwtParserBuilder and() { + protected void changed() { zipAlgs = new IdRegistry<>(StandardCompressionAlgorithms.NAME, getCollection()); - return super.and(); } }; } @@ -315,9 +313,8 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder { public NestedCollection enc() { return new DefaultNestedCollection(this, this.encAlgs.values()) { @Override - public JwtParserBuilder and() { + public void changed() { encAlgs = new IdRegistry<>(StandardEncryptionAlgorithms.NAME, getCollection()); - return super.and(); } }; } @@ -326,9 +323,8 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder { public NestedCollection, JwtParserBuilder> sig() { return new DefaultNestedCollection, JwtParserBuilder>(this, this.sigAlgs.values()) { @Override - public JwtParserBuilder and() { + public void changed() { sigAlgs = new IdRegistry<>(StandardSecureDigestAlgorithms.NAME, getCollection()); - return super.and(); } }; } @@ -337,9 +333,8 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder { public NestedCollection, JwtParserBuilder> key() { return new DefaultNestedCollection, JwtParserBuilder>(this, this.keyAlgs.values()) { @Override - public JwtParserBuilder and() { + public void changed() { keyAlgs = new IdRegistry<>(StandardKeyAlgorithms.NAME, getCollection()); - return super.and(); } }; } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DelegatingClaimsMutator.java b/impl/src/main/java/io/jsonwebtoken/impl/DelegatingClaimsMutator.java index 9c408a15..fd5c4833 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DelegatingClaimsMutator.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DelegatingClaimsMutator.java @@ -130,12 +130,12 @@ public class DelegatingClaimsMutator & C @Override public T single(String audience) { return audienceSingle(audience); + // DO NOT call changed() here - we don't want to replace the value with a collection } @Override - public T and() { + protected void changed() { put(DefaultClaims.AUDIENCE, Collections.asSet(getCollection())); - return super.and(); } }; } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultCollectionMutator.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultCollectionMutator.java index 9a64d7e6..cdb7fd8f 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultCollectionMutator.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultCollectionMutator.java @@ -37,38 +37,52 @@ public class DefaultCollectionMutator> impl return (M) this; } - @Override - public M add(E e) { - if (Objects.isEmpty(e)) return self(); + private boolean doAdd(E e) { + if (Objects.isEmpty(e)) return false; if (e instanceof Identifiable && !Strings.hasText(((Identifiable) e).getId())) { String msg = e.getClass() + " getId() value cannot be null or empty."; throw new IllegalArgumentException(msg); } - this.collection.remove(e); - this.collection.add(e); + return this.collection.add(e); + } + + @Override + public M add(E e) { + if (doAdd(e)) changed(); return self(); } @Override public M remove(E e) { - this.collection.remove(e); + if (this.collection.remove(e)) changed(); return self(); } @Override public M add(Collection c) { + boolean changed = false; for (E element : Collections.nullSafe(c)) { - add(element); + changed = doAdd(element) || changed; } + if (changed) changed(); return self(); } @Override public M clear() { + boolean changed = !Collections.isEmpty(this.collection); this.collection.clear(); + if (changed) changed(); return self(); } + /** + * Callback for subclasses that wish to be notified if the internal collection has changed via builder mutation + * methods. + */ + protected void changed() { + } + protected Collection getCollection() { return Collections.immutable(this.collection); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java index 5ac3b4e5..e580b349 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java @@ -111,11 +111,10 @@ abstract class AbstractJwkBuilder, T extends Jwk public NestedCollection operations() { return new DefaultNestedCollection(self(), this.DELEGATE.getOperations()) { @Override - public T and() { + protected void changed() { Collection c = getCollection(); opsPolicy.validate(c); DELEGATE.setOperations(c); - return super.and(); } }; } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy index 591b4b07..c6bc8abc 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy @@ -791,4 +791,47 @@ class DefaultJwtBuilderTest { assertEquals three, claims.aud } + /** + * Asserts that if a .audience() builder is used, and its .and() method is not called, the change to the + * audience is still applied when building the JWT. + * @see JJWT Issue 916 + * @since JJWT_RELEASE_VERSION + */ + @Test + void testAudienceWithoutConjunction() { + def aud = 'my-web' + def builder = Jwts.builder() + builder.audience().add(aud) // no .and() call + def jwt = builder.compact() + + // assert that the resulting claims has the audience array set as expected: + def parsed = Jwts.parser().unsecured().build().parseUnsecuredClaims(jwt) + assertEquals aud, parsed.payload.getAudience()[0] + } + + /** + * Asserts that if a .header().critical() builder is used, and its .and() method is not called, the change to the + * crit collection is still applied when building the header. + * @see JJWT Issue 916 + * @since JJWT_RELEASE_VERSION + */ + @Test + void testCritWithoutConjunction() { + def crit = 'test' + def builder = Jwts.builder().issuer('me') + def headerBuilder = builder.header() + headerBuilder.critical().add(crit) // no .and() method + headerBuilder.add(crit, 'foo') // no .and() method + builder.signWith(TestKeys.HS256) + def jwt = builder.compact() + + def headerBytes = Decoders.BASE64URL.decode(jwt.split('\\.')[0]) + def headerMap = Services.get(Deserializer).deserialize(Streams.reader(headerBytes)) as Map + + def expected = [crit] as Set + def val = headerMap.get('crit') as Set + assertNotNull val + assertEquals expected, val + } + } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtHeaderBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtHeaderBuilderTest.groovy index 29519283..5c9df8ef 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtHeaderBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtHeaderBuilderTest.groovy @@ -507,6 +507,22 @@ class DefaultJwtHeaderBuilderTest { assertEquals expected, header.getCritical() } + /** + * Asserts that if a .critical() builder is used, and its .and() method is not called, the change to the + * crit collection is still applied when building the header. + * @see JJWT Issue 916 + * @since JJWT_RELEASE_VERSION + */ + @Test + void testCritWithoutConjunction() { + def crit = 'test' + def builder = jws() + builder.add(crit, 'foo').critical().add(crit) // no .and() method + def header = builder.build() as ProtectedHeader + def expected = [crit] as Set + assertEquals expected, header.getCritical() + } + @Test void testCritSingleNullIgnored() { def crit = 'test' diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy index 71479b36..9cb7ef7c 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy @@ -48,6 +48,22 @@ class DefaultJwtParserBuilderTest { assertTrue builder.@critical.isEmpty() } + /** + * Asserts that if a .critical() builder is used, and its .and() method is not called, the change to the + * crit collection is still applied when building the parser. + * @see JJWT Issue 916 + * @since JJWT_RELEASE_VERSION + */ + @Test + void testCriticalWithoutConjunction() { + builder.critical().add('foo') // no .and() call + assertFalse builder.@critical.isEmpty() + assertTrue builder.@critical.contains('foo') + def parser = builder.build() + assertFalse parser.@critical.isEmpty() + assertTrue parser.@critical.contains('foo') + } + @Test void testSetProvider() { Provider provider = createMock(Provider) @@ -173,6 +189,21 @@ class DefaultJwtParserBuilderTest { assertSame codec, parser.zipAlgs.locate(header) } + /** + * Asserts that if a .zip() builder is used, and its .and() method is not called, the change to the + * compression algorithm collection is still applied when building the parser. + * @see JJWT Issue 916 + * @since JJWT_RELEASE_VERSION + */ + @Test + void testAddCompressionAlgorithmWithoutConjunction() { + def codec = new TestCompressionCodec(id: 'test') + builder.zip().add(codec) // no .and() call + def parser = builder.build() + def header = Jwts.header().add('zip', codec.getId()).build() + assertSame codec, parser.zipAlgs.locate(header) + } + @Test void testAddCompressionAlgorithmsOverrideDefaults() { def header = Jwts.header().add('zip', 'DEF').build() @@ -211,6 +242,21 @@ class DefaultJwtParserBuilderTest { assertSame custom, parser.encAlgs.apply(header) // custom one, not standard impl } + /** + * Asserts that if an .enc() builder is used, and its .and() method is not called, the change to the + * encryption algorithm collection is still applied when building the parser. + * @see JJWT Issue 916 + * @since JJWT_RELEASE_VERSION + */ + @Test + void testAddEncryptionAlgorithmWithoutConjunction() { + def alg = new TestAeadAlgorithm(id: 'test') + builder.enc().add(alg) // no .and() call + def parser = builder.build() as DefaultJwtParser + def header = Jwts.header().add('alg', 'foo').add('enc', alg.getId()).build() as JweHeader + assertSame alg, parser.encAlgs.apply(header) + } + @Test void testCaseSensitiveEncryptionAlgorithm() { def alg = Jwts.ENC.A256GCM @@ -239,6 +285,23 @@ class DefaultJwtParserBuilderTest { assertSame custom, parser.keyAlgs.apply(header) // custom one, not standard impl } + /** + * Asserts that if an .key() builder is used, and its .and() method is not called, the change to the + * key algorithm collection is still applied when building the parser. + * @see JJWT Issue 916 + * @since JJWT_RELEASE_VERSION + */ + @Test + void testAddKeyAlgorithmWithoutConjunction() { + def alg = new TestKeyAlgorithm(id: 'test') + builder.key().add(alg) // no .and() call + def parser = builder.build() as DefaultJwtParser + def header = Jwts.header() + .add('enc', 'foo') + .add('alg', alg.getId()).build() as JweHeader + assertSame alg, parser.keyAlgs.apply(header) + } + @Test void testCaseSensitiveKeyAlgorithm() { def alg = Jwts.KEY.A256GCMKW @@ -268,6 +331,21 @@ class DefaultJwtParserBuilderTest { assertSame custom, parser.sigAlgs.apply(header) // custom one, not standard impl } + /** + * Asserts that if an .sig() builder is used, and its .and() method is not called, the change to the + * signature algorithm collection is still applied when building the parser. + * @see JJWT Issue 916 + * @since JJWT_RELEASE_VERSION + */ + @Test + void testAddSignatureAlgorithmWithoutConjunction() { + def alg = new TestMacAlgorithm(id: 'test') + builder.sig().add(alg) // no .and() call + def parser = builder.build() as DefaultJwtParser + def header = Jwts.header().add('alg', alg.getId()).build() as JwsHeader + assertSame alg, parser.sigAlgs.apply(header) + } + @Test void testCaseSensitiveSignatureAlgorithm() { def alg = Jwts.SIG.HS256 diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/DefaultCollectionMutatorTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/DefaultCollectionMutatorTest.groovy index 213a39fe..e1b6d402 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/DefaultCollectionMutatorTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/DefaultCollectionMutatorTest.groovy @@ -27,11 +27,18 @@ import static org.junit.Assert.* */ class DefaultCollectionMutatorTest { + private int changeCount private DefaultCollectionMutator m @Before void setUp() { - m = new DefaultCollectionMutator(null) + changeCount = 0 + m = new DefaultCollectionMutator(null) { + @Override + protected void changed() { + changeCount++ + } + } } @Test @@ -51,9 +58,17 @@ class DefaultCollectionMutatorTest { void add() { def val = 'hello' m.add(val) + assertEquals 1, changeCount assertEquals Collections.singleton(val), m.getCollection() } + @Test + void addDuplicateDoesNotTriggerChange() { + m.add('hello') + m.add('hello') //already in the set, no change should be reflected + assertEquals 1, changeCount + } + @Test void addCollection() { def vals = ['hello', 'world'] @@ -67,6 +82,17 @@ class DefaultCollectionMutatorTest { assertFalse i.hasNext() } + /** + * Asserts that if a collection is added, each internal addition to the collection doesn't call changed(); instead + * changed() is only called once after they've all been added to the collection + */ + @Test + void addCollectionTriggersSingleChange() { + def c = ['hello', 'world'] + m.add(c) + assertEquals 1, changeCount // only one change triggered, not c.size() + } + @Test(expected = IllegalArgumentException) void addIdentifiableWithNullId() { def e = new Identifiable() { @@ -96,6 +122,12 @@ class DefaultCollectionMutatorTest { assertEquals Collections.singleton('world'), m.getCollection() } + @Test + void removeMissingDoesNotTriggerChange() { + m.remove('foo') // not in the collection, no change should be registered + assertEquals 0, changeCount + } + @Test void clear() { m.add('one').add('two').add(['three', 'four']) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy index ce73f8ac..ee7a75da 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy @@ -239,7 +239,21 @@ class AbstractJwkBuilderTest { .related(Jwks.OP.VERIFY.id).build() def builder = builder().operationPolicy(Jwks.OP.policy().add(op).build()) def jwk = builder.operations().add(Collections.setOf(op, Jwks.OP.VERIFY)).and().build() as Jwk - assertSame op, jwk.getOperations().find({it.id == 'sign'}) + assertSame op, jwk.getOperations().find({ it.id == 'sign' }) + } + + /** + * Asserts that if a .operations() builder is used, and its .and() method is not called, the change to the + * operations collection is still applied when building the JWK. + * @see JJWT Issue 916 + * @since JJWT_RELEASE_VERSION + */ + @Test + void testOperationsWithoutConjunction() { + def builder = builder() + builder.operations().clear().add(Jwks.OP.DERIVE_BITS) // no .and() call + def jwk = builder.build() + assertEquals(Jwks.OP.DERIVE_BITS, jwk.getOperations()[0]) } @Test From efffa86b8dad09968b7739e44411bf79c7d725c7 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Wed, 31 Jan 2024 18:52:53 -0800 Subject: [PATCH 16/17] Released 0.12.5 (#918) * Preparing for 0.12.5 release * [maven-release-plugin] prepare release 0.12.5 * [maven-release-plugin] prepare for next development iteration --- README.md | 26 +++++++++---------- api/pom.xml | 2 +- extensions/gson/pom.xml | 2 +- extensions/jackson/pom.xml | 2 +- extensions/orgjson/pom.xml | 2 +- extensions/pom.xml | 2 +- impl/pom.xml | 2 +- .../impl/DefaultJwtBuilderTest.groovy | 4 +-- .../impl/DefaultJwtHeaderBuilderTest.groovy | 2 +- .../impl/DefaultJwtParserBuilderTest.groovy | 10 +++---- .../security/AbstractJwkBuilderTest.groovy | 2 +- pom.xml | 2 +- tdjar/pom.xml | 2 +- 13 files changed, 30 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 797e307b..7387f3b9 100644 --- a/README.md +++ b/README.md @@ -543,18 +543,18 @@ If you're building a (non-Android) JDK project, you will want to define the foll io.jsonwebtoken jjwt-api - 0.12.4 + 0.12.5 io.jsonwebtoken jjwt-impl - 0.12.4 + 0.12.5 runtime io.jsonwebtoken jjwt-jackson - 0.12.4 + 0.12.5 runtime ``` @@ -3049,7 +3049,7 @@ scope which is the typical JJWT default). That is: ```groovy dependencies { - implementation 'io.jsonwebtoken:jjwt-jackson:0.12.4' + implementation 'io.jsonwebtoken:jjwt-jackson:0.12.5' } ``` @@ -3157,7 +3157,7 @@ scope which is the typical JJWT default). That is: io.jsonwebtoken jjwt-gson - 0.12.4 + 0.12.5 compile ``` @@ -3166,7 +3166,7 @@ scope which is the typical JJWT default). That is: ```groovy dependencies { - implementation 'io.jsonwebtoken:jjwt-gson:0.12.4' + implementation 'io.jsonwebtoken:jjwt-gson:0.12.5' } ``` diff --git a/api/pom.xml b/api/pom.xml index 6ee645b6..56a1f775 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -21,7 +21,7 @@ io.jsonwebtoken jjwt-root - 0.12.5-SNAPSHOT + 0.12.6-SNAPSHOT ../pom.xml diff --git a/extensions/gson/pom.xml b/extensions/gson/pom.xml index 4b3a296e..6a7dcac5 100644 --- a/extensions/gson/pom.xml +++ b/extensions/gson/pom.xml @@ -21,7 +21,7 @@ io.jsonwebtoken jjwt-root - 0.12.5-SNAPSHOT + 0.12.6-SNAPSHOT ../../pom.xml diff --git a/extensions/jackson/pom.xml b/extensions/jackson/pom.xml index 00f09665..5d2d66fd 100644 --- a/extensions/jackson/pom.xml +++ b/extensions/jackson/pom.xml @@ -21,7 +21,7 @@ io.jsonwebtoken jjwt-root - 0.12.5-SNAPSHOT + 0.12.6-SNAPSHOT ../../pom.xml diff --git a/extensions/orgjson/pom.xml b/extensions/orgjson/pom.xml index f37fe1b0..f531f128 100644 --- a/extensions/orgjson/pom.xml +++ b/extensions/orgjson/pom.xml @@ -21,7 +21,7 @@ io.jsonwebtoken jjwt-root - 0.12.5-SNAPSHOT + 0.12.6-SNAPSHOT ../../pom.xml diff --git a/extensions/pom.xml b/extensions/pom.xml index 0ee8810b..bbc28b53 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -21,7 +21,7 @@ io.jsonwebtoken jjwt-root - 0.12.5-SNAPSHOT + 0.12.6-SNAPSHOT ../pom.xml diff --git a/impl/pom.xml b/impl/pom.xml index 7c1f137d..e9fe705d 100644 --- a/impl/pom.xml +++ b/impl/pom.xml @@ -21,7 +21,7 @@ io.jsonwebtoken jjwt-root - 0.12.5-SNAPSHOT + 0.12.6-SNAPSHOT ../pom.xml diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy index c6bc8abc..93963e00 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy @@ -795,7 +795,7 @@ class DefaultJwtBuilderTest { * Asserts that if a .audience() builder is used, and its .and() method is not called, the change to the * audience is still applied when building the JWT. * @see JJWT Issue 916 - * @since JJWT_RELEASE_VERSION + * @since 0.12.5 */ @Test void testAudienceWithoutConjunction() { @@ -813,7 +813,7 @@ class DefaultJwtBuilderTest { * Asserts that if a .header().critical() builder is used, and its .and() method is not called, the change to the * crit collection is still applied when building the header. * @see JJWT Issue 916 - * @since JJWT_RELEASE_VERSION + * @since 0.12.5 */ @Test void testCritWithoutConjunction() { diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtHeaderBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtHeaderBuilderTest.groovy index 5c9df8ef..049f5644 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtHeaderBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtHeaderBuilderTest.groovy @@ -511,7 +511,7 @@ class DefaultJwtHeaderBuilderTest { * Asserts that if a .critical() builder is used, and its .and() method is not called, the change to the * crit collection is still applied when building the header. * @see JJWT Issue 916 - * @since JJWT_RELEASE_VERSION + * @since 0.12.5 */ @Test void testCritWithoutConjunction() { diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy index 9cb7ef7c..c874b804 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy @@ -52,7 +52,7 @@ class DefaultJwtParserBuilderTest { * Asserts that if a .critical() builder is used, and its .and() method is not called, the change to the * crit collection is still applied when building the parser. * @see JJWT Issue 916 - * @since JJWT_RELEASE_VERSION + * @since 0.12.5 */ @Test void testCriticalWithoutConjunction() { @@ -193,7 +193,7 @@ class DefaultJwtParserBuilderTest { * Asserts that if a .zip() builder is used, and its .and() method is not called, the change to the * compression algorithm collection is still applied when building the parser. * @see JJWT Issue 916 - * @since JJWT_RELEASE_VERSION + * @since 0.12.5 */ @Test void testAddCompressionAlgorithmWithoutConjunction() { @@ -246,7 +246,7 @@ class DefaultJwtParserBuilderTest { * Asserts that if an .enc() builder is used, and its .and() method is not called, the change to the * encryption algorithm collection is still applied when building the parser. * @see JJWT Issue 916 - * @since JJWT_RELEASE_VERSION + * @since 0.12.5 */ @Test void testAddEncryptionAlgorithmWithoutConjunction() { @@ -289,7 +289,7 @@ class DefaultJwtParserBuilderTest { * Asserts that if an .key() builder is used, and its .and() method is not called, the change to the * key algorithm collection is still applied when building the parser. * @see JJWT Issue 916 - * @since JJWT_RELEASE_VERSION + * @since 0.12.5 */ @Test void testAddKeyAlgorithmWithoutConjunction() { @@ -335,7 +335,7 @@ class DefaultJwtParserBuilderTest { * Asserts that if an .sig() builder is used, and its .and() method is not called, the change to the * signature algorithm collection is still applied when building the parser. * @see JJWT Issue 916 - * @since JJWT_RELEASE_VERSION + * @since 0.12.5 */ @Test void testAddSignatureAlgorithmWithoutConjunction() { diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy index ee7a75da..e183b7aa 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy @@ -246,7 +246,7 @@ class AbstractJwkBuilderTest { * Asserts that if a .operations() builder is used, and its .and() method is not called, the change to the * operations collection is still applied when building the JWK. * @see JJWT Issue 916 - * @since JJWT_RELEASE_VERSION + * @since 0.12.5 */ @Test void testOperationsWithoutConjunction() { diff --git a/pom.xml b/pom.xml index a49bb2a1..88f7ec47 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ io.jsonwebtoken jjwt-root - 0.12.5-SNAPSHOT + 0.12.6-SNAPSHOT JJWT JSON Web Token support for the JVM and Android pom diff --git a/tdjar/pom.xml b/tdjar/pom.xml index c22c9c55..43e8bd20 100644 --- a/tdjar/pom.xml +++ b/tdjar/pom.xml @@ -21,7 +21,7 @@ io.jsonwebtoken jjwt-root - 0.12.5-SNAPSHOT + 0.12.6-SNAPSHOT ../pom.xml From 26948610fbef81eba867cbaad54b516d1874c70a Mon Sep 17 00:00:00 2001 From: Brian Demers Date: Tue, 6 Feb 2024 14:51:33 -0500 Subject: [PATCH 17/17] Use Acsiidoc as README format (#777) Performed by running `kramdoc README.md`, then manual tweaks to adjust formatting Excludes adoc files from license checks --- README.md => README.adoc | 3936 +++++++++++++++++++++----------------- pom.xml | 1 + 2 files changed, 2163 insertions(+), 1774 deletions(-) rename README.md => README.adoc (52%) diff --git a/README.md b/README.adoc similarity index 52% rename from README.md rename to README.adoc index 7387f3b9..a3f31e2a 100644 --- a/README.md +++ b/README.adoc @@ -1,489 +1,513 @@ -[![Build Status](https://github.com/jwtk/jjwt/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/jwtk/jjwt/actions/workflows/ci.yml?query=branch%3Amaster) -[![Coverage Status](https://coveralls.io/repos/github/jwtk/jjwt/badge.svg?branch=master)](https://coveralls.io/github/jwtk/jjwt?branch=master) -[![Vuln score](https://snyk-widget.herokuapp.com/badge/mvn/io.jsonwebtoken/jjwt-root/badge.svg)](https://snyk-widget.herokuapp.com/badge/mvn/io.jsonwebtoken/jjwt-root/badge.svg) -[![Known Vulns](https://snyk.io/test/github/jwtk/jjwt/badge.svg)](https://snyk.io/test/github/jwtk/jjwt/badge.svg) +:doctype: book += Java JWT: JSON Web Token for Java and Android +:project-version: 0.12.5 +:toc: +:toc-title: +:toc-placement!: +:toclevels: 4 -# Java JWT: JSON Web Token for Java and Android +ifdef::env-github[] +:tip-caption: ✏️TIP +:note-caption: ℹ️ NOTE +:important-caption: ‼️IMPORTANT +:caution-caption: ⛔️CAUTION +:warning-caption: ⚠️WARNING +endif::[] -JJWT aims to be the easiest to use and understand library for creating and verifying JSON Web Tokens (JWTs) and +// Macros +:fn-require-java8-plus: Requires Java 8 or a compatible JCA Provider (like BouncyCastle) in the runtime classpath. +:fn-require-java11-plus: Requires Java 11 or a compatible JCA Provider (like BouncyCastle) in the runtime classpath. +:fn-require-java15-plus: Requires Java 15 or a compatible JCA Provider (like BouncyCastle) in the runtime classpath. + +image:https://github.com/jwtk/jjwt/actions/workflows/ci.yml/badge.svg?branch=master[Build Status,link=https://github.com/jwtk/jjwt/actions/workflows/ci.yml?query=branch%3Amaster] +image:https://coveralls.io/repos/github/jwtk/jjwt/badge.svg?branch=master[Coverage Status,link=https://coveralls.io/github/jwtk/jjwt?branch=master] +image:https://snyk-widget.herokuapp.com/badge/mvn/io.jsonwebtoken/jjwt-root/badge.svg[Vuln score,link=https://snyk-widget.herokuapp.com/badge/mvn/io.jsonwebtoken/jjwt-root/badge.svg] +image:https://snyk.io/test/github/jwtk/jjwt/badge.svg[Known Vulns,link=https://snyk.io/test/github/jwtk/jjwt/badge.svg] + +JJWT aims to be the easiest to use and understand library for creating and verifying JSON Web Tokens (JWTs) and JSON Web Keys (JWKs) on the JVM and Android. JJWT is a pure Java implementation based exclusively on the -[JOSE Working Group](https://datatracker.ietf.org/wg/jose/documents/) RFC specifications: +https://datatracker.ietf.org/wg/jose/documents/[JOSE Working Group] RFC specifications: -* [RFC 7519: JSON Web Token (JWT)](https://tools.ietf.org/html/rfc7519) -* [RFC 7515: JSON Web Signature (JWS)](https://tools.ietf.org/html/rfc7515) -* [RFC 7516: JSON Web Encryption (JWE)](https://tools.ietf.org/html/rfc7516) -* [RFC 7517: JSON Web Key (JWK)](https://tools.ietf.org/html/rfc7517) -* [RFC 7518: JSON Web Algorithms (JWA)](https://tools.ietf.org/html/rfc7518) -* [RFC 7638: JSON Web Key Thumbprint](https://www.rfc-editor.org/rfc/rfc7638.html) -* [RFC 9278: JSON Web Key Thumbprint URI](https://www.rfc-editor.org/rfc/rfc9278.html) -* [RFC 7797: JWS Unencoded Payload Option](https://www.rfc-editor.org/rfc/rfc7797.html) -* [RFC 8037: Edwards Curve algorithms and JWKs](https://www.rfc-editor.org/rfc/rfc8037) +* https://tools.ietf.org/html/rfc7519[RFC 7519: JSON Web Token (JWT)] +* https://tools.ietf.org/html/rfc7515[RFC 7515: JSON Web Signature (JWS)] +* https://tools.ietf.org/html/rfc7516[RFC 7516: JSON Web Encryption (JWE)] +* https://tools.ietf.org/html/rfc7517[RFC 7517: JSON Web Key (JWK)] +* https://tools.ietf.org/html/rfc7518[RFC 7518: JSON Web Algorithms (JWA)] +* https://www.rfc-editor.org/rfc/rfc7638.html[RFC 7638: JSON Web Key Thumbprint] +* https://www.rfc-editor.org/rfc/rfc9278.html[RFC 9278: JSON Web Key Thumbprint URI] +* https://www.rfc-editor.org/rfc/rfc7797.html[RFC 7797: JWS Unencoded Payload Option] +* https://www.rfc-editor.org/rfc/rfc8037[RFC 8037: Edwards Curve algorithms and JWKs] -It was created by [Les Hazlewood](https://github.com/lhazlewood) -and is supported and maintained by a [community](https://github.com/jwtk/jjwt/graphs/contributors) of contributors. +It was created by https://github.com/lhazlewood[Les Hazlewood] +and is supported and maintained by a https://github.com/jwtk/jjwt/graphs/contributors[community] of contributors. -JJWT is open source under the terms of the [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0). +JJWT is open source under the terms of the http://www.apache.org/licenses/LICENSE-2.0[Apache 2.0 License]. -## Table of Contents +==== +[discrete] +== Table of Contents +--- +toc::[] +==== -* [Features](#features) - * [Currently Unsupported Features](#features-unsupported) -* [Community](#community) - * [Getting Help](#help) - * [Questions](#help-questions) - * [Bugs and Feature Requests](#help-issues) - * [Contributing](#contributing) - * [Pull Requests](#contributing-pull-requests) - * [Help Wanted](#contributing-help-wanted) -* [What is a JSON Web Token?](#overview) - * [JWT Example](#overview-example-jwt) - * [JWS Example](#overview-example-jws) - * [JWE Example](#overview-example-jwe) -* [Installation](#install) - * [JDK Projects](#install-jdk) - * [Maven](#install-jdk-maven) - * [Gradle](#install-jdk-gradle) - * [Android Projects](#install-android) - * [Dependencies](#install-android-dependencies) - * [Proguard Exclusions](#install-android-proguard) - * [Bouncy Castle](#install-android-bc) - * [Understanding JJWT Dependencies](#install-understandingdependencies) -* [Quickstart](#quickstart) -* [Create a JWT](#jwt-create) - * [Header](#jwt-header) - * [Header Builder](#jwt-header-builder) - * [Header Parameters](#jwt-header-params) - * [Header Map](#jwt-header-map) - * [Payload](#jwt-payload) - * [Arbitrary Content](#jwt-content) - * [Claims](#jwt-claims) - * [Standard Claims](#jwt-claims-standard) - * [Custom Claims](#jwt-claims-custom) - * [Claims Instance](#jwt-claims-instance) - * [Claims Map](#jwt-claims-map) - * [Compression](#jwt-compression) -* [Read a JWT](#jwt-read) - * [Static Parsing Key](#jwt-read-key) - * [Dynamic Parsing Key Lookup](#key-locator) - * [Custom Key Locator](#key-locator-custom) - * [Key Locator Strategy](#key-locator-strategy) - * [Key Locator Return Values](#key-locator-retvals) - * [Provider-constrained Keys (PKCS11, HSM, etc)](#key-locator-provider) - * [Claim Assertions](#jwt-read-claims) - * [Accounting for Clock Skew](#jwt-read-clock) - * [Custom Clock Support](#jwt-read-clock-custom) - * [Decompression](#jwt-read-decompression) -* [Signed JWTs](#jws) - * [Standard Signature Algorithms](#jws-alg) - * [Signature Algorithm Keys](#jws-key) - * [HMAC-SHA](#jws-key-hmacsha) - * [RSA](#jws-key-rsa) - * [Elliptic Curve](#jws-key-ecdsa) - * [Creating Safe Keys](#jws-key-create) - * [Secret Keys](#jws-key-create-secret) - * [Asymetric Keys](#jws-key-create-asym) - * [Create a JWS](#jws-create) - * [Signing Key](#jws-create-key) - * [SecretKey Formats](#jws-create-key-secret) - * [Signature Algorithm Override](#jws-create-key-algoverride) - * [Compression](#jws-create-compression) - * [Read a JWS](#jws-read) - * [Verification Key](#jws-read-key) - * [Verification Key Locator](#jws-read-key-locator) - * [Decompression](#jws-read-decompression) - * [Unencoded Payload Option](#jws-unencoded) - * [Detached Payload Example](#jws-unencoded-detached) - * [Non-Detached Payload Example](#jws-unencoded-nondetached) -* [Encrypted JWTs](#jwe) - * [JWE Encryption Algorithms](#jwe-enc) - * [JWE Symmetric Encryption](#jwe-enc-symmetric) - * [JWE Key Management Algorithms](#jwe-alg) - * [JWE Standard Key Management Algorithms](#jwe-alg-standard) - * [JWE RSA Key Encryption](#jwe-alg-rsa) - * [JWE AES Key Encryption](#jwe-alg-aes) - * [JWE Direct Key Encryption](#jwe-alg-dir) - * [JWE Password-based Key Encryption](#jwe-alg-pbes2) - * [JWE Elliptic Curve Diffie-Hellman Ephemeral Static Key Agreement](#jwe-alg-ecdhes) - * [Create a JWE](#jwe-create) - * [JWE Compression](#jwe-compression) - * [Read a JWE](#jwe-read) - * [JWE Decryption Key](#jwe-read-key) - * [JWE Decryption Key Locator](#jwe-key-locator) - * [ECDH-ES Decryption with PKCS11 PrivateKeys](#jwe-key-pkcs11) - * [JWE Decompression](#jwe-read-decompression) -* [JSON Web Keys (JWKs)](#jwk) - * [Create a JWK](#jwk-create) - * [Read a JWK](#jwk-read) - * [PrivateKey JWKs](#jwk-private) - * [Private JWK `PublicKey`](#jwk-private-public) - * [Private JWK from `KeyPair`](#jwk-private-keypair) - * [Private JWK Public Conversion](#jwk-private-topub) - * [JWK Thumbprints](#jwk-thumbprint) - * [JWK Thumbprint as Key ID](jwk-thumbprint-kid) - * [JWK Thumbprint URI](#jwk-thumbprint-uri) - * [JWK Security Considerations](#jwk-security) - * [JWK `toString()` Safety](#jwk-tostring) -* [JWK Sets](#jwkset) - * [Create a JWK Set](#jwkset-create) - * [Read a JWK Set](#jwkset-read) -* [Compression](#compression) - * [Custom Compression Algorithm](#compression-custom) -* [JSON Processor](#json) - * [Custom JSON Processor](#json-custom) - * [Jackson ObjectMapper](#json-jackson) - * [Custom Claim Types](#json-jackson-custom-types) - * [Gson](#json-gson) -* [Base64 Support](#base64) - * [Base64 in Security Contexts](#base64-security) - * [Base64 is not Encryption](#base64-not-encryption) - * [Changing Base64 Characters](#base64-changing-characters) - * [Custom Base64 Codec](#base64-custom) -* [Examples](#examples) - * [JWS Signed with HMAC](#example-jws-hs) - * [JWS Signed with RSA](#example-jws-rsa) - * [JWS Signed with ECDSA](#example-jws-ecdsa) - * [JWE Encrypted Directly with a SecretKey](#example-jwe-dir) - * [JWE Encrypted with RSA](#example-jwe-rsa) - * [JWE Encrypted with AES Key Wrap](#example-jwe-aeskw) - * [JWE Encrypted with ECDH-ES](#example-jwe-ecdhes) - * [JWE Encrypted with a Password](#example-jwe-password) - * [SecretKey JWK](#example-jwk-secret) - * [RSA Public JWK](#example-jwk-rsapub) - * [RSA Private JWK](#example-jwk-rsapriv) - * [Elliptic Curve Public JWK](#example-jwk-ecpub) - * [Elliptic Curve Private JWK](#example-jwk-ecpriv) - * [Edwards Elliptic Curve Public JWK](#example-jwk-edpub) - * [Edwards Elliptic Curve Private JWK](#example-jwk-edpriv) +++++++++++++ - -## Features +== Features - * Fully functional on all Java 7+ JDKs and Android - * Automatic security best practices and assertions - * Easy to learn and read API - * Convenient and readable [fluent](http://en.wikipedia.org/wiki/Fluent_interface) interfaces, great for IDE - auto-completion to write code quickly - * Fully RFC specification compliant on all implemented functionality, tested against RFC-specified test vectors - * Stable implementation with almost 1,700 tests and enforced 100% test code coverage. Every single method, statement - and conditional branch variant in the entire codebase is tested and required to pass on every build. - * Creating, parsing and verifying digitally signed compact JWTs (aka JWSs) with all standard JWS algorithms: - - | Identifier | Signature Algorithm | - |------------|-------------------------------------------------------------------| - | `HS256` | HMAC using SHA-256 | - | `HS384` | HMAC using SHA-384 | - | `HS512` | HMAC using SHA-512 | - | `ES256` | ECDSA using P-256 and SHA-256 | - | `ES384` | ECDSA using P-384 and SHA-384 | - | `ES512` | ECDSA using P-521 and SHA-512 | - | `RS256` | RSASSA-PKCS-v1_5 using SHA-256 | - | `RS384` | RSASSA-PKCS-v1_5 using SHA-384 | - | `RS512` | RSASSA-PKCS-v1_5 using SHA-512 | - | `PS256` | RSASSA-PSS using SHA-256 and MGF1 with SHA-2561 | - | `PS384` | RSASSA-PSS using SHA-384 and MGF1 with SHA-3841 | - | `PS512` | RSASSA-PSS using SHA-512 and MGF1 with SHA-5121 | - | `EdDSA` | Edwards-curve Digital Signature Algorithm2 | +* Fully functional on all Java 7+ JDKs and Android +* Automatic security best practices and assertions +* Easy to learn and read API +* Convenient and readable http://en.wikipedia.org/wiki/Fluent_interface[fluent] interfaces, great for IDE +auto-completion to write code quickly +* Fully RFC specification compliant on all implemented functionality, tested against RFC-specified test vectors +* Stable implementation with almost 1,700 tests and enforced 100% test code coverage. Every single method, statement +and conditional branch variant in the entire codebase is tested and required to pass on every build. +* Creating, parsing and verifying digitally signed compact JWTs (aka JWSs) with all standard JWS algorithms: ++ +|=== +| Identifier | Signature Algorithm - 1. Requires Java 11 or a compatible JCA Provider (like BouncyCastle) in the runtime classpath. +| `HS256` +| HMAC using SHA-256 - 2. Requires Java 15 or a compatible JCA Provider (like BouncyCastle) in the runtime classpath. +| `HS384` +| HMAC using SHA-384 - * Creating, parsing and decrypting encrypted compact JWTs (aka JWEs) with all standard JWE encryption algorithms: - - | Identifier | Encryption Algorithm | - |----------------------------------|--------------------------------------------------------------------------------------------------------------------------| - | A128CBC‑HS256 | [AES_128_CBC_HMAC_SHA_256](https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.3) authenticated encryption algorithm | - | `A192CBC-HS384` | [AES_192_CBC_HMAC_SHA_384](https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.4) authenticated encryption algorithm | - | `A256CBC-HS512` | [AES_256_CBC_HMAC_SHA_512](https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.5) authenticated encryption algorithm | - | `A128GCM` | AES GCM using 128-bit key3 | - | `A192GCM` | AES GCM using 192-bit key3 | - | `A256GCM` | AES GCM using 256-bit key3 | - - 3. Requires Java 8 or a compatible JCA Provider (like BouncyCastle) in the runtime classpath. +| `HS512` +| HMAC using SHA-512 - * All Key Management Algorithms for obtaining JWE encryption and decryption keys: - - | Identifier | Key Management Algorithm | - |----------------------|-------------------------------------------------------------------------------| - | `RSA1_5` | RSAES-PKCS1-v1_5 | - | `RSA-OAEP` | RSAES OAEP using default parameters | - | `RSA-OAEP-256` | RSAES OAEP using SHA-256 and MGF1 with SHA-256 | - | `A128KW` | AES Key Wrap with default initial value using 128-bit key | - | `A192KW` | AES Key Wrap with default initial value using 192-bit key | - | `A256KW` | AES Key Wrap with default initial value using 256-bit key | - | `dir` | Direct use of a shared symmetric key as the CEK | - | `ECDH-ES` | Elliptic Curve Diffie-Hellman Ephemeral Static key agreement using Concat KDF | - | `ECDH-ES+A128KW` | ECDH-ES using Concat KDF and CEK wrapped with "A128KW" | - | `ECDH-ES+A192KW` | ECDH-ES using Concat KDF and CEK wrapped with "A192KW" | - | `ECDH-ES+A256KW` | ECDH-ES using Concat KDF and CEK wrapped with "A256KW" | - | `A128GCMKW` | Key wrapping with AES GCM using 128-bit key4 | - | `A192GCMKW` | Key wrapping with AES GCM using 192-bit key4 | - | `A256GCMKW` | Key wrapping with AES GCM using 256-bit key4 | - | `PBES2-HS256+A128KW` | PBES2 with HMAC SHA-256 and "A128KW" wrapping4 | - | `PBES2-HS384+A192KW` | PBES2 with HMAC SHA-384 and "A192KW" wrapping4 | - | PBES2‑HS512+A256KW | PBES2 with HMAC SHA-512 and "A256KW" wrapping4 | - - 4. Requires Java 8 or a compatible JCA Provider (like BouncyCastle) in the runtime classpath. +| `ES256` +| ECDSA using P-256 and SHA-256 - * Creating, parsing and verifying JSON Web Keys (JWKs) in all standard JWA key formats using native Java `Key` types: - - | JWK Key Format | Java `Key` Type | JJWT `Jwk` Type | - |----------------------------|------------------------------------|-------------------| - | Symmetric Key | `SecretKey` | `SecretJwk` | - | Elliptic Curve Public Key | `ECPublicKey` | `EcPublicJwk` | - | Elliptic Curve Private Key | `ECPrivateKey` | `EcPrivateJwk` | - | RSA Public Key | `RSAPublicKey` | `RsaPublicJwk` | - | RSA Private Key | `RSAPrivateKey` | `RsaPrivateJwk` | - | XDH Private Key | `XECPublicKey`5 | `OctetPublicJwk` | - | XDH Private Key | `XECPrivateKey`5 | `OctetPrivateJwk` | - | EdDSA Public Key | `EdECPublicKey`6 | `OctetPublicJwk` | - | EdDSA Private Key | `EdECPublicKey`6 | `OctetPrivateJwk` | +| `ES384` +| ECDSA using P-384 and SHA-384 - 5. Requires Java 11 or a compatible JCA Provider (like BouncyCastle) in the runtime classpath. +| `ES512` +| ECDSA using P-521 and SHA-512 - 6. Requires Java 15 or a compatible JCA Provider (like BouncyCastle) in the runtime classpath. +| `RS256` +| RSASSA-PKCS-v1_5 using SHA-256 - * Convenience enhancements beyond the specification such as - * Payload compression for any large JWT, not just JWEs - * Claims assertions (requiring specific values) - * Claim POJO marshaling and unmarshalling when using a compatible JSON parser (e.g. Jackson) - * Secure Key generation based on desired JWA algorithms - * and more... - - -### Currently Unsupported Features +| `RS384` +| RSASSA-PKCS-v1_5 using SHA-384 -* [Non-compact](https://tools.ietf.org/html/rfc7515#section-7.2) serialization and parsing. +| `RS512` +| RSASSA-PKCS-v1_5 using SHA-512 + +| `PS256` +| RSASSA-PSS using SHA-256 and MGF1 with SHA-256^*1*^ + +| `PS384` +| RSASSA-PSS using SHA-384 and MGF1 with SHA-384^*1*^ + +| `PS512` +| RSASSA-PSS using SHA-512 and MGF1 with SHA-512^*1*^ + +| `EdDSA` +| Edwards-curve Digital Signature Algorithm^*2*^ +|=== ++ +^*1.*{sp}{fn-require-java11-plus}^ ++ +^*2*.{sp}{fn-require-java15-plus}^ + +* Creating, parsing and decrypting encrypted compact JWTs (aka JWEs) with all standard JWE encryption algorithms: ++ +|=== +| Identifier | Encryption Algorithm + +| `A128CBC‑HS256` +| https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.3[AES_128_CBC_HMAC_SHA_256] authenticated encryption algorithm + +| `A192CBC-HS384` +| https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.4[AES_192_CBC_HMAC_SHA_384] authenticated encryption algorithm + +| `A256CBC-HS512` +| https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.5[AES_256_CBC_HMAC_SHA_512] authenticated encryption algorithm + +| `A128GCM` +| AES GCM using 128-bit key^*1*^ + +| `A192GCM` +| AES GCM using 192-bit key^*1*^ + +| `A256GCM` +| AES GCM using 256-bit key^*1*^ +|=== ++ +^*1*.{sp}{fn-require-java8-plus}^ + +* All Key Management Algorithms for obtaining JWE encryption and decryption keys: ++ +|=== +| Identifier | Key Management Algorithm + +| `RSA1_5` +| RSAES-PKCS1-v1_5 + +| `RSA-OAEP` +| RSAES OAEP using default parameters + +| `RSA-OAEP-256` +| RSAES OAEP using SHA-256 and MGF1 with SHA-256 + +| `A128KW` +| AES Key Wrap with default initial value using 128-bit key + +| `A192KW` +| AES Key Wrap with default initial value using 192-bit key + +| `A256KW` +| AES Key Wrap with default initial value using 256-bit key + +| `dir` +| Direct use of a shared symmetric key as the CEK + +| `ECDH-ES` +| Elliptic Curve Diffie-Hellman Ephemeral Static key agreement using Concat KDF + +| `ECDH-ES+A128KW` +| ECDH-ES using Concat KDF and CEK wrapped with "A128KW" + +| `ECDH-ES+A192KW` +| ECDH-ES using Concat KDF and CEK wrapped with "A192KW" + +| `ECDH-ES+A256KW` +| ECDH-ES using Concat KDF and CEK wrapped with "A256KW" + +| `A128GCMKW` +| Key wrapping with AES GCM using 128-bit key^*1*^ + +| `A192GCMKW` +| Key wrapping with AES GCM using 192-bit key^*1*^ + +| `A256GCMKW` +| Key wrapping with AES GCM using 256-bit key^*1*^ + +| `PBES2-HS256+A128KW` +| PBES2 with HMAC SHA-256 and "A128KW" wrapping^*1*^ + +| `PBES2-HS384+A192KW` +| PBES2 with HMAC SHA-384 and "A192KW" wrapping^*1*^ + +| `PBES2‑HS512+A256KW` +| PBES2 with HMAC SHA-512 and "A256KW" wrapping^*1*^ +|=== ++ +^*1*.{sp}{fn-require-java8-plus}^ + +* Creating, parsing and verifying JSON Web Keys (JWKs) in all standard JWA key formats using native Java `Key` types: ++ +|=== +| JWK Key Format | Java `Key` Type | JJWT `Jwk` Type + +| Symmetric Key +| `SecretKey` +| `SecretJwk` + +| Elliptic Curve Public Key +| `ECPublicKey` +| `EcPublicJwk` + +| Elliptic Curve Private Key +| `ECPrivateKey` +| `EcPrivateJwk` + +| RSA Public Key +| `RSAPublicKey` +| `RsaPublicJwk` + +| RSA Private Key +| `RSAPrivateKey` +| `RsaPrivateJwk` + +| XDH Private Key +| `XECPublicKey`^*1*^ +| `OctetPublicJwk` + +| XDH Private Key +| `XECPrivateKey`^*1*^ +| `OctetPrivateJwk` + +| EdDSA Public Key +| `EdECPublicKey`^*2*^ +| `OctetPublicJwk` + +| EdDSA Private Key +| `EdECPublicKey`^*2*^ +| `OctetPrivateJwk` +|=== ++ +^*1*.{sp}{fn-require-java15-plus}^ ++ +^*2*.{sp}{fn-require-java15-plus}^ + +* Convenience enhancements beyond the specification such as + ** Payload compression for any large JWT, not just JWEs + ** Claims assertions (requiring specific values) + ** Claim POJO marshaling and unmarshalling when using a compatible JSON parser (e.g. Jackson) + ** Secure Key generation based on desired JWA algorithms + ** and more... + +++++++++++++ + +=== Currently Unsupported Features + +* https://tools.ietf.org/html/rfc7515#section-7.2[Non-compact] serialization and parsing. This feature may be implemented in a future release. Community contributions are welcome! - -## Community +++++++++++++ - -### Getting Help +== Community -If you have trouble using JJWT, please first read the documentation on this page before asking questions. We try -very hard to ensure JJWT's documentation is robust, categorized with a table of contents, and up to date for each +++++++++++++ + +=== Getting Help + +If you have trouble using JJWT, please first read the documentation on this page before asking questions. We try +very hard to ensure JJWT's documentation is robust, categorized with a table of contents, and up to date for each release. - -#### Questions +++++++++++++ + +==== Questions If the documentation or the API JavaDoc isn't sufficient, and you either have usability questions or are confused -about something, please [ask your question here](https://github.com/jwtk/jjwt/discussions/new?category=q-a). However: +about something, please https://github.com/jwtk/jjwt/discussions/new?category=q-a[ask your question here]. However: -**Please do not create a GitHub issue to ask a question.** +*Please do not create a GitHub issue to ask a question.* -We use GitHub Issues to track actionable work that requires changes to JJWT's design and/or codebase. If you have a -usability question, instead please -[ask your question here](https://github.com/jwtk/jjwt/discussions/new?category=q-a), and we can convert that to an +We use GitHub Issues to track actionable work that requires changes to JJWT's design and/or codebase. If you have a +usability question, instead please +https://github.com/jwtk/jjwt/discussions/new?category=q-a[ask your question here], and we can convert that to an issue if necessary. -**If a GitHub Issue is created that does not represent actionable work for JJWT's codebase, it will be promptly -closed.** +*If a GitHub Issue is created that does not represent actionable work for JJWT's codebase, it will be promptly +closed.* - -#### Bugs, Feature Requests, Ideas and General Discussions +++++++++++++ -If you do not have a usability question and believe you have a legitimate bug or feature request, -please [discuss it here](https://github.com/jwtk/jjwt/discussions) **_FIRST_**. Please do a quick search first to +==== Bugs, Feature Requests, Ideas and General Discussions + +If you do not have a usability question and believe you have a legitimate bug or feature request, +please https://github.com/jwtk/jjwt/discussions[discuss it here] *_FIRST_*. Please do a quick search first to see if an existing discussion related to yours exist already and join that existing discussion if necesary. -If you feel like you'd like to help fix a bug or implement the new feature yourself, please read the Contributing +If you feel like you'd like to help fix a bug or implement the new feature yourself, please read the Contributing section next before starting any work. - -### Contributing +++++++++++++ - -#### Pull Requests +=== Contributing -Simple Pull Requests that fix anything other than JJWT core code (documentation, JavaDoc, typos, test cases, etc) are +++++++++++++ + +==== Pull Requests + +Simple Pull Requests that fix anything other than JJWT core code (documentation, JavaDoc, typos, test cases, etc) are always appreciated and have a high likelihood of being merged quickly. Please send them! -However, if you want or feel the need to change JJWT's functionality or core code, please do not issue a pull request -without [starting a new JJWT discussion](https://github.com/jwtk/jjwt/discussions) and discussing your desired -changes **first**, _before you start working on it_. +However, if you want or feel the need to change JJWT's functionality or core code, please do not issue a pull request +without https://github.com/jwtk/jjwt/discussions[starting a new JJWT discussion] and discussing your desired +changes *first*, _before you start working on it_. -It would be a shame to reject your earnest and genuinely-appreciated pull request if it might not align with the +It would be a shame to reject your earnest and genuinely-appreciated pull request if it might not align with the project's goals, design expectations or planned functionality. We've sadly had to reject large PRs in the past because -they were out of sync with project or design expectations - all because the PR author didn't first check in with +they were out of sync with project or design expectations - all because the PR author didn't first check in with the team first before working on a solution. -So, please [create a new JJWT discussion](https://github.com/jwtk/jjwt/discussions) first to discuss, and then we +So, please https://github.com/jwtk/jjwt/discussions[create a new JJWT discussion] first to discuss, and then we can see easily convert the discussion to an issue and then see if (or how) a PR is warranted. Thank you! - -#### Help Wanted +++++++++++++ -If you would like to help, but don't know where to start, please visit the -[Help Wanted Issues](https://github.com/jwtk/jjwt/labels/help%20wanted) page and pick any of the +==== Help Wanted + +If you would like to help, but don't know where to start, please visit the +https://github.com/jwtk/jjwt/labels/help%20wanted[Help Wanted Issues] page and pick any of the ones there, and we'll be happy to discuss and answer questions in the issue comments. -If any of those don't appeal to you, no worries! Any help you would like to offer would be -appreciated based on the above caveats concerning [contributing pull requests](#contributing-pull-requests). Feel free -to [discuss or ask questions first](https://github.com/jwtk/jjwt/discussions) if you're not sure. :) +If any of those don't appeal to you, no worries! Any help you would like to offer would be +appreciated based on the above caveats concerning <>. Feel free +to https://github.com/jwtk/jjwt/discussions[discuss or ask questions first] if you're not sure. :) - -## What is a JSON Web Token? +++++++++++++ -JSON Web Token (JWT) is a _general-purpose_ text-based messaging format for transmitting information in a -compact and secure way. Contrary to popular belief, JWT is not just useful for sending and receiving identity tokens +== What is a JSON Web Token? + +JSON Web Token (JWT) is a _general-purpose_ text-based messaging format for transmitting information in a +compact and secure way. Contrary to popular belief, JWT is not just useful for sending and receiving identity tokens on the web - even if that is the most common use case. JWTs can be used as messages for _any_ type of data. A JWT in its simplest form contains two parts: - 1. The primary data within the JWT, called the `payload`, and - 2. A JSON `Object` with name/value pairs that represent metadata about the `payload` and the - message itself, called the `header`. +. The primary data within the JWT, called the `payload`, and +. A JSON `Object` with name/value pairs that represent metadata about the `payload` and the +message itself, called the `header`. -A JWT `payload` can be absolutely anything at all - anything that can be represented as a byte array, such as Strings, +A JWT `payload` can be absolutely anything at all - anything that can be represented as a byte array, such as Strings, images, documents, etc. -But because a JWT `header` is a JSON `Object`, it would make sense that a JWT `payload` could also be a JSON -`Object` as well. In many cases, developers like the `payload` to be JSON that -represents data about a user or computer or similar identity concept. When used this way, the `payload` is called a -JSON `Claims` object, and each name/value pair within that object is called a `claim` - each piece of information +But because a JWT `header` is a JSON `Object`, it would make sense that a JWT `payload` could also be a JSON +`Object` as well. In many cases, developers like the `payload` to be JSON that +represents data about a user or computer or similar identity concept. When used this way, the `payload` is called a +JSON `Claims` object, and each name/value pair within that object is called a `claim` - each piece of information within 'claims' something about an identity. -And while it is useful to 'claim' something about an identity, really anyone can do that. What's important is that you +And while it is useful to 'claim' something about an identity, really anyone can do that. What's important is that you _trust_ the claims by verifying they come from a person or computer you trust. -A nice feature of JWTs is that they can be secured in various ways. A JWT can be cryptographically signed (making it -what we call a [JWS](https://tools.ietf.org/html/rfc7515)) or encrypted (making it a -[JWE](https://tools.ietf.org/html/rfc7516)). This adds a powerful layer of verifiability to the JWT - a +A nice feature of JWTs is that they can be secured in various ways. A JWT can be cryptographically signed (making it +what we call a https://tools.ietf.org/html/rfc7515[JWS]) or encrypted (making it a +https://tools.ietf.org/html/rfc7516[JWE]). This adds a powerful layer of verifiability to the JWT - a JWS or JWE recipient can have a high degree of confidence it comes from someone they trust by verifying a signature or decrypting it. It is this feature of verifiability that makes JWT a good choice for sending and receiving secure information, like identity claims. Finally, JSON with whitespace for human readability is nice, but it doesn't make for a very efficient message -format. Therefore, JWTs can be _compacted_ (and even compressed) to a minimal representation - basically +format. Therefore, JWTs can be _compacted_ (and even compressed) to a minimal representation - basically Base64URL-encoded strings - so they can be transmitted around the web more efficiently, such as in HTTP headers or URLs. - -### JWT Example +++++++++++++ -Once you have a `payload` and `header`, how are they compacted for web transmission, and what does the final JWT +=== JWT Example + +Once you have a `payload` and `header`, how are they compacted for web transmission, and what does the final JWT actually look like? Let's walk through a simplified version of the process with some pseudocode: -1. Assume we have a JWT with a JSON `header` and a simple text message payload: +. Assume we have a JWT with a JSON `header` and a simple text message payload: ++ +*header* ++ +---- +{ + "alg": "none" +} +---- ++ +*payload* ++ +---- +The true sign of intelligence is not knowledge but imagination. +---- - **header** - ``` - { - "alg": "none" - } - ``` +. Remove all unnecessary whitespace in the JSON: ++ +[,groovy] +---- +String header = '{"alg":"none"}' +String payload = 'The true sign of intelligence is not knowledge but imagination.' +---- - **payload** - ``` - The true sign of intelligence is not knowledge but imagination. - ``` +. Get the UTF-8 bytes and Base64URL-encode each: ++ +[,groovy] +---- +String encodedHeader = base64URLEncode( header.getBytes("UTF-8") ) +String encodedPayload = base64URLEncode( payload.getBytes("UTF-8") ) +---- -2. Remove all unnecessary whitespace in the JSON: - - ```groovy - String header = '{"alg":"none"}' - String payload = 'The true sign of intelligence is not knowledge but imagination.' - ``` - -3. Get the UTF-8 bytes and Base64URL-encode each: - - ```groovy - String encodedHeader = base64URLEncode( header.getBytes("UTF-8") ) - String encodedPayload = base64URLEncode( payload.getBytes("UTF-8") ) - ``` - -4. Join the encoded header and claims with period ('.') characters: - - ```groovy - String compact = encodedHeader + '.' + encodedPayload + '.' - ``` +. Join the encoded header and claims with period ('.') characters: ++ +[,groovy] +---- +String compact = encodedHeader + '.' + encodedPayload + '.' +---- The final concatenated `compact` JWT String looks like this: -``` +---- eyJhbGciOiJub25lIn0.VGhlIHRydWUgc2lnbiBvZiBpbnRlbGxpZ2VuY2UgaXMgbm90IGtub3dsZWRnZSBidXQgaW1hZ2luYXRpb24u. -``` +---- This is called an 'unprotected' JWT because no security was involved - no digital signatures or encryption to 'protect' the JWT to ensure it cannot be changed by 3rd parties. -If we wanted to digitally sign the compact form so that we could at least guarantee that no-one changes the data +If we wanted to digitally sign the compact form so that we could at least guarantee that no-one changes the data without us detecting it, we'd have to perform a few more steps, shown next. - -### JWS Example +++++++++++++ + +=== JWS Example Instead of a plain text payload, the next example will use probably the most common type of payload - a JSON claims `Object` containing information about a particular identity. We'll also digitally sign the JWT to ensure it cannot be changed by a 3rd party without us knowing. -1. Assume we have a JSON `header` and a claims `payload`: +. Assume we have a JSON `header` and a claims `payload`: ++ +*header* ++ +[,json] +---- +{ + "alg": "HS256" +} +---- ++ +*payload* ++ +[,json] +---- +{ + "sub": "Joe" +} +---- ++ +In this case, the `header` indicates that the `HS256` (HMAC using SHA-256) algorithm will be used to cryptographically sign +the JWT. Also, the `payload` JSON object has a single claim, `sub` with value `Joe`. ++ +There are a number of standard claims, called https://tools.ietf.org/html/rfc7519#section-4.1[Registered Claims], +in the specification and `sub` (for 'Subject') is one of them. - **header** - ```json - { - "alg": "HS256" - } - ``` +. Remove all unnecessary whitespace in both JSON objects: ++ +[,groovy] +---- +String header = '{"alg":"HS256"}' +String claims = '{"sub":"Joe"}' +---- - **payload** - ```json - { - "sub": "Joe" - } - ``` +. Get their UTF-8 bytes and Base64URL-encode each: ++ +[,groovy] +---- +String encodedHeader = base64URLEncode( header.getBytes("UTF-8") ) +String encodedClaims = base64URLEncode( claims.getBytes("UTF-8") ) +---- - In this case, the `header` indicates that the `HS256` (HMAC using SHA-256) algorithm will be used to cryptographically sign - the JWT. Also, the `payload` JSON object has a single claim, `sub` with value `Joe`. - - There are a number of standard claims, called [Registered Claims](https://tools.ietf.org/html/rfc7519#section-4.1), - in the specification and `sub` (for 'Subject') is one of them. +. Concatenate the encoded header and claims with a period character '.' delimiter: ++ +[,groovy] +---- +String concatenated = encodedHeader + '.' + encodedClaims +---- -2. Remove all unnecessary whitespace in both JSON objects: +. Use a sufficiently-strong cryptographic secret or private key, along with a signing algorithm of your choice + (we'll use HMAC-SHA-256 here), and sign the concatenated string: ++ +[,groovy] +---- + SecretKey key = getMySecretKey() + byte[] signature = hmacSha256( concatenated, key ) +---- - ```groovy - String header = '{"alg":"HS256"}' - String claims = '{"sub":"Joe"}' - ``` - -3. Get their UTF-8 bytes and Base64URL-encode each: - - ```groovy - String encodedHeader = base64URLEncode( header.getBytes("UTF-8") ) - String encodedClaims = base64URLEncode( claims.getBytes("UTF-8") ) - ``` - -4. Concatenate the encoded header and claims with a period character '.' delimiter: - - ```groovy - String concatenated = encodedHeader + '.' + encodedClaims - ``` - -5. Use a sufficiently-strong cryptographic secret or private key, along with a signing algorithm of your choice - (we'll use HMAC-SHA-256 here), and sign the concatenated string: - - ```groovy - SecretKey key = getMySecretKey() - byte[] signature = hmacSha256( concatenated, key ) - ``` - -6. Because signatures are always byte arrays, Base64URL-encode the signature and join it to the `concatenated` string - with a period character '.' delimiter: - - ```groovy - String compact = concatenated + '.' + base64URLEncode( signature ) - ``` +. Because signatures are always byte arrays, Base64URL-encode the signature and join it to the `concatenated` string +with a period character '.' delimiter: ++ +[,groovy] +---- +String compact = concatenated + '.' + base64URLEncode( signature ) +---- And there you have it, the final `compact` String looks like this: -``` +---- eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKb2UifQ.1KP0SsvENi7Uz1oQc07aXTL7kpQG5jBNIybqr60AlD4 -``` +---- This is called a 'JWS' - short for _signed_ JWT. @@ -491,12 +515,13 @@ Of course, no one would want to do this manually in code, and worse, if you get serious security problems and weaknesses. As a result, JJWT was created to handle all of this for you: JJWT completely automates both the creation of JWSs and the parsing and verification of JWSs for you. - -### JWE Example +++++++++++++ -So far we have seen an unprotected JWT and a cryptographically signed JWT (called a 'JWS'). One of the things -that is inherent to both of these two is that all the information within them can be seen by anyone - all the data in -both the header and the payload is publicly visible. JWS just ensures the data hasn't been changed by anyone - +=== JWE Example + +So far we have seen an unprotected JWT and a cryptographically signed JWT (called a 'JWS'). One of the things +that is inherent to both of these two is that all the information within them can be seen by anyone - all the data in +both the header and the payload is publicly visible. JWS just ensures the data hasn't been changed by anyone - it doesn't prevent anyone from seeing it. Many times, this is just fine because the data within them is not sensitive information. @@ -505,62 +530,66 @@ postal address or social security number or bank account number? In these cases, we'd want a fully-encrypted JWT, called a 'JWE' for short. A JWE uses cryptography to ensure that the payload remains fully encrypted _and_ authenticated so unauthorized parties cannot see data within, nor change the data -without being detected. Specifically, the JWE specification requires that -[Authenticated Encryption with Associated Data](https://en.wikipedia.org/wiki/Authenticated_encryption#Authenticated_encryption_with_associated_data_(AEAD)) +without being detected. Specifically, the JWE specification requires that +https://en.wikipedia.org/wiki/Authenticated_encryption#Authenticated_encryption_with_associated_data_(AEAD)[Authenticated Encryption with Associated Data] algorithms are used to fully encrypt and protect data. A full overview of AEAD algorithms are out of scope for this documentation, but here's an example of a final compact JWE that utilizes these algorithms (line breaks are for readability only): -``` +---- eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0. 6KB707dM9YTIgHtLvtgWQ8mKwboJW3of9locizkDTHzBC2IlrT1oOQ. AxY8DCtDaGlsbGljb3RoZQ. KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY. U0m_YmjN04DJvceFICbCVQ -``` +---- Next we'll cover how to install JJWT in your project, and then we'll see how to use JJWT's nice fluent API instead of risky string manipulation to quickly and safely build JWTs, JWSs, and JWEs. - -## Installation +++++++++++++ + +== Installation Use your favorite Maven-compatible build tool to pull the dependencies from Maven Central. -The dependencies could differ slightly if you are working with a [JDK project](#install-jdk) or an -[Android project](#install-android). +The dependencies could differ slightly if you are working with a <> or an +<>. - -### JDK Projects +++++++++++++ + +=== JDK Projects If you're building a (non-Android) JDK project, you will want to define the following dependencies: - -#### Maven +++++++++++++ -```xml +==== Maven + +[,xml,subs="+attributes"] +---- io.jsonwebtoken jjwt-api - 0.12.5 + {project-version} io.jsonwebtoken jjwt-impl - 0.12.5 + {project-version} runtime io.jsonwebtoken jjwt-jackson - 0.12.5 + {project-version} runtime +---- -``` +++++++++++++ - -#### Gradle +==== Gradle -```groovy +[,groovy,subs="+attributes"] +---- dependencies { - implementation 'io.jsonwebtoken:jjwt-api:0.12.5' - runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5' - runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5' // or 'io.jsonwebtoken:jjwt-gson:0.12.5' for gson - /* + implementation 'io.jsonwebtoken:jjwt-api:{project-version}' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:{project-version}' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:{project-version}' // or 'io.jsonwebtoken:jjwt-gson:{project-version}' for gson + /* Uncomment this next dependency if you are using: - JDK 10 or earlier, and you want to use RSASSA-PSS (PS256, PS384, PS512) signature algorithms. - JDK 10 or earlier, and you want to use EdECDH (X25519 or X448) Elliptic Curve Diffie-Hellman encryption. @@ -589,27 +619,30 @@ dependencies { */ // runtimeOnly 'org.bouncycastle:bcprov-jdk18on:1.76' // or bcprov-jdk15to18 on JDK 7 } -``` +---- - -### Android Projects +++++++++++++ + +=== Android Projects Android projects will want to define the following dependencies and Proguard exclusions, and optional BouncyCastle `Provider`: - -#### Dependencies +++++++++++++ + +==== Dependencies Add the dependencies to your project: -```groovy +[,groovy,subs="+attributes"] +---- dependencies { - api('io.jsonwebtoken:jjwt-api:0.12.5') - runtimeOnly('io.jsonwebtoken:jjwt-impl:0.12.5') - runtimeOnly('io.jsonwebtoken:jjwt-orgjson:0.12.5') { + api('io.jsonwebtoken:jjwt-api:{project-version}') + runtimeOnly('io.jsonwebtoken:jjwt-impl:{project-version}') + runtimeOnly('io.jsonwebtoken:jjwt-orgjson:{project-version}') { exclude(group: 'org.json', module: 'json') //provided by Android natively } - /* + /* Uncomment this next dependency if you want to use: - RSASSA-PSS (PS256, PS384, PS512) signature algorithms. - EdECDH (X25519 or X448) Elliptic Curve Diffie-Hellman encryption. @@ -618,14 +651,15 @@ dependencies { */ //implementation('org.bouncycastle:bcprov-jdk18on:1.76') // or bcprov-jdk15to18 for JDK 7 } -``` +---- - -#### Proguard +++++++++++++ -You can use the following [Android Proguard](https://developer.android.com/studio/build/shrink-code) exclusion rules: +==== Proguard -``` +You can use the following https://developer.android.com/studio/build/shrink-code[Android Proguard] exclusion rules: + +---- -keepattributes InnerClasses -keep class io.jsonwebtoken.** { *; } @@ -635,21 +669,24 @@ You can use the following [Android Proguard](https://developer.android.com/studi -keep class org.bouncycastle.** { *; } -keepnames class org.bouncycastle.** { *; } -dontwarn org.bouncycastle.** -``` +---- - -#### Bouncy Castle +++++++++++++ -If you want to use JWT RSASSA-PSS algorithms (i.e. `PS256`, `PS384`, and `PS512`), EdECDH (`X25512` or `X448`) -Elliptic Curve Diffie-Hellman encryption, EdDSA (`Ed25519` or `Ed448`) signature algorithms, or you just want to +==== Bouncy Castle + +If you want to use JWT RSASSA-PSS algorithms (i.e. `PS256`, `PS384`, and `PS512`), EdECDH (`X25512` or `X448`) +Elliptic Curve Diffie-Hellman encryption, EdDSA (`Ed25519` or `Ed448`) signature algorithms, or you just want to ensure your Android application is running an updated version of BouncyCastle, you will need to: -1. Uncomment the BouncyCastle dependency as commented above in the [dependencies](#install-android-dependencies) section. -2. Replace the legacy Android custom `BC` provider with the updated one. -Provider registration needs to be done _early_ in the application's lifecycle, preferably in your application's +. Uncomment the BouncyCastle dependency as commented above in the <> section. +. Replace the legacy Android custom `BC` provider with the updated one. + +Provider registration needs to be done _early_ in the application's lifecycle, preferably in your application's main `Activity` class as a static initialization block. For example: -```kotlin +[,kotlin] +---- class MainActivity : AppCompatActivity() { companion object { @@ -661,12 +698,13 @@ class MainActivity : AppCompatActivity() { // ... etc ... } -``` +---- - -### Understanding JJWT Dependencies +++++++++++++ -Notice the above JJWT dependency declarations all have only one compile-time dependency and the rest are declared as +=== Understanding JJWT Dependencies + +Notice the above JJWT dependency declarations all have only one compile-time dependency and the rest are declared as _runtime_ dependencies. This is because JJWT is designed so you only depend on the APIs that are explicitly designed for you to use in @@ -674,26 +712,29 @@ your applications and all other internal implementation details - that can chang runtime-only dependencies. This is an extremely important point if you want to ensure stable JJWT usage and upgrades over time: -> **Warning** -> -> **JJWT guarantees semantic versioning compatibility for all of its artifacts _except_ the `jjwt-impl` .jar. No such -guarantee is made for the `jjwt-impl` .jar and internal changes in that .jar can happen at any time. Never add the -`jjwt-impl` .jar to your project with `compile` scope - always declare it with `runtime` scope.** +[WARNING] +==== +JJWT guarantees semantic versioning compatibility for all of its artifacts _except_ the `jjwt-impl` .jar. No such +guarantee is made for the `jjwt-impl` .jar and internal changes in that .jar can happen at any time. Never add the +`jjwt-impl` .jar to your project with `compile` scope - always declare it with `runtime` scope. +==== This is done to benefit you: great care goes into curating the `jjwt-api` .jar and ensuring it contains what you need -and remains backwards compatible as much as is possible so you can depend on that safely with compile scope. The -runtime `jjwt-impl` .jar strategy affords the JJWT developers the flexibility to change the internal packages and -implementations whenever and however necessary. This helps us implement features, fix bugs, and ship new releases to +and remains backwards compatible as much as is possible so you can depend on that safely with compile scope. The +runtime `jjwt-impl` .jar strategy affords the JJWT developers the flexibility to change the internal packages and +implementations whenever and however necessary. This helps us implement features, fix bugs, and ship new releases to you more quickly and efficiently. - -## Quickstart +++++++++++++ -Most complexity is hidden behind a convenient and readable builder-based -[fluent interface](http://en.wikipedia.org/wiki/Fluent_interface), great for relying on IDE auto-completion to write +== Quickstart + +Most complexity is hidden behind a convenient and readable builder-based +http://en.wikipedia.org/wiki/Fluent_interface[fluent interface], great for relying on IDE auto-completion to write code quickly. Here's an example: -```java +[,java] +---- import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import java.security.Key; @@ -703,42 +744,45 @@ import java.security.Key; SecretKey key = Jwts.SIG.HS256.key().build(); String jws = Jwts.builder().subject("Joe").signWith(key).compact(); -``` +---- How easy was that!? In this case, we are: - - 1. *building* a JWT that will have the -[registered claim](https://tools.ietf.org/html/rfc7519#section-4.1) `sub` (Subject) set to `Joe`. We are then - 2. *signing* the JWT using a key suitable for the HMAC-SHA-256 algorithm. Finally, we are - 3. *compacting* it into its final `String` form. A signed JWT is called a 'JWS'. + +. _building_ a JWT that will have the +https://tools.ietf.org/html/rfc7519#section-4.1[registered claim] `sub` (Subject) set to `Joe`. We are then +. _signing_ the JWT using a key suitable for the HMAC-SHA-256 algorithm. Finally, we are +. _compacting_ it into its final `String` form. A signed JWT is called a 'JWS'. The resultant `jws` String looks like this: -``` +---- eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKb2UifQ.1KP0SsvENi7Uz1oQc07aXTL7kpQG5jBNIybqr60AlD4 -``` +---- Now let's verify the JWT (you should always discard JWTs that don't match an expected signature): -```java +[,java] +---- assert Jwts.parser().verifyWith(key).build().parseSignedClaims(jws).getPayload().getSubject().equals("Joe"); -``` +---- -There are two things going on here. The `key` from before is being used to verify the signature of the JWT. If it -fails to verify the JWT, a `SignatureException` (which extends `JwtException`) is thrown. Assuming the JWT is -verified, we parse the claims and assert that that subject is set to `Joe`. You have to love code one-liners +There are two things going on here. The `key` from before is being used to verify the signature of the JWT. If it +fails to verify the JWT, a `SignatureException` (which extends `JwtException`) is thrown. Assuming the JWT is +verified, we parse the claims and assert that that subject is set to `Joe`. You have to love code one-liners that pack a punch! -> **Note** -> -> **Type-safe JWTs:** To get a type-safe `Claims` JWT result, call the `parseSignedClaims` method (since there are many +[NOTE] +==== +*Type-safe JWTs:* To get a type-safe `Claims` JWT result, call the `parseSignedClaims` method (since there are many similar methods available). You will get an `UnsupportedJwtException` if you parse your JWT with wrong method. +==== But what if parsing or signature validation failed? You can catch `JwtException` and react accordingly: -```java +[,java] +---- try { Jwts.parser().verifyWith(key).build().parseSignedClaims(compactJws); @@ -749,66 +793,74 @@ try { //don't trust the JWT! } -``` +---- Now that we've had a quickstart 'taste' of how to create and parse JWTs, let's cover JJWT's API in-depth. - -## Creating a JWT +++++++++++++ + +== Creating a JWT You create a JWT as follows: -1. Use the `Jwts.builder()` method to create a `JwtBuilder` instance. -2. Optionally set any [`header` parameters](#jwt-header) as desired. -3. Call builder methods to set the payload [content](#jwt-content) or [claims](#jwt-claims). -4. Optionally call `signWith` or `encryptWith` methods if you want to digitally sign or encrypt the JWT. -5. Call the `compact()` method to produce the resulting compact JWT string. +. Use the `Jwts.builder()` method to create a `JwtBuilder` instance. +. Optionally set any <> as desired. +. Call builder methods to set the payload <> or <>. +. Optionally call `signWith` or `encryptWith` methods if you want to digitally sign or encrypt the JWT. +. Call the `compact()` method to produce the resulting compact JWT string. For example: -```java +[,java] +---- String jwt = Jwts.builder() // (1) - + .header() // (2) optional .keyId("aKeyId") .and() - + .subject("Bob") // (3) JSON Claims, or //.content(aByteArray, "text/plain") // any byte[] content, with media type - + .signWith(signingKey) // (4) if signing, or //.encryptWith(key, keyAlg, encryptionAlg) // if encrypting - - .compact(); // (5) -``` -* The JWT `payload` may be either `byte[]` content (via `content`) _or_ JSON Claims + .compact(); // (5) +---- + +* The JWT `payload` may be either `byte[]` content (via `content`) _or_ JSON Claims (such as `subject`, `claims`, etc), but not both. * Either digital signatures (`signWith`) or encryption (`encryptWith`) may be used, but not both. -> **Warning** -> -> **Unprotected JWTs**: If you do not use the `signWith` or `encryptWith` builder methods, **an Unprotected JWT will be -> created, which offers no security protection at all**. If you need security protection, consider either -> [digitally signing](#jws) or [encrypting](#jwe) the JWT before calling the `compact()` builder method. +[WARNING] +==== +*Unprotected JWTs*: If you do not use the `signWith` or `encryptWith` builder methods, *an Unprotected JWT will be +created, which offers no security protection at all*. If you need security protection, consider either +<> or <> the JWT before calling the `compact()` builder method. +==== - -### JWT Header +++++++++++++++++++++++++ +// legacy anchors for old links + +=== JWT Header A JWT header is a JSON `Object` that provides metadata about the contents, format, and any cryptographic operations relevant to the JWT `payload`. JJWT provides a number of ways of setting the entire header and/or multiple individual header parameters (name/value pairs). - -#### JwtBuilder Header +++++++++++++++++++++++++ +// legacy anchors for old links + +==== JwtBuilder Header The easiest and recommended way to set one or more JWT header parameters (name/value pairs) is to use the -`JwtBuilder`'s `header()` builder as desired, and then call its `and()` method to return back +``JwtBuilder``'s `header()` builder as desired, and then call its `and()` method to return back to the `JwtBuilder` for further configuration. For example: -```java +[,java] +---- String jwt = Jwts.builder() - + .header() // <---- .keyId("aKeyId") .x509Url(aUri) @@ -816,86 +868,94 @@ String jwt = Jwts.builder() .add(mapValues) // ... etc ... .and() // go back to the JwtBuilder - + .subject("Joe") // resume JwtBuilder calls... - // ... etc ... + // ... etc ... .compact(); -``` +---- The `JwtBuilder` `header()` builder also supports automatically calculating X.509 thumbprints and other builder-style benefits that a simple property getter/setter object would not do. -> **Note** -> -> **Automatic Headers**: You do not need to set the `alg`, `enc` or `zip` headers - JJWT will always set them -> automatically as needed. +[NOTE] +==== +*Automatic Headers*: You do not need to set the `alg`, `enc` or `zip` headers - JJWT will always set them +automatically as needed. +==== - -##### Custom Header Parameters -In addition to type-safe builder methods for standard header parameters, `JwtBuilder.header()` can also support +++++++++++++ + +===== Custom Header Parameters + +In addition to type-safe builder methods for standard header parameters, `JwtBuilder.header()` can also support arbitrary name/value pairs via the `add` method: -```java +[,java] +---- Jwts.builder() - + .header() .add("aHeaderName", aValue) // ... etc ... .and() // return to the JwtBuilder - -// ... etc ... -``` - -##### Header Parameter Map +// ... etc ... +---- + +++++++++++++++++++++++++ +// legacy anchors for old links + +===== Header Parameter Map + The `add` method is also overloaded to support multiple parameters in a `Map`: -```java +[,java] +---- Jwts.builder() - + .header() .add(multipleHeaderParamsMap) // ... etc ... .and() // return to the JwtBuilder - -// ... etc ... -``` -#### Jwts HeaderBuilder +// ... etc ... +---- + +==== Jwts HeaderBuilder Using `Jwts.builder().header()` shown above is the preferred way to modify a header when using the `JwtBuilder`. -However, if you would like to create a 'standalone' `Header` outside of the context of using the `JwtBuilder`, you +However, if you would like to create a 'standalone' `Header` outside of the context of using the `JwtBuilder`, you can use `Jwts.header()` instead to return an independent `Header` builder. For example: -```java -Header header = Jwts.header() +[,java] +---- +Header header = Jwts.header() .keyId("aKeyId") .x509Url(aUri) .add("someName", anyValue) .add(mapValues) // ... etc ... - + .build() // <---- not 'and()' -``` +---- There are only two differences between `Jwts.header()` and `Jwts.builder().header()`: -1. `Jwts.header()` builds a 'detached' `Header` that is not associated with any particular JWT, whereas - `Jwts.builder().header()` always modifies the header of the immediate JWT being constructed by its parent - `JwtBuilder`. - - -2. `Jwts.header()` has a `build()` method to produce an explicit `Header` instance and - `Jwts.builder().header()` does not (it has an `and()` method instead) because its parent `JwtBuilder` will implicitly - create the header instance when necessary. +. `Jwts.header()` builds a 'detached' `Header` that is not associated with any particular JWT, whereas +`Jwts.builder().header()` always modifies the header of the immediate JWT being constructed by its parent +`JwtBuilder`. +. `Jwts.header()` has a `build()` method to produce an explicit `Header` instance and +`Jwts.builder().header()` does not (it has an `and()` method instead) because its parent `JwtBuilder` will implicitly +create the header instance when necessary. A standalone header might be useful if you want to aggregate common header parameters in a single 'template' -instance so you don't have to repeat them for each `JwtBuilder` usage. Then this 'template' `Header` can be used to +instance so you don't have to repeat them for each `JwtBuilder` usage. Then this 'template' `Header` can be used to populate `JwtBuilder` usages by just appending it to the `JwtBuilder` header, for example: -```java +[,java] +---- // perhaps somewhere in application configuration: Header commonHeaders = Jwts.header() .issuer("My Company") @@ -915,12 +975,13 @@ String jwt = Jwts.builder() .subject("whatever") // ... etc ... .compact(); -``` +---- - -### JWT Payload +++++++++++++ -A JWT `payload` can be anything at all - anything that can be represented as a byte array, such as text, images, +=== JWT Payload + +A JWT `payload` can be anything at all - anything that can be represented as a byte array, such as text, images, documents, and more. But since a JWT `header` is always JSON, it makes sense that the `payload` could also be JSON, especially for representing identity claims. @@ -931,30 +992,33 @@ As a result, the `JwtBuilder` supports two distinct payload options: Either option may be used, but not both. Using both will cause `compact()` to throw an exception. - -#### Arbitrary Content +++++++++++++ + +==== Arbitrary Content You can set the JWT payload to be any arbitrary byte array content by using the `JwtBuilder` `content` method. For example: -```java +[,java] +---- byte[] content = "Hello World".getBytes(StandardCharsets.UTF_8); String jwt = Jwts.builder() .content(content, "text/plain") // <--- - + // ... etc ... - + .build(); -``` +---- Notice this particular example of `content` uses the two-argument convenience variant: -1. The first argument is the actual byte content to set as the JWT payload -2. The second argument is a String identifier of an IANA Media Type. + +. The first argument is the actual byte content to set as the JWT payload +. The second argument is a String identifier of an IANA Media Type. The second argument will cause the `JwtBuilder` to automatically set the `cty` (Content Type) header according to the -JWT specification's [recommended compact format](https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.10). +JWT specification's https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.10[recommended compact format]. This two-argument variant is typically recommended over the single-argument `content(byte[])` method because it guarantees the JWT recipient can inspect the `cty` header to determine how to convert the `payload` byte array into @@ -964,200 +1028,226 @@ Without setting the `cty` header, the JWT recipient _must_ know via out-of-band the byte array, which is usually less convenient and always requires code changes if the content format ever changes. For these reasons, it is strongly recommended to use the two-argument `content` method variant. - -#### JWT Claims +++++++++++++++++++++++++ +// legacy anchors for old links -Instead of a content byte array, a JWT payload may contain assertions or claims for a JWT recipient. In -this case, the payload is a `Claims` JSON `Object`, and JJWT supports claims creation with type-safe +==== JWT Claims + +Instead of a content byte array, a JWT payload may contain assertions or claims for a JWT recipient. In +this case, the payload is a `Claims` JSON `Object`, and JJWT supports claims creation with type-safe builder methods. - -##### Standard Claims +++++++++++++++++++++++++ +// legacy anchors for old links + +===== Standard Claims The `JwtBuilder` provides convenient builder methods for standard registered Claim names defined in the JWT specification. They are: -* `issuer`: sets the [`iss` (Issuer) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.1) -* `subject`: sets the [`sub` (Subject) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.2) -* `audience`: sets the [`aud` (Audience) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.3) -* `expiration`: sets the [`exp` (Expiration Time) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.4) -* `notBefore`: sets the [`nbf` (Not Before) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.5) -* `issuedAt`: sets the [`iat` (Issued At) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.6) -* `id`: sets the [`jti` (JWT ID) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.7) +* `issuer`: sets the https://tools.ietf.org/html/rfc7519#section-4.1.1[`iss` (Issuer) Claim] +* `subject`: sets the https://tools.ietf.org/html/rfc7519#section-4.1.2[`sub` (Subject) Claim] +* `audience`: sets the https://tools.ietf.org/html/rfc7519#section-4.1.3[`aud` (Audience) Claim] +* `expiration`: sets the https://tools.ietf.org/html/rfc7519#section-4.1.4[`exp` (Expiration Time) Claim] +* `notBefore`: sets the https://tools.ietf.org/html/rfc7519#section-4.1.5[`nbf` (Not Before) Claim] +* `issuedAt`: sets the https://tools.ietf.org/html/rfc7519#section-4.1.6[`iat` (Issued At) Claim] +* `id`: sets the https://tools.ietf.org/html/rfc7519#section-4.1.7[`jti` (JWT ID) Claim] For example: -```java +[,java] +---- String jws = Jwts.builder() .issuer("me") .subject("Bob") - .audience().add("you").and() + .audience().add("you").and() .expiration(expiration) //a java.util.Date - .notBefore(notBefore) //a java.util.Date + .notBefore(notBefore) //a java.util.Date .issuedAt(new Date()) // for example, now .id(UUID.randomUUID().toString()) //just an example id - - /// ... etc ... -``` - -##### Custom Claims + /// ... etc ... +---- + +++++++++++++++++++++++++ +// legacy anchors for old links + +===== Custom Claims If you need to set one or more custom claims that don't match the standard setter method claims shown above, you can simply call the `JwtBuilder` `claim` method one or more times as needed: -```java +[,java] +---- String jws = Jwts.builder() .claim("hello", "world") - + // ... etc ... -``` +---- Each time `claim` is called, it simply appends the key-value pair to an internal `Claims` builder, potentially overwriting any existing identically-named key/value pair. -Obviously, you do not need to call `claim` for any [standard claim name](#jws-create-claims-standard), and it is +Obviously, you do not need to call `claim` for any <>, and it is recommended instead to call the standard respective type-safe named builder method as this enhances readability. - - - -##### Claims Map +++++++++++++ +// legacy anchors for old links +++++++++++++ +++++++++++++++++++++++++ +// legacy anchors for old links + +===== Claims Map If you want to add multiple claims at once, you can use `JwtBuilder` `claims(Map)` method: -```java +[,java] +---- Map claims = getMyClaimsMap(); //implement me String jws = Jwts.builder() .claims(claims) - + // ... etc ... -``` +---- - -### JWT Compression +++++++++++++++++++++++++ +// legacy anchors for old links -If your JWT payload is large (contains a lot of data), you might want to compress the JWT to reduce its size. Note -that this is *not* a standard feature for all JWTs - only JWEs - and is not likely to be supported by other JWT +=== JWT Compression + +If your JWT payload is large (contains a lot of data), you might want to compress the JWT to reduce its size. Note +that this is _not_ a standard feature for all JWTs - only JWEs - and is not likely to be supported by other JWT libraries for non-JWE tokens. JJWT supports compression for both JWSs and JWEs, however. -Please see the main [Compression](#compression) section to see how to compress and decompress JWTs. +Please see the main <> section to see how to compress and decompress JWTs. - -## Reading a JWT +++++++++++++ + +== Reading a JWT You read (parse) a JWT as follows: -1. Use the `Jwts.parser()` method to create a `JwtParserBuilder` instance. -2. Optionally call `keyLocator`, `verifyWith` or `decryptWith` methods if you expect to parse [signed](#jws) or [encrypted](#jwe) JWTs. -3. Call the `build()` method on the `JwtParserBuilder` to create and return a thread-safe `JwtParser`. -4. Call one of the various `parse*` methods with your compact JWT string, depending on the type of JWT you expect. -5. Wrap the `parse*` call in a try/catch block in case parsing, signature verification, or decryption fails. +. Use the `Jwts.parser()` method to create a `JwtParserBuilder` instance. +. Optionally call `keyLocator`, `verifyWith` or `decryptWith` methods if you expect to parse <> or <> JWTs. +. Call the `build()` method on the `JwtParserBuilder` to create and return a thread-safe `JwtParser`. +. Call one of the various `parse*` methods with your compact JWT string, depending on the type of JWT you expect. +. Wrap the `parse*` call in a try/catch block in case parsing, signature verification, or decryption fails. For example: -```java +[,java] +---- Jwt jwt; try { jwt = Jwts.parser() // (1) - - .keyLocator(keyLocator) // (2) dynamically locate signing or encryption keys + + .keyLocator(keyLocator) // (2) dynamically locate signing or encryption keys //.verifyWith(key) // or a constant key used to verify all signed JWTs //.decryptWith(key) // or a constant key used to decrypt all encrypted JWTs - + .build() // (3) - + .parse(compact); // (4) or parseSignedClaims, parseEncryptedClaims, parseSignedContent, etc - + // we can safely trust the JWT - + catch (JwtException ex) { // (5) - + // we *cannot* use the JWT as intended by its creator } -``` +---- -> **Note** -> -> **Type-safe JWTs:** If you are certain your parser will only ever encounter a specific kind of JWT (for example, you only -> ever use signed JWTs with `Claims` payloads, or encrypted JWTs with `byte[]` content payloads, etc), you can call the -> associated type-safe `parseSignedClaims`, `parseEncryptedClaims`, (etc) method variant instead of the generic `parse` method. -> -> These `parse*` methods will return the type-safe JWT you are expecting, for example, a `Jws` or `Jwe` -> instead of a generic `Jwt` instance. +[NOTE] +==== +*Type-safe JWTs:* If you are certain your parser will only ever encounter a specific kind of JWT (for example, you only +ever use signed JWTs with `Claims` payloads, or encrypted JWTs with `byte[]` content payloads, etc), you can call the +associated type-safe `parseSignedClaims`, `parseEncryptedClaims`, (etc) method variant instead of the generic `parse` method. - -### Constant Parsing Key +These `parse*` methods will return the type-safe JWT you are expecting, for example, a `Jws` or `Jwe` +instead of a generic `Jwt` instance. +==== -If the JWT parsed is a JWS or JWE, a key will be necessary to verify the signature or decrypt it. If a JWS and -signature verification fails, or if a JWE and decryption fails, the JWT cannot be safely trusted and should be -discarded. +++++++++++++ + +=== Constant Parsing Key + +If the JWT parsed is a JWS or JWE, a key will be necessary to verify the signature or decrypt it. If a JWS and +signature verification fails, or if a JWE and decryption fails, the JWT cannot be safely trusted and should be +discarded. So which key do we use? -* If parsing a JWS and the JWS was signed with a `SecretKey`, the same `SecretKey` should be specified on the - `JwtParserBuilder`. For example: +* If parsing a JWS and the JWS was signed with a `SecretKey`, the same `SecretKey` should be specified on the +`JwtParserBuilder`. For example: ++ +[,java] +---- +Jwts.parser() - ```java - Jwts.parser() - - .verifyWith(secretKey) // <---- - - .build() - .parseSignedClaims(jwsString); - ``` -* If parsing a JWS and the JWS was signed with a `PrivateKey`, that key's corresponding `PublicKey` (not the - `PrivateKey`) should be specified on the `JwtParserBuilder`. For example: + .verifyWith(secretKey) // <---- - ```java - Jwts.parser() - - .verifyWith(publicKey) // <---- publicKey, not privateKey - - .build() - .parseSignedClaims(jwsString); - ``` -* If parsing a JWE and the JWE was encrypted with direct encryption using a `SecretKey`, the same `SecretKey` should be - specified on the `JwtParserBuilder`. For example: + .build() + .parseSignedClaims(jwsString); +---- - ```java - Jwts.parser() - - .decryptWith(secretKey) // <---- or a Password from Keys.password(charArray) - - .build() - .parseEncryptedClaims(jweString); - ``` -* If parsing a JWE and the JWE was encrypted with a key algorithm using with a `PublicKey`, that key's corresponding - `PrivateKey` (not the `PublicKey`) should be specified on the `JwtParserBuilder`. For example: +* If parsing a JWS and the JWS was signed with a `PrivateKey`, that key's corresponding `PublicKey` (not the +`PrivateKey`) should be specified on the `JwtParserBuilder`. For example: ++ +[,java] +---- +Jwts.parser() - ```java - Jwts.parser() - - .decryptWith(privateKey) // <---- privateKey, not publicKey - - .build() - .parseEncryptedClaims(jweString); - ``` - -#### Multiple Keys? + .verifyWith(publicKey) // <---- publicKey, not privateKey + + .build() + .parseSignedClaims(jwsString); +---- + +* If parsing a JWE and the JWE was encrypted with direct encryption using a `SecretKey`, the same `SecretKey` should be +specified on the `JwtParserBuilder`. For example: ++ +[,java] +---- +Jwts.parser() + + .decryptWith(secretKey) // <---- or a Password from Keys.password(charArray) + + .build() + .parseEncryptedClaims(jweString); +---- + +* If parsing a JWE and the JWE was encrypted with a key algorithm using with a `PublicKey`, that key's corresponding +`PrivateKey` (not the `PublicKey`) should be specified on the `JwtParserBuilder`. For example: ++ +[,java] +---- +Jwts.parser() + + .decryptWith(privateKey) // <---- privateKey, not publicKey + + .build() + .parseEncryptedClaims(jweString); +---- + +==== Multiple Keys? But you might have noticed something - what if your application doesn't use just a single `SecretKey` or `KeyPair`? What -if JWSs and JWEs can be created with different `SecretKey`s or public/private keys, or a combination of both? How do +if JWSs and JWEs can be created with different ``SecretKey``s or public/private keys, or a combination of both? How do you know which key to specify if you don't inspect the JWT first? -In these cases, you can't call the `JwtParserBuilder`'s `verifyWith` or `decryptWith` methods with a single key - +In these cases, you can't call the ``JwtParserBuilder``'s `verifyWith` or `decryptWith` methods with a single key - instead, you'll need to configure a parsing Key Locator, discussed next. - -### Dynamic Key Lookup +++++++++++++ + +=== Dynamic Key Lookup It is common in many applications to receive JWTs that can be encrypted or signed by different cryptographic keys. For example, maybe a JWT created to assert a specific user identity uses a Key specific to that exact user. Or perhaps JWTs @@ -1165,40 +1255,43 @@ specific to a particular customer all use that customer's Key. Or maybe your ap encrypted with a key specific to your application for your own use (e.g. a user session token). In all of these and similar scenarios, you won't know which key was used to sign or encrypt a JWT until the JWT is -received, at parse time, so you can't 'hard code' any verification or decryption key using the `JwtParserBuilder`'s +received, at parse time, so you can't 'hard code' any verification or decryption key using the ``JwtParserBuilder``'s `verifyWith` or `decryptWith` methods. Those are only to be used when the same key is used to verify or decrypt -*all* JWSs or JWEs, which won't work for dynamically signed or encrypted JWTs. +_all_ JWSs or JWEs, which won't work for dynamically signed or encrypted JWTs. - -#### Key Locator +++++++++++++ + +==== Key Locator If you need to support dynamic key lookup when encountering JWTs, you'll need to implement the `Locator` interface and specify an instance on the `JwtParserBuilder` via the `keyLocator` method. For example: -```java +[,java] +---- Locator keyLocator = getMyKeyLocator(); Jwts.parser() .keyLocator(keyLocator) // <---- - + .build() // ... etc ... -``` +---- A `Locator` is used to lookup _both_ JWS signature verification keys _and_ JWE decryption keys. You need to determine which key to return based on information in the JWT `header`, for example: -```java +[,java] +---- public class MyKeyLocator extends LocatorAdapter { - + @Override public Key locate(ProtectedHeader header) { // a JwsHeader or JweHeader // implement me } } -``` +---- The `JwtParser` will invoke the `locate` method after parsing the JWT `header`, but _before parsing the `payload`, or verifying any JWS signature or decrypting any JWE ciphertext_. This allows you to inspect the `header` argument @@ -1206,38 +1299,41 @@ for any information that can help you look up the `Key` to use for verifying _th powerful for applications with more complex security models that might use different keys at different times or for different users or customers. - -#### Key Locator Strategy +++++++++++++ + +==== Key Locator Strategy What data might you inspect to determine how to lookup a signature verification or decryption key? The JWT specifications' preferred approach is to set a `kid` (Key ID) header value when the JWT is being created, for example: -```java +[,java] +---- Key key = getSigningKey(); // or getEncryptionKey() for JWE String keyId = getKeyId(key); //any mechanism you have to associate a key with an ID is fine String jws = Jwts.builder() - + .header().keyId(keyId).and() // <--- add `kid` header - + .signWith(key) // for JWS //.encryptWith(key, keyAlg, encryptionAlg) // for JWE .compact(); -``` +---- Then during parsing, your `Locator` implementation can inspect the `header` to get the `kid` value and then use it to look up the verification or decryption key from somewhere, like a database, keystore or Hardware Security Module (HSM). For example: -```java +[,java] +---- public class MyKeyLocator extends LocatorAdapter { - + @Override public Key locate(ProtectedHeader header) { // both JwsHeader and JweHeader extend ProtectedHeader - + //inspect the header, lookup and return the verification key String keyId = header.getKeyId(); //or any other parameter that you need to inspect @@ -1246,97 +1342,101 @@ public class MyKeyLocator extends LocatorAdapter { return key; } } -``` +---- -Note that inspecting the `header.getKeyId()` is just the most common approach to look up a key - you could inspect any -number of header parameters to determine how to lookup the verification or decryption key. It is all based on how +Note that inspecting the `header.getKeyId()` is just the most common approach to look up a key - you could inspect any +number of header parameters to determine how to lookup the verification or decryption key. It is all based on how the JWT was created. If you extend `LocatorAdapter` as shown above, but for some reason have different lookup strategies for signature verification keys versus decryption keys, you can forego overriding the `locate(ProtectedHeader)` method in favor of two respective `locate(JwsHeader)` and `locate(JweHeader)` methods: -```java +[,java] +---- public class MyKeyLocator extends LocatorAdapter { - + @Override public Key locate(JwsHeader header) { String keyId = header.getKeyId(); //or any other parameter that you need to inspect return lookupSignatureVerificationKey(keyId); //implement me } - + @Override public Key locate(JweHeader header) { String keyId = header.getKeyId(); //or any other parameter// that you need to inspect return lookupDecryptionKey(keyId); //implement me } } -``` -> **Note** -> -> **Simpler Lookup**: If possible, try to keep the key lookup strategy the same between JWSs and JWEs (i.e. using -> only `locate(ProtectedHeader)`), preferably using only -> the `kid` (Key ID) header value or perhaps a public key thumbprint. You will find the implementation is much -> simpler and easier to maintain over time, and also creates smaller headers for compact transmission. +---- - -#### Key Locator Return Values +[NOTE] +==== +*Simpler Lookup*: If possible, try to keep the key lookup strategy the same between JWSs and JWEs (i.e. using +only `locate(ProtectedHeader)`), preferably using only +the `kid` (Key ID) header value or perhaps a public key thumbprint. You will find the implementation is much +simpler and easier to maintain over time, and also creates smaller headers for compact transmission. +==== + +++++++++++++ + +==== Key Locator Return Values Regardless of which implementation strategy you choose, remember to return the appropriate type of key depending on the type of JWS or JWE algorithm used. That is: * For JWS: - * For HMAC-based signature algorithms, the returned verification key should be a `SecretKey`, and, - * For asymmetric signature algorithms, the returned verification key should be a `PublicKey` (not a `PrivateKey`). + ** For HMAC-based signature algorithms, the returned verification key should be a `SecretKey`, and, + ** For asymmetric signature algorithms, the returned verification key should be a `PublicKey` (not a `PrivateKey`). * For JWE: - * For JWE direct encryption, the returned decryption key should be a `SecretKey`. - * For password-based key derivation algorithms, the returned decryption key should be a - `io.jsonwebtoken.security.Password`. You can create a `Password` instance by calling - `Keys.password(char[] passwordCharacters)`. - * For asymmetric key management algorithms, the returned decryption key should be a `PrivateKey` (not a `PublicKey`). + ** For JWE direct encryption, the returned decryption key should be a `SecretKey`. + ** For password-based key derivation algorithms, the returned decryption key should be a +`io.jsonwebtoken.security.Password`. You can create a `Password` instance by calling +`Keys.password(char[] passwordCharacters)`. + ** For asymmetric key management algorithms, the returned decryption key should be a `PrivateKey` (not a `PublicKey`). - -#### Provider-constrained Keys +++++++++++++ + +==== Provider-constrained Keys If any verification or decryption key returned from a Key `Locator` must be used with a specific security `Provider` (such as for PKCS11 or Hardware Security Module (HSM) keys), you must make that `Provider` available for JWT parsing in one of 3 ways, listed in order of recommendation and simplicity: -1. [Configure the Provider in the JVM](https://docs.oracle.com/en/java/javase/17/security/howtoimplaprovider.html#GUID-831AA25F-F702-442D-A2E4-8DA6DEA16F33), - either by modifying the `java.security` file or by registering the `Provider` dynamically via - [Security.addProvider(Provider)](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/security/Security.html#addProvider(java.security.Provider)). - This is the recommended approach so you do not need to modify code anywhere that may need to parse JWTs. +. https://docs.oracle.com/en/java/javase/17/security/howtoimplaprovider.html#GUID-831AA25F-F702-442D-A2E4-8DA6DEA16F33[Configure the Provider in the JVM], +either by modifying the `java.security` file or by registering the `Provider` dynamically via +https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/security/Security.html#addProvider(java.security.Provider)[Security.addProvider(Provider)]. +This is the recommended approach so you do not need to modify code anywhere that may need to parse JWTs. +. Set the `Provider` as the parser default by calling `JwtParserBuilder#provider(Provider)`. This will +ensure the provider is used by default with _all_ located keys unless overridden by a key-specific Provider. This +is only recommended when you are confident that all JWTs encountered by the parser instance will use keys +attributed to the same `Provider`, unless overridden by a specific key. +. Associate the `Provider` with a specific key using `Keys.builder` so it is used for that key only. This option is +useful if some located keys require a specific provider, while other located keys can assume a default provider. For +example: ++ +[,java] +---- +public Key locate(Header header) { + PrivateKey /* or SecretKey */ key = findKey(header); // implement me -2. Set the `Provider` as the parser default by calling `JwtParserBuilder#provider(Provider)`. This will - ensure the provider is used by default with _all_ located keys unless overridden by a key-specific Provider. This - is only recommended when you are confident that all JWTs encountered by the parser instance will use keys - attributed to the same `Provider`, unless overridden by a specific key. + Provider keySpecificProvider = findKeyProvider(key); // implement me + if (keySpecificProvider != null) { + // Ensure the key-specific provider (e.g. for PKCS11 or HSM) will be used + // during decryption with the KeyAlgorithm in the JWE 'alg' header + return Keys.builder(key).provider(keySpecificProvider).build(); + } + // otherwise default provider is fine: + return key; +} +---- -3. Associate the `Provider` with a specific key using `Keys.builder` so it is used for that key only. This option is - useful if some located keys require a specific provider, while other located keys can assume a default provider. For - example: - - ```java - public Key locate(Header header) { - - PrivateKey /* or SecretKey */ key = findKey(header); // implement me - - Provider keySpecificProvider = findKeyProvider(key); // implement me - if (keySpecificProvider != null) { - // Ensure the key-specific provider (e.g. for PKCS11 or HSM) will be used - // during decryption with the KeyAlgorithm in the JWE 'alg' header - return Keys.builder(key).provider(keySpecificProvider).build(); - } - - // otherwise default provider is fine: - return key; - } - ``` +++++++++++++++++++++++++ +// legacy anchor for old links - -### Claim Assertions +=== Claim Assertions You can enforce that the JWT you are parsing conforms to expectations that you require and are important for your application. @@ -1345,18 +1445,20 @@ For example, let's say that you require that the JWT you are parsing has a speci otherwise you may not trust the token. You can do that by using one of the various `require`* methods on the `JwtParserBuilder`: -```java +[,java] +---- try { Jwts.parser().requireSubject("jsmith")/* etc... */.build().parse(s); } catch (InvalidClaimException ice) { // the sub claim was missing or did not have a 'jsmith' value } -``` +---- If it is important to react to a missing vs an incorrect value, instead of catching `InvalidClaimException`, you can catch either `MissingClaimException` or `IncorrectClaimException`: -```java +[,java] +---- try { Jwts.parser().requireSubject("jsmith")/* etc... */.build().parse(s); } catch(MissingClaimException mce) { @@ -1364,122 +1466,162 @@ try { } catch(IncorrectClaimException ice) { // the parsed JWT had a sub claim, but its value was not equal to 'jsmith' } -``` +---- You can also require custom claims by using the `require(claimName, requiredValue)` method - for example: -```java +[,java] +---- try { Jwts.parser().require("myClaim", "myRequiredValue")/* etc... */.build().parse(s); } catch(InvalidClaimException ice) { // the 'myClaim' claim was missing or did not have a 'myRequiredValue' value } -``` +---- + (or, again, you could catch either `MissingClaimException` or `IncorrectClaimException` instead). -Please see the `JwtParserBuilder` class and/or JavaDoc for a full list of the various `require`* methods you may use +Please see the `JwtParserBuilder` class and/or JavaDoc for a full list of the various `require`* methods you may use for claims assertions. - -### Accounting for Clock Skew +++++++++++++++++++++++++ +// legacy anchor for old links + +=== Accounting for Clock Skew When parsing a JWT, you might find that `exp` or `nbf` claim assertions fail (throw exceptions) because the clock on the parsing machine is not perfectly in sync with the clock on the machine that created the JWT. This can cause obvious problems since `exp` and `nbf` are time-based assertions, and clock times need to be reliably in sync for shared assertions. -You can account for these differences (usually no more than a few minutes) when parsing using the `JwtParserBuilder`'s +You can account for these differences (usually no more than a few minutes) when parsing using the ``JwtParserBuilder``'s `clockSkewSeconds`. For example: -```java +[,java] +---- long seconds = 3 * 60; //3 minutes Jwts.parser() - + .clockSkewSeconds(seconds) // <---- - + // ... etc ... .build() .parse(jwt); -``` +---- + This ensures that minor clock differences between the machines can be ignored. Two or three minutes should be more than enough; it would be fairly strange if a production machine's clock was more than 5 minutes difference from most atomic clocks around the world. - -#### Custom Clock Support +++++++++++++++++++++++++ +// legacy anchor for old links + +==== Custom Clock Support If the above `clockSkewSeconds` isn't sufficient for your needs, the timestamps created -during parsing for timestamp comparisons can be obtained via a custom time source. Call the `JwtParserBuilder`'s +during parsing for timestamp comparisons can be obtained via a custom time source. Call the ``JwtParserBuilder``'s `clock` method with an implementation of the `io.jsonwebtoken.Clock` interface. For example: - ```java +[,java] +---- Clock clock = new MyClock(); Jwts.parser().clock(myClock) //... etc ... -``` +---- -The `JwtParser`'s default `Clock` implementation simply returns `new Date()` to reflect the time when parsing occurs, +The ``JwtParser``'s default `Clock` implementation simply returns `new Date()` to reflect the time when parsing occurs, as most would expect. However, supplying your own clock could be useful, especially when writing test cases to guarantee deterministic behavior. - -### JWT Decompression +++++++++++++ + +=== JWT Decompression If you used JJWT to compress a JWT and you used a custom compression algorithm, you will need to tell the `JwtParserBuilder` how to resolve your `CompressionAlgorithm` to decompress the JWT. -Please see the [Compression](#compression) section below to see how to decompress JWTs during parsing. +Please see the <> section below to see how to decompress JWTs during parsing. - -## Signed JWTs +++++++++++++ -The JWT specification provides for the ability to -[cryptographically _sign_](https://en.wikipedia.org/wiki/Digital_signature) a JWT. Signing a JWT: - -1. guarantees the JWT was created by someone we know (it is authentic) as well as -2. guarantees that no-one has manipulated or changed the JWT after it was created (its integrity is maintained). +== Signed JWTs -These two properties - authenticity and integrity - assure us that a JWT contains information we can trust. If a +The JWT specification provides for the ability to +https://en.wikipedia.org/wiki/Digital_signature[cryptographically _sign_] a JWT. Signing a JWT: + +. guarantees the JWT was created by someone we know (it is authentic) as well as +. guarantees that no-one has manipulated or changed the JWT after it was created (its integrity is maintained). + +These two properties - authenticity and integrity - assure us that a JWT contains information we can trust. If a JWT fails authenticity or integrity checks, we should always reject that JWT because we can't trust it. -But before we dig in to showing you how to create a JWS using JJWT, let's briefly discuss Signature Algorithms and -Keys, specifically as they relate to the JWT specifications. Understanding them is critical to being able to create a +But before we dig in to showing you how to create a JWS using JJWT, let's briefly discuss Signature Algorithms and +Keys, specifically as they relate to the JWT specifications. Understanding them is critical to being able to create a JWS properly. - -### Standard Signature Algorithms +++++++++++++ + +=== Standard Signature Algorithms The JWT specifications identify 13 standard signature algorithms - 3 secret key algorithms and 10 asymmetric key algorithms: -| Identifier | Signature Algorithm | -| --- | --- | -| `HS256` | HMAC using SHA-256 | -| `HS384` | HMAC using SHA-384 | -| `HS512` | HMAC using SHA-512 | -| `ES256` | ECDSA using P-256 and SHA-256 | -| `ES384` | ECDSA using P-384 and SHA-384 | -| `ES512` | ECDSA using P-521 and SHA-512 | -| `RS256` | RSASSA-PKCS-v1_5 using SHA-256 | -| `RS384` | RSASSA-PKCS-v1_5 using SHA-384 | -| `RS512` | RSASSA-PKCS-v1_5 using SHA-512 | -| `PS256` | RSASSA-PSS using SHA-256 and MGF1 with SHA-2561 | -| `PS384` | RSASSA-PSS using SHA-384 and MGF1 with SHA-3841 | -| `PS512` | RSASSA-PSS using SHA-512 and MGF1 with SHA-5121 | -| `EdDSA` | Edwards-Curve Digital Signature Algorithm (EdDSA)2 | +|=== +| Identifier | Signature Algorithm -1. Requires Java 11 or a compatible JCA Provider (like BouncyCastle) in the runtime classpath. +| `HS256` +| HMAC using SHA-256 -2. Requires Java 15 or a compatible JCA Provider (like BouncyCastle) in the runtime classpath. +| `HS384` +| HMAC using SHA-384 + +| `HS512` +| HMAC using SHA-512 + +| `ES256` +| ECDSA using P-256 and SHA-256 + +| `ES384` +| ECDSA using P-384 and SHA-384 + +| `ES512` +| ECDSA using P-521 and SHA-512 + +| `RS256` +| RSASSA-PKCS-v1_5 using SHA-256 + +| `RS384` +| RSASSA-PKCS-v1_5 using SHA-384 + +| `RS512` +| RSASSA-PKCS-v1_5 using SHA-512 + +| `PS256` +| RSASSA-PSS using SHA-256 and MGF1 with SHA-256^*1*^ + +| `PS384` +| RSASSA-PSS using SHA-384 and MGF1 with SHA-384^*1*^ + +| `PS512` +| RSASSA-PSS using SHA-512 and MGF1 with SHA-512^*1*^ + +| `EdDSA` +| Edwards-Curve Digital Signature Algorithm (EdDSA)^*2*^ +|=== + +^*1*.{sp}{fn-require-java15-plus}^ + +^*2*.{sp}{fn-require-java15-plus}^ These are all represented as constants in the `io.jsonwebtoken.Jwts.SIG` registry class. - -### Signature Algorithms Keys +++++++++++++ -What's really important about the above standard signature algorithms - other than their security properties - is that -the JWT specification [RFC 7518, Sections 3.2 through 3.5](https://tools.ietf.org/html/rfc7518#section-3) +=== Signature Algorithms Keys + +What's really important about the above standard signature algorithms - other than their security properties - is that +the JWT specification https://tools.ietf.org/html/rfc7518#section-3[RFC 7518, Sections 3.2 through 3.5] _requires_ (mandates) that you MUST use keys that are sufficiently strong for a chosen algorithm. This means that JJWT - a specification-compliant library - will also enforce that you use sufficiently strong keys @@ -1493,31 +1635,31 @@ one wants completely insecure JWTs, right? Right! So what are the key strength requirements? - -#### HMAC-SHA +++++++++++++ + +==== HMAC-SHA JWT HMAC-SHA signature algorithms `HS256`, `HS384`, and `HS512` require a secret key that is _at least_ as many bits as -the algorithm's signature (digest) length per [RFC 7512 Section 3.2](https://tools.ietf.org/html/rfc7518#section-3.2). +the algorithm's signature (digest) length per https://tools.ietf.org/html/rfc7518#section-3.2[RFC 7512 Section 3.2]. This means: * `HS256` is HMAC-SHA-256, and that produces digests that are 256 bits (32 bytes) long, so `HS256` _requires_ that you - use a secret key that is at least 32 bytes long. - +use a secret key that is at least 32 bytes long. * `HS384` is HMAC-SHA-384, and that produces digests that are 384 bits (48 bytes) long, so `HS384` _requires_ that you - use a secret key that is at least 48 bytes long. - +use a secret key that is at least 48 bytes long. * `HS512` is HMAC-SHA-512, and that produces digests that are 512 bits (64 bytes) long, so `HS512` _requires_ that you - use a secret key that is at least 64 bytes long. - - -#### RSA +use a secret key that is at least 64 bytes long. + +++++++++++++ + +==== RSA JWT RSA signature algorithms `RS256`, `RS384`, `RS512`, `PS256`, `PS384` and `PS512` all require a minimum key length -(aka an RSA modulus bit length) of `2048` bits per RFC 7512 Sections -[3.3](https://tools.ietf.org/html/rfc7518#section-3.3) and [3.5](https://tools.ietf.org/html/rfc7518#section-3.5). +(aka an RSA modulus bit length) of `2048` bits per RFC 7512 Sections +https://tools.ietf.org/html/rfc7518#section-3.3[3.3] and https://tools.ietf.org/html/rfc7518#section-3.5[3.5]. Anything smaller than this (such as 1024 bits) will be rejected with an `WeakKeyException`. -That said, in keeping with best practices and increasing key lengths for security longevity, JJWT +That said, in keeping with best practices and increasing key lengths for security longevity, JJWT recommends that you use: * at least 2048 bit keys with `RS256` and `PS256` @@ -1527,180 +1669,204 @@ recommends that you use: These are only JJWT suggestions and not requirements. JJWT only enforces JWT specification requirements and for any RSA key, the requirement is the RSA key (modulus) length in bits MUST be >= 2048 bits. - -#### Elliptic Curve +++++++++++++ + +==== Elliptic Curve JWT Elliptic Curve signature algorithms `ES256`, `ES384`, and `ES512` all require a key length -(aka an Elliptic Curve order bit length) equal to the algorithm signature's individual -`R` and `S` components per [RFC 7512 Section 3.4](https://tools.ietf.org/html/rfc7518#section-3.4). This means: +(aka an Elliptic Curve order bit length) equal to the algorithm signature's individual +`R` and `S` components per https://tools.ietf.org/html/rfc7518#section-3.4[RFC 7512 Section 3.4]. This means: * `ES256` requires that you use a private key that is exactly 256 bits (32 bytes) long. - * `ES384` requires that you use a private key that is exactly 384 bits (48 bytes) long. - * `ES512` requires that you use a private key that is exactly 521 bits (65 or 66 bytes) long (depending on format). - -#### Edwards Curve +++++++++++++ -The JWT Edwards Curve signature algorithm `EdDSA` supports two sizes of private and public `EdECKey`s (these types +==== Edwards Curve + +The JWT Edwards Curve signature algorithm `EdDSA` supports two sizes of private and public ``EdECKey``s (these types were introduced in Java 15): * `Ed25519` algorithm keys must be 256 bits (32 bytes) long and produce signatures 512 bits (64 bytes) long. - * `Ed448` algorithm keys must be 456 bits (57 bytes) long and produce signatures 912 bits (114 bytes) long. - -#### Creating Safe Keys +++++++++++++ + +==== Creating Safe Keys If you don't want to think about bit length requirements or just want to make your life easier, JJWT has provided convenient builder classes that can generate sufficiently secure keys for any given JWT signature algorithm you might want to use. - -##### Secret Keys +++++++++++++ -If you want to generate a sufficiently strong `SecretKey` for use with the JWT HMAC-SHA algorithms, use the respective +===== Secret Keys + +If you want to generate a sufficiently strong `SecretKey` for use with the JWT HMAC-SHA algorithms, use the respective algorithm's `key()` builder method: -```java +[,java] +---- SecretKey key = Jwts.SIG.HS256.key().build(); //or HS384.key() or HS512.key() -``` +---- -Under the hood, JJWT uses the JCA default provider's `KeyGenerator` to create a secure-random key with the correct +Under the hood, JJWT uses the JCA default provider's `KeyGenerator` to create a secure-random key with the correct minimum length for the given algorithm. If you want to specify a specific JCA `Provider` or `SecureRandom` to use during key generation, you may specify those as builder arguments. For example: -```java +[,java] +---- SecretKey key = Jwts.SIG.HS256.key().provider(aProvider).random(aSecureRandom).build(); -``` +---- If you need to save this new `SecretKey`, you can Base64 (or Base64URL) encode it: -```java +[,java] +---- String secretString = Encoders.BASE64.encode(key.getEncoded()); -``` +---- -Ensure you save the resulting `secretString` somewhere safe - -[Base64-encoding is not encryption](#base64-not-encryption), so it's still considered sensitive information. You can +Ensure you save the resulting `secretString` somewhere safe - +<>, so it's still considered sensitive information. You can further encrypt it, etc, before saving to disk (for example). - -##### Asymmetric Keys +++++++++++++ + +===== Asymmetric Keys If you want to generate sufficiently strong Elliptic Curve or RSA asymmetric key pairs for use with JWT ECDSA or RSA algorithms, use an algorithm's respective `keyPair()` builder method: -```java +[,java] +---- KeyPair keyPair = Jwts.SIG.RS256.keyPair().build(); //or RS384, RS512, PS256, etc... -``` +---- -Once you've generated a `KeyPair`, you can use the private key (`keyPair.getPrivate()`) to create a JWS and the +Once you've generated a `KeyPair`, you can use the private key (`keyPair.getPrivate()`) to create a JWS and the public key (`keyPair.getPublic()`) to parse/verify a JWS. -> **Note** -> -> * **The `PS256`, `PS384`, and `PS512` algorithms require JDK 11 or a compatible JCA Provider -> (like BouncyCastle) in the runtime classpath.** -> -> -> * **The `EdDSA` algorithms requires JDK 15 or a compatible JCA Provider (like BouncyCastle) in the runtime classpath.** -> -> If you want to use either set of algorithms, and you are on an earlier JDK that does not support them, -> see the [Installation](#Installation) section to see how to enable BouncyCastle. All other algorithms are -> natively supported by the JDK. +[NOTE] +==== +* *The `PS256`, `PS384`, and `PS512` algorithms require JDK 11 or a compatible JCA Provider +(like BouncyCastle) in the runtime classpath.* +* *The `EdDSA` algorithms requires JDK 15 or a compatible JCA Provider (like BouncyCastle) in the runtime classpath.* - -### Creating a JWS +If you want to use either set of algorithms, and you are on an earlier JDK that does not support them, +see the <> section to see how to enable BouncyCastle. All other algorithms are +natively supported by the JDK. +==== + +++++++++++++ + +=== Creating a JWS You create a JWS as follows: -1. Use the `Jwts.builder()` method to create a `JwtBuilder` instance. -2. Call `JwtBuilder` methods to set the `payload` content or claims and any header parameters as desired. -3. Specify the `SecretKey` or asymmetric `PrivateKey` you want to use to sign the JWT. -4. Finally, call the `compact()` method to compact and sign, producing the final jws. +. Use the `Jwts.builder()` method to create a `JwtBuilder` instance. +. Call `JwtBuilder` methods to set the `payload` content or claims and any header parameters as desired. +. Specify the `SecretKey` or asymmetric `PrivateKey` you want to use to sign the JWT. +. Finally, call the `compact()` method to compact and sign, producing the final jws. For example: -```java +[,java] +---- String jws = Jwts.builder() // (1) - .subject("Bob") // (2) + .subject("Bob") // (2) .signWith(key) // (3) <--- - + .compact(); // (4) -``` +---- - -#### Signing Key +++++++++++++ -It is usually recommended to specify the signing key by calling the `JwtBuilder`'s `signWith` method and let JJWT +==== Signing Key + +It is usually recommended to specify the signing key by calling the ``JwtBuilder``'s `signWith` method and let JJWT determine the most secure algorithm allowed for the specified key.: -```java +[,java] +---- String jws = Jwts.builder() // ... etc ... - + .signWith(key) // <--- - + .compact(); -``` +---- For example, if you call `signWith` with a `SecretKey` that is 256 bits (32 bytes) long, it is not strong enough for `HS384` or `HS512`, so JJWT will automatically sign the JWT using `HS256`. -When using `signWith` JJWT will also automatically set the required `alg` header with the associated algorithm +When using `signWith` JJWT will also automatically set the required `alg` header with the associated algorithm identifier. Similarly, if you called `signWith` with an RSA `PrivateKey` that was 4096 bits long, JJWT will use the `RS512` algorithm and automatically set the `alg` header to `RS512`. -The same selection logic applies for Elliptic Curve `PrivateKey`s. +The same selection logic applies for Elliptic Curve ``PrivateKey``s. -> **Note** -> -> **You cannot sign JWTs with `PublicKey`s as this is always insecure.** JJWT will reject any specified -> `PublicKey` for signing with an `InvalidKeyException`. +[NOTE] +==== +*You cannot sign JWTs with ``PublicKey``s as this is always insecure.* JJWT will reject any specified +`PublicKey` for signing with an `InvalidKeyException`. +==== - -##### SecretKey Formats +++++++++++++ -If you want to sign a JWS using HMAC-SHA algorithms, and you have a secret key `String` or -[encoded byte array](https://docs.oracle.com/javase/8/docs/api/java/security/Key.html#getEncoded--), you will need +===== SecretKey Formats + +If you want to sign a JWS using HMAC-SHA algorithms, and you have a secret key `String` or +https://docs.oracle.com/javase/8/docs/api/java/security/Key.html#getEncoded--[encoded byte array], you will need to convert it into a `SecretKey` instance to use as the `signWith` method argument. If your secret key is: -* An [encoded byte array](https://docs.oracle.com/javase/8/docs/api/java/security/Key.html#getEncoded--): - ```java - SecretKey key = Keys.hmacShaKeyFor(encodedKeyBytes); - ``` +* An https://docs.oracle.com/javase/8/docs/api/java/security/Key.html#getEncoded--[encoded byte array]: ++ +[,java] +---- +SecretKey key = Keys.hmacShaKeyFor(encodedKeyBytes); +---- + * A Base64-encoded string: - ```java - SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretString)); - ``` ++ +[,java] +---- +SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretString)); +---- + * A Base64URL-encoded string: - ```java - SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretString)); - ``` ++ +[,java] +---- +SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretString)); +---- + * A raw (non-encoded) string (e.g. a password String): - ```java - Password key = Keys.password(secretString.toCharArray()); - ``` ++ +[,java] +---- +Password key = Keys.password(secretString.toCharArray()); +---- -> **Warning** -> -> It is almost always incorrect to call any variant of `secretString.getBytes` in any cryptographic context. -> Safe cryptographic keys are never represented as direct (unencoded) strings. If you have a password that should -> be represented as a `Key` for `HMAC-SHA` algorithms, it is _strongly_ recommended to use a key derivation -> algorithm to derive a cryptographically-strong `Key` from the password, and never use the password directly. +[WARNING] +==== +It is almost always incorrect to call any variant of `secretString.getBytes` in any cryptographic context. + +Safe cryptographic keys are never represented as direct (unencoded) strings. If you have a password that should +be represented as a `Key` for `HMAC-SHA` algorithms, it is _strongly_ recommended to use a key derivation +algorithm to derive a cryptographically-strong `Key` from the password, and never use the password directly. +==== - -##### SignatureAlgorithm Override +++++++++++++ + +===== SignatureAlgorithm Override In some specific cases, you might want to override JJWT's default selected signature algorithm for a given key. @@ -1708,184 +1874,196 @@ For example, if you have an RSA `PrivateKey` that is 2048 bits, JJWT would autom If you wanted to use `RS384` or `RS512` instead, you could manually specify it with the overloaded `signWith` method that accepts the `SignatureAlgorithm` as an additional argument: -```java +[,java] +---- .signWith(privateKey, Jwts.SIG.RS512) // <--- - - .compact(); -``` + .compact(); +---- This is allowed because the JWT specification allows any RSA algorithm strength for any RSA key >= 2048 bits. JJWT just prefers `RS512` for keys >= 4096 bits, followed by `RS384` for keys >= 3072 bits and finally `RS256` for keys >= 2048 bits. -**In all cases however, regardless of your chosen algorithms, JJWT will assert that the specified key is allowed to be -used for that algorithm when possible according to the JWT specification requirements.** +*In all cases however, regardless of your chosen algorithms, JJWT will assert that the specified key is allowed to be +used for that algorithm when possible according to the JWT specification requirements.* - -#### JWS Compression +++++++++++++ -If your JWT payload is large (contains a lot of data), and you are certain that JJWT will also be the same library -that reads/parses your JWS, you might want to compress the JWS to reduce its size. +==== JWS Compression -> **Warning** -> -> **Not Standard for JWS**: JJWT supports compression for JWS, but it is not a standard feature for JWS. The -> JWT RFC specifications standardize this _only_ for JWEs, and it is not likely to be supported by other JWT libraries -> for JWS. Use JWS compression only if you are certain that JJWT (or another library that supports JWS compression) -> will be parsing the JWS +If your JWT payload is large (contains a lot of data), and you are certain that JJWT will also be the same library +that reads/parses your JWS, you might want to compress the JWS to reduce its size. -Please see the main [Compression](#compression) section to see how to compress and decompress JWTs. +[WARNING] +==== +*Not Standard for JWS*: JJWT supports compression for JWS, but it is not a standard feature for JWS. The +JWT RFC specifications standardize this _only_ for JWEs, and it is not likely to be supported by other JWT libraries +for JWS. Use JWS compression only if you are certain that JJWT (or another library that supports JWS compression) +will be parsing the JWS. +==== - -### Reading a JWS +Please see the main <> section to see how to compress and decompress JWTs. + +++++++++++++ + +=== Reading a JWS You read (parse) a JWS as follows: -1. Use the `Jwts.parser()` method to create a `JwtParserBuilder` instance. -2. Call either [keyLocator](#key-locator) or `verifyWith` methods to determine the key used to verify the JWS signature. -3. Call the `build()` method on the `JwtParserBuilder` to return a thread-safe `JwtParser`. -4. Finally, call the `parseSignedClaims(String)` method with your jws `String`, producing the original JWS. -5. The entire call is wrapped in a try/catch block in case parsing or signature validation fails. We'll cover - exceptions and causes for failure later. +. Use the `Jwts.parser()` method to create a `JwtParserBuilder` instance. +. Call either <> or `verifyWith` methods to determine the key used to verify the JWS signature. +. Call the `build()` method on the `JwtParserBuilder` to return a thread-safe `JwtParser`. +. Finally, call the `parseSignedClaims(String)` method with your jws `String`, producing the original JWS. +. The entire call is wrapped in a try/catch block in case parsing or signature validation fails. We'll cover +exceptions and causes for failure later. For example: -```java +[,java] +---- Jws jws; try { jws = Jwts.parser() // (1) - - .keyLocator(keyLocator) // (2) dynamically lookup verification keys based on each JWS + + .keyLocator(keyLocator) // (2) dynamically lookup verification keys based on each JWS //.verifyWith(key) // or a static key used to verify all encountered JWSs - + .build() // (3) .parseSignedClaims(jwsString); // (4) or parseSignedContent(jwsString) - + // we can safely trust the JWT - + catch (JwtException ex) { // (5) - + // we *cannot* use the JWT as intended by its creator } -``` +---- -> **Note** -> -> **Type-safe JWSs:** -> * If you are expecting a JWS with a Claims `payload`, call the `JwtParser`'s `parseSignedClaims` method. -> * If you are expecting a JWS with a content `payload`, call the `JwtParser`'s `parseSignedContent` method. +[NOTE] +==== +.Type-safe JWSs - -#### Verification Key +* If you are expecting a JWS with a Claims `payload`, call the ``JwtParser``'s `parseSignedClaims` method. +* If you are expecting a JWS with a content `payload`, call the ``JwtParser``'s `parseSignedContent` method. +==== + +++++++++++++ + +==== Verification Key The most important thing to do when reading a JWS is to specify the key used to verify the JWS's -cryptographic signature. If signature verification fails, the JWT cannot be safely trusted and should be +cryptographic signature. If signature verification fails, the JWT cannot be safely trusted and should be discarded. So which key do we use for verification? -* If the jws was signed with a `SecretKey`, the same `SecretKey` should be specified on the `JwtParserBuilder`. +* If the jws was signed with a `SecretKey`, the same `SecretKey` should be specified on the `JwtParserBuilder`. + For example: ++ +[,java] +---- +Jwts.parser() - ```java - Jwts.parser() - - .verifyWith(secretKey) // <---- - - .build() - .parseSignedClaims(jwsString); - ``` -* If the jws was signed with a `PrivateKey`, that key's corresponding `PublicKey` (not the `PrivateKey`) should be - specified on the `JwtParserBuilder`. For example: + .verifyWith(secretKey) // <---- - ```java - Jwts.parser() - - .verifyWith(publicKey) // <---- publicKey, not privateKey - - .build() - .parseSignedClaims(jwsString); - ``` + .build() + .parseSignedClaims(jwsString); +---- + +* If the jws was signed with a `PrivateKey`, that key's corresponding `PublicKey` (not the `PrivateKey`) should be +specified on the `JwtParserBuilder`. For example: ++ +[,java] +---- +Jwts.parser() + + .verifyWith(publicKey) // <---- publicKey, not privateKey + + .build() + .parseSignedClaims(jwsString); +---- + +++++++++++++++++++++++++ +// legacy anchors for old links + +==== Verification Key Locator - -#### Verification Key Locator - But you might have noticed something - what if your application doesn't use just a single `SecretKey` or `KeyPair`? What -if JWSs can be created with different `SecretKey`s or public/private keys, or a combination of both? How do you +if JWSs can be created with different ``SecretKey``s or public/private keys, or a combination of both? How do you know which key to specify if you can't inspect the JWT first? -In these cases, you can't call the `JwtParserBuilder`'s `verifyWith` method with a single key - instead, you'll need a -Key Locator. Please see the [Key Lookup](#key-locator) section to see how to dynamically obtain different keys when +In these cases, you can't call the ``JwtParserBuilder``'s `verifyWith` method with a single key - instead, you'll need a +Key Locator. Please see the <> section to see how to dynamically obtain different keys when parsing JWSs or JWEs. - -#### JWS Decompression +++++++++++++ -If you used JJWT to compress a JWS and you used a custom compression algorithm, you will need to tell the +==== JWS Decompression + +If you used JJWT to compress a JWS and you used a custom compression algorithm, you will need to tell the `JwtParserBuilder` how to resolve your `CompressionAlgorithm` to decompress the JWT. -Please see the [Compression](#compression) section below to see how to decompress JWTs during parsing. +Please see the <> section below to see how to decompress JWTs during parsing. - -### Unencoded Payload Option +++++++++++++ + +=== Unencoded Payload Option In some cases, especially if a JWS payload is large, it could be desirable to _not_ Base64URL-encode the JWS payload, or even exclude the payload from the compact JWS string entirely. The JWT RFC specifications provide support -for these use cases via the -[JSON Web Signature (JWS) Unencoded Payload Option](https://www.rfc-editor.org/rfc/rfc7797.html) specification, +for these use cases via the +https://www.rfc-editor.org/rfc/rfc7797.html[JSON Web Signature (JWS) Unencoded Payload Option] specification, which JJWT supports. This option comes with both benefits and disadvantages: -#### Benefits +==== Benefits A JWS producer can still create a JWS string to use for payload integrity verification without having to either: -1. Base64URL-encode the (potentially very large) payload, saving the time that could take. +. Base64URL-encode the (potentially very large) payload, saving the time that could take. +. Include the payload in the compact JWS string at all. Omitting the payload from the JWS compact string +entirely produces smaller JWSs that can be more efficient to transfer. +==== Disadvantages -2. Include the payload in the compact JWS string at all. Omitting the payload from the JWS compact string - entirely produces smaller JWSs that can be more efficient to transfer. +. Your application, and not JJWT, incurs the responsibility to ensure the payload is not modified during transmission +so the recipient can verify the JWS signature. For example, by using a sufficiently strong TLS (https) cipher +suite as well as any additional care before and after transmission, since +https://tozny.com/blog/end-to-end-encryption-vs-https/[TLS does not guarantee end-to-end security]. +. If you choose to include the unencoded payload in the JWS compact string, your application +https://www.rfc-editor.org/rfc/rfc7797.html#section-5.2[MUST] ensure that the payload does not contain a +period (`.`) character anywhere in the payload. The JWS recipient will experience parsing errors otherwise. -#### Disadvantages +Before attempting to use this option, one should be aware of the RFC's +https://www.rfc-editor.org/rfc/rfc7797.html#section-8[security considerations] first. -1. Your application, and not JJWT, incurs the responsibility to ensure the payload is not modified during transmission - so the recipient can verify the JWS signature. For example, by using a sufficiently strong TLS (https) cipher - suite as well as any additional care before and after transmission, since - [TLS does not guarantee end-to-end security](https://tozny.com/blog/end-to-end-encryption-vs-https/). +[NOTE] +==== +.Protected JWS Only +The RFC specification defines the Unencoded Payload option for use only with JWSs. It may not be used with +with unprotected JWTs or encrypted JWEs. +==== -2. If you choose to include the unencoded payload in the JWS compact string, your application - [MUST](https://www.rfc-editor.org/rfc/rfc7797.html#section-5.2) ensure that the payload does not contain a - period (`.`) character anywhere in the payload. The JWS recipient will experience parsing errors otherwise. +++++++++++++ +==== Detached Payload Example -Before attempting to use this option, one should be aware of the RFC's -[security considerations](https://www.rfc-editor.org/rfc/rfc7797.html#section-8) first. - -> **Note** -> -> **Protected JWS Only** -> -> The RFC specification defines the Unencoded Payload option for use only with JWSs. It may not be used with -> with unprotected JWTs or encrypted JWEs. - - -#### Detached Payload Example - -This example shows creating and parsing a compact JWS using an unencoded payload that is detached, i.e. where the +This example shows creating and parsing a compact JWS using an unencoded payload that is detached, i.e. where the payload is not embedded in the compact JWS string at all. We need to do three things during creation: -1. Specify the JWS signing key; it's a JWS and still needs to be signed. -2. Specify the raw payload bytes via the `JwtBuilder`'s `content` method. -3. Indicate that the payload should _not_ be Base64Url-encoded using the `JwtBuilder`'s `encodePayload(false)` method. - -```java +. Specify the JWS signing key; it's a JWS and still needs to be signed. +. Specify the raw payload bytes via the ``JwtBuilder``'s `content` method. +. Indicate that the payload should _not_ be Base64Url-encoded using the ``JwtBuilder``'s `encodePayload(false)` method. + +[,java] +---- // create a test key for this example: SecretKey testKey = Jwts.SIG.HS512.key().build(); @@ -1896,36 +2074,39 @@ String jws = Jwts.builder().signWith(testKey) // #1 .content(content) // #2 .encodePayload(false) // #3 .compact(); -``` +---- To parse the resulting `jws` string, we need to do two things when creating the `JwtParser`: -1. Specify the signature verification key. -3. Specify the externally-transmitted unencoded payload bytes, required for signature verification. +. Specify the signature verification key. +. Specify the externally-transmitted unencoded payload bytes, required for signature verification. -```java +[,java] +---- Jws parsed = Jwts.parser().verifyWith(testKey) // 1 .build() .parseSignedContent(jws, content); // 2 - + assertArrayEquals(content, parsed.getPayload()); -``` +---- - -#### Non-Detached Payload Example +++++++++++++ -This example shows creating and parsing a compact JWS with what the RFC calls a 'non-detached' unencoded payload, i.e. +==== Non-Detached Payload Example + +This example shows creating and parsing a compact JWS with what the RFC calls a 'non-detached' unencoded payload, i.e. a raw string directly embedded as the payload in the compact JWS string. We need to do three things during creation: -1. Specify the JWS signing key; it's a JWS and still needs to be signed. -2. Specify the raw payload string via the `JwtBuilder`'s `content` method. Per - [the RFC](https://www.rfc-editor.org/rfc/rfc7797.html#section-5.2), the payload string **_MUST NOT contain any - period (`.`) characters_**. -3. Indicate that the payload should _not_ be Base64Url-encoded using the `JwtBuilder`'s `encodePayload(false)` method. +. Specify the JWS signing key; it's a JWS and still needs to be signed. +. Specify the raw payload string via the ``JwtBuilder``'s `content` method. Per +https://www.rfc-editor.org/rfc/rfc7797.html#section-5.2[the RFC], the payload string *_MUST NOT contain any +period (`.`) characters_*. +. Indicate that the payload should _not_ be Base64Url-encoded using the ``JwtBuilder``'s `encodePayload(false)` method. -```java +[,java] +---- // create a test key for this example: SecretKey testKey = Jwts.SIG.HS512.key().build(); @@ -1935,13 +2116,13 @@ String jws = Jwts.builder().signWith(testKey) // #1 .content(claimsString) // #2 .encodePayload(false) // #3 .compact(); -``` +---- If you were to print the `jws` string, you'd see something like this: -``` +---- eyJhbGciOiJIUzUxMiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19.{"sub":"joe","iss":"me"}.wkoxYEd//...etc... -``` +---- See how the `claimsString` is embedded directly as the center `payload` token instead of a standard Base64URL value? This is why no period (`.`) characters can exist in the payload. If they did, any standard JWT parser would see more @@ -1949,190 +2130,252 @@ than two periods total, which is required for parsing standard JWSs. To parse the resulting `jws` string, we need to do two things when creating the `JwtParser`: -1. Specify the signature verification key. -2. Indicate that we want to support Unencoded Payload Option JWSs by enabling the `b64` `crit` header parameter. +. Specify the signature verification key. +. Indicate that we want to support Unencoded Payload Option JWSs by enabling the `b64` `crit` header parameter. -```java +[,java] +---- Jws parsed = Jwts.parser().verifyWith(testKey) // 1 .critical().add("b64").and() // 2 .build() - .parseSignedClaims(jws); + .parseSignedClaims(jws); assert "joe".equals(parsed.getPayload().getSubject()); assert "me".equals(parsed.getPayload().getIssuer()); -``` +---- -Did you notice we used the `.parseSignedClaims(String)` method instead of `.parseSignedClaims(String, byte[])`? This is +Did you notice we used the `.parseSignedClaims(String)` method instead of `.parseSignedClaims(String, byte[])`? This is because the non-detached payload is already present and JJWT has what it needs for signature verification. -Additionally, we needed to specify the `b64` critical value: because we're not using the two-argument +Additionally, we needed to specify the `b64` critical value: because we're not using the two-argument `parseSignedClaims(jws, content)` method, the parser has no way of knowing if you wish to allow or support unencoded payloads. Unencoded payloads have additional security considerations as described above, so they are disabled by the parser by default unless you indicate you want to support them by using `critical().add("b64")`. -Finally, even if the payload contains a non-detached String, you could still use the two-argument method using the +Finally, even if the payload contains a non-detached String, you could still use the two-argument method using the payload String's UTF-8 bytes instead: -```java +[,java] +---- parsed = Jwts.parser().verifyWith(testKey) .build() .parseSignedClaims(jws, claimsString.getBytes(StandardCharsets.UTF_8)); // <--- -``` +---- - -## Encrypted JWTs +++++++++++++ + +== Encrypted JWTs The JWT specification also provides for the ability to encrypt and decrypt a JWT. Encrypting a JWT: -1. guarantees that no-one other than the intended JWT recipient can see the JWT `payload` (it is confidential), and -2. guarantees that no-one has manipulated or changed the JWT after it was created (its integrity is maintained). +. guarantees that no-one other than the intended JWT recipient can see the JWT `payload` (it is confidential), and +. guarantees that no-one has manipulated or changed the JWT after it was created (its integrity is maintained). -These two properties - confidentiality and integrity - assure us that an encrypted JWT contains a `payload` that +These two properties - confidentiality and integrity - assure us that an encrypted JWT contains a `payload` that no-one else can see, _nor_ has anyone changed or altered the data in transit. Encryption and confidentiality seem somewhat obvious: if you encrypt a message, it is confidential by the notion that -random 3rd parties cannot make sense of the encrypted message. But some might be surprised to know that **_general -encryption does _not_ guarantee that someone hasn't tampered/altered an encrypted message in transit_**. Most of us -assume that if a message can be decrypted, then the message would be authentic and unchanged - after all, if you can +random 3rd parties cannot make sense of the encrypted message. But some might be surprised to know that *_general +encryption does _not_ guarantee that someone hasn't tampered/altered an encrypted message in transit_*. Most of us +assume that if a message can be decrypted, then the message would be authentic and unchanged - after all, if you can decrypt it, it must not have been tampered with, right? Because if it was changed, decryption would surely fail, right? -Unfortunately, this is not actually guaranteed in all cryptographic ciphers. There are certain attack vectors where -it is possible to change an encrypted payload (called 'ciphertext'), and the message recipient is still able to -successfully decrypt the (modified) payload. In these cases, the ciphertext integrity was not maintained - a -malicious 3rd party could intercept a message and change the payload content, even if they don't understand what is +Unfortunately, this is not actually guaranteed in all cryptographic ciphers. There are certain attack vectors where +it is possible to change an encrypted payload (called 'ciphertext'), and the message recipient is still able to +successfully decrypt the (modified) payload. In these cases, the ciphertext integrity was not maintained - a +malicious 3rd party could intercept a message and change the payload content, even if they don't understand what is inside the payload, and the message recipient could never know. -To combat this, there is a category of encryption algorithms that ensures both confidentiality _and_ integrity of the -ciphertext data. These types of algorithms are called -[Authenticated Encryption](https://en.wikipedia.org/wiki/Authenticated_encryption) algorithms. +To combat this, there is a category of encryption algorithms that ensures both confidentiality _and_ integrity of the +ciphertext data. These types of algorithms are called +https://en.wikipedia.org/wiki/Authenticated_encryption[Authenticated Encryption] algorithms. As a result, to ensure JWTs do not suffer from this problem, the JWE RFC specifications require that any encryption -algorithm used to encrypt a JWT _MUST_ be an Authenticated Encryption algorithm. JWT users can be sufficiently +algorithm used to encrypt a JWT _MUST_ be an Authenticated Encryption algorithm. JWT users can be sufficiently confident their encrypted JWTs maintain the properties of both confidentiality and integrity. - -### JWE Encryption Algorithms +++++++++++++ + +=== JWE Encryption Algorithms The JWT specification defines 6 standard Authenticated Encryption algorithms used to encrypt a JWT `payload`: -| Identifier | Required Key Bit Length | Encryption Algorithm | -|--------------------------------- | ----------------------- | -------------------- | -| A128CBC‑HS256 | 256 | [AES_128_CBC_HMAC_SHA_256](https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.3) authenticated encryption algorithm | -| `A192CBC-HS384` | 384 | [AES_192_CBC_HMAC_SHA_384](https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.4) authenticated encryption algorithm | -| `A256CBC-HS512` | 512 | [AES_256_CBC_HMAC_SHA_512](https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.5) authenticated encryption algorithm | -| `A128GCM` | 128 | AES GCM using 128-bit key1 | -| `A192GCM` | 192 | AES GCM using 192-bit key1 | -| `A256GCM` | 256 | AES GCM using 256-bit key1 | +|=== +| Identifier | Required Key Bit Length | Encryption Algorithm -1. Requires Java 8+ or a compatible JCA Provider (like BouncyCastle) in the runtime classpath. +| `A128CBC‑HS256` +| 256 +| https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.3[AES_128_CBC_HMAC_SHA_256] authenticated encryption algorithm -These are all represented as constants in the `io.jsonwebtoken.Jwts.ENC` registry singleton as +| `A192CBC-HS384` +| 384 +| https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.4[AES_192_CBC_HMAC_SHA_384] authenticated encryption algorithm + +| `A256CBC-HS512` +| 512 +| https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.5[AES_256_CBC_HMAC_SHA_512] authenticated encryption algorithm + +| `A128GCM` +| 128 +| AES GCM using 128-bit key^*1*^ + +| `A192GCM` +| 192 +| AES GCM using 192-bit key^*1*^ + +| `A256GCM` +| 256 +| AES GCM using 256-bit key^*1*^ +|=== + +^*1*.{sp}{fn-require-java8-plus}^ + +These are all represented as constants in the `io.jsonwebtoken.Jwts.ENC` registry singleton as implementations of the `io.jsonwebtoken.security.AeadAlgorithm` interface. As shown in the table above, each algorithm requires a key of sufficient length. The JWT specification -[RFC 7518, Sections 5.2.3 through 5.3](https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.3) -_requires_ (mandates) that you MUST use keys that are sufficiently strong for a chosen algorithm. This means that +https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.3[RFC 7518, Sections 5.2.3 through 5.3] +_requires_ (mandates) that you MUST use keys that are sufficiently strong for a chosen algorithm. This means that JJWT - a specification-compliant library - will also enforce that you use sufficiently strong keys for the algorithms you choose. If you provide a weak key for a given algorithm, JJWT will reject it and throw an exception. -The reason why the JWT specification, and consequently JJWT, mandates key lengths is that the security model of a -particular algorithm can completely break down if you don't adhere to the mandatory key properties of the algorithm, +The reason why the JWT specification, and consequently JJWT, mandates key lengths is that the security model of a +particular algorithm can completely break down if you don't adhere to the mandatory key properties of the algorithm, effectively having no security at all. - -#### Symmetric Ciphers +++++++++++++ -You might have noticed something about the above Authenticated Encryption algorithms: they're all variants of the -AES algorithm, and AES always uses a symmetric (secret) key to perform encryption and decryption. That's kind of +==== Symmetric Ciphers + +You might have noticed something about the above Authenticated Encryption algorithms: they're all variants of the +AES algorithm, and AES always uses a symmetric (secret) key to perform encryption and decryption. That's kind of strange, isn't it? -What about RSA and Elliptic Curve asymmetric key cryptography? And Diffie-Hellman key exchange? What about +What about RSA and Elliptic Curve asymmetric key cryptography? And Diffie-Hellman key exchange? What about password-based key derivation algorithms? Surely any of those could be desirable depending on the use case, no? -Yes, they definitely can, and the JWT specifications do support them, albeit indirectly: those other -algorithms _are_ indeed supported and used, but they aren't used to encrypt the JWT `payload` directly. They are +Yes, they definitely can, and the JWT specifications do support them, albeit indirectly: those other +algorithms _are_ indeed supported and used, but they aren't used to encrypt the JWT `payload` directly. They are used to _produce_ the actual key used to encrypt the `JWT` payload. -This is all done via the JWT specification's concept of a Key Management Algorithm, covered next. After we cover that, +This is all done via the JWT specification's concept of a Key Management Algorithm, covered next. After we cover that, we'll show you how to encrypt and parse your own JWTs with the `JwtBuilder` and `JwtParserBuilder`. - -### JWE Key Management Algorithms +++++++++++++ -As stated above, all standard JWA Encryption Algorithms are AES-based authenticated encryption algorithms. So what +=== JWE Key Management Algorithms + +As stated above, all standard JWA Encryption Algorithms are AES-based authenticated encryption algorithms. So what about RSA and Elliptic Curve cryptography? And password-based key derivation, or Diffie-Hellman exchange? -All of those are supported as well, but they are not used directly for encryption. They are used to _produce_ the +All of those are supported as well, but they are not used directly for encryption. They are used to _produce_ the key that will be used to directly encrypt the JWT `payload`. That is, JWT encryption can be thought of as a two-step process, shown in the following pseudocode: -```groovy +[,groovy] +---- Key algorithmKey = getKeyManagementAlgorithmKey(); // PublicKey, SecretKey, or Password SecretKey contentEncryptionKey = keyManagementAlgorithm.produceEncryptionKey(algorithmKey); // 1 byte[] ciphertext = encryptionAlgorithm.encrypt(payload, contentEncryptionKey); // 2 -``` +---- Steps: -1. Use the `algorithmKey` to produce the actual key that will be used to encrypt the payload. The JWT specifications - call this result the 'Content Encryption Key'. -2. Take the resulting Content Encryption Key and use it directly with the Authenticated Encryption algorithm to - actually encrypt the JWT `payload`. +. Use the `algorithmKey` to produce the actual key that will be used to encrypt the payload. The JWT specifications +call this result the 'Content Encryption Key'. +. Take the resulting Content Encryption Key and use it directly with the Authenticated Encryption algorithm to +actually encrypt the JWT `payload`. So why the indirection? Why not just use any `PublicKey`, `SecretKey` or `Password` to encrypt the `payload` _directly_ ? There are quite a few reasons for this. -1. Asymmetric key encryption (like RSA and Elliptic Curve) tends to be slow. Like _really_ slow. Symmetric key - cipher algorithms in contrast are _really fast_. This matters a lot in production applications that could be - handling a JWT on every HTTP request, which could be thousands per second. -2. RSA encryption (for example) can only encrypt a relatively small amount of data. A 2048-bit RSA key can only - encrypt up to a maximum of 245 bytes. A 4096-bit RSA key can only encrypt up to a maximum of 501 bytes. There are - plenty of JWTs that can exceed 245 bytes, and that would make RSA unusable. -3. Passwords usually make for very poor encryption keys - they often have poor entropy, or they themselves are - often too short to be used directly with algorithms that mandate minimum key lengths to help ensure safety. +. Asymmetric key encryption (like RSA and Elliptic Curve) tends to be slow. Like _really_ slow. Symmetric key +cipher algorithms in contrast are _really fast_. This matters a lot in production applications that could be +handling a JWT on every HTTP request, which could be thousands per second. +. RSA encryption (for example) can only encrypt a relatively small amount of data. A 2048-bit RSA key can only +encrypt up to a maximum of 245 bytes. A 4096-bit RSA key can only encrypt up to a maximum of 501 bytes. There are +plenty of JWTs that can exceed 245 bytes, and that would make RSA unusable. +. Passwords usually make for very poor encryption keys - they often have poor entropy, or they themselves are +often too short to be used directly with algorithms that mandate minimum key lengths to help ensure safety. For these reasons and more, using one secure algorithm to generate or encrypt a key used for another (very fast) secure -algorithm has been proven to be a great way to increase security through many more secure algorithms while -also still resulting in very fast and secure output. This is after all how TLS (for https encryption) works - -two parties can use more complex cryptography (like RSA or Elliptic Curve) to negotiate a small, fast encryption key. +algorithm has been proven to be a great way to increase security through many more secure algorithms while +also still resulting in very fast and secure output. This is after all how TLS (for https encryption) works - +two parties can use more complex cryptography (like RSA or Elliptic Curve) to negotiate a small, fast encryption key. This fast encryption key is produced during the 'TLS handshake' and is called the TLS 'session key'. So the JWT specifications work much in the same way: one key from any number of various algorithm types can be used to produce a final symmetric key, and that symmetric key is used to encrypt the JWT `payload`. - -#### JWE Standard Key Management Algorithms +++++++++++++ -The JWT specification defines 17 standard Key Management Algorithms used to produce the JWE +==== JWE Standard Key Management Algorithms + +The JWT specification defines 17 standard Key Management Algorithms used to produce the JWE Content Encryption Key (CEK): -| Identifier | Key Management Algorithm | -| --- |-------------------------------------------------------------------------------| -| `RSA1_5` | RSAES-PKCS1-v1_5 | -| `RSA-OAEP` | RSAES OAEP using default parameters | -| `RSA-OAEP-256` | RSAES OAEP using SHA-256 and MGF1 with SHA-256 | -| `A128KW` | AES Key Wrap with default initial value using 128-bit key | -| `A192KW` | AES Key Wrap with default initial value using 192-bit key | -| `A256KW` | AES Key Wrap with default initial value using 256-bit key | -| `dir` | Direct use of a shared symmetric key as the Content Encryption Key | -| `ECDH-ES` | Elliptic Curve Diffie-Hellman Ephemeral Static key agreement using Concat KDF | -| `ECDH-ES+A128KW` | ECDH-ES using Concat KDF and CEK wrapped with "A128KW" | -| `ECDH-ES+A192KW` | ECDH-ES using Concat KDF and CEK wrapped with "A192KW" | -| `ECDH-ES+A256KW` | ECDH-ES using Concat KDF and CEK wrapped with "A256KW" | -| `A128GCMKW` | Key wrapping with AES GCM using 128-bit key3 | -| `A192GCMKW` | Key wrapping with AES GCM using 192-bit key3 | -| `A256GCMKW` | Key wrapping with AES GCM using 256-bit key3 | -| `PBES2-HS256+A128KW` | PBES2 with HMAC SHA-256 and "A128KW" wrapping3 | -| `PBES2-HS384+A192KW` | PBES2 with HMAC SHA-384 and "A192KW" wrapping3 | -| PBES2‑HS512+A256KW | PBES2 with HMAC SHA-512 and "A256KW" wrapping3 | +|=== +| Identifier | Key Management Algorithm -3. Requires Java 8 or a compatible JCA Provider (like BouncyCastle) in the runtime classpath. +| `RSA1_5` +| RSAES-PKCS1-v1_5 + +| `RSA-OAEP` +| RSAES OAEP using default parameters + +| `RSA-OAEP-256` +| RSAES OAEP using SHA-256 and MGF1 with SHA-256 + +| `A128KW` +| AES Key Wrap with default initial value using 128-bit key + +| `A192KW` +| AES Key Wrap with default initial value using 192-bit key + +| `A256KW` +| AES Key Wrap with default initial value using 256-bit key + +| `dir` +| Direct use of a shared symmetric key as the Content Encryption Key + +| `ECDH-ES` +| Elliptic Curve Diffie-Hellman Ephemeral Static key agreement using Concat KDF + +| `ECDH-ES+A128KW` +| ECDH-ES using Concat KDF and CEK wrapped with "A128KW" + +| `ECDH-ES+A192KW` +| ECDH-ES using Concat KDF and CEK wrapped with "A192KW" + +| `ECDH-ES+A256KW` +| ECDH-ES using Concat KDF and CEK wrapped with "A256KW" + +| `A128GCMKW` +| Key wrapping with AES GCM using 128-bit key^*1*^ + +| `A192GCMKW` +| Key wrapping with AES GCM using 192-bit key^*1*^ + +| `A256GCMKW` +| Key wrapping with AES GCM using 256-bit key^*1*^ + +| `PBES2-HS256+A128KW` +| PBES2 with HMAC SHA-256 and "A128KW" wrapping^*1*^ + +| `PBES2-HS384+A192KW` +| PBES2 with HMAC SHA-384 and "A192KW" wrapping^*1*^ + +| `PBES2‑HS512+A256KW` +| PBES2 with HMAC SHA-512 and "A256KW" wrapping^*1*^ +|=== + +^*1*.{sp}{fn-require-java8-plus}^ These are all represented as constants in the `io.jsonwebtoken.Jwts.KEY` registry singleton as implementations of the `io.jsonwebtoken.security.KeyAlgorithm` interface. @@ -2140,16 +2383,17 @@ implementations of the `io.jsonwebtoken.security.KeyAlgorithm` interface. But 17 algorithms are a lot to choose from. When would you use them? The sections below describe when you might choose each category of algorithms and how they behave. - -##### RSA Key Encryption +++++++++++++ + +===== RSA Key Encryption The JWT RSA key management algorithms `RSA1_5`, `RSA-OAEP`, and `RSA-OAEP-256` are used when you want to use the -JWE recipient's RSA _public_ key during encryption. This ensures that only the JWE recipient can decrypt +JWE recipient's RSA _public_ key during encryption. This ensures that only the JWE recipient can decrypt and read the JWE (using their RSA `private` key). During JWE creation, these algorithms: -* Generate a new secure-random Content Encryption Key (CEK) suitable for the desired [encryption algorithm](#jwe-enc). +* Generate a new secure-random Content Encryption Key (CEK) suitable for the desired <>. * Encrypt the JWE payload with the desired encryption algorithm using the new CEK, producing the JWE payload ciphertext. * Encrypt the CEK itself with the specified RSA key wrap algorithm using the JWE recipient's RSA public key. * Embed the payload ciphertext and encrypted CEK in the resulting JWE. @@ -2157,54 +2401,58 @@ During JWE creation, these algorithms: During JWE decryption, these algorithms: * Retrieve the encrypted Content Encryption Key (CEK) embedded in the JWE. -* Decrypt the encrypted CEK with the discovered RSA key unwrap algorithm using the JWE recipient's RSA private key, - producing the decrypted Content Encryption Key (CEK). -* Decrypt the JWE ciphertext payload with the JWE's identified [encryption algorithm](#jwe-enc) using the decrypted CEK. +* Decrypt the encrypted CEK with the discovered RSA key unwrap algorithm using the JWE recipient's RSA private key, +producing the decrypted Content Encryption Key (CEK). +* Decrypt the JWE ciphertext payload with the JWE's identified <> using the decrypted CEK. -> **Warning** -> -> RFC 7518 Sections [4.2](https://www.rfc-editor.org/rfc/rfc7518.html#section-4.2) and -> [4.3](https://www.rfc-editor.org/rfc/rfc7518.html#section-4.3) _require_ (mandate) that RSA keys >= 2048 bits -> MUST be used with these algorithms. JJWT will throw an exception if it detects weaker keys being used. +[WARNING] +==== +RFC 7518 Sections https://www.rfc-editor.org/rfc/rfc7518.html#section-4.2[4.2] and +https://www.rfc-editor.org/rfc/rfc7518.html#section-4.3[4.3] _require_ (mandate) that RSA keys >= 2048 bits +MUST be used with these algorithms. JJWT will throw an exception if it detects weaker keys being used. +==== - -##### AES Key Encryption +++++++++++++ -The JWT AES key management algorithms `A128KW`, `A192KW`, `A256KW`, `A128GCMKW`, `A192GCMKW`, and `A256GCMKW` are -used when you have a symmetric secret key, but you don't want to use that secret key to directly +===== AES Key Encryption + +The JWT AES key management algorithms `A128KW`, `A192KW`, `A256KW`, `A128GCMKW`, `A192GCMKW`, and `A256GCMKW` are +used when you have a symmetric secret key, but you don't want to use that secret key to directly encrypt/decrypt the JWT. -Instead, a new secure-random key is generated each time a JWE is created, and that new/random key is used to directly +Instead, a new secure-random key is generated each time a JWE is created, and that new/random key is used to directly encrypt/decrypt the JWT payload. The secure-random key is itself encrypted with your symmetric secret key using the AES Wrap algorithm, and the encrypted key is embedded in the resulting JWE. -This allows the JWE to be encrypted with a random short-lived key, reducing material exposure of the potentially +This allows the JWE to be encrypted with a random short-lived key, reducing material exposure of the potentially longer-lived symmetric secret key. -Because these particular algorithms use a symmetric secret key, they are best suited when the JWE creator and +Because these particular algorithms use a symmetric secret key, they are best suited when the JWE creator and receiver are the same, ensuring the secret key does not need to be shared with multiple parties. During JWE creation, these algorithms: -* Generate a new secure-random Content Encryption Key (CEK) suitable for the desired [encryption algorithm](#jwe-enc). +* Generate a new secure-random Content Encryption Key (CEK) suitable for the desired <>. * Encrypt the JWE payload with the desired encryption algorithm using the new CEK, producing the JWE payload ciphertext. -* Encrypt the CEK itself with the specified AES key algorithm (either AES Key Wrap or AES with GCM encryption), - producing the encrypted CEK. +* Encrypt the CEK itself with the specified AES key algorithm (either AES Key Wrap or AES with GCM encryption), +producing the encrypted CEK. * Embed the payload ciphertext and encrypted CEK in the resulting JWE. During JWE decryption, these algorithms: * Retrieve the encrypted Content Encryption Key (CEK) embedded in the JWE. * Decrypt the encrypted CEK with the discovered AES key algorithm using the symmetric secret key. -* Decrypt the JWE ciphertext payload with the JWE's identified [encryption algorithm](#jwe-enc) using the decrypted CEK. +* Decrypt the JWE ciphertext payload with the JWE's identified <> using the decrypted CEK. -> **Warning** -> -> The symmetric key used for the AES key algorithms MUST be 128, 192 or 256 bits as required by the specific AES -> key algorithm. JJWT will throw an exception if it detects weaker keys than what is required. +[WARNING] +==== +The symmetric key used for the AES key algorithms MUST be 128, 192 or 256 bits as required by the specific AES +key algorithm. JJWT will throw an exception if it detects weaker keys than what is required. +==== - -##### Direct Key Encryption +++++++++++++ + +===== Direct Key Encryption The JWT `dir` (direct) key management algorithm is used when you have a symmetric secret key, and you want to use it to directly encrypt the JWT payload. @@ -2217,24 +2465,26 @@ a 'no op' key algorithm, allowing the shared key to be used to directly encrypt During JWE creation, this algorithm: -* Encrypts the JWE payload with the desired encryption algorithm directly using the symmetric secret key, - producing the JWE payload ciphertext. +* Encrypts the JWE payload with the desired encryption algorithm directly using the symmetric secret key, +producing the JWE payload ciphertext. * Embeds the payload ciphertext in the resulting JWE. -Note that because this algorithm does not produce an encrypted key value, an encrypted CEK is _not_ embedded in the +Note that because this algorithm does not produce an encrypted key value, an encrypted CEK is _not_ embedded in the resulting JWE. -During JWE decryption, this algorithm decrypts the JWE ciphertext payload with the JWE's -identified [encryption algorithm](#jwe-enc) directly using the symmetric secret key. No encrypted CEK is used. +During JWE decryption, this algorithm decrypts the JWE ciphertext payload with the JWE's +identified <> directly using the symmetric secret key. No encrypted CEK is used. -> **Warning** -> -> The symmetric secret key MUST be 128, 192 or 256 bits as required by the associated -> [AEAD encryption algorithm](#jwe-enc) used to encrypt the payload. JJWT will throw an exception if it detects -> weaker keys than what is required. +[WARNING] +==== +The symmetric secret key MUST be 128, 192 or 256 bits as required by the associated +<> used to encrypt the payload. JJWT will throw an exception if it detects +weaker keys than what is required. +==== - -##### Password-Based Key Encryption +++++++++++++ + +===== Password-Based Key Encryption The JWT password-based key encryption algorithms `PBES2-HS256+A128KW`, `PBES2-HS384+A192KW`, and `PBES2-HS512+A256KW` are used when you want to use a password (character array) to encrypt and decrypt a JWT. @@ -2243,7 +2493,7 @@ However, because passwords are usually too weak or problematic to use directly i algorithms utilize key derivation techniques with work factors (e.g. computation iterations) and secure-random salts to produce stronger cryptographic keys suitable for cryptographic operations. -This allows the payload to be encrypted with a random short-lived cryptographically-stronger key, reducing the need to +This allows the payload to be encrypted with a random short-lived cryptographically-stronger key, reducing the need to expose the longer-lived (and potentially weaker) password. Because these algorithms use a secret password, they are best suited when the JWE creator and receiver are the @@ -2251,327 +2501,356 @@ same, ensuring the secret password does not need to be shared with multiple part During JWE creation, these algorithms: -* Generate a new secure-random Content Encryption Key (CEK) suitable for the desired [encryption algorithm](#jwe-enc). +* Generate a new secure-random Content Encryption Key (CEK) suitable for the desired <>. * Encrypt the JWE payload with the desired encryption algorithm using the new CEK, producing the JWE payload ciphertext. -* Derive a 'key encryption key' (KEK) with the desired "PBES2 with HMAC SHA" algorithm using the password, a suitable - number of computational iterations, and a secure-random salt value. +* Derive a 'key encryption key' (KEK) with the desired "PBES2 with HMAC SHA" algorithm using the password, a suitable +number of computational iterations, and a secure-random salt value. * Encrypt the generated CEK with the corresponding AES Key Wrap algorithm using the password-derived KEK. * Embed the payload ciphertext and encrypted CEK in the resulting JWE. -> **Note** -> -> **Secure defaults**: When using these algorithms, if you do not specify a work factor (i.e. number of computational -> iterations), JJWT will automatically use an -> [OWASP PBKDF2 recommended](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2) -> default appropriate for the specified `PBES2` algorithm. +[NOTE] +==== +.Secure defaults + +When using these algorithms, if you do not specify a work factor (i.e. number of computational +iterations), JJWT will automatically use an +https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2[OWASP PBKDF2 recommended] +default appropriate for the specified `PBES2` algorithm. +==== During JWE decryption, these algorithms: * Retrieve the encrypted Content Encryption Key (CEK) embedded in the JWE. * Derive the 'key encryption key' (KEK) with the discovered "PBES2 with HMAC SHA" algorithm using the password and the - number of computational iterations and secure-random salt value discovered in the JWE header. +number of computational iterations and secure-random salt value discovered in the JWE header. * Decrypt the encrypted CEK with the corresponding AES Key Unwrap algorithm using the password-derived KEK. -* Decrypt the JWE ciphertext payload with the JWE's identified [encryption algorithm](#jwe-enc) using the decrypted CEK. +* Decrypt the JWE ciphertext payload with the JWE's identified <> using the decrypted CEK. - -##### Elliptic Curve Diffie-Hellman Ephemeral Static Key Agreement (ECDH-ES) +++++++++++++ -The JWT Elliptic Curve Diffie-Hellman Ephemeral Static key agreement algorithms `ECDH-ES`, `ECDH-ES+A128KW`, -`ECDH-ES+A192KW`, and `ECDH-ES+A256KW` are used when you want to use the JWE recipient's Elliptic Curve _public_ key -during encryption. This ensures that only the JWE recipient can decrypt and read the JWE (using their Elliptic Curve +===== Elliptic Curve Diffie-Hellman Ephemeral Static Key Agreement (ECDH-ES) + +The JWT Elliptic Curve Diffie-Hellman Ephemeral Static key agreement algorithms `ECDH-ES`, `ECDH-ES+A128KW`, +`ECDH-ES+A192KW`, and `ECDH-ES+A256KW` are used when you want to use the JWE recipient's Elliptic Curve _public_ key +during encryption. This ensures that only the JWE recipient can decrypt and read the JWE (using their Elliptic Curve _private_ key). During JWE creation, these algorithms: * Obtain the Content Encryption Key (CEK) used to encrypt the JWE payload as follows: - * Inspect the JWE recipient's Elliptic Curve public key and determine its Curve. - * Generate a new secure-random ephemeral Elliptic Curve public/private key pair on this same Curve. - * Add the ephemeral EC public key to the JWE - [epk header](https://www.rfc-editor.org/rfc/rfc7518.html#section-4.6.1.1) for inclusion in the final JWE. - * Produce an ECDH shared secret with the ECDH Key Agreement algorithm using the JWE recipient's EC public key - and the ephemeral EC private key. - * Derive a symmetric secret key with the Concat Key Derivation Function - ([NIST.800-56A](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-56Ar2.pdf), Section 5.8.1) using - this ECDH shared secret and any provided - [PartyUInfo](https://www.rfc-editor.org/rfc/rfc7518.html#section-4.6.1.2) and/or - [PartyVInfo](https://www.rfc-editor.org/rfc/rfc7518.html#section-4.6.1.3). - * If the key algorithm is `ECDH-ES`: - * Use the Concat KDF-derived symmetric secret key directly as the Content Encryption Key (CEK). No encrypted key - is created, nor embedded in the resulting JWE. - * Otherwise, if the key algorithm is `ECDH-ES+A128KW`, `ECDH-ES+A192KW`, or `ECDH-ES+A256KW`: - * Generate a new secure-random Content Encryption Key (CEK) suitable for the desired [encryption algorithm](#jwe-enc). - * Encrypt this new CEK with the corresponding AES Key Wrap algorithm using the Concat KDF-derived secret key, - producing the encrypted CEK. - * Embed the encrypted CEK in the resulting JWE. -* Encrypt the JWE payload with the desired encryption algorithm using the obtained CEK, producing the JWE payload - ciphertext. + ** Inspect the JWE recipient's Elliptic Curve public key and determine its Curve. + ** Generate a new secure-random ephemeral Elliptic Curve public/private key pair on this same Curve. + ** Add the ephemeral EC public key to the JWE +https://www.rfc-editor.org/rfc/rfc7518.html#section-4.6.1.1[epk header] for inclusion in the final JWE. + ** Produce an ECDH shared secret with the ECDH Key Agreement algorithm using the JWE recipient's EC public key +and the ephemeral EC private key. + ** Derive a symmetric secret key with the Concat Key Derivation Function +(https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-56Ar2.pdf[NIST.800-56A], Section 5.8.1) using +this ECDH shared secret and any provided +https://www.rfc-editor.org/rfc/rfc7518.html#section-4.6.1.2[PartyUInfo] and/or +https://www.rfc-editor.org/rfc/rfc7518.html#section-4.6.1.3[PartyVInfo]. + ** If the key algorithm is `ECDH-ES`: + *** Use the Concat KDF-derived symmetric secret key directly as the Content Encryption Key (CEK). No encrypted key +is created, nor embedded in the resulting JWE. + ** Otherwise, if the key algorithm is `ECDH-ES+A128KW`, `ECDH-ES+A192KW`, or `ECDH-ES+A256KW`: + *** Generate a new secure-random Content Encryption Key (CEK) suitable for the desired <>. + *** Encrypt this new CEK with the corresponding AES Key Wrap algorithm using the Concat KDF-derived secret key, +producing the encrypted CEK. + *** Embed the encrypted CEK in the resulting JWE. +* Encrypt the JWE payload with the desired encryption algorithm using the obtained CEK, producing the JWE payload +ciphertext. * Embed the payload ciphertext in the resulting JWE. During JWE decryption, these algorithms: * Obtain the Content Encryption Key (CEK) used to decrypt the JWE payload as follows: - * Retrieve the required ephemeral Elliptic Curve public key from the JWE's - [epk header](https://www.rfc-editor.org/rfc/rfc7518.html#section-4.6.1.1). - * Ensure the ephemeral EC public key exists on the same curve as the JWE recipient's EC private key. - * Produce the ECDH shared secret with the ECDH Key Agreement algorithm using the JWE recipient's EC private key - and the ephemeral EC public key. - * Derive a symmetric secret key with the Concat Key Derivation Function - ([NIST.800-56A](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-56Ar2.pdf), Section 5.8.1) using - this ECDH shared secret and any - [PartyUInfo](https://www.rfc-editor.org/rfc/rfc7518.html#section-4.6.1.2) and/or - [PartyVInfo](https://www.rfc-editor.org/rfc/rfc7518.html#section-4.6.1.3) found in the JWE header. - * If the key algorithm is `ECDH-ES`: - * Use the Concat KDF-derived secret key directly as the Content Encryption Key (CEK). No encrypted key is used. - * Otherwise, if the key algorithm is `ECDH-ES+A128KW`, `ECDH-ES+A192KW`, or `ECDH-ES+A256KW`: - * Obtain the encrypted key ciphertext embedded in the JWE. - * Decrypt the encrypted key ciphertext with the associated AES Key Unwrap algorithm using the Concat KDF-derived - secret key, producing the unencrypted Content Encryption Key (CEK). + ** Retrieve the required ephemeral Elliptic Curve public key from the JWE's +https://www.rfc-editor.org/rfc/rfc7518.html#section-4.6.1.1[epk header]. + ** Ensure the ephemeral EC public key exists on the same curve as the JWE recipient's EC private key. + ** Produce the ECDH shared secret with the ECDH Key Agreement algorithm using the JWE recipient's EC private key +and the ephemeral EC public key. + ** Derive a symmetric secret key with the Concat Key Derivation Function +(https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-56Ar2.pdf[NIST.800-56A], Section 5.8.1) using +this ECDH shared secret and any +https://www.rfc-editor.org/rfc/rfc7518.html#section-4.6.1.2[PartyUInfo] and/or +https://www.rfc-editor.org/rfc/rfc7518.html#section-4.6.1.3[PartyVInfo] found in the JWE header. + ** If the key algorithm is `ECDH-ES`: + *** Use the Concat KDF-derived secret key directly as the Content Encryption Key (CEK). No encrypted key is used. + ** Otherwise, if the key algorithm is `ECDH-ES+A128KW`, `ECDH-ES+A192KW`, or `ECDH-ES+A256KW`: + *** Obtain the encrypted key ciphertext embedded in the JWE. + *** Decrypt the encrypted key ciphertext with the associated AES Key Unwrap algorithm using the Concat KDF-derived +secret key, producing the unencrypted Content Encryption Key (CEK). * Decrypt the JWE payload ciphertext with the JWE's discovered encryption algorithm using the obtained CEK. - -### Creating a JWE +++++++++++++ + +=== Creating a JWE Now that we know the difference between a JWE Encryption Algorithm and a JWE Key Management Algorithm, how do we use them to encrypt a JWT? You create an encrypted JWT (called a 'JWE') as follows: -1. Use the `Jwts.builder()` method to create a `JwtBuilder` instance. -2. Call `JwtBuilder` methods to set the `payload` content or claims and any [header](#jws-create-header) parameters as desired. -3. Call the `encryptWith` method, specifying the Key, Key Algorithm, and Encryption Algorithm you want to use. -4. Finally, call the `compact()` method to compact and encrypt, producing the final jwe. +. Use the `Jwts.builder()` method to create a `JwtBuilder` instance. +. Call `JwtBuilder` methods to set the `payload` content or claims and any <> parameters as desired. +. Call the `encryptWith` method, specifying the Key, Key Algorithm, and Encryption Algorithm you want to use. +. Finally, call the `compact()` method to compact and encrypt, producing the final jwe. For example: -```java +[,java] +---- String jwe = Jwts.builder() // (1) - .subject("Bob") // (2) + .subject("Bob") // (2) .encryptWith(key, keyAlgorithm, encryptionAlgorithm) // (3) - - .compact(); // (4) -``` -Before calling `compact()`, you may set any [header](#jws-create-header) parameters and [claims](#jws-create-claims) + .compact(); // (4) +---- + +Before calling `compact()`, you may set any <> parameters and <> exactly the same way as described for JWS. - -#### JWE Compression +++++++++++++ -If your JWT payload or Claims set is large (contains a lot of data), you might want to compress the JWE to reduce -its size. Please see the main [Compression](#compression) section to see how to compress and decompress JWTs. +==== JWE Compression - -### Reading a JWE +If your JWT payload or Claims set is large (contains a lot of data), you might want to compress the JWE to reduce +its size. Please see the main <> section to see how to compress and decompress JWTs. + +++++++++++++ + +=== Reading a JWE You read (parse) a JWE as follows: -1. Use the `Jwts.parser()` method to create a `JwtParserBuilder` instance. -2. Call either [keyLocator](#key-locator) or `decryptWith` methods to determine the key used to decrypt the JWE. -4. Call the `JwtParserBuilder`'s `build()` method to create a thread-safe `JwtParser`. -5. Parse the jwe string with the `JwtParser`'s `parseEncryptedClaims` or `parseEncryptedContent` method. -6. Wrap the entire call is in a try/catch block in case decryption or integrity verification fails. +. Use the `Jwts.parser()` method to create a `JwtParserBuilder` instance. +. Call either <> or `decryptWith` methods to determine the key used to decrypt the JWE. +. Call the ``JwtParserBuilder``'s `build()` method to create a thread-safe `JwtParser`. +. Parse the jwe string with the ``JwtParser``'s `parseEncryptedClaims` or `parseEncryptedContent` method. +. Wrap the entire call is in a try/catch block in case decryption or integrity verification fails. For example: -```java +[,java] +---- Jwe jwe; try { jwe = Jwts.parser() // (1) - .keyLocator(keyLocator) // (2) dynamically lookup decryption keys based on each JWE + .keyLocator(keyLocator) // (2) dynamically lookup decryption keys based on each JWE //.decryptWith(key) // or a static key used to decrypt all encountered JWEs - + .build() // (3) .parseEncryptedClaims(jweString); // (4) or parseEncryptedContent(jweString); - + // we can safely trust the JWT - + catch (JwtException ex) { // (5) - + // we *cannot* use the JWT as intended by its creator } -``` +---- -> **Note** -> -> **Type-safe JWEs:** -> * If you are expecting a JWE with a Claims `payload`, call the `JwtParser`'s `parseEncryptedClaims` method. -> * If you are expecting a JWE with a content `payload`, call the `JwtParser`'s `parseEncryptedContent` method. +[NOTE] +==== +.Type-safe JWEs - -#### Decryption Key +* If you are expecting a JWE with a Claims `payload`, call the ``JwtParser``'s `parseEncryptedClaims` method. +* If you are expecting a JWE with a content `payload`, call the ``JwtParser``'s `parseEncryptedContent` method. +==== + +++++++++++++ + +==== Decryption Key The most important thing to do when reading a JWE is to specify the key used during decryption. If decryption or integrity protection checks fail, the JWT cannot be safely trusted and should be discarded. So which key do we use for decryption? -* If the jwe was encrypted _directly_ with a `SecretKey`, the same `SecretKey` must be specified on the - `JwtParserBuilder`. For example: +* If the jwe was encrypted _directly_ with a `SecretKey`, the same `SecretKey` must be specified on the +`JwtParserBuilder`. For example: ++ +[,java] +---- +Jwts.parser() - ```java - Jwts.parser() - - .decryptWith(secretKey) // <---- - - .build() - .parseEncryptedClaims(jweString); - ``` -* If the jwe was encrypted using a key produced by a Password-based key derivation `KeyAlgorithm`, the same - `Password` must be specified on the `JwtParserBuilder`. For example: + .decryptWith(secretKey) // <---- - ```java - Password password = Keys.password(passwordChars); - - Jwts.parser() - - .decryptWith(password) // <---- an `io.jsonwebtoken.security.Password` instance - - .build() - .parseEncryptedClaims(jweString); - ``` -* If the jwe was encrypted with a key produced by an asymmetric `KeyAlgorithm`, the corresponding `PrivateKey` (not - the `PublicKey`) must be specified on the `JwtParserBuilder`. For example: + .build() + .parseEncryptedClaims(jweString); +---- - ```java - Jwts.parser() - - .decryptWith(privateKey) // <---- a `PrivateKey`, not a `PublicKey` - - .build() - .parseSignedClaims(jweString); - ``` +* If the jwe was encrypted using a key produced by a Password-based key derivation `KeyAlgorithm`, the same +`Password` must be specified on the `JwtParserBuilder`. For example: ++ +[,java] +---- +Password password = Keys.password(passwordChars); - -#### Decryption Key Locator +Jwts.parser() + + .decryptWith(password) // <---- an `io.jsonwebtoken.security.Password` instance + + .build() + .parseEncryptedClaims(jweString); +---- + +* If the jwe was encrypted with a key produced by an asymmetric `KeyAlgorithm`, the corresponding `PrivateKey` (not +the `PublicKey`) must be specified on the `JwtParserBuilder`. For example: ++ +[,java] +---- +Jwts.parser() + + .decryptWith(privateKey) // <---- a `PrivateKey`, not a `PublicKey` + + .build() + .parseSignedClaims(jweString); +---- + +++++++++++++ + +==== Decryption Key Locator What if your application doesn't use just a single `SecretKey` or `KeyPair`? What -if JWEs can be created with different `SecretKey`s, `Password`s or public/private keys, or a combination of all of +if JWEs can be created with different ``SecretKey``s, ``Password``s or public/private keys, or a combination of all of them? How do you know which key to specify if you can't inspect the JWT first? -In these cases, you can't call the `JwtParserBuilder`'s `decryptWith` method with a single key - instead, you'll need -to use a Key `Locator`. Please see the [Key Lookup](#key-locator) section to see how to dynamically obtain different +In these cases, you can't call the ``JwtParserBuilder``'s `decryptWith` method with a single key - instead, you'll need +to use a Key `Locator`. Please see the <> section to see how to dynamically obtain different keys when parsing JWSs or JWEs. - -#### ECDH-ES Decryption with PKCS11 PrivateKeys +++++++++++++ + +==== ECDH-ES Decryption with PKCS11 PrivateKeys The JWT `ECDH-ES`, `ECDH-ES+A128KW`, `ECDH-ES+A192KW`, and `ECDH-ES+A256KW` key algorithms validate JWE input using -public key information, even when using `PrivateKey`s to decrypt. Ordinarily this is automatically performed -by JJWT when your `PrivateKey` instances implement the -[ECKey](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/security/interfaces/ECKey.html) or -[EdECKey](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/security/interfaces/EdECKey.html) +public key information, even when using ``PrivateKey``s to decrypt. Ordinarily this is automatically performed +by JJWT when your `PrivateKey` instances implement the +https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/security/interfaces/ECKey.html[ECKey] or +https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/security/interfaces/EdECKey.html[EdECKey] (or BouncyCastle equivalent) interfaces, which is the case for most JCA `Provider` implementations. -However, if your decryption `PrivateKey`s are stored in a Hardware Security Module (HSM) and/or you use the -[SunPKCS11 Provider](https://docs.oracle.com/en/java/javase/17/security/pkcs11-reference-guide1.html#GUID-6DA72F34-6C6A-4F7D-ADBA-5811576A9331), +However, if your decryption ``PrivateKey``s are stored in a Hardware Security Module (HSM) and/or you use the +https://docs.oracle.com/en/java/javase/17/security/pkcs11-reference-guide1.html#GUID-6DA72F34-6C6A-4F7D-ADBA-5811576A9331[SunPKCS11 Provider], it is likely that your `PrivateKey` instances _do not_ implement `ECKey`. In these cases, you need to provide both the PKCS11 `PrivateKey` and it's companion `PublicKey` during decryption by using the `Keys.builder` method. For example: -```java +[,java] +---- KeyPair pair = getMyPkcs11KeyPair(); PrivateKey jwtParserDecryptionKey = Keys.builder(pair.getPrivate()) .publicKey(pair.getPublic()) // PublicKey must implement ECKey or EdECKey or BouncyCastle equivalent .build(); -``` +---- -You then use the resulting `jwtParserDecryptionKey` (not `pair.getPrivate()`) with the `JwtParserBuilder` or as -the return value from a custom [Key Locator](#key-locator) implementation. For example: +You then use the resulting `jwtParserDecryptionKey` (not `pair.getPrivate()`) with the `JwtParserBuilder` or as +the return value from a custom <> implementation. For example: -```java +[,java] +---- PrivateKey decryptionKey = Keys.builder(pkcs11PrivateKey).publicKey(pkcs11PublicKey).build(); Jwts.parser() .decryptWith(decryptionKey) // <---- .build() .parseEncryptedClaims(jweString); -``` +---- Or as the return value from your key locator: -```java +[,java] +---- Jwts.parser() .keyLocator(keyLocator) // your keyLocator.locate(header) would return Keys.builder... .build() .parseEncryptedClaims(jweString); -``` +---- -Please see the [Provider-constrained Keys](#key-locator-provider) section for more information, as well as +Please see the <> section for more information, as well as code examples of how to implement a Key `Locator` using the `Keys.builder` technique. - -#### JWE Decompression +++++++++++++ -If a JWE is compressed using the `DEF` ([DEFLATE](https://www.rfc-editor.org/rfc/rfc1951)) or `GZIP` -([GZIP](https://www.rfc-editor.org/rfc/rfc1952.html)) compression algorithms, it will automatically be decompressed +==== JWE Decompression + +If a JWE is compressed using the `DEF` (https://www.rfc-editor.org/rfc/rfc1951[DEFLATE]) or `GZIP` +(https://www.rfc-editor.org/rfc/rfc1952.html[GZIP]) compression algorithms, it will automatically be decompressed after decryption, and there is nothing you need to configure. If, however, a custom compression algorithm was used to compress the JWE, you will need to tell the `JwtParserBuilder` how to resolve your `CompressionAlgorithm` to decompress the JWT. -Please see the [Compression](#compression) section below to see how to decompress JWTs during parsing. +Please see the <> section below to see how to decompress JWTs during parsing. - -## JSON Web Keys (JWKs) +++++++++++++ -[JSON Web Keys](https://www.rfc-editor.org/rfc/rfc7517.html) (JWKs) are JSON serializations of cryptographic keys, +== JSON Web Keys (JWKs) + +https://www.rfc-editor.org/rfc/rfc7517.html[JSON Web Keys] (JWKs) are JSON serializations of cryptographic keys, allowing key material to be embedded in JWTs or transmitted between parties in a standard JSON-based text format. They are essentially a JSON-based alternative to other text-based key formats, such as the -[DER, PEM and PKCS12](https://serverfault.com/a/9717) text strings or files commonly used when configuring TLS on web +https://serverfault.com/a/9717[DER, PEM and PKCS12] text strings or files commonly used when configuring TLS on web servers, for example. For example, an identity web service may expose its RSA or Elliptic Curve Public Keys to 3rd parties in the JWK format. -A client may then parse the public key JWKs to verify the service's [JWS](#jws) tokens, as well as send encrypted -information to the service using [JWE](#jwe)s. +A client may then parse the public key JWKs to verify the service's <> tokens, as well as send encrypted +information to the service using <>s. JWKs can be converted to and from standard Java `Key` types as expected using the same builder/parser patterns we've seen for JWTs. - -### Create a JWK +++++++++++++ + +=== Create a JWK You create a JWK as follows: -1. Use the `Jwks.builder()` method to create a `JwkBuilder` instance. -2. Call the `key` method with the Java key you wish to represent as a JWK. -3. Call builder methods to set any additional key parameters or metadata, such as a `kid` (Key ID), X509 Certificates, - etc as desired. -4. Call the `build()` method to produce the resulting JWK. +. Use the `Jwks.builder()` method to create a `JwkBuilder` instance. +. Call the `key` method with the Java key you wish to represent as a JWK. +. Call builder methods to set any additional key parameters or metadata, such as a `kid` (Key ID), X509 Certificates, +etc as desired. +. Call the `build()` method to produce the resulting JWK. For example: -```java +[,java] +---- SecretKey key = getSecretKey(); // or RSA or EC PublicKey or PrivateKey SecretJwk = Jwks.builder().key(key) // (1) and (2) - - .id("mySecretKeyId") // (3) - // ... etc ... - - .build(); // (4) -``` -#### JWK from a Map + .id("mySecretKeyId") // (3) + // ... etc ... + + .build(); // (4) +---- + +==== JWK from a Map If you have a `Map` of name/value pairs that reflect an existing JWK, you add them and build a type-safe `Jwk` instance: -```java +[,java] +---- Map jwkValues = getMyJwkMap(); Jwk jwk = Jwks.builder().add(jwkValues).build(); -``` +---- - -### Read a JWK +++++++++++++ + +=== Read a JWK You can read/parse a JWK by building a `JwkParser` and parsing the JWK JSON string with its `parse` method: -```java +[,java] +---- String json = getJwkJsonString(); Jwk jwk = Jwks.parser() //.provider(aJcaProvider) // optional @@ -2580,46 +2859,52 @@ Jwk jwk = Jwks.parser() .parse(json); // actually parse the JSON Key key = jwk.toKey(); // convert to a Java Key instance -``` -As shown above you can specify a custom JCA Provider or [JSON deserializer](#json) in the same way as the `JwtBuilder`. +---- - -### PrivateKey JWKs +As shown above you can specify a custom JCA Provider or <> in the same way as the `JwtBuilder`. + +++++++++++++ + +=== PrivateKey JWKs Unlike Java, the JWA specification requires a private JWKs to contain _both_ public key _and_ private key material -(see [RFC 7518, Section 6.1.1](https://www.rfc-editor.org/rfc/rfc7518.html#section-6.2.2) and -[RFC 7518, Section 6.3.2](https://www.rfc-editor.org/rfc/rfc7518.html#section-6.3.2)). +(see https://www.rfc-editor.org/rfc/rfc7518.html#section-6.2.2[RFC 7518, Section 6.1.1] and +https://www.rfc-editor.org/rfc/rfc7518.html#section-6.3.2[RFC 7518, Section 6.3.2]). -In this sense, a private JWK (represented as a `PrivateJwk` or a subtype, such as `RsaPrivateJwk`, `EcPrivateJwk`, etc) -can be thought of more like a Java `KeyPair` instance. Consequently, when creating a `PrivateJwk` instance, -the `PrivateKey`'s corresponding `PublicKey` is required. +In this sense, a private JWK (represented as a `PrivateJwk` or a subtype, such as `RsaPrivateJwk`, `EcPrivateJwk`, etc) +can be thought of more like a Java `KeyPair` instance. Consequently, when creating a `PrivateJwk` instance, +the ``PrivateKey``'s corresponding `PublicKey` is required. - -#### Private JWK `PublicKey` +++++++++++++ -If you do not provide a `PublicKey` when creating a `PrivateJwk`, JJWT will automatically derive the `PublicKey` from -the `PrivateKey` instance if possible. However, because this can add +==== Private JWK `PublicKey` + +If you do not provide a `PublicKey` when creating a `PrivateJwk`, JJWT will automatically derive the `PublicKey` from +the `PrivateKey` instance if possible. However, because this can add some computing time, it is typically recommended to provide the `PublicKey` when possible to avoid this extra work. For example: -```java +[,java] +---- RSAPrivateKey rsaPrivateKey = getRSAPrivateKey(); // or ECPrivateKey RsaPrivateJwk jwk = Jwks.builder().key(rsaPrivateKey) //.publicKey(rsaPublicKey) // optional, but recommended to avoid extra computation work - - .build(); -``` - -#### Private JWK from KeyPair + .build(); +---- + +++++++++++++ + +==== Private JWK from KeyPair If you have a Java `KeyPair` instance, then you have both the public and private key material necessary to create a `PrivateJwk`. For example: -```java +[,java] +---- KeyPair rsaKeyPair = getRSAKeyPair(); RsaPrivateJwk rsaPrivJwk = Jwks.builder().rsaKeyPair(rsaKeyPair).build(); @@ -2628,24 +2913,27 @@ EcPrivateJwk ecPrivJwk = Jwks.builder().ecKeyPair(ecKeyPair).build(); KeyPair edEcKeyPair = getEdECKeyPair(); OctetPrivateJwk edEcPrivJwk = Jwks.builder().octetKeyPair(edEcKeyPair).build(); -``` +---- Note that: + * An exception will be thrown when calling `rsaKeyPair` if the specified `KeyPair` instance does not contain -`RSAPublicKey` and `RSAPrivateKey` instances. +`RSAPublicKey` and `RSAPrivateKey` instances. * Similarly, an exception will be thrown when calling `ecKeyPair` if -the `KeyPair` instance does not contain `ECPublicKey` and `ECPrivateKey` instances. -* Finally, an exception will be +the `KeyPair` instance does not contain `ECPublicKey` and `ECPrivateKey` instances. +* Finally, an exception will be thrown when calling `octetKeyPair` if the `KeyPair` instance does not contain X25519, X448, Ed25519, or Ed448 keys (introduced in JDK 11 and 15 or when using BouncyCastle). - -#### Private JWK Public Conversion +++++++++++++ + +==== Private JWK Public Conversion Because private JWKs contain public key material, you can always obtain the private JWK's corresponding public JWK and Java `PublicKey` or `KeyPair`. For example: -```java +[,java] +---- RsaPrivateJwk privateJwk = Jwks.builder().key(rsaPrivateKey).build(); // or ecPrivateKey or edEcPrivateKey // Get the matching public JWK and/or PublicKey: @@ -2653,20 +2941,22 @@ RsaPublicJwk pubJwk = privateJwk.toPublicJwk(); // JWK instance RSAPublicKey pubKey = pubJwk.toKey(); // Java PublicKey instance KeyPair pair = privateJwk.toKeyPair(); // io.jsonwebtoken.security.KeyPair retains key types java.security.KeyPair jdkPair = pair.toJavaKeyPair(); // does not retain pub/private key types -``` +---- - -### JWK Thumbprints +++++++++++++ -A [JWK Thumbprint](https://www.rfc-editor.org/rfc/rfc7638.html) is a digest (aka hash) of a canonical JSON +=== JWK Thumbprints + +A https://www.rfc-editor.org/rfc/rfc7638.html[JWK Thumbprint] is a digest (aka hash) of a canonical JSON representation of a JWK's public properties. 'Canonical' in this case means that only RFC-specified values in any JWK -are used in an exact order thumbprint calculation. This ensures that anyone can calculate a JWK's same exact +are used in an exact order thumbprint calculation. This ensures that anyone can calculate a JWK's same exact thumbprint, regardless of custom parameters or JSON key/value ordering differences in a JWK. -All `Jwk` instances support [JWK Thumbprint](https://www.rfc-editor.org/rfc/rfc7638.html)s via the +All `Jwk` instances support https://www.rfc-editor.org/rfc/rfc7638.html[JWK Thumbprint]s via the `thumbprint()` and `thumbprint(HashAlgorithm)` methods: -```java +[,java] +---- HashAlgorithm hashAlg = Jwks.HASH.SHA256; // or SHA384, SHA512, etc. Jwk jwk = Jwks.builder(). /* ... */ .build(); @@ -2674,141 +2964,157 @@ Jwk jwk = Jwks.builder(). /* ... */ .build(); JwkThumbprint sha256Thumbprint = jwk.thumbprint(); // SHA-256 thumbprint by default JwkThumbprint anotherThumbprint = jwk.thumbprint(Jwks.HASH.SHA512); // or a specified hash algorithm -``` +---- The resulting `JwkThumbprint` instance provides some useful methods: * `jwkThumbprint.toByteArray()`: the thumbprint's actual digest bytes - i.e. the raw output from the hash algorithm * `jwkThumbprint.toString()`: the digest bytes as a Base64URL-encoded string * `jwkThumbprint.getHashAlgorithm()`: the specific `HashAlgorithm` used to compute the thumbprint. Many standard IANA - hash algorithms are available as constants in the `Jwks.HASH` utility class. -* `jwkThumbprint.toURI()`: the thumbprint's canonical URI as defined by the [JWK Thumbprint URI](https://www.rfc-editor.org/rfc/rfc9278.html) specification + hash algorithms are available as constants in the `Jwks.HASH` utility class. +* `jwkThumbprint.toURI()`: the thumbprint's canonical URI as defined by the https://www.rfc-editor.org/rfc/rfc9278.html[JWK Thumbprint URI] specification - -#### JWK Thumbprint as a Key ID +++++++++++++ + +==== JWK Thumbprint as a Key ID Because a thumbprint is an order-guaranteed unique digest of a JWK, JWK thumbprints are often used as convenient unique identifiers for a JWK (e.g. the JWK's `kid` (Key ID) value). These identifiers can be useful when -[locating keys](#key-locator) for JWS signature verification or JWE decryption, for example. +<> for JWS signature verification or JWE decryption, for example. For example: -```java +[,java] +---- String kid = jwk.thumbprint().toString(); // Thumbprint bytes as a Base64URL-encoded string Key key = findKey(kid); assert jwk.toKey().equals(key); -``` +---- However, because `Jwk` instances are immutable, you can't set the key id after the JWK is created. For example, the following is not possible: -```java +[,java] +---- String kid = jwk.thumbprint().toString(); jwk.setId(kid) // Jwks are immutable - there is no `setId` method -``` +---- Instead, you may use the `idFromThumbprint` methods on the `JwkBuilder` when creating a `Jwk`: -```java +[,java] +---- Jwk jwk = Jwks.builder().key(aKey) .idFromThumbprint() // or idFromThumbprint(HashAlgorithm) .build(); -``` +---- Calling either `idFromThumbprint` method will ensure that calling `jwk.getId()` equals `thumbprint.toString()` (which is `Encoders.BASE64URL.encode(thumbprint.toByteArray())`). - -#### JWK Thumbprint URI +++++++++++++ -A JWK's thumbprint's canonical URI as defined by the [JWK Thumbprint URI](https://www.rfc-editor.org/rfc/rfc9278.html) +==== JWK Thumbprint URI + +A JWK's thumbprint's canonical URI as defined by the https://www.rfc-editor.org/rfc/rfc9278.html[JWK Thumbprint URI] specification may be obtained by calling the thumbprint's `toURI()` method: -```java +[,java] +---- URI canonicalThumbprintURI = jwk.thumbprint().toURI(); -``` +---- Per the RFC specification, if you call `canonicalThumbprintURI.toString()`, you would see a string that looks like this: -```text +[,text] +---- urn:ietf:params:oauth:jwk-thumbprint:HASH_ALG_ID:BASE64URL_DIGEST -``` +---- where: + * `urn:ietf:params:oauth:jwk-thumbprint:` is the URI scheme+prefix * `HASH_ALG_ID` is the standard identifier used to compute the thumbprint as defined in the - [IANA Named Information Hash Algorithm Registry](https://www.iana.org/assignments/named-information/named-information.xhtml). - This is the same as `thumbprint.getHashAlgorithm().getId()`. +https://www.iana.org/assignments/named-information/named-information.xhtml[IANA Named Information Hash Algorithm Registry]. +This is the same as `thumbprint.getHashAlgorithm().getId()`. * `BASE64URL_DIGEST` is the Base64URL-encoded thumbprint bytes, equal to `jwkThumbprint.toString()`. - -### JWK Security Considerations +++++++++++++ -Because they contain secret or private key material, `SecretJwk` and `PrivateJwk` (e.g. `RsaPrivateJwk`, +=== JWK Security Considerations + +Because they contain secret or private key material, `SecretJwk` and `PrivateJwk` (e.g. `RsaPrivateJwk`, + `EcPrivateJwk`, etc) instances should be used with great care and never accidentally transmitted to 3rd parties. -Even so, JJWT's `Jwk` implementations will suppress certain values in `toString()` output for safety as described +Even so, JJWT's `Jwk` implementations will suppress certain values in `toString()` output for safety as described next. - -#### JWK `toString()` Safety +++++++++++++ -Because it would be incredibly easy to accidentally print key material to `System.out.println()` or application +==== JWK `toString()` Safety + +Because it would be incredibly easy to accidentally print key material to `System.out.println()` or application logs, all `Jwk` implementations will print redacted values instead of actual secret or private key material. -For example, consider the following Secret JWK JSON example from -[RFC 7515, Appendix A.1.1](https://www.rfc-editor.org/rfc/rfc7515#appendix-A.1.1): +For example, consider the following Secret JWK JSON example from +https://www.rfc-editor.org/rfc/rfc7515#appendix-A.1.1[RFC 7515, Appendix A.1.1]: -```json +[,json] +---- { "kty": "oct", "k": "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow", "kid": "HMAC key used in https://www.rfc-editor.org/rfc/rfc7515#appendix-A.1.1 example." } -``` +---- -The `k` value (`AyAyM1SysPpby...`) reflects secure key material and should never be accidentally +The `k` value (`+AyAyM1SysPpby...+`) reflects secure key material and should never be accidentally exposed. -If you were to parse this JSON as a `Jwk`, calling `toString()` will _NOT_ print this value. It will +If you were to parse this JSON as a `Jwk`, calling `toString()` will _NOT_ print this value. It will instead print the string literal `` for any secret or private key data value. For example: -```java +[,java] +---- String json = getExampleSecretKeyJson(); Jwk jwk = Jwks.parser().build().parse(json); System.out.printn(jwk); -``` +---- This code would print the following string literal to the System console: -```text +[,text] +---- {kty=oct, k=, kid=HMAC key used in https://www.rfc-editor.org/rfc/rfc7515#appendix-A.1.1 example.} -``` +---- -This is true for all secret or private key members in `SecretJwk` and `PrivateJwk` (e.g. `RsaPrivateJwk`, +This is true for all secret or private key members in `SecretJwk` and `PrivateJwk` (e.g. `RsaPrivateJwk`, `EcPrivateJwk`, etc) instances. - -## JWK Sets +++++++++++++ + +== JWK Sets The JWK specification specification also defines the concept of a -[JWK Set](https://datatracker.ietf.org/doc/html/rfc7517#section-5): +https://datatracker.ietf.org/doc/html/rfc7517#section-5[JWK Set]: - A JWK Set is a JSON object that represents a set of JWKs. The JSON - object MUST have a "keys" member, with its value being an array of - JWKs. + A JWK Set is a JSON object that represents a set of JWKs. The JSON + object MUST have a "keys" member, with its value being an array of + JWKs. For example: -```json +[,txt] +---- { "keys": [jwk1, jwk2, ...] } -``` -Where `jwk1`, `jwk2`, etc., are each a single [JWK](#jwk) JSON Object. +---- + +Where `jwk1`, `jwk2`, etc., are each a single <> JSON Object. A JWK Set _may_ have other members that are peers to the `keys` member, but the JWK specification does not define any others - any such additional members would be custom or unique based on an application's needs or preferences. @@ -2821,20 +3127,22 @@ to obtain the keys that might be used to verify or decrypt JWTs sent by the web JWK Sets are (mostly) simple collections of JWKs, and they are easily supported by JJWT with parallel builder/parser concepts we've seen above. - -### Create a JWK Set +++++++++++++ + +=== Create a JWK Set You create a JWK Set as follows: -1. Use the `Jwks.set()` method to create a `JwkSetBuilder` instance. -2. Call the `add(Jwk)` method any number of times to add one or more JWKs to the set. -3. Call builder methods to set any additional JSON members if desired, or the `operationPolicy(KeyOperationPolicy)` - builder method to control what key operations may be assigned to any given JWK added to the set. -4. Call the `build()` method to produce the resulting JWK Set. +. Use the `Jwks.set()` method to create a `JwkSetBuilder` instance. +. Call the `add(Jwk)` method any number of times to add one or more JWKs to the set. +. Call builder methods to set any additional JSON members if desired, or the `operationPolicy(KeyOperationPolicy)` +builder method to control what key operations may be assigned to any given JWK added to the set. +. Call the `build()` method to produce the resulting JWK Set. For example: -```java +[,java] +---- Jwk jwk = Jwks.builder()/* ... */.build(); SecretJwk = Jwks.set() // 1 .add(jwk) // 2, appends a key @@ -2844,24 +3152,26 @@ SecretJwk = Jwks.set() // 1 //.operationPolicy(Jwks.OP // 3, optional // .policy() // /* etc... */ - // .build()) + // .build()) //.provider(aJcaProvider) // optional .build(); // (4) -``` +---- -As shown, you can optionally configure the `.operationPolicy(KeyOperationPolicy)` method using a +As shown, you can optionally configure the `.operationPolicy(KeyOperationPolicy)` method using a `Jwts.OP.policy()` builder. A `KeyOperationPolicy` allows you control what operations are allowed for any JWK -before being added to the JWK Set; any JWK that does not match the policy will be rejected and not added to the set. -JJWT internally defaults to a standard RFC-compliant policy, but you can create a +before being added to the JWK Set; any JWK that does not match the policy will be rejected and not added to the set. +JJWT internally defaults to a standard RFC-compliant policy, but you can create a policy to override the default if desired using the `Jwks.OP.policy()` builder method. - -### Read a JWK Set +++++++++++++ -You can read/parse a JWK Set by building a JWK Set `Parser` and parsing the JWK Set JSON with one of its various +=== Read a JWK Set + +You can read/parse a JWK Set by building a JWK Set `Parser` and parsing the JWK Set JSON with one of its various `parse` methods: -```java +[,java] +---- JwkSet jwkSet = Jwks.setParser() //.provider(aJcaProvider) // optional //.deserializer(deserializer) // optional @@ -2870,228 +3180,246 @@ JwkSet jwkSet = Jwks.setParser() .parse(json); // actually parse JSON String, InputStream, Reader, etc. jwkSet.forEach(jwk -> System.out.println(jwk)); -``` +---- -As shown above, you can specify a custom JCA Provider, [JSON deserializer](#json) or `KeyOperationPolicy` in the -same way as the `JwkSetBuilder`. Any JWK that does not match the default (or configured) policy will be +As shown above, you can specify a custom JCA Provider, <> or `KeyOperationPolicy` in the +same way as the `JwkSetBuilder`. Any JWK that does not match the default (or configured) policy will be rejected. You can create a policy to override the default if desired using the `Jwks.OP.policy()` builder method. - -## Compression +++++++++++++ -> **Warning** -> -> **The JWT specification standardizes compression for JWEs (Encrypted JWTs) ONLY, however JJWT supports it for JWS -> (Signed JWTs) as well**. -> -> If you are positive that a JWS you create with JJWT will _also_ be parsed with JJWT, -> you can use this feature with both JWEs and JWSs, otherwise it is best to only use it for JWEs. +== Compression -If a JWT's `payload` is sufficiently large - that is, it is a large content byte array or JSON with a lot of -name/value pairs (or individual values are very large or verbose) - you can reduce the size of the compact JWT by +[WARNING] +==== +The JWT specification standardizes compression for JWEs (Encrypted JWTs) ONLY, however JJWT supports it for JWS +(Signed JWTs) as well. + +If you are positive that a JWS you create with JJWT will _also_ be parsed with JJWT, +you can use this feature with both JWEs and JWSs, otherwise it is best to only use it for JWEs. +==== + +If a JWT's `payload` is sufficiently large - that is, it is a large content byte array or JSON with a lot of +name/value pairs (or individual values are very large or verbose) - you can reduce the size of the compact JWT by compressing the payload. -This might be important to you if the resulting JWT is used in a URL for example, since URLs are best kept under -4096 characters due to browser, user mail agent, or HTTP gateway compatibility issues. Smaller JWTs also help reduce +This might be important to you if the resulting JWT is used in a URL for example, since URLs are best kept under +4096 characters due to browser, user mail agent, or HTTP gateway compatibility issues. Smaller JWTs also help reduce bandwidth utilization, which may or may not be important depending on your application's volume or needs. -If you want to compress your JWT, you can use the `JwtBuilder`'s `compressWith(CompressionAlgorithm)` method. For +If you want to compress your JWT, you can use the ``JwtBuilder``'s `compressWith(CompressionAlgorithm)` method. For example: -```java +[,java] +---- Jwts.builder() - - .compressWith(Jwts.ZIP.DEF) // DEFLATE compression algorithm - - // .. etc ... -``` -If you use any of the algorithm constants in the `Jwts.ZIP` class, that's it, you're done. You don't have to -do anything during parsing or configure the `JwtParserBuilder` for compression - JJWT will automatically decompress + .compressWith(Jwts.ZIP.DEF) // DEFLATE compression algorithm + + // .. etc ... +---- + +If you use any of the algorithm constants in the `Jwts.ZIP` class, that's it, you're done. You don't have to +do anything during parsing or configure the `JwtParserBuilder` for compression - JJWT will automatically decompress the payload as expected. - -### Custom Compression Algorithm +++++++++++++ -If the default `Jwts.ZIP` compression algorithms are not suitable for your needs, you can create your own +=== Custom Compression Algorithm + +If the default `Jwts.ZIP` compression algorithms are not suitable for your needs, you can create your own `CompressionAlgorithm` implementation(s). -Just as you would with the default algorithms, you may specify that you want a JWT compressed by calling the -`JwtBuilder`'s `compressWith` method, supplying your custom implementation instance. For example: +Just as you would with the default algorithms, you may specify that you want a JWT compressed by calling the +``JwtBuilder``'s `compressWith` method, supplying your custom implementation instance. For example: -```java +[,java] +---- CompressionAlgorithm myAlg = new MyCompressionAlgorithm(); Jwts.builder() - - .compressWith(myAlg) // <---- - - // .. etc ... -``` -When you call `compressWith`, the JWT `payload` will be compressed with your algorithm, and the -[`zip` (Compression Algorithm)](https://www.rfc-editor.org/rfc/rfc7516.html#section-4.1.3) -header will automatically be set to the value returned by your algorithm's `algorithm.getId()` method as + .compressWith(myAlg) // <---- + + // .. etc ... +---- + +When you call `compressWith`, the JWT `payload` will be compressed with your algorithm, and the +https://www.rfc-editor.org/rfc/rfc7516.html#section-4.1.3[`zip` (Compression Algorithm)] +header will automatically be set to the value returned by your algorithm's `algorithm.getId()` method as required by the JWT specification. - -However, the `JwtParser` needs to be aware of this custom algorithm as well, so it can use it while parsing. You do this -by modifying the `JwtParserBuilder`'s `zip()` collection. For example: +++++++++++++ +// legacy link +However, the `JwtParser` needs to be aware of this custom algorithm as well, so it can use it while parsing. You do this +by modifying the ``JwtParserBuilder``'s `zip()` collection. For example: -```java +[,java] +---- CompressionAlgorithm myAlg = new MyCompressionAlgorithm(); Jwts.parser() .zip().add(myAlg).and() // <---- - + // .. etc ... -``` +---- This adds additional `CompressionAlgorithm` implementations to the parser's overall total set of supported compression algorithms (which already includes all of the `Jwts.ZIP` algorithms by default). The parser will then automatically check to see if the JWT `zip` header has been set to see if a compression -algorithm has been used to compress the JWT. If set, the parser will automatically look up your +algorithm has been used to compress the JWT. If set, the parser will automatically look up your `CompressionAlgorithm` by its `getId()` value, and use it to decompress the JWT. - -## JSON Support +++++++++++++ -A `JwtBuilder` will serialize the `Header` and `Claims` maps (and potentially any Java objects they -contain) to JSON with a `Serializer>` instance. Similarly, a `JwtParser` will +== JSON Support + +A `JwtBuilder` will serialize the `Header` and `Claims` maps (and potentially any Java objects they +contain) to JSON with a `Serializer>` instance. Similarly, a `JwtParser` will deserialize JSON into the `Header` and `Claims` using a `Deserializer>` instance. -If you don't explicitly configure a `JwtBuilder`'s `Serializer` or a `JwtParserBuilder`'s `Deserializer`, JJWT will -automatically attempt to discover and use the following JSON implementations if found in the runtime classpath. +If you don't explicitly configure a ``JwtBuilder``'s `Serializer` or a ``JwtParserBuilder``'s `Deserializer`, JJWT will +automatically attempt to discover and use the following JSON implementations if found in the runtime classpath. + They are checked in order, and the first one found is used: -1. Jackson: This will automatically be used if you specify `io.jsonwebtoken:jjwt-jackson` as a project runtime - dependency. Jackson supports POJOs as claims with full marshaling/unmarshaling as necessary. - -2. Gson: This will automatically be used if you specify `io.jsonwebtoken:jjwt-gson` as a project runtime dependency. - Gson also supports POJOs as claims with full marshaling/unmarshaling as necessary. - -3. JSON-Java (`org.json`): This will be used automatically if you specify `io.jsonwebtoken:jjwt-orgjson` as a - project runtime dependency. - - > **Note** - > - > `org.json` APIs are natively enabled in Android environments so this is the recommended JSON processor for - > Android applications _unless_ you want to use POJOs as claims. The `org.json` library supports simple - > Object-to-JSON marshaling, but it *does not* support JSON-to-Object unmarshalling. +. Jackson: This will automatically be used if you specify `io.jsonwebtoken:jjwt-jackson` as a project runtime +dependency. Jackson supports POJOs as claims with full marshaling/unmarshaling as necessary. +. Gson: This will automatically be used if you specify `io.jsonwebtoken:jjwt-gson` as a project runtime dependency. +Gson also supports POJOs as claims with full marshaling/unmarshaling as necessary. +. JSON-Java (`org.json`): This will be used automatically if you specify `io.jsonwebtoken:jjwt-orgjson` as a +project runtime dependency. ++ +[NOTE] +==== +`org.json` APIs are natively enabled in Android environments so this is the recommended JSON processor for +Android applications _unless_ you want to use POJOs as claims. The `org.json` library supports simple +Object-to-JSON marshaling, but it _does not_ support JSON-to-Object unmarshalling. +==== -**If you want to use POJOs as claim values, use either the `io.jsonwebtoken:jjwt-jackson` or -`io.jsonwebtoken:jjwt-gson` dependency** (or implement your own Serializer and Deserializer if desired). **But beware**, -Jackson will force a sizable (> 1 MB) dependency to an Android application thus increasing the app download size for +*If you want to use POJOs as claim values, use either the `io.jsonwebtoken:jjwt-jackson` or +`io.jsonwebtoken:jjwt-gson` dependency* (or implement your own Serializer and Deserializer if desired). *But beware*, +Jackson will force a sizable (> 1 MB) dependency to an Android application thus increasing the app download size for mobile users. - -### Custom JSON Processor +++++++++++++ -If you don't want to use JJWT's runtime dependency approach, or just want to customize how JSON serialization and +=== Custom JSON Processor + +If you don't want to use JJWT's runtime dependency approach, or just want to customize how JSON serialization and deserialization works, you can implement the `Serializer` and `Deserializer` interfaces and specify instances of them on the `JwtBuilder` and `JwtParserBuilder` respectively. For example: When creating a JWT: -```java +[,java] +---- Serializer> serializer = getMySerializer(); //implement me Jwts.builder() .json(serializer) - + // ... etc ... -``` +---- When reading a JWT: -```java +[,java] +---- Deserializer> deserializer = getMyDeserializer(); //implement me Jwts.parser() .json(deserializer) - - // ... etc ... -``` - -### Jackson JSON Processor + // ... etc ... +---- + +++++++++++++ + +=== Jackson JSON Processor If you want to use Jackson for JSON processing, just including the `io.jsonwebtoken:jjwt-jackson` dependency as a runtime dependency is all that is necessary in most projects, since Gradle and Maven will automatically pull in the necessary Jackson dependencies as well. -After including this dependency, JJWT will automatically find the Jackson implementation on the runtime classpath and +After including this dependency, JJWT will automatically find the Jackson implementation on the runtime classpath and use it internally for JSON parsing. There is nothing else you need to do - JJWT will automatically create a new Jackson ObjectMapper for its needs as required. -However, if you have an application-wide Jackson `ObjectMapper` (as is typically recommended for most applications), +However, if you have an application-wide Jackson `ObjectMapper` (as is typically recommended for most applications), you can configure JJWT to use your own `ObjectMapper` instead. -You do this by declaring the `io.jsonwebtoken:jjwt-jackson` dependency with **compile** scope (not runtime +You do this by declaring the `io.jsonwebtoken:jjwt-jackson` dependency with *compile* scope (not runtime scope which is the typical JJWT default). That is: -**Maven** +*Maven* -```xml +[,xml,subs="+attributes"] +---- io.jsonwebtoken jjwt-jackson - 0.12.5 + {project-version} compile -``` +---- -**Gradle or Android** +*Gradle or Android* -```groovy +[,groovy,subs="+attributes"] +---- dependencies { - implementation 'io.jsonwebtoken:jjwt-jackson:0.12.5' + implementation 'io.jsonwebtoken:jjwt-jackson:{project-version}' } -``` +---- And then you can specify the `JacksonSerializer` using your own `ObjectMapper` on the `JwtBuilder`: -```java +[,java] +---- ObjectMapper objectMapper = getMyObjectMapper(); //implement me String jws = Jwts.builder() .json(new JacksonSerializer(objectMapper)) - + // ... etc ... -``` +---- and the `JacksonDeserializer` using your `ObjectMapper` on the `JwtParserBuilder`: -```java +[,java] +---- ObjectMapper objectMapper = getMyObjectMapper(); //implement me Jwts.parser() .json(new JacksonDeserializer(objectMapper)) - + // ... etc ... -``` +---- - -#### Parsing of Custom Claim Types +++++++++++++ -By default, JJWT will only convert simple claim types: String, Date, Long, Integer, Short and Byte. If you need to -deserialize other types you can configure the `JacksonDeserializer` by passing a `Map` of claim names to types in +==== Parsing of Custom Claim Types + +By default, JJWT will only convert simple claim types: String, Date, Long, Integer, Short and Byte. If you need to +deserialize other types you can configure the `JacksonDeserializer` by passing a `Map` of claim names to types in through a constructor. For example: -```java +[,java] +---- new JacksonDeserializer(Maps.of("user", User.class).build()) -``` +---- -This would trigger the value in the `user` claim to be deserialized into the custom type of `User`. Given the claims +This would trigger the value in the `user` claim to be deserialized into the custom type of `User`. Given the claims payload of: -```json +[,json] +---- { "issuer": "https://example.com/issuer", "user": { @@ -3099,11 +3427,12 @@ payload of: "lastName": "Coder" } } -``` +---- The `User` object could be retrieved from the `user` claim with the following code: -```java +[,java] +---- Jwts.parser() .json(new JacksonDeserializer(Maps.of("user", User.class).build())) // <----- @@ -3113,157 +3442,169 @@ Jwts.parser() .parseUnprotectedClaims(aJwtString) .getPayload() - + .get("user", User.class); // <----- -``` +---- -> **Note** -> -> Using this constructor is mutually exclusive with the `JacksonDeserializer(ObjectMapper)` constructor -> [described above](#json-jackson). This is because JJWT configures an `ObjectMapper` directly and could have negative -> consequences for a shared `ObjectMapper` instance. This should work for most applications, if you need a more advanced -> parsing options, [configure the mapper directly](#json-jackson). +[NOTE] +==== +Using this constructor is mutually exclusive with the `JacksonDeserializer(ObjectMapper)` constructor +<>. This is because JJWT configures an `ObjectMapper` directly and could have negative +consequences for a shared `ObjectMapper` instance. This should work for most applications, if you need a more advanced +parsing options, <>. +==== - -### Gson JSON Processor +++++++++++++ + +=== Gson JSON Processor If you want to use Gson for JSON processing, just including the `io.jsonwebtoken:jjwt-gson` dependency as a runtime dependency is all that is necessary in most projects, since Gradle and Maven will automatically pull in the necessary Gson dependencies as well. -After including this dependency, JJWT will automatically find the Gson implementation on the runtime classpath and -use it internally for JSON parsing. There is nothing else you need to do - just declaring the dependency is +After including this dependency, JJWT will automatically find the Gson implementation on the runtime classpath and +use it internally for JSON parsing. There is nothing else you need to do - just declaring the dependency is all that is required, no code or config is necessary. If you're curious, JJWT will automatically create an internal default Gson instance for its own needs as follows: -```java +[,java] +---- new GsonBuilder() - .registerTypeHierarchyAdapter(io.jsonwebtoken.lang.Supplier.class, GsonSupplierSerializer.INSTANCE) + .registerTypeHierarchyAdapter(io.jsonwebtoken.lang.Supplier.class, GsonSupplierSerializer.INSTANCE) .disableHtmlEscaping().create(); -``` +---- The `registerTypeHierarchyAdapter` builder call is required to serialize JWKs with secret or private values. -However, if you prefer to use a different Gson instance instead of JJWT's default, you can configure JJWT to use your +However, if you prefer to use a different Gson instance instead of JJWT's default, you can configure JJWT to use your own - just don't forget to register the necessary JJWT type hierarchy adapter. -You do this by declaring the `io.jsonwebtoken:jjwt-gson` dependency with **compile** scope (not runtime +You do this by declaring the `io.jsonwebtoken:jjwt-gson` dependency with *compile* scope (not runtime scope which is the typical JJWT default). That is: -**Maven** +*Maven* -```xml +[,xml,subs="+attributes"] +---- io.jsonwebtoken jjwt-gson - 0.12.5 + {project-version} compile -``` +---- -**Gradle or Android** +*Gradle or Android* -```groovy +[,groovy,subs="+attributes"] +---- dependencies { - implementation 'io.jsonwebtoken:jjwt-gson:0.12.5' + implementation 'io.jsonwebtoken:jjwt-gson:{project-version}' } -``` +---- And then you can specify the `GsonSerializer` using your own `Gson` instance on the `JwtBuilder`: -```java +[,java] +---- Gson gson = new GsonBuilder() - // don't forget this line!: + // don't forget this line!: .registerTypeHierarchyAdapter(io.jsonwebtoken.lang.Supplier.class, GsonSupplierSerializer.INSTANCE) .disableHtmlEscaping().create(); String jws = Jwts.builder() .json(new GsonSerializer(gson)) - + // ... etc ... -``` +---- and the `GsonDeserializer` using your `Gson` instance on the `JwtParser`: -```java +[,java] +---- Gson gson = getGson(); //implement me Jwts.parser() .json(new GsonDeserializer(gson)) - + // ... etc ... -``` +---- Again, as shown above, it is critical to create your `Gson` instance using the `GsonBuilder` and include the line: -```java +[,java] +---- .registerTypeHierarchyAdapter(io.jsonwebtoken.lang.Supplier.class, GsonSupplierSerializer.INSTANCE) -``` +---- to ensure JWK serialization works as expected. - -## Base64 Support +++++++++++++ -JJWT uses a very fast pure-Java [Base64](https://tools.ietf.org/html/rfc4648) codec for Base64 and +== Base64 Support + +JJWT uses a very fast pure-Java https://tools.ietf.org/html/rfc4648[Base64] codec for Base64 and Base64Url encoding and decoding that is guaranteed to work deterministically in all JDK and Android environments. -You can access JJWT's encoders and decoders using the `io.jsonwebtoken.io.Encoders` and `io.jsonwebtoken.io.Decoders` +You can access JJWT's encoders and decoders using the `io.jsonwebtoken.io.Encoders` and `io.jsonwebtoken.io.Decoders` utility classes. `io.jsonwebtoken.io.Encoders`: -* `BASE64` is an RFC 4648 [Base64](https://tools.ietf.org/html/rfc4648#section-4) encoder -* `BASE64URL` is an RFC 4648 [Base64URL](https://tools.ietf.org/html/rfc4648#section-5) encoder +* `BASE64` is an RFC 4648 https://tools.ietf.org/html/rfc4648#section-4[Base64] encoder +* `BASE64URL` is an RFC 4648 https://tools.ietf.org/html/rfc4648#section-5[Base64URL] encoder `io.jsonwebtoken.io.Decoders`: -* `BASE64` is an RFC 4648 [Base64](https://tools.ietf.org/html/rfc4648#section-4) decoder -* `BASE64URL` is an RFC 4648 [Base64URL](https://tools.ietf.org/html/rfc4648#section-5) decoder +* `BASE64` is an RFC 4648 https://tools.ietf.org/html/rfc4648#section-4[Base64] decoder +* `BASE64URL` is an RFC 4648 https://tools.ietf.org/html/rfc4648#section-5[Base64URL] decoder - -### Understanding Base64 in Security Contexts +++++++++++++ + +=== Understanding Base64 in Security Contexts All cryptographic operations, like encryption and message digest calculations, result in binary data - raw byte arrays. Because raw byte arrays cannot be represented natively in JSON, the JWT -specifications employ the Base64URL encoding scheme to represent these raw byte values in JSON documents or compound +specifications employ the Base64URL encoding scheme to represent these raw byte values in JSON documents or compound structures like a JWT. -This means that the Base64 and Base64URL algorithms take a raw byte array and converts the bytes into a string suitable +This means that the Base64 and Base64URL algorithms take a raw byte array and converts the bytes into a string suitable to use in text documents and protocols like HTTP. These algorithms can also convert these strings back into the original raw byte arrays for decryption or signature verification as necessary. -That's nice and convenient, but there are two very important properties of Base64 (and Base64URL) text strings that +That's nice and convenient, but there are two very important properties of Base64 (and Base64URL) text strings that are critical to remember when they are used in security scenarios like with JWTs: -* [Base64 is not encryption](#base64-not-encryption) -* [Changing Base64 characters](#base64-changing-characters) **does not automatically invalidate data**. +* <> +* <> *does not automatically invalidate data*. - -#### Base64 is not encryption - -Base64-encoded text is _not_ encrypted. +++++++++++++ -While a byte array representation can be converted to text with the Base64 algorithms, -anyone in the world can take Base64-encoded text, decode it with any standard Base64 decoder, and obtain the +==== Base64 is not encryption + +Base64-encoded text is _not_ encrypted. + +While a byte array representation can be converted to text with the Base64 algorithms, +anyone in the world can take Base64-encoded text, decode it with any standard Base64 decoder, and obtain the underlying raw byte array data. No key or secret is required to decode Base64 text - anyone can do it. -Based on this, when encoding sensitive byte data with Base64 - like a shared or private key - **the resulting -string is NOT safe to expose publicly**. +Based on this, when encoding sensitive byte data with Base64 - like a shared or private key - *the resulting +string is NOT safe to expose publicly*. A base64-encoded key is still sensitive information and must be kept as secret and as safe as the original source of the bytes (e.g. a Java `PrivateKey` or `SecretKey` instance). -After Base64-encoding data into a string, it is possible to then encrypt the string to keep it safe from prying +After Base64-encoding data into a string, it is possible to then encrypt the string to keep it safe from prying eyes if desired, but this is different. Encryption is not encoding. They are separate concepts. - -#### Changing Base64 Characters +++++++++++++ + +==== Changing Base64 Characters In an effort to see if signatures or encryption is truly validated correctly, some try to edit a JWT string - particularly the Base64-encoded signature part - to see if the edited string fails security validations. @@ -3274,108 +3615,122 @@ _But this doesn't always work. Changing base64 characters is an invalid test_. Why? -Because of the way the Base64 algorithm works, there are multiple Base64 strings that can represent the same raw byte +Because of the way the Base64 algorithm works, there are multiple Base64 strings that can represent the same raw byte array. -Going into the details of the Base64 algorithm is out of scope for this documentation, but there are many good -Stackoverflow [answers](https://stackoverflow.com/questions/33663113/multiple-strings-base64-decoded-to-same-byte-array?noredirect=1&lq=1) -and [JJWT issue comments](https://github.com/jwtk/jjwt/issues/211#issuecomment-283076269) that explain this in detail. -Here's one [good answer](https://stackoverflow.com/questions/29941270/why-do-base64-decode-produce-same-byte-array-for-different-strings): +Going into the details of the Base64 algorithm is out of scope for this documentation, but there are many good +Stackoverflow https://stackoverflow.com/questions/33663113/multiple-strings-base64-decoded-to-same-byte-array?noredirect=1&lq=1[answers] +and https://github.com/jwtk/jjwt/issues/211#issuecomment-283076269[JJWT issue comments] that explain this in detail. +Here's one https://stackoverflow.com/questions/29941270/why-do-base64-decode-produce-same-byte-array-for-different-strings[good answer]: -> Remember that Base64 encodes each 8 bit entity into 6 bit chars. The resulting string then needs exactly -> 11 * 8 / 6 bytes, or 14 2/3 chars. But you can't write partial characters. Only the first 4 bits (or 2/3 of the -> last char) are significant. The last two bits are not decoded. Thus all of: -> -> dGVzdCBzdHJpbmo -> dGVzdCBzdHJpbmp -> dGVzdCBzdHJpbmq -> dGVzdCBzdHJpbmr -> All decode to the same 11 bytes (116, 101, 115, 116, 32, 115, 116, 114, 105, 110, 106). +[IMPORTANT] +==== +Remember that Base64 encodes each 8 bit entity into 6 bit chars. The resulting string then needs exactly +11 * 8 / 6 bytes, or 14 2/3 chars. But you can't write partial characters. Only the first 4 bits (or 2/3 of the +last char) are significant. The last two bits are not decoded. Thus all of: + +[,text] +---- +dGVzdCBzdHJpbmo +dGVzdCBzdHJpbmp +dGVzdCBzdHJpbmq +dGVzdCBzdHJpbmr +---- +All decode to the same 11 bytes (116, 101, 115, 116, 32, 115, 116, 114, 105, 110, 106). +==== As you can see by the above 4 examples, they all decode to the same exact 11 bytes. So just changing one or two characters at the end of a Base64 string may not work and can often result in an invalid test. - -##### Adding Invalid Characters +++++++++++++ -JJWT's default Base64/Base64URL decoders automatically ignore illegal Base64 characters located in the beginning and -end of an encoded string. Therefore, prepending or appending invalid characters like `{` or `]` or similar will also +===== Adding Invalid Characters + +JJWT's default Base64/Base64URL decoders automatically ignore illegal Base64 characters located in the beginning and +end of an encoded string. Therefore, prepending or appending invalid characters like `{` or `]` or similar will also not fail JJWT's signature checks either. Why? -Because such edits - whether changing a trailing character or two, or appending invalid characters - do not actually -change the _real_ signature, which in cryptographic contexts, is always a byte array. Instead, tests like these +Because such edits - whether changing a trailing character or two, or appending invalid characters - do not actually +change the _real_ signature, which in cryptographic contexts, is always a byte array. Instead, tests like these change a text encoding of the byte array, and as we covered above, they are different things. So JJWT 'cares' more about the real byte array and less about its text encoding because that is what actually matters -in cryptographic operations. In this sense, JJWT follows the [Robustness Principle](https://en.wikipedia.org/wiki/Robustness_principle) -in being _slightly_ lenient on what is accepted per the rules of Base64, but if anything in the real underlying +in cryptographic operations. In this sense, JJWT follows the https://en.wikipedia.org/wiki/Robustness_principle[Robustness Principle] +in being _slightly_ lenient on what is accepted per the rules of Base64, but if anything in the real underlying byte array is changed, then yes, JJWT's cryptographic assertions will definitely fail. -To help understand JJWT's approach, we have to remember why signatures exist. From our documentation above on -[signing JWTs](#jws): +To help understand JJWT's approach, we have to remember why signatures exist. From our documentation above on +<>: -> * guarantees it was created by someone we know (it is authentic), as well as -> * guarantees that no-one has manipulated or changed it after it was created (its integrity is maintained). +____ +* guarantees it was created by someone we know (it is authentic), as well as +* guarantees that no-one has manipulated or changed it after it was created (its integrity is maintained). +____ -Just prepending or appending invalid text to try to 'trick' the algorithm doesn't change the integrity of the -underlying claims or signature byte arrays, nor the authenticity of the claims byte array, because those byte +Just prepending or appending invalid text to try to 'trick' the algorithm doesn't change the integrity of the +underlying claims or signature byte arrays, nor the authenticity of the claims byte array, because those byte arrays are still obtained intact. -Please see [JJWT Issue #518](https://github.com/jwtk/jjwt/issues/518) and its referenced issues and links for more +Please see https://github.com/jwtk/jjwt/issues/518[JJWT Issue #518] and its referenced issues and links for more information. - -### Custom Base64 +++++++++++++ + +=== Custom Base64 If for some reason you want to specify your own Base64Url encoder and decoder, you can use the `JwtBuilder` `encoder` method to set the encoder: -```java +[,java] +---- Encoder encoder = getMyBase64UrlEncoder(); //implement me String jws = Jwts.builder() .b64Url(encoder) - + // ... etc ... -``` +---- -and the `JwtParserBuilder`'s `decoder` method to set the decoder: +and the ``JwtParserBuilder``'s `decoder` method to set the decoder: -```java +[,java] +---- Decoder decoder = getMyBase64UrlDecoder(); //implement me Jwts.parser() .b64Url(decoder) - + // ... etc ... -``` +---- - -## Examples +++++++++++++ -* [JWS Signed with HMAC](#example-jws-hs) -* [JWS Signed with RSA](#example-jws-rsa) -* [JWS Signed with ECDSA](#example-jws-ecdsa) -* [JWS Signed with EdDSA](#example-jws-eddsa) -* [JWE Encrypted Directly with a SecretKey](#example-jwe-dir) -* [JWE Encrypted with RSA](#example-jwe-rsa) -* [JWE Encrypted with AES Key Wrap](#example-jwe-aeskw) -* [JWE Encrypted with ECDH-ES](#example-jwe-ecdhes) -* [JWE Encrypted with a Password](#example-jwe-password) -* [SecretKey JWK](#example-jwk-secret) -* [RSA Public JWK](#example-jwk-rsapub) -* [RSA Private JWK](#example-jwk-rsapriv) -* [Elliptic Curve Public JWK](#example-jwk-ecpub) -* [Elliptic Curve Private JWK](#example-jwk-ecpriv) -* [Edwards Elliptic Curve Public JWK](#example-jwk-edpub) -* [Edwards Elliptic Curve Private JWK](#example-jwk-edpriv) +== Examples - -### JWT Signed with HMAC +* <> +* <> +* <> +* <> +* <> +* <> +* <> +* <> +* <> +* <> +* <> +* <> +* <> +* <> +* <> +* <> -This is an example showing how to digitally sign a JWT using an [HMAC](https://en.wikipedia.org/wiki/HMAC) +++++++++++++ + +=== JWT Signed with HMAC + +This is an example showing how to digitally sign a JWT using an https://en.wikipedia.org/wiki/HMAC[HMAC] (hash-based message authentication code). The JWT specifications define 3 standard HMAC signing algorithms: * `HS256`: HMAC with SHA-256. This requires a 256-bit (32 byte) `SecretKey` or larger. @@ -3384,7 +3739,8 @@ This is an example showing how to digitally sign a JWT using an [HMAC](https://e Example: -```java +[,java] +---- // Create a test key suitable for the desired HMAC-SHA algorithm: MacAlgorithm alg = Jwts.SIG.HS512; //or HS384 or HS256 SecretKey key = alg.key().build(); @@ -3399,19 +3755,21 @@ String jws = Jwts.builder().content(content, "text/plain").signWith(key, alg).co content = Jwts.parser().verifyWith(key).build().parseSignedContent(jws).getPayload(); assert message.equals(new String(content, StandardCharsets.UTF_8)); -``` +---- - -### JWT Signed with RSA +++++++++++++ -This is an example showing how to digitally sign and verify a JWT using RSA cryptography. The JWT specifications -define [6 standard RSA signing algorithms](#jws-alg). All 6 require that [RSA keys 2048-bits or larger](#jws-key-rsa) +=== JWT Signed with RSA + +This is an example showing how to digitally sign and verify a JWT using RSA cryptography. The JWT specifications +define <>. All 6 require that <> must be used. In this example, Bob will sign a JWT using his RSA private key, and Alice can verify it came from Bob using Bob's RSA public key: -```java +[,java] +---- // Create a test key suitable for the desired RSA signature algorithm: SignatureAlgorithm alg = Jwts.SIG.RS512; //or PS512, RS256, etc... KeyPair pair = alg.keyPair().build(); @@ -3427,13 +3785,14 @@ String subject = Jwts.parser() .build().parseSignedClaims(jws).getPayload().getSubject(); assert "Alice".equals(subject); -``` +---- - -### JWT Signed with ECDSA +++++++++++++ + +=== JWT Signed with ECDSA This is an example showing how to digitally sign and verify a JWT using the Elliptic Curve Digital Signature Algorithm. -The JWT specifications define [3 standard ECDSA signing algorithms](#jws-alg): +The JWT specifications define <>: * `ES256`: ECDSA using P-256 and SHA-256. This requires an EC Key exactly 256 bits (32 bytes) long. * `ES384`: ECDSA using P-384 and SHA-384. This requires an EC Key exactly 384 bits (48 bytes) long. @@ -3442,7 +3801,8 @@ The JWT specifications define [3 standard ECDSA signing algorithms](#jws-alg): In this example, Bob will sign a JWT using his EC private key, and Alice can verify it came from Bob using Bob's EC public key: -```java +[,java] +---- // Create a test key suitable for the desired ECDSA signature algorithm: SignatureAlgorithm alg = Jwts.SIG.ES512; //or ES256 or ES384 KeyPair pair = alg.keyPair().build(); @@ -3458,35 +3818,38 @@ String subject = Jwts.parser() .build().parseSignedClaims(jws).getPayload().getSubject(); assert "Alice".equals(subject); -``` +---- - -### JWT Signed with EdDSA +++++++++++++ -This is an example showing how to digitally sign and verify a JWT using the -[Edwards Curve Digital Signature Algorithm](https://www.rfc-editor.org/rfc/rfc8032) using +=== JWT Signed with EdDSA + +This is an example showing how to digitally sign and verify a JWT using the +https://www.rfc-editor.org/rfc/rfc8032[Edwards Curve Digital Signature Algorithm] using `Ed25519` or `Ed448` keys. -> **Note** -> -> **The `Ed25519` and `Ed448` algorithms require JDK 15 or a compatible JCA Provider -> (like BouncyCastle) in the runtime classpath.** -> -> If you are using JDK 14 or earlier and you want to use them, see -> the [Installation](#Installation) section to see how to enable BouncyCastle. +[NOTE] +==== +The `Ed25519` and `Ed448` algorithms require JDK 15 or a compatible JCA Provider +(like BouncyCastle) in the runtime classpath. -The `EdDSA` signature algorithm is defined for JWS in [RFC 8037, Section 3.1](https://www.rfc-editor.org/rfc/rfc8037#section-3.1) +If you are using JDK 14 or earlier and you want to use them, see +the <> section to see how to enable BouncyCastle. +==== + +The `EdDSA` signature algorithm is defined for JWS in https://www.rfc-editor.org/rfc/rfc8037#section-3.1[RFC 8037, Section 3.1] using keys for two Edwards curves: -* `Ed25519`: `EdDSA` using curve `Ed25519`. `Ed25519` algorithm keys must be 255 bits long and produce - signatures 512 bits (64 bytes) long. -* `Ed448`: `EdDSA` using curve `Ed448`. `Ed448` algorithm keys must be 448 bits long and produce signatures - 912 bits (114 bytes) long. +* `Ed25519`: `EdDSA` using curve `Ed25519`. `Ed25519` algorithm keys must be 255 bits long and produce + signatures 512 bits (64 bytes) long. +* `Ed448`: `EdDSA` using curve `Ed448`. `Ed448` algorithm keys must be 448 bits long and produce signatures + 912 bits (114 bytes) long. -In this example, Bob will sign a JWT using his Edwards Curve private key, and Alice can verify it came from Bob +In this example, Bob will sign a JWT using his Edwards Curve private key, and Alice can verify it came from Bob using Bob's Edwards Curve public key: -```java +[,java] +---- // Create a test key suitable for the EdDSA signature algorithm using Ed25519 or Ed448 keys: Curve curve = Jwks.CRV.Ed25519; //or Ed448 KeyPair pair = curve.keyPair().build(); @@ -3502,30 +3865,32 @@ String subject = Jwts.parser() .build().parseSignedClaims(jws).getPayload().getSubject(); assert "Alice".equals(subject); -``` +---- - -### JWT Encrypted Directly with a SecretKey +++++++++++++ -This is an example showing how to encrypt a JWT [directly using a symmetric secret key](#jwe-alg-dir). The -JWT specifications define [6 standard AEAD Encryption algorithms](#jwe-enc): +=== JWT Encrypted Directly with a SecretKey + +This is an example showing how to encrypt a JWT <>. The +JWT specifications define <>: * `A128GCM`: AES GCM using a 128-bit (16 byte) `SecretKey` or larger. * `A192GCM`: AES GCM using a 192-bit (24 byte) `SecretKey` or larger. * `A256GCM`: AES GCM using a 256-bit (32 byte) `SecretKey` or larger. -* `A128CBC-HS256`: [AES_128_CBC_HMAC_SHA_256](https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.3) using a - 256-bit (32 byte) `SecretKey`. -* `A192CBC-HS384`: [AES_192_CBC_HMAC_SHA_384](https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.4) using a - 384-bit (48 byte) `SecretKey`. -* `A256CBC-HS512`: [AES_256_CBC_HMAC_SHA_512](https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.5) using a - 512-bit (64 byte) `SecretKey`. +* `A128CBC-HS256`: https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.3[AES_128_CBC_HMAC_SHA_256] using a +256-bit (32 byte) `SecretKey`. +* `A192CBC-HS384`: https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.4[AES_192_CBC_HMAC_SHA_384] using a +384-bit (48 byte) `SecretKey`. +* `A256CBC-HS512`: https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.5[AES_256_CBC_HMAC_SHA_512] using a +512-bit (64 byte) `SecretKey`. The AES GCM (`A128GCM`, `A192GCM` and `A256GCM`) algorithms are strongly recommended - they are faster and more efficient than the `A*CBC-HS*` variants, but they do require JDK 8 or later (or JDK 7 + BouncyCastle). Example: -```java +[,java] +---- // Create a test key suitable for the desired payload encryption algorithm: // (A*GCM algorithms are recommended, but require JDK >= 8 or BouncyCastle) AeadAlgorithm enc = Jwts.ENC.A256GCM; //or A128GCM, A192GCM, A256CBC-HS512, etc... @@ -3541,21 +3906,23 @@ String jwe = Jwts.builder().content(content, "text/plain").encryptWith(key, enc) content = Jwts.parser().decryptWith(key).build().parseEncryptedContent(jwe).getPayload(); assert message.equals(new String(content, StandardCharsets.UTF_8)); -``` +---- - -### JWT Encrypted with RSA +++++++++++++ + +=== JWT Encrypted with RSA This is an example showing how to encrypt and decrypt a JWT using RSA cryptography. -Because RSA cannot encrypt much data, RSA is used to encrypt and decrypt a secure-random key, and that generated key -in turn is used to actually encrypt the payload as described in the [RSA Key Encryption](jwe-alg-rsa) section +Because RSA cannot encrypt much data, RSA is used to encrypt and decrypt a secure-random key, and that generated key +in turn is used to actually encrypt the payload as described in the link:jwe-alg-rsa[RSA Key Encryption] section above. As such, RSA Key Algorithms must be paired with an AEAD Encryption Algorithm, as shown below. -In this example, Bob will encrypt a JWT using Alice's RSA public key to ensure only she may read it. Alice can then +In this example, Bob will encrypt a JWT using Alice's RSA public key to ensure only she may read it. Alice can then decrypt the JWT using her RSA private key: -```java +[,java] +---- // Create a test KeyPair suitable for the desired RSA key algorithm: KeyPair pair = Jwts.SIG.RS512.keyPair().build(); @@ -3575,23 +3942,25 @@ Set audience = Jwts.parser() .build().parseEncryptedClaims(jwe).getPayload().getAudience(); assert audience.contains("Alice"); -``` +---- - -### JWT Encrypted with AES Key Wrap +++++++++++++ + +=== JWT Encrypted with AES Key Wrap This is an example showing how to encrypt and decrypt a JWT using AES Key Wrap algorithms. These algorithms use AES to encrypt and decrypt a secure-random key, and that generated key in turn is used to actually encrypt -the payload as described in the [AES Key Encryption](jwe-alg-aes) section above. This allows the payload to be -encrypted with a random short-lived key, reducing material exposure of the potentially longer-lived symmetric secret -key. This approach requires the AES Key Wrap algorithms to be paired with an AEAD content encryption algorithm, +the payload as described in the link:jwe-alg-aes[AES Key Encryption] section above. This allows the payload to be +encrypted with a random short-lived key, reducing material exposure of the potentially longer-lived symmetric secret +key. This approach requires the AES Key Wrap algorithms to be paired with an AEAD content encryption algorithm, as shown below. The AES GCM Key Wrap algorithms (`A128GCMKW`, `A192GCMKW` and `A256GCMKW`) are preferred - they are faster and more efficient than the `A*KW` variants, but they do require JDK 8 or later (or JDK 7 + BouncyCastle). -```java +[,java] +---- // Create a test SecretKey suitable for the desired AES Key Wrap algorithm: SecretKeyAlgorithm alg = Jwts.KEY.A256GCMKW; //or A192GCMKW, A128GCMKW, A256KW, etc... SecretKey key = alg.key().build(); @@ -3607,23 +3976,25 @@ String issuer = Jwts.parser().decryptWith(key).build() .parseEncryptedClaims(jwe).getPayload().getIssuer(); assert "me".equals(issuer); -``` +---- - -### JWT Encrypted with ECDH-ES +++++++++++++ -This is an example showing how to encrypt and decrypt a JWT using Elliptic Curve Diffie-Hellman Ephemeral Static +=== JWT Encrypted with ECDH-ES + +This is an example showing how to encrypt and decrypt a JWT using Elliptic Curve Diffie-Hellman Ephemeral Static Key Agreement (ECDH-ES) algorithms. -These algorithms use ECDH-ES to encrypt and decrypt a secure-random key, and that -generated key in turn is used to actually encrypt the payload as described in the -[Elliptic Curve Diffie-Hellman Ephemeral Static Key Agreement](jwe-alg-ecdhes) section above. Because of this, ECDH-ES +These algorithms use ECDH-ES to encrypt and decrypt a secure-random key, and that +generated key in turn is used to actually encrypt the payload as described in the +link:jwe-alg-ecdhes[Elliptic Curve Diffie-Hellman Ephemeral Static Key Agreement] section above. Because of this, ECDH-ES Key Algorithms must be paired with an AEAD Encryption Algorithm, as shown below. -In this example, Bob will encrypt a JWT using Alice's Elliptic Curve public key to ensure only she may read it. +In this example, Bob will encrypt a JWT using Alice's Elliptic Curve public key to ensure only she may read it. + Alice can then decrypt the JWT using her Elliptic Curve private key: -```java +[,java] +---- // Create a test KeyPair suitable for the desired EC key algorithm: KeyPair pair = Jwts.SIG.ES512.keyPair().build(); @@ -3643,22 +4014,24 @@ Set audience = Jwts.parser() .build().parseEncryptedClaims(jwe).getPayload().getAudience(); assert audience.contains("Alice"); -``` +---- - -### JWT Encrypted with a Password +++++++++++++ + +=== JWT Encrypted with a Password This is an example showing how to encrypt and decrypt a JWT using Password-based key-derivation algorithms. -These algorithms use a password to securely derive a random key, and that derived random key in turn is used to actually -encrypt the payload as described in the [Password-based Key Encryption](jwe-alg-pbes2) section above. This allows +These algorithms use a password to securely derive a random key, and that derived random key in turn is used to actually +encrypt the payload as described in the link:jwe-alg-pbes2[Password-based Key Encryption] section above. This allows the payload to be encrypted with a random short-lived cryptographically-stronger key, reducing the need to -expose the longer-lived (and potentially weaker) password. +expose the longer-lived (and potentially weaker) password. -This approach requires the Password-based Key Wrap algorithms to be paired with an AEAD content encryption algorithm, +This approach requires the Password-based Key Wrap algorithms to be paired with an AEAD content encryption algorithm, as shown below. -```java +[,java] +---- //DO NOT use this example password in a real app, it is well-known to password crackers: String pw = "correct horse battery staple"; Password password = Keys.password(pw.toCharArray()); @@ -3667,10 +4040,10 @@ Password password = Keys.password(pw.toCharArray()); KeyAlgorithm alg = Jwts.KEY.PBES2_HS512_A256KW; //or PBES2_HS384_A192KW or PBES2_HS256_A128KW // Optionally choose the number of PBES2 computational iterations to use to derive the key. -// This is optional - if you do not specify a value, JJWT will automatically choose a value -// based on your chosen PBES2 algorithm and OWASP PBKDF2 recommendations here: +// This is optional - if you do not specify a value, JJWT will automatically choose a value +// based on your chosen PBES2 algorithm and OWASP PBKDF2 recommendations here: // https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2 -// +// // If you do specify a value, ensure the iterations are large enough for your desired alg //int pbkdf2Iterations = 120000; //for HS512. Needs to be much higher for smaller hash algs. @@ -3689,14 +4062,16 @@ String issuer = Jwts.parser().decryptWith(password) .build().parseEncryptedClaims(jwe).getPayload().getIssuer(); assert "me".equals(issuer); -``` +---- - -### SecretKey JWK +++++++++++++ + +=== SecretKey JWK Example creating and parsing a secret JWK: -```java +[,java] +---- SecretKey key = Jwts.SIG.HS512.key().build(); // or HS384 or HS256 SecretJwk jwk = Jwks.builder().key(key).idFromThumbprint().build(); @@ -3709,14 +4084,16 @@ Jwk parsed = Jwks.parser().build().parse(jwkJson); assert parsed instanceof SecretJwk; assert jwk.equals(parsed); -``` +---- - -### RSA Public JWK +++++++++++++ + +=== RSA Public JWK Example creating and parsing an RSA Public JWK: -```java +[,java] +---- RSAPublicKey key = (RSAPublicKey)Jwts.SIG.RS512.keyPair().build().getPublic(); RsaPublicJwk jwk = Jwks.builder().key(key).idFromThumbprint().build(); @@ -3729,14 +4106,16 @@ Jwk parsed = Jwks.parser().build().parse(jwkJson); assert parsed instanceof RsaPublicJwk; assert jwk.equals(parsed); -``` +---- - -### RSA Private JWK +++++++++++++ + +=== RSA Private JWK Example creating and parsing an RSA Private JWK: -```java +[,java] +---- KeyPair pair = Jwts.SIG.RS512.keyPair().build(); RSAPublicKey pubKey = (RSAPublicKey) pair.getPublic(); RSAPrivateKey privKey = (RSAPrivateKey) pair.getPrivate(); @@ -3755,14 +4134,16 @@ Jwk parsed = Jwks.parser().build().parse(jwkJson); assert parsed instanceof RsaPrivateJwk; assert privJwk.equals(parsed); -``` +---- - -### Elliptic Curve Public JWK +++++++++++++ + +=== Elliptic Curve Public JWK Example creating and parsing an Elliptic Curve Public JWK: -```java +[,java] +---- ECPublicKey key = (ECPublicKey) Jwts.SIG.ES512.keyPair().build().getPublic(); EcPublicJwk jwk = Jwks.builder().key(key).idFromThumbprint().build(); @@ -3775,14 +4156,16 @@ Jwk parsed = Jwks.parser().build().parse(jwkJson); assert parsed instanceof EcPublicJwk; assert jwk.equals(parsed); -``` +---- - -### Elliptic Curve Private JWK +++++++++++++ + +=== Elliptic Curve Private JWK Example creating and parsing an Elliptic Curve Private JWK: -```java +[,java] +---- KeyPair pair = Jwts.SIG.ES512.keyPair().build(); ECPublicKey pubKey = (ECPublicKey) pair.getPublic(); ECPrivateKey privKey = (ECPrivateKey) pair.getPrivate(); @@ -3801,16 +4184,18 @@ Jwk parsed = Jwks.parser().build().parse(jwkJson); assert parsed instanceof EcPrivateJwk; assert privJwk.equals(parsed); -``` +---- - -### Edwards Elliptic Curve Public JWK +++++++++++++ + +=== Edwards Elliptic Curve Public JWK Example creating and parsing an Edwards Elliptic Curve (Ed25519, Ed448, X25519, X448) Public JWK -(the JWT [RFC 8037](https://www.rfc-editor.org/rfc/rfc8037) specification calls these `Octet` keys, hence the +(the JWT https://www.rfc-editor.org/rfc/rfc8037[RFC 8037] specification calls these `Octet` keys, hence the `OctetPublicJwk` interface names): -```java +[,java] +---- PublicKey key = Jwks.CRV.Ed25519.keyPair().build().getPublic(); // or Ed448, X25519, X448 OctetPublicJwk jwk = builder().octetKey(key).idFromThumbprint().build(); @@ -3823,16 +4208,18 @@ Jwk parsed = Jwks.parser().build().parse(jwkJson); assert parsed instanceof OctetPublicJwk; assert jwk.equals(parsed); -``` +---- - -### Edwards Elliptic Curve Private JWK +++++++++++++ + +=== Edwards Elliptic Curve Private JWK Example creating and parsing an Edwards Elliptic Curve (Ed25519, Ed448, X25519, X448) Private JWK -(the JWT [RFC 8037](https://www.rfc-editor.org/rfc/rfc8037) specification calls these `Octet` keys, hence the +(the JWT https://www.rfc-editor.org/rfc/rfc8037[RFC 8037] specification calls these `Octet` keys, hence the `OctetPrivateJwk` and `OctetPublicJwk` interface names): -```java +[,java] +---- KeyPair pair = Jwks.CRV.Ed448.keyPair().build(); // or Ed25519, X25519, X448 PublicKey pubKey = pair.getPublic(); PrivateKey privKey = pair.getPrivate(); @@ -3851,22 +4238,23 @@ Jwk parsed = Jwks.parser().build().parse(jwkJson); assert parsed instanceof OctetPrivateJwk; assert privJwk.equals(parsed); -``` +---- -## Learn More +== Learn More -- [JSON Web Token for Java and Android](https://web.archive.org/web/20230427122653/https://stormpath.com/blog/jjwt-how-it-works-why) -- [How to Create and Verify JWTs in Java](https://web.archive.org/web/20230426235608/https://stormpath.com/blog/jwt-java-create-verify) -- [Where to Store Your JWTs - Cookies vs HTML5 Web Storage](https://web.archive.org/web/20230428094039/https://stormpath.com/blog/where-to-store-your-jwts-cookies-vs-html5-web-storage) -- [Use JWT the Right Way!](https://web.archive.org/web/20230428184004/https://stormpath.com/blog/jwt-the-right-way) -- [Token Authentication for Java Applications](https://web.archive.org/web/20230427151310/https://stormpath.com/blog/token-auth-for-java) -- [JJWT Changelog](CHANGELOG.md) +* https://web.archive.org/web/20230427122653/https://stormpath.com/blog/jjwt-how-it-works-why[JSON Web Token for Java and Android] +* https://web.archive.org/web/20230426235608/https://stormpath.com/blog/jwt-java-create-verify[How to Create and Verify JWTs in Java] +* https://web.archive.org/web/20230428094039/https://stormpath.com/blog/where-to-store-your-jwts-cookies-vs-html5-web-storage[Where to Store Your JWTs - Cookies vs HTML5 Web Storage] +* https://web.archive.org/web/20230428184004/https://stormpath.com/blog/jwt-the-right-way[Use JWT the Right Way!] +* https://web.archive.org/web/20230427151310/https://stormpath.com/blog/token-auth-for-java[Token Authentication for Java Applications] +* xref:CHANGELOG.adoc[JJWT Changelog] -## Author +== Author -Maintained by Les Hazlewood & the extended Java community :heart: +Maintained by Les Hazlewood & the extended Java community :heart: - -## License +++++++++++++ -This project is open-source via the [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0). +== License + +This project is open-source via the http://www.apache.org/licenses/LICENSE-2.0[Apache 2.0 License]. diff --git a/pom.xml b/pom.xml index 88f7ec47..286e663c 100644 --- a/pom.xml +++ b/pom.xml @@ -325,6 +325,7 @@ .gitattributes **/genkeys **/softhsm + **.adoc