From 854bb8944cbb113a63596265c769a8689f651d37 Mon Sep 17 00:00:00 2001 From: lhazlewood <121180+lhazlewood@users.noreply.github.com> Date: Fri, 29 Sep 2023 13:42:36 -0700 Subject: [PATCH] Builder NestedCollection support (#841) - Added Conjunctor, CollectionMutator, and NestedCollection - Added JwkBuilder#operations() NestedCollection builder and removed #operation(KeyOperation) and #operations(Collection) - KeyOperationPolicyBuilder now extends CollectionMutator - Replaced ProtectedHeaderMutator#critical* methods with critical() NestedCollection - Replaced JwtParserBuilder#critical* methods with critical() NestedCollection - Replaced ClaimsMutator#audience* methods with audience() AudienceCollection - Replaced JwtParserBuilder#add* methods with new collection builder methods: enc(), key(), sig() and zip() --- .../java/io/jsonwebtoken/ClaimsMutator.java | 84 +++---- .../CompressionCodecResolver.java | 6 +- .../main/java/io/jsonwebtoken/JwtBuilder.java | 39 +--- .../io/jsonwebtoken/JwtHandlerAdapter.java | 6 + .../io/jsonwebtoken/JwtParserBuilder.java | 216 +++++++++++------- .../java/io/jsonwebtoken/LocatorAdapter.java | 1 + .../jsonwebtoken/ProtectedHeaderMutator.java | 29 +-- .../jsonwebtoken/io/AbstractDeserializer.java | 8 +- .../jsonwebtoken/io/AbstractSerializer.java | 10 +- .../jsonwebtoken/io/CompressionAlgorithm.java | 11 +- .../main/java/io/jsonwebtoken/io/Decoder.java | 2 + .../java/io/jsonwebtoken/lang/Classes.java | 9 + .../jsonwebtoken/lang/CollectionMutator.java | 61 +++++ .../java/io/jsonwebtoken/lang/Conjunctor.java | 33 +++ .../jsonwebtoken/lang/NestedCollection.java | 27 +++ .../java/io/jsonwebtoken/lang/Strings.java | 6 + .../security/AsymmetricJwkBuilder.java | 3 +- .../io/jsonwebtoken/security/JwkBuilder.java | 82 ++----- .../security/KeyOperationBuilder.java | 4 +- .../security/KeyOperationPolicied.java | 6 +- .../security/KeyOperationPolicy.java | 4 +- .../security/KeyOperationPolicyBuilder.java | 14 +- .../jsonwebtoken/security/SecureRequest.java | 1 + .../security/SecurityBuilder.java | 1 + .../impl/AbstractAudienceCollection.java | 34 +++ .../impl/DefaultJweHeaderMutator.java | 34 +-- .../jsonwebtoken/impl/DefaultJwtBuilder.java | 18 +- .../jsonwebtoken/impl/DefaultJwtParser.java | 36 +-- .../impl/DefaultJwtParserBuilder.java | 106 +++++---- .../impl/DelegateAudienceCollection.java | 69 ++++++ .../impl/DelegatingClaimsMutator.java | 54 ++--- .../java/io/jsonwebtoken/impl/IdLocator.java | 25 +- .../io/StandardCompressionAlgorithms.java | 9 +- .../impl/lang/DefaultCollectionMutator.java | 75 ++++++ ...stry.java => DefaultNestedCollection.java} | 26 +-- .../impl/lang/DefaultRegistry.java | 21 ++ .../io/jsonwebtoken/impl/lang/IdRegistry.java | 13 +- .../impl/security/AbstractJwkBuilder.java | 26 +-- .../impl/security/DefaultJwkContext.java | 2 +- .../security/DefaultKeyOperationPolicy.java | 2 +- .../DefaultKeyOperationPolicyBuilder.java | 34 +-- .../impl/security/JwkContext.java | 2 +- .../impl/security/StandardCurves.java | 7 +- .../StandardEncryptionAlgorithms.java | 9 +- .../impl/security/StandardHashAlgorithms.java | 7 +- .../impl/security/StandardKeyAlgorithms.java | 9 +- .../impl/security/StandardKeyOperations.java | 7 +- .../StandardSecureDigestAlgorithms.java | 9 +- .../io/jsonwebtoken/JwtParserTest.groovy | 4 +- .../groovy/io/jsonwebtoken/JwtsTest.groovy | 33 +-- .../groovy/io/jsonwebtoken/RFC7797Test.groovy | 30 ++- .../impl/AbstractProtectedHeaderTest.groovy | 2 +- .../impl/DefaultJwtBuilderTest.groovy | 45 ++-- .../impl/DefaultJwtHeaderBuilderTest.groovy | 24 +- .../impl/DefaultJwtParserBuilderTest.groovy | 138 +++-------- .../impl/DefaultJwtParserTest.groovy | 4 +- .../jsonwebtoken/impl/DefaultJwtTest.groovy | 2 +- .../impl/DefaultMutableJweHeaderTest.groovy | 1 + .../DelegateAudienceCollectionTest.groovy | 46 ++++ .../io/jsonwebtoken/impl/IdLocatorTest.groovy | 4 +- .../lang/DefaultCollectionMutatorTest.groovy | 106 +++++++++ .../impl/lang/DefaultRegistryTest.groovy | 29 ++- .../impl/lang/DelegatingRegistryTest.groovy | 47 ---- .../security/AbstractJwkBuilderTest.groovy | 14 +- .../security/EdSignatureAlgorithmTest.groovy | 2 +- .../impl/security/JwksTest.groovy | 13 +- .../impl/security/SecretJwkFactoryTest.groovy | 6 +- .../impl/security/TestAeadAlgorithm.groovy | 47 ++++ .../impl/security/TestKeyAlgorithm.groovy | 40 ++++ .../impl/security/TestMacAlgorithm.groovy | 51 +++++ .../security/EncryptionAlgorithmsTest.groovy | 7 - .../security/KeyAlgorithmsTest.groovy | 16 +- .../security/StandardAlgorithmsTest.groovy | 14 -- .../io/jsonwebtoken/all/JavaReadmeTest.java | 10 +- 74 files changed, 1215 insertions(+), 817 deletions(-) create mode 100644 api/src/main/java/io/jsonwebtoken/lang/CollectionMutator.java create mode 100644 api/src/main/java/io/jsonwebtoken/lang/Conjunctor.java create mode 100644 api/src/main/java/io/jsonwebtoken/lang/NestedCollection.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/AbstractAudienceCollection.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/DelegateAudienceCollection.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultCollectionMutator.java rename impl/src/main/java/io/jsonwebtoken/impl/lang/{DelegatingRegistry.java => DefaultNestedCollection.java} (52%) create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/DelegateAudienceCollectionTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/lang/DefaultCollectionMutatorTest.groovy delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/lang/DelegatingRegistryTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/TestAeadAlgorithm.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/TestKeyAlgorithm.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/TestMacAlgorithm.groovy diff --git a/api/src/main/java/io/jsonwebtoken/ClaimsMutator.java b/api/src/main/java/io/jsonwebtoken/ClaimsMutator.java index c3e2bf73..b6cfa175 100644 --- a/api/src/main/java/io/jsonwebtoken/ClaimsMutator.java +++ b/api/src/main/java/io/jsonwebtoken/ClaimsMutator.java @@ -15,6 +15,8 @@ */ package io.jsonwebtoken; +import io.jsonwebtoken.lang.NestedCollection; + import java.util.Collection; import java.util.Date; @@ -77,61 +79,29 @@ public interface ClaimsMutator> { * claim as a single String, NOT a String array. This method exists only for producing * JWTs sent to legacy recipients that are unable to interpret the {@code aud} value as a JSON String Array; it is * strongly recommended to avoid calling this method whenever possible and favor the - * {@link #audience(String)} or {@link #audience(Collection)} methods instead, as they ensure a single deterministic - * data type for recipients. + * {@link #audience()}.{@link AudienceCollection#add(Object) add(String)} and + * {@link AudienceCollection#add(Collection) add(Collection)} methods instead, as they ensure a single + * deterministic data type for recipients. * * @param aud the JWT {@code aud} value or {@code null} to remove the property from the JSON map. * @return the {@code Claims} instance for method chaining. - * @deprecated since JJWT_RELEASE_VERSION in favor of the shorter and more modern builder-style named - * {@link #audience(String)}. This method will be removed before the JJWT 1.0 release. + * @deprecated since JJWT_RELEASE_VERSION in favor of {@link #audience()}. This method will be removed before + * the JJWT 1.0 release. */ @Deprecated T setAudience(String aud); /** - * Sets the JWT aud (audience) - * Claim as a single String, NOT a String array. This method exists only for producing - * JWTs sent to legacy recipients that are unable to interpret the {@code aud} value as a JSON String Array; it is - * strongly recommended to avoid calling this method whenever possible and favor the - * {@link #audience(String)} or {@link #audience(Collection)} methods instead, as they ensure a single deterministic - * data type for recipients. - * - * @param aud the value to use as the {@code aud} Claim single-String value (and not an array of Strings), or - * {@code null}, empty or whitespace to remove the property from the JSON map. - * @return the instance for method chaining - * @since JJWT_RELEASE_VERSION - * @deprecated This is technically not deprecated because the JWT RFC mandates support for single string values, - * but it is marked as deprecated to discourage its use when possible. - */ - // DO NOT REMOVE EVER. This is a required RFC feature, but marked as deprecated to discourage its use - @Deprecated - T audienceSingle(String aud); - - /** - * Adds (appends) the specified {@code aud} value to the {@link #audience(Collection) audience} Claim set - * (JSON Array) unless it is {@code null}, empty, whitespace-only or already exists in the set. - * - *

This method may be called multiple times.

- * - * @param aud a JWT {@code aud} value to add to the {@link #audience(Collection) audience} Claim set. - * @return the {@code Claims} instance for method chaining. - * @throws IllegalArgumentException if the {@code aud} argument is null or empty. - * @since JJWT_RELEASE_VERSION - */ - T audience(String aud); - - /** - * Adds (appends) the specified values to the JWT + * Configures the JWT * aud (audience) Claim * set, quietly ignoring any null, empty, whitespace-only, or existing value already in the set. * - *

This method may be called multiple times.

- * - * @param aud the values to add to the {@code aud} Claim set (JSON Array) - * @return the instance for method chaining + * @return the {@link AudienceCollection AudienceCollection} to use for {@code aud} configuration. + * @see AudienceCollection AudienceCollection + * @see AudienceCollection#single(String) AudienceCollection.single(String) * @since JJWT_RELEASE_VERSION */ - T audience(Collection aud); + AudienceCollection audience(); /** * Sets the JWT @@ -246,4 +216,34 @@ public interface ClaimsMutator> { * @since JJWT_RELEASE_VERSION */ T id(String jti); + + /** + * 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. + * + * @param

the type of ClaimsMutator to return for method chaining. + * @see #single(String) + * @since JJWT_RELEASE_VERSION + */ + interface AudienceCollection

extends NestedCollection { + + /** + * Sets the JWT aud (audience) + * Claim as a single String, NOT a String array. This method exists only for producing + * JWTs sent to legacy recipients that are unable to interpret the {@code aud} value as a JSON String Array; + * it is strongly recommended to avoid calling this method whenever possible and favor the + * {@link #add(Object) add(String)} or {@link #add(Collection)} methods instead, as they ensure a single + * deterministic data type for recipients. + * + * @param aud the value to use as the {@code aud} Claim single-String value (and not an array of Strings), or + * {@code null}, empty or whitespace to remove the property from the JSON map. + * @return the instance for method chaining + * @since JJWT_RELEASE_VERSION + * @deprecated This is technically not deprecated because the JWT RFC mandates support for single string values, + * but it is marked as deprecated to discourage its use when possible. + */ + // DO NOT REMOVE EVER. This is a required RFC feature, but marked as deprecated to discourage its use + @Deprecated + P single(String aud); + } } diff --git a/api/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java b/api/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java index 07d6f198..58df7406 100644 --- a/api/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java +++ b/api/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java @@ -15,8 +15,6 @@ */ package io.jsonwebtoken; -import java.util.Collection; - /** * Looks for a JWT {@code zip} header, and if found, returns the corresponding {@link CompressionCodec} the parser * can use to decompress the JWT body. @@ -31,9 +29,9 @@ import java.util.Collection; * {@link io.jsonwebtoken.JwtParserBuilder#setCompressionCodecResolver(CompressionCodecResolver) parsing} JWTs.

* * @see JwtParserBuilder#setCompressionCodecResolver(CompressionCodecResolver) - * @see JwtParserBuilder#addCompressionAlgorithms(Collection) + * @see JwtParserBuilder#zip() * @since 0.6.0 - * @deprecated in favor of {@link JwtParserBuilder#addCompressionAlgorithms(Collection)} + * @deprecated in favor of {@link JwtParserBuilder#zip()} */ @SuppressWarnings("DeprecatedIsStillUsed") @Deprecated diff --git a/api/src/main/java/io/jsonwebtoken/JwtBuilder.java b/api/src/main/java/io/jsonwebtoken/JwtBuilder.java index 5fd48413..d2ad6f51 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JwtBuilder.java @@ -20,6 +20,7 @@ import io.jsonwebtoken.io.Decoder; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.io.Encoder; import io.jsonwebtoken.io.Serializer; +import io.jsonwebtoken.lang.Conjunctor; import io.jsonwebtoken.lang.MapMutator; import io.jsonwebtoken.security.AeadAlgorithm; import io.jsonwebtoken.security.InvalidKeyException; @@ -402,9 +403,9 @@ public interface JwtBuilder extends ClaimsMutator { * String jwt = Jwts.builder() * * .claims() - * .subject("Joe") - * .audience("you") * .issuer("me") + * .subject("Joe") + * .audience().add("you").and() * .add("customClaim", customValue) * .add(myClaimsMap) * // ... etc ... @@ -514,20 +515,6 @@ public interface JwtBuilder extends ClaimsMutator { // for better/targeted JavaDoc JwtBuilder subject(String sub); - /** - * Sets the JWT Claims - * aud (audience) claim. A {@code null} value will remove the property from the Claims. - * This is a convenience wrapper for: - *
-     * {@link #claims()}.{@link ClaimsMutator#audience(String) audience(aud)}.{@link BuilderClaims#and() and()}
- * - * @param aud the JWT {@code aud} value or {@code null} to remove the property from the Claims map. - * @return the builder instance for method chaining. - */ - @Override - // for better/targeted JavaDoc - JwtBuilder audience(String aud); - /** * Sets the JWT Claims * exp (expiration) claim. A {@code null} value will remove the property from the Claims. @@ -1052,14 +1039,8 @@ public interface JwtBuilder extends ClaimsMutator { * * @since JJWT_RELEASE_VERSION */ - interface BuilderClaims extends MapMutator, ClaimsMutator { - - /** - * Returns the associated JwtBuilder for continued configuration. - * - * @return the associated JwtBuilder for continued configuration. - */ - JwtBuilder and(); + interface BuilderClaims extends MapMutator, ClaimsMutator, + Conjunctor { } /** @@ -1069,13 +1050,7 @@ public interface JwtBuilder extends ClaimsMutator { * * @since JJWT_RELEASE_VERSION */ - interface BuilderHeader extends JweHeaderMutator, X509Builder { - - /** - * Returns the associated JwtBuilder for continued configuration. - * - * @return the associated JwtBuilder for continued configuration. - */ - JwtBuilder and(); + interface BuilderHeader extends JweHeaderMutator, X509Builder, + Conjunctor { } } diff --git a/api/src/main/java/io/jsonwebtoken/JwtHandlerAdapter.java b/api/src/main/java/io/jsonwebtoken/JwtHandlerAdapter.java index aadec334..3fc139da 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtHandlerAdapter.java +++ b/api/src/main/java/io/jsonwebtoken/JwtHandlerAdapter.java @@ -30,6 +30,12 @@ package io.jsonwebtoken; */ public abstract class JwtHandlerAdapter implements JwtHandler { + /** + * Default constructor, does not initialize any internal state. + */ + public JwtHandlerAdapter() { + } + @Override public T onContentJwt(Jwt jwt) { throw new UnsupportedJwtException("Unprotected content JWTs are not supported."); diff --git a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java index 7b15d92a..63ebbff6 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java @@ -19,6 +19,8 @@ import io.jsonwebtoken.io.CompressionAlgorithm; import io.jsonwebtoken.io.Decoder; import io.jsonwebtoken.io.Deserializer; import io.jsonwebtoken.lang.Builder; +import io.jsonwebtoken.lang.Conjunctor; +import io.jsonwebtoken.lang.NestedCollection; import io.jsonwebtoken.security.AeadAlgorithm; import io.jsonwebtoken.security.KeyAlgorithm; import io.jsonwebtoken.security.SecureDigestAlgorithm; @@ -29,10 +31,8 @@ import java.security.Key; import java.security.PrivateKey; import java.security.Provider; import java.security.PublicKey; -import java.util.Collection; import java.util.Date; import java.util.Map; -import java.util.Set; /** * A builder to construct a {@link JwtParser}. Example usage: @@ -96,25 +96,22 @@ public interface JwtParserBuilder extends Builder { */ JwtParserBuilder unsecuredDecompression(); - JwtParserBuilder critical(String crit); - /** - * Specifies the {@link ProtectedHeader} parameter names used in JWT extensions supported by the application. If + * Configures the {@link ProtectedHeader} parameter names used in JWT extensions supported by the application. If * 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. * *

Extension Behavior

* - *

The {@code crit} set argument only identifies header parameter names that are used in extensions supported + *

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.

* - * @param crit the header parameter names used in JWT extensions supported by the application. - * @return the builder for method chaining. + * @return the {@link NestedCollection} to use for {@code crit} configuration. * @see ProtectedHeader#getCritical() * @since JJWT_RELEASE_VERSION */ - JwtParserBuilder critical(Set crit); + NestedCollection critical(); /** * Sets the JCA Provider to use during cryptographic signature and key decryption operations, or {@code null} if the @@ -552,117 +549,174 @@ public interface JwtParserBuilder extends Builder { JwtParserBuilder setSigningKeyResolver(SigningKeyResolver signingKeyResolver); /** - * Adds the specified compression algorithms to the parser's total set of supported compression algorithms, - * replacing any existing ones with identical {@link Identifiable#getId() id}s. - * If the parser encounters a JWT {@code zip} header value that equals a compression algorithm's - * {@link Identifiable#getId() id}, that algorithm will be used for decompression. + * Configures the parser's supported {@link AeadAlgorithm}s used to decrypt JWE payloads. If the parser + * encounters a JWE {@link JweHeader#getEncryptionAlgorithm() enc} header value that equals an + * AEAD algorithm's {@link Identifiable#getId() id}, that algorithm will be used to decrypt the JWT + * payload. * - *

There may be only one registered {@code CompressionAlgorithm} per CaSe-SeNsItIvE {@code id}, and the - * {@code algs} collection is added in iteration order; if a duplicate id is found when iterating the {@code algs} - * collection, the later algorithm will evict any existing algorithm with the same {@code id}.

+ *

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...
* *

Standard Algorithms and Overrides

* - *

The {@link Jwts.ZIP} compression algorithms are supported by default and do not need - * to be added via this method, but beware: any algorithm in the {@code algs} collection with a - * JWA standard {@link Identifiable#getId() id} will replace the JJWT standard algorithm implementation. - * This is to allow application developers to favor their own implementations over JJWT's default implementations - * if necessary (for example, to support legacy or custom behavior). + *

All JWA-standard AEAD encryption algorithms in the {@link Jwts.ENC} registry are supported by default and + * do not need to be added. The collection may be useful however for removing some algorithms (for example, + * any algorithms not used by the application, or those not compatible with application security requirements), + * or for adding custom implementations.

* - * @param algs collection of compression algorithms to add to the parser's total set of supported compression - * algorithms, replacing any existing ones with the same exact (CaSe-SeNsItIvE) - * {@link CompressionAlgorithm#getId() id}s. - * @return the builder for method chaining. - * @see Jwts.ZIP + *

Custom Implementations

+ * + *

There may be only one registered {@code AeadAlgorithm} per algorithm {@code id}, and any algorithm + * instances that are {@link io.jsonwebtoken.lang.CollectionMutator#add(Object) add}ed to this collection with a + * duplicate ID will evict any existing or previously-added algorithm with the same {@code id}. But beware: + * + *

+ * Any algorithm instance added to this collection with a JWA-standard {@link Identifiable#getId() id} will + * replace (override) the JJWT standard algorithm implementation.
+ * + *

This is to allow application developers to favor their + * own implementations over JJWT's default implementations if necessary (for example, to support legacy or + * custom behavior).

+ * + * @return the {@link NestedCollection} to use to configure the AEAD encryption algorithms available when parsing. + * @see JwtBuilder#encryptWith(Key, KeyAlgorithm, AeadAlgorithm) + * @see Jwts.ENC + * @see "enc" (Encryption Algorithm) Header Parameter + * @see Encryption Algorithm Name (id) requirements * @since JJWT_RELEASE_VERSION */ - JwtParserBuilder addCompressionAlgorithms(Collection algs); + NestedCollection enc(); /** - * Adds the specified AEAD encryption algorithms to the parser's total set of supported encryption algorithms, - * replacing any existing algorithms with the same exact (CaSe-SeNsItIvE) {@link AeadAlgorithm#getId() id}s. - * If the parser encounters a JWT {@code enc} header value that equals an encryption algorithm's - * {@link Identifiable#getId() id}, that algorithm will be used for decryption. + * Configures the parser's supported {@link KeyAlgorithm}s used to obtain a JWE's decryption key. If the + * parser encounters a JWE {@link JweHeader#getAlgorithm()} alg} header value that equals a {@code KeyAlgorithm}'s + * {@link Identifiable#getId() id}, that key algorithm will be used to obtain the JWE's decryption key. * - *

There may be only one registered {@code AeadAlgorithm} per algorithm {@code id}, and the {@code algs} - * collection is added in iteration order; if a duplicate id is found when iterating the {@code algs} - * collection, the later algorithm will evict any existing algorithm with the same {@code id}.

+ *

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...
* *

Standard Algorithms and Overrides

* - *

All JWA standard encryption algorithms in {@link Jwts.ENC} are supported by default and do not need - * to be added via this method, but beware: any algorithm in the {@code algs} collection with a - * JWA standard {@link Identifiable#getId() id} will replace the JJWT standard algorithm implementation. - * This is to allow application developers to favor their own implementations over JJWT's default implementations - * if necessary (for example, to support legacy or custom behavior). + *

All JWA-standard key encryption algorithms in the {@link Jwts.KEY} registry are supported by default and + * do not need to be added. The collection may be useful however for removing some algorithms (for example, + * any algorithms not used by the application, or those not compatible with application security requirements), + * or for adding custom implementations.

* - * @param algs collection of AEAD encryption algorithms to add to the parser's total set of supported - * encryption algorithms, replacing any existing algorithms with the same - * {@link AeadAlgorithm#getId() id}s. - * @return the builder for method chaining. + *

Custom Implementations

+ * + *

There may be only one registered {@code KeyAlgorithm} per algorithm {@code id}, and any algorithm + * instances that are {@link io.jsonwebtoken.lang.CollectionMutator#add(Object) add}ed to this collection with a + * duplicate ID will evict any existing or previously-added algorithm with the same {@code id}. But beware: + * + *

+ * Any algorithm instance added to this collection with a JWA-standard {@link Identifiable#getId() id} will + * replace (override) the JJWT standard algorithm implementation.
+ * + *

This is to allow application developers to favor their + * own implementations over JJWT's default implementations if necessary (for example, to support legacy or + * custom behavior).

+ * + * @return the {@link NestedCollection} to use to configure the key algorithms available when parsing. + * @see JwtBuilder#encryptWith(Key, KeyAlgorithm, AeadAlgorithm) + * @see Jwts.KEY + * @see JWE "alg" (Algorithm) Header Parameter + * @see Key Algorithm Name (id) requirements * @since JJWT_RELEASE_VERSION */ - JwtParserBuilder addEncryptionAlgorithms(Collection algs); + NestedCollection, JwtParserBuilder> key(); /** - * Adds the specified signature algorithms to the parser's total set of supported signature algorithms, - * replacing any existing algorithms with the same exact (CaSe-SeNsItIvE) {@link Identifiable#getId() id}s. - * If the parser encounters a JWS {@code alg} header value that equals a signature algorithm's - * {@link Identifiable#getId() id}, that algorithm will be used for signature verification. + * Configures the parser's supported + * {@link io.jsonwebtoken.security.SignatureAlgorithm SignatureAlgorithm} and + * {@link io.jsonwebtoken.security.MacAlgorithm MacAlgorithm}s used to verify JWS signatures. If the parser + * encounters a JWS {@link ProtectedHeader#getAlgorithm() alg} header value that equals a signature or MAC + * algorithm's {@link Identifiable#getId() id}, that algorithm will be used to verify the JWS signature. * - *

There may be only one registered {@code SecureDigestAlgorithm} per algorithm {@code id}, and the - * {@code algs} collection is added in iteration order; if a duplicate id is found when iterating the - * {@code algs} argument, the later algorithm will evict any existing algorithm with the same {@code id}.

+ *

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...
* *

Standard Algorithms and Overrides

* - *

All JWA standard signature and MAC algorithms in {@link Jwts.SIG} are supported by default and do not need - * to be added via this method, but beware: any algorithm in the {@code algs} collection with a - * JWA standard {@link Identifiable#getId() id} will replace the JJWT standard - * algorithm implementation. This is to allow application developers to favor their own implementations over - * JJWT's default implementations if necessary (for example, to support legacy or custom behavior). + *

All JWA-standard signature and MAC algorithms in the {@link Jwts.SIG} registry are supported by default and + * do not need to be added. The collection may be useful however for removing some algorithms (for example, + * any algorithms not used by the application, or those not compatible with application security requirements), or + * for adding custom implementations.

* - * @param algs collection of signature algorithms to add to the parser's total set of supported signature - * algorithms, replacing any existing algorithms with the same exact (CaSe-SeNsItIvE) - * {@link Identifiable#getId() id}s. - * @return the builder for method chaining. + *

Custom Implementations

+ * + *

There may be only one registered {@code SecureDigestAlgorithm} per algorithm {@code id}, and any algorithm + * instances that are {@link io.jsonwebtoken.lang.CollectionMutator#add(Object) add}ed to this collection with a + * duplicate ID will evict any existing or previously-added algorithm with the same {@code id}. But beware: + * + *

+ * Any algorithm instance added to this collection with a JWA-standard {@link Identifiable#getId() id} will + * replace (override) the JJWT standard algorithm implementation.
+ * + *

This is to allow application developers to favor their + * own implementations over JJWT's default implementations if necessary (for example, to support legacy or + * custom behavior).

+ * + * @return the {@link NestedCollection} to use to configure the signature and MAC algorithms available when parsing. + * @see JwtBuilder#signWith(Key, SecureDigestAlgorithm) + * @see Jwts.SIG + * @see JWS "alg" (Algorithm) Header Parameter * @see Algorithm Name (id) requirements * @since JJWT_RELEASE_VERSION */ - JwtParserBuilder addSignatureAlgorithms(Collection> algs); + NestedCollection, JwtParserBuilder> sig(); /** - * Adds the specified key management algorithms to the parser's total set of supported key algorithms, - * replacing any existing algorithms with the same exact (CaSe-SeNsItIvE) {@link KeyAlgorithm#getId() id}s. - * If the parser encounters a JWE {@code alg} header value that equals a key management algorithm's - * {@link Identifiable#getId() id}, that algorithm will be used to obtain the content decryption key. + * Configures the parser's supported {@link CompressionAlgorithm}s used to decompress JWT payloads. If the parser + * encounters a JWT {@link ProtectedHeader#getCompressionAlgorithm() zip} header value that equals a + * compression algorithm's {@link Identifiable#getId() id}, that algorithm will be used to decompress the JWT + * payload. * - *

There may be only one registered {@code KeyAlgorithm} per algorithm {@code id}, and the {@code algs} - * collection is added in iteration order; if a duplicate id is found when iterating the {@code algs} - * argument, the later algorithm will evict any existing algorithm with the same {@code id}.

+ *

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...
* *

Standard Algorithms and Overrides

* - *

All JWA standard key management algorithms in {@link Jwts.KEY} are supported by default and do not need - * to be added via this method, but beware: any algorithm in the {@code algs} collection with a - * JWA standard {@link Identifiable#getId() id} will replace the JJWT standard algorithm implementation. - * This is to allow application developers to favor their own implementations over JJWT's default implementations - * if necessary (for example, to support legacy or custom behavior). + *

All JWA-standard compression algorithms in the {@link Jwts.ZIP} registry are supported by default and + * do not need to be added. The collection may be useful however for removing some algorithms (for example, + * any algorithms not used by the application), or for adding custom implementations.

* - * @param algs collection of key management algorithms to add to the parser's total set of supported key - * management algorithms, replacing any existing algorithms with the same exact (CaSe-SeNsItIvE) - * {@link KeyAlgorithm#getId() id}s. - * @return the builder for method chaining. + *

Custom Implementations

+ * + *

There may be only one registered {@code CompressionAlgorithm} per algorithm {@code id}, and any algorithm + * instances that are {@link io.jsonwebtoken.lang.CollectionMutator#add(Object) add}ed to this collection with a + * duplicate ID will evict any existing or previously-added algorithm with the same {@code id}. But beware: + * + *

+ * Any algorithm instance added to this collection with a JWA-standard {@link Identifiable#getId() id} will + * replace (override) the JJWT standard algorithm implementation.
+ * + *

This is to allow application developers to favor their + * own implementations over JJWT's default implementations if necessary (for example, to support legacy or + * custom behavior).

+ * + * @return the {@link NestedCollection} to use to configure the compression algorithms available when parsing. + * @see JwtBuilder#compressWith(CompressionAlgorithm) + * @see Jwts.ZIP + * @see "zip" (Compression Algorithm) Header Parameter + * @see Compression Algorithm Name (id) requirements * @since JJWT_RELEASE_VERSION */ - JwtParserBuilder addKeyAlgorithms(Collection> algs); + NestedCollection zip(); /** *

Deprecated as of JJWT JJWT_RELEASE_VERSION. This method will be removed before the 1.0 release.

* *

This method has been deprecated as of JJWT version JJWT_RELEASE_VERSION because it imposed unnecessary * implementation requirements on application developers when simply adding to a compression algorithm collection - * would suffice. Use the {@link #addCompressionAlgorithms(Collection)} method instead to add + * would suffice. Use the {@link #zip()} method instead to add * any custom algorithm implementations without needing to also implement a Locator implementation.

* *

Previous Documentation

@@ -682,8 +736,8 @@ public interface JwtParserBuilder extends Builder { * * @param compressionCodecResolver the compression codec resolver used to decompress the JWT body. * @return the parser builder for method chaining. - * @deprecated since JJWT_RELEASE_VERSION in favor of {@link #addCompressionAlgorithms(Collection)}. - * This method will be removed before the 1.0 release. + * @deprecated since JJWT_RELEASE_VERSION in favor of {@link #zip()}. This method will be removed before the + * 1.0 release. */ @Deprecated JwtParserBuilder setCompressionCodecResolver(CompressionCodecResolver compressionCodecResolver); diff --git a/api/src/main/java/io/jsonwebtoken/LocatorAdapter.java b/api/src/main/java/io/jsonwebtoken/LocatorAdapter.java index 8ff369cb..a64035d1 100644 --- a/api/src/main/java/io/jsonwebtoken/LocatorAdapter.java +++ b/api/src/main/java/io/jsonwebtoken/LocatorAdapter.java @@ -23,6 +23,7 @@ import io.jsonwebtoken.lang.Assert; * {@link #locate(JweHeader)} methods for type-specific logic if desired when the encountered header is an * unprotected JWT, or an integrity-protected JWT (either a JWS or JWE). * + * @param the type of object to locate * @since JJWT_RELEASE_VERSION */ public abstract class LocatorAdapter implements Locator { diff --git a/api/src/main/java/io/jsonwebtoken/ProtectedHeaderMutator.java b/api/src/main/java/io/jsonwebtoken/ProtectedHeaderMutator.java index 4fc20eae..371735ef 100644 --- a/api/src/main/java/io/jsonwebtoken/ProtectedHeaderMutator.java +++ b/api/src/main/java/io/jsonwebtoken/ProtectedHeaderMutator.java @@ -15,11 +15,12 @@ */ package io.jsonwebtoken; +import io.jsonwebtoken.lang.Conjunctor; +import io.jsonwebtoken.lang.NestedCollection; import io.jsonwebtoken.security.PublicJwk; import io.jsonwebtoken.security.X509Mutator; import java.net.URI; -import java.util.Collection; /** * Mutation (modifications) to a {@link ProtectedHeader Header} instance. @@ -30,29 +31,17 @@ import java.util.Collection; public interface ProtectedHeaderMutator> extends HeaderMutator, X509Mutator { /** - * Adds the name of a header parameter used by a JWT or JWA specification extension that MUST be understood - * and supported by the JWT recipient. A {@code null}, empty, whitespace-only or already existing value is ignored. + * 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: + *
+     * builder.critical().add("headerName").{@link Conjunctor#and() and()} // etc...
* - * @param crit the name of a header parameter used by a JWT or JWA specification extension that MUST be - * understood and supported by the JWT recipient. - * @return the header for method chaining. + * @return the {@link NestedCollection} to use for {@code crit} configuration. * @see JWS crit (Critical) Header Parameter * @see JWS crit (Critical) Header Parameter */ - T critical(String crit); - - /** - * Adds names of header parameters used by JWT or JWA specification extensions that MUST be - * understood and supported by the JWT recipient. {@code null}, empty, whitespace-only or already existing - * values are ignored. - * - * @param crit names of header parameters used by JWT or JWA specification extensions that MUST be - * understood and supported by the JWT recipient - * @return the header for method chaining. - * @see JWS crit (Critical) Header Parameter - * @see JWS crit (Critical) Header Parameter - */ - T critical(Collection crit); + NestedCollection critical(); /** * Sets the {@code jwk} (JSON Web Key) associated with the JWT. When set for a {@link JwsHeader}, the diff --git a/api/src/main/java/io/jsonwebtoken/io/AbstractDeserializer.java b/api/src/main/java/io/jsonwebtoken/io/AbstractDeserializer.java index 5d46ab56..a9a946e8 100644 --- a/api/src/main/java/io/jsonwebtoken/io/AbstractDeserializer.java +++ b/api/src/main/java/io/jsonwebtoken/io/AbstractDeserializer.java @@ -36,6 +36,12 @@ public abstract class AbstractDeserializer implements Deserializer { private static final byte[] EMPTY_BYTES = new byte[0]; + /** + * Default constructor, does not initialize any internal state. + */ + protected AbstractDeserializer() { + } + /** * {@inheritDoc} */ @@ -67,7 +73,7 @@ public abstract class AbstractDeserializer implements Deserializer { * * @param in the input stream to read * @return the deserialized Java object - * @throws DeserializationException if there is a problem reading the stream or creating the expected Java object + * @throws Exception if there is a problem reading the stream or creating the expected Java object */ protected abstract T doDeserialize(InputStream in) throws Exception; } diff --git a/api/src/main/java/io/jsonwebtoken/io/AbstractSerializer.java b/api/src/main/java/io/jsonwebtoken/io/AbstractSerializer.java index 9135ad98..f74d558d 100644 --- a/api/src/main/java/io/jsonwebtoken/io/AbstractSerializer.java +++ b/api/src/main/java/io/jsonwebtoken/io/AbstractSerializer.java @@ -29,6 +29,12 @@ import java.io.OutputStream; */ public abstract class AbstractSerializer implements Serializer { + /** + * Default constructor, does not initialize any internal state. + */ + protected AbstractSerializer() { + } + /** * {@inheritDoc} */ @@ -61,8 +67,8 @@ public abstract class AbstractSerializer implements Serializer { * * @param t the object to convert to a byte stream * @param out the stream to write to - * @throws SerializationException if there is a problem converting the object to a byte stream or writing the - * bytes to the {@code out}put stream. + * @throws Exception if there is a problem converting the object to a byte stream or writing the + * bytes to the {@code out}put stream. * @since JJWT_RELEASE_VERSION */ protected abstract void doSerialize(T t, OutputStream out) throws Exception; diff --git a/api/src/main/java/io/jsonwebtoken/io/CompressionAlgorithm.java b/api/src/main/java/io/jsonwebtoken/io/CompressionAlgorithm.java index 37a48f47..72e0533c 100644 --- a/api/src/main/java/io/jsonwebtoken/io/CompressionAlgorithm.java +++ b/api/src/main/java/io/jsonwebtoken/io/CompressionAlgorithm.java @@ -16,11 +16,12 @@ package io.jsonwebtoken.io; import io.jsonwebtoken.Identifiable; +import io.jsonwebtoken.JwtBuilder; +import io.jsonwebtoken.JwtParserBuilder; import io.jsonwebtoken.Jwts; import java.io.InputStream; import java.io.OutputStream; -import java.util.Collection; /** * Compresses and decompresses byte streams. @@ -32,10 +33,12 @@ import java.util.Collection; * zip header value.

* *

Custom Implementations

+ * *

A custom implementation of this interface may be used when creating a JWT by calling the - * {@link io.jsonwebtoken.JwtBuilder#compressWith(CompressionAlgorithm)} method. To ensure that parsing is - * possible, the parser must be aware of the implementation by calling - * {@link io.jsonwebtoken.JwtParserBuilder#addCompressionAlgorithms(Collection)} during parser construction.

+ * {@link JwtBuilder#compressWith(CompressionAlgorithm)} method.

+ * + *

To ensure that parsing is possible, the parser must be aware of the implementation by adding it to the + * {@link JwtParserBuilder#zip()} collection during parser construction.

* * @see Jwts.ZIP#DEF * @see Jwts.ZIP#GZIP diff --git a/api/src/main/java/io/jsonwebtoken/io/Decoder.java b/api/src/main/java/io/jsonwebtoken/io/Decoder.java index da02e805..2cf4fe89 100644 --- a/api/src/main/java/io/jsonwebtoken/io/Decoder.java +++ b/api/src/main/java/io/jsonwebtoken/io/Decoder.java @@ -18,6 +18,8 @@ package io.jsonwebtoken.io; /** * A decoder converts an already-encoded data value to a desired data type. * + * @param decoding input type + * @param decoding output type * @since 0.10.0 */ public interface Decoder { diff --git a/api/src/main/java/io/jsonwebtoken/lang/Classes.java b/api/src/main/java/io/jsonwebtoken/lang/Classes.java index 1d3a659b..65d8715b 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Classes.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Classes.java @@ -330,6 +330,15 @@ public final class Classes { } } + /** + * Returns the {@code instance}'s named (declared) field value. + * + * @param instance the instance with the internal field + * @param fieldName the name of the field to inspect + * @param fieldType the type of field to inspect + * @param field instance value type + * @return the field value + */ public static T getFieldValue(Object instance, String fieldName, Class fieldType) { if (instance == null) return null; try { diff --git a/api/src/main/java/io/jsonwebtoken/lang/CollectionMutator.java b/api/src/main/java/io/jsonwebtoken/lang/CollectionMutator.java new file mode 100644 index 00000000..a7916b3b --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/lang/CollectionMutator.java @@ -0,0 +1,61 @@ +/* + * Copyright © 2023 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.lang; + +import java.util.Collection; + +/** + * Mutation (modifications) to a {@link java.util.Collection} instance while also supporting method chaining. The + * {@link Collection#add(Object)}, {@link Collection#addAll(Collection)}, {@link Collection#remove(Object)}, and + * {@link Collection#clear()} methods do not support method chaining, so this interface enables that behavior. + * + * @param the type of elements in the collection + * @param the mutator subtype, for method chaining + * @since JJWT_RELEASE_VERSION + */ +public interface CollectionMutator> { + + /** + * Adds the specified element to the collection. + * + * @param e the element to add. + * @return the mutator/builder for method chaining. + */ + M add(E e); + + /** + * Adds the elements to the collection in iteration order. + * + * @param c the collection to add + * @return the mutator/builder for method chaining. + */ + M add(Collection c); + + /** + * Removes all elements in the collection. + * + * @return the mutator/builder for method chaining. + */ + M clear(); + + /** + * Removes the specified element from the collection. + * + * @param e the element to remove. + * @return the mutator/builder for method chaining. + */ + M remove(E e); +} diff --git a/api/src/main/java/io/jsonwebtoken/lang/Conjunctor.java b/api/src/main/java/io/jsonwebtoken/lang/Conjunctor.java new file mode 100644 index 00000000..30cad3b1 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/lang/Conjunctor.java @@ -0,0 +1,33 @@ +/* + * Copyright © 2023 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.lang; + +/** + * A {@code Conjunctor} supplies a joined object. It is typically used for nested builders to return + * to the source/original builder. + * + * @param the type of joined object to return. + * @since JJWT_RELEASE_VERSION + */ +public interface Conjunctor { + + /** + * Returns the joined object. + * + * @return the joined object. + */ + T and(); +} diff --git a/api/src/main/java/io/jsonwebtoken/lang/NestedCollection.java b/api/src/main/java/io/jsonwebtoken/lang/NestedCollection.java new file mode 100644 index 00000000..387dfe88 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/lang/NestedCollection.java @@ -0,0 +1,27 @@ +/* + * Copyright © 2023 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.lang; + +/** + * A {@link CollectionMutator} that can return access to its parent via the {@link Conjunctor#and() and()} method for + * continued configuration. + * + * @param the type of elements in the collection + * @param

the parent to return + * @since JJWT_RELEASE_VERSION + */ +public interface NestedCollection extends CollectionMutator>, Conjunctor

{ +} diff --git a/api/src/main/java/io/jsonwebtoken/lang/Strings.java b/api/src/main/java/io/jsonwebtoken/lang/Strings.java index 0d9d78a0..442a533b 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Strings.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Strings.java @@ -275,6 +275,12 @@ public final class Strings { return new String(asciiBytes, StandardCharsets.US_ASCII); } + /** + * Returns the {@link StandardCharsets#US_ASCII US_ASCII}-encoded bytes of the specified {@code CharSequence}. + * + * @param s the {@code CharSequence} to encode to {@code US_ASCII}. + * @return the {@link StandardCharsets#US_ASCII US_ASCII}-encoded bytes of the specified {@code CharSequence}. + */ public static byte[] ascii(CharSequence s) { byte[] bytes = null; if (s != null) { diff --git a/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwkBuilder.java index ca57c5d0..84583434 100644 --- a/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwkBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwkBuilder.java @@ -16,7 +16,6 @@ package io.jsonwebtoken.security; import java.security.Key; -import java.util.Collection; /** * A {@link JwkBuilder} that builds asymmetric (public or private) JWKs. @@ -69,7 +68,7 @@ public interface AsymmetricJwkBuilder, * *

Per * JWK RFC 7517, Section 4.3, last paragraph, - * the use (Public Key Use) and {@link #operations(Collection) key_ops (Key Operations)} members + * the use (Public Key Use) and {@link #operations() key_ops (Key Operations)} members * SHOULD NOT be used together; however, if both are used, the information they convey MUST be * consistent. Applications should specify which of these members they use, if either is to be used by the * application.

diff --git a/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java index b55761bd..af41efb0 100644 --- a/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java @@ -15,10 +15,11 @@ */ package io.jsonwebtoken.security; +import io.jsonwebtoken.lang.Conjunctor; import io.jsonwebtoken.lang.MapMutator; +import io.jsonwebtoken.lang.NestedCollection; import java.security.Key; -import java.util.Collection; /** * A {@link SecurityBuilder} that produces a JWK. A JWK is an immutable set of name/value pairs that represent a @@ -104,83 +105,34 @@ public interface JwkBuilder, T extends JwkBuilde T idFromThumbprint(HashAlgorithm alg); /** - * Specifies an operation for which the key may be used by adding it to the - * JWK {@code key_ops} (Key Operations) - * Parameter values. This method may be called multiple times. + * Configures the key operations for which + * 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...
* - *

The {@code key_ops} (key operations) parameter identifies the operation(s) for which the key is - * intended to be used. The {@code key_ops} parameter is intended for use cases in which public, - * private, or symmetric keys may be present.

- * - *

Security Vulnerability Notice

- * - *

Multiple unrelated key operations SHOULD NOT be specified for a key because of the potential - * vulnerabilities associated with using the same key with multiple algorithms. Thus, the combinations - * {@link Jwks.OP#SIGN sign} with {@link Jwks.OP#VERIFY verify}, - * {@link Jwks.OP#ENCRYPT encrypt} with {@link Jwks.OP#DECRYPT decrypt}, and - * {@link Jwks.OP#WRAP_KEY wrapKey} with {@link Jwks.OP#UNWRAP_KEY unwrapKey} are permitted, but other combinations - * SHOULD NOT be used. This is enforced by the builder's key operation - * {@link #operationPolicy(KeyOperationPolicy) policy}.

+ *

The {@code and()} method 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.

* *

Standard {@code KeyOperation}s and Overrides

* *

All RFC-standard JWK Key Operations in the {@link Jwks.OP} registry are supported via the builder's default - * operations {@link #operationPolicy(KeyOperationPolicy) policy}, but other (custom) values + * {@link #operationPolicy(KeyOperationPolicy) operationPolicy}, but other (custom) values * MAY be specified (for example, using a {@link Jwks.OP#builder()}).

* *

If the {@code JwkBuilder} is being used to rebuild or parse an existing JWK however, any custom operations - * should be enabled for the {@code JwkBuilder} by {@link #operationPolicy(KeyOperationPolicy) specifying} - * an operations policy that includes the custom values (e.g. via + * should be enabled by configuring an {@link #operationPolicy(KeyOperationPolicy) operationPolicy} + * that includes the custom values (e.g. via * {@link Jwks.OP#policy()}.{@link KeyOperationPolicyBuilder#add(KeyOperation) add(customKeyOperation)}).

* *

For best interoperability with other applications however, it is recommended to use only the {@link Jwks.OP} * constants.

* - * @param operation the value to add to the JWK {@code key_ops} value set - * @return the builder for method chaining. - * @throws IllegalArgumentException if the {@code op} is not permitted by the operations - * {@link #operationPolicy(KeyOperationPolicy) policy}. + * @return the {@link NestedCollection} to use for {@code key_ops} configuration. * @see Jwks.OP + * @see RFC 7517: key_ops (Key Operations) Parameter */ - T operation(KeyOperation operation) throws IllegalArgumentException; - - /** - * Adds {@code ops} to the JWK {@code key_ops} - * (Key Operations) Parameter values. - * - *

The {@code key_ops} (key operations) parameter identifies the operation(s) for which the key is - * intended to be used. The {@code key_ops} parameter is intended for use cases in which public, - * private, or symmetric keys may be present.

- * - *

Security Vulnerability Notice

- * - *

Multiple unrelated key operations SHOULD NOT be specified for a key because of the potential - * vulnerabilities associated with using the same key with multiple algorithms. Thus, the combinations - * {@link Jwks.OP#SIGN sign} with {@link Jwks.OP#VERIFY verify}, - * {@link Jwks.OP#ENCRYPT encrypt} with {@link Jwks.OP#DECRYPT decrypt}, and - * {@link Jwks.OP#WRAP_KEY wrapKey} with {@link Jwks.OP#UNWRAP_KEY unwrapKey} are permitted, but other combinations - * SHOULD NOT be used. This is enforced by the builder's default - * operation {@link #operationPolicy(KeyOperationPolicy) policy}.

- * - *

Standard {@code KeyOperation}s and Overrides

- * - *

All RFC-standard JWK Key Operations in the {@link Jwks.OP} registry are supported via the builder's default - * operations {@link #operationPolicy(KeyOperationPolicy) policy}, but other (custom) values - * MAY be specified (for example, using a {@link Jwks.OP#builder()}).

- * - *

If the {@code JwkBuilder} is being used to rebuild or parse an existing JWK however, any custom operations - * should be enabled for the {@code JwkBuilder} by {@link #operationPolicy(KeyOperationPolicy) specifying} - * an operations policy that includes the custom values (e.g. via - * {@link Jwks.OP#policy()}.{@link KeyOperationPolicyBuilder#add(KeyOperation) add(customKeyOperation)}).

- * - *

For best interoperability with other applications however, it is recommended to use only the {@link Jwks.OP} - * constants.

- * - * @param ops the {@code KeyOperation} values to add to the JWK {@code key_ops} value set - * @return the builder for method chaining. - * @throws IllegalArgumentException if any of the operations are not permitted by the operations - * {@link #operationPolicy(KeyOperationPolicy) policy}. - * @see Jwks.OP - */ - T operations(Collection ops) throws IllegalArgumentException; + NestedCollection operations(); } diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyOperationBuilder.java b/api/src/main/java/io/jsonwebtoken/security/KeyOperationBuilder.java index ccfaec38..cb24e45b 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyOperationBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyOperationBuilder.java @@ -17,11 +17,9 @@ package io.jsonwebtoken.security; import io.jsonwebtoken.lang.Builder; -import java.util.Collection; - /** * A {@code KeyOperationBuilder} produces {@link KeyOperation} instances that may be added to a JWK's - * {@link JwkBuilder#operations(Collection) key operations} parameter. This is primarily only useful for creating + * {@link JwkBuilder#operations() key operations} parameter. This is primarily only useful for creating * custom (non-standard) {@code KeyOperation}s for use with a custom {@link KeyOperationPolicy}, as all standard ones * are available already via the {@link Jwks.OP} registry singleton. * diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyOperationPolicied.java b/api/src/main/java/io/jsonwebtoken/security/KeyOperationPolicied.java index a386fbfd..49e89387 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyOperationPolicied.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyOperationPolicied.java @@ -33,8 +33,10 @@ public interface KeyOperationPolicied> { *
      * Multiple unrelated key operations SHOULD NOT be specified for a key
      * because of the potential vulnerabilities associated with using the
-     * same key with multiple algorithms.
-     * 
+ * same key with multiple algorithms. Thus, the combinations "{@link Jwks.OP#SIGN sign}" + * with "{@link Jwks.OP#VERIFY verify}", "{@link Jwks.OP#ENCRYPT encrypt}" with "{@link Jwks.OP#DECRYPT decrypt}", and "{@link Jwks.OP#WRAP_KEY wrapKey}" with + * "{@link Jwks.OP#UNWRAP_KEY unwrapKey}" are permitted, but other combinations SHOULD NOT be used. + * * * *

If you wish to enable a different policy, perhaps to support additional custom {@code KeyOperation} values, diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyOperationPolicy.java b/api/src/main/java/io/jsonwebtoken/security/KeyOperationPolicy.java index d94536f1..b36aaa31 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyOperationPolicy.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyOperationPolicy.java @@ -20,8 +20,8 @@ import java.util.Collection; /** * A key operation policy determines which {@link KeyOperation}s may be assigned to a JWK. * - * @since JJWT_RELEASE_VERSION * @see JwkBuilder#operationPolicy(KeyOperationPolicy) + * @since JJWT_RELEASE_VERSION */ public interface KeyOperationPolicy { @@ -39,5 +39,5 @@ public interface KeyOperationPolicy { * @param ops the operations to validate */ @SuppressWarnings("GrazieInspection") - void validate(Collection ops) throws IllegalArgumentException; + void validate(Collection ops) throws IllegalArgumentException; } diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyOperationPolicyBuilder.java b/api/src/main/java/io/jsonwebtoken/security/KeyOperationPolicyBuilder.java index dcd39729..3873cc2f 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyOperationPolicyBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyOperationPolicyBuilder.java @@ -17,6 +17,7 @@ package io.jsonwebtoken.security; import io.jsonwebtoken.Identifiable; import io.jsonwebtoken.lang.Builder; +import io.jsonwebtoken.lang.CollectionMutator; import java.util.Collection; @@ -32,7 +33,8 @@ import java.util.Collection; * @see Jwks.OP#builder() * @since JJWT_RELEASE_VERSION */ -public interface KeyOperationPolicyBuilder extends Builder { +public interface KeyOperationPolicyBuilder extends CollectionMutator, + Builder { /** * Allows a JWK to have unrelated {@link KeyOperation}s in its {@code key_ops} parameter values. Be careful @@ -72,8 +74,10 @@ public interface KeyOperationPolicyBuilder extends Builder { * @see Jwks.OP * @see Jwks.OP#builder() * @see JwkBuilder#operationPolicy(KeyOperationPolicy) - * @see JwkBuilder#operations(Collection) + * @see JwkBuilder#operations() */ + @Override + // for better JavaDoc KeyOperationPolicyBuilder add(KeyOperation op); /** @@ -101,8 +105,10 @@ public interface KeyOperationPolicyBuilder extends Builder { * @see Jwks.OP * @see Jwks.OP#builder() * @see JwkBuilder#operationPolicy(KeyOperationPolicy) - * @see JwkBuilder#operations(Collection) + * @see JwkBuilder#operations() */ - KeyOperationPolicyBuilder add(Collection ops); + @Override + // for better JavaDoc + KeyOperationPolicyBuilder add(Collection ops); } diff --git a/api/src/main/java/io/jsonwebtoken/security/SecureRequest.java b/api/src/main/java/io/jsonwebtoken/security/SecureRequest.java index cd0cd764..25749db7 100644 --- a/api/src/main/java/io/jsonwebtoken/security/SecureRequest.java +++ b/api/src/main/java/io/jsonwebtoken/security/SecureRequest.java @@ -20,6 +20,7 @@ import java.security.Key; /** * A request to a cryptographic algorithm requiring a {@link Key}. * + * @param the type of payload in the request * @param they type of key used by the algorithm during the request * @since JJWT_RELEASE_VERSION */ diff --git a/api/src/main/java/io/jsonwebtoken/security/SecurityBuilder.java b/api/src/main/java/io/jsonwebtoken/security/SecurityBuilder.java index d0333a87..40e216b8 100644 --- a/api/src/main/java/io/jsonwebtoken/security/SecurityBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/SecurityBuilder.java @@ -25,6 +25,7 @@ import java.security.SecureRandom; * during instance creation, such as a {@link java.security.Provider} or {@link java.security.SecureRandom}. * * @param The type of object that will be created each time {@link #build()} is invoked. + * @param the type of SecurityBuilder returned for method chaining * @see #provider(Provider) * @see #random(SecureRandom) * @since JJWT_RELEASE_VERSION diff --git a/impl/src/main/java/io/jsonwebtoken/impl/AbstractAudienceCollection.java b/impl/src/main/java/io/jsonwebtoken/impl/AbstractAudienceCollection.java new file mode 100644 index 00000000..d2a17edd --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/AbstractAudienceCollection.java @@ -0,0 +1,34 @@ +/* + * Copyright © 2023 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; + +import io.jsonwebtoken.ClaimsMutator; +import io.jsonwebtoken.impl.lang.DefaultNestedCollection; + +import java.util.Collection; + +/** + * Abstract NestedCollection that requires the AudienceCollection interface to be implemented. + * + * @param

type of parent to return + * @since JJWT_RELEASE_VERSION + */ +abstract class AbstractAudienceCollection

extends DefaultNestedCollection + implements ClaimsMutator.AudienceCollection

{ + protected AbstractAudienceCollection(P parent, Collection seed) { + super(parent, seed); + } +} \ No newline at end of file diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeaderMutator.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeaderMutator.java index 50cb36c3..93db6612 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeaderMutator.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeaderMutator.java @@ -16,19 +16,18 @@ package io.jsonwebtoken.impl; import io.jsonwebtoken.JweHeaderMutator; +import io.jsonwebtoken.impl.lang.DefaultNestedCollection; import io.jsonwebtoken.impl.lang.DelegatingMapMutator; import io.jsonwebtoken.impl.lang.Parameter; import io.jsonwebtoken.impl.security.X509BuilderSupport; import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.lang.NestedCollection; import io.jsonwebtoken.lang.Strings; import io.jsonwebtoken.security.PublicJwk; import java.net.URI; import java.security.cert.X509Certificate; -import java.util.Collection; -import java.util.LinkedHashSet; import java.util.List; -import java.util.Set; /** * @param return type for method chaining @@ -105,29 +104,14 @@ public class DefaultJweHeaderMutator> // ============================================================= @Override - public T critical(String crit) { - crit = Strings.clean(crit); - if (Strings.hasText(crit)) { - critical(Collections.setOf(crit)); - } - return self(); - } - - @Override - public T critical(Collection crit) { - if (!Collections.isEmpty(crit)) { - Set existing = Collections.nullSafe(this.DELEGATE.get(DefaultProtectedHeader.CRIT)); - Set set = new LinkedHashSet<>(existing.size() + crit.size()); - set.addAll(existing); - for (String s : crit) { - s = Strings.clean(s); - if (s != null) { - set.add(s); - } + public NestedCollection critical() { + return new DefaultNestedCollection(self(), this.DELEGATE.get(DefaultProtectedHeader.CRIT)) { + @Override + public T and() { + put(DefaultProtectedHeader.CRIT, Collections.asSet(getCollection())); + return super.and(); } - put(DefaultProtectedHeader.CRIT, set); - } - return self(); + }; } @Override diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java index 4865c714..ce9cb2c6 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java @@ -74,7 +74,6 @@ import java.security.PrivateKey; import java.security.Provider; import java.security.PublicKey; import java.security.SecureRandom; -import java.util.Collection; import java.util.Date; import java.util.LinkedHashSet; import java.util.Map; @@ -435,19 +434,8 @@ public class DefaultJwtBuilder implements JwtBuilder { } @Override - public JwtBuilder audienceSingle(String aud) { - //noinspection deprecation - return claims().audienceSingle(aud).and(); - } - - @Override - public JwtBuilder audience(String aud) { - return claims().audience(aud).and(); - } - - @Override - public JwtBuilder audience(Collection aud) { - return claims().audience(aud).and(); + public AudienceCollection audience() { + return new DelegateAudienceCollection<>((JwtBuilder) this, claims().audience()); } @Override @@ -586,7 +574,7 @@ public class DefaultJwtBuilder implements JwtBuilder { this.headerBuilder.add(DefaultHeader.ALGORITHM.getId(), sigAlg.getId()); if (!this.encodePayload) { // b64 extension: String id = DefaultJwsHeader.B64.getId(); - this.headerBuilder.critical(id).add(id, false); + this.headerBuilder.critical().add(id).and().add(id, false); } final JwsHeader header = Assert.isInstanceOf(JwsHeader.class, this.headerBuilder.build()); encodeAndWrite("JWS Protected Header", header, jws); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index bd5df7e9..749322b6 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -59,6 +59,7 @@ import io.jsonwebtoken.lang.Classes; import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.DateFormats; import io.jsonwebtoken.lang.Objects; +import io.jsonwebtoken.lang.Registry; import io.jsonwebtoken.lang.Strings; import io.jsonwebtoken.security.AeadAlgorithm; import io.jsonwebtoken.security.DecryptAeadRequest; @@ -188,13 +189,13 @@ public class DefaultJwtParser implements JwtParser { private final boolean unsecuredDecompression; - private final Function> sigAlgFn; + private final Function> sigAlgs; - private final Function encAlgFn; + private final Function encAlgs; - private final Function> keyAlgFn; + private final Function> keyAlgs; - private final Function zipAlgFn; + private final Function zipAlgs; private final Locator keyLocator; @@ -224,10 +225,10 @@ public class DefaultJwtParser implements JwtParser { Decoder base64UrlDecoder, Deserializer> deserializer, CompressionCodecResolver compressionCodecResolver, - Collection extraZipAlgs, - Collection> extraSigAlgs, - Collection> extraKeyAlgs, - Collection extraEncAlgs) { + Registry zipAlgs, + Registry> sigAlgs, + Registry> keyAlgs, + Registry encAlgs) { this.provider = provider; this.unsecured = unsecured; this.unsecuredDecompression = unsecuredDecompression; @@ -239,12 +240,11 @@ public class DefaultJwtParser implements JwtParser { this.expectedClaims = Jwts.claims().add(expectedClaims); this.decoder = Assert.notNull(base64UrlDecoder, "base64UrlDecoder cannot be null."); this.deserializer = Assert.notNull(deserializer, "JSON Deserializer cannot be null."); - - this.sigAlgFn = new IdLocator<>(DefaultHeader.ALGORITHM, Jwts.SIG.get(), extraSigAlgs, MISSING_JWS_ALG_MSG); - this.keyAlgFn = new IdLocator<>(DefaultHeader.ALGORITHM, Jwts.KEY.get(), extraKeyAlgs, MISSING_JWE_ALG_MSG); - this.encAlgFn = new IdLocator<>(DefaultJweHeader.ENCRYPTION_ALGORITHM, Jwts.ENC.get(), extraEncAlgs, MISSING_ENC_MSG); - this.zipAlgFn = compressionCodecResolver != null ? new CompressionCodecLocator(compressionCodecResolver) : - new IdLocator<>(DefaultHeader.COMPRESSION_ALGORITHM, Jwts.ZIP.get(), extraZipAlgs, null); + this.sigAlgs = new IdLocator<>(DefaultHeader.ALGORITHM, sigAlgs, MISSING_JWS_ALG_MSG); + this.keyAlgs = new IdLocator<>(DefaultHeader.ALGORITHM, keyAlgs, MISSING_JWE_ALG_MSG); + this.encAlgs = new IdLocator<>(DefaultJweHeader.ENCRYPTION_ALGORITHM, encAlgs, MISSING_ENC_MSG); + this.zipAlgs = compressionCodecResolver != null ? new CompressionCodecLocator(compressionCodecResolver) : + new IdLocator<>(DefaultHeader.COMPRESSION_ALGORITHM, zipAlgs, null); } @Override @@ -271,7 +271,7 @@ public class DefaultJwtParser implements JwtParser { SecureDigestAlgorithm algorithm; try { - algorithm = (SecureDigestAlgorithm) sigAlgFn.apply(jwsHeader); + algorithm = (SecureDigestAlgorithm) sigAlgs.apply(jwsHeader); } catch (UnsupportedJwtException e) { //For backwards compatibility. TODO: remove this try/catch block for 1.0 and let UnsupportedJwtException propagate String msg = "Unsupported signature algorithm '" + alg + "'"; @@ -531,10 +531,10 @@ public class DefaultJwtParser implements JwtParser { if (!Strings.hasText(enc)) { throw new MalformedJwtException(MISSING_ENC_MSG); } - final AeadAlgorithm encAlg = this.encAlgFn.apply(jweHeader); + final AeadAlgorithm encAlg = this.encAlgs.apply(jweHeader); Assert.stateNotNull(encAlg, "JWE Encryption Algorithm cannot be null."); - @SuppressWarnings("rawtypes") final KeyAlgorithm keyAlg = this.keyAlgFn.apply(jweHeader); + @SuppressWarnings("rawtypes") final KeyAlgorithm keyAlg = this.keyAlgs.apply(jweHeader); Assert.stateNotNull(keyAlg, "JWE Key Algorithm cannot be null."); Key key = this.keyLocator.locate(jweHeader); @@ -578,7 +578,7 @@ public class DefaultJwtParser implements JwtParser { integrityVerified = true; // no exception means signature verified } - final CompressionAlgorithm compressionAlgorithm = zipAlgFn.apply(header); + final CompressionAlgorithm compressionAlgorithm = zipAlgs.apply(header); if (compressionAlgorithm != null) { if (!integrityVerified) { if (!payloadBase64UrlEncoded) { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java index 63ce91e6..9b57af0c 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java @@ -24,15 +24,22 @@ import io.jsonwebtoken.Jwts; import io.jsonwebtoken.Locator; import io.jsonwebtoken.SigningKeyResolver; import io.jsonwebtoken.impl.io.DelegateStringDecoder; +import io.jsonwebtoken.impl.io.StandardCompressionAlgorithms; +import io.jsonwebtoken.impl.lang.DefaultNestedCollection; +import io.jsonwebtoken.impl.lang.IdRegistry; import io.jsonwebtoken.impl.lang.Services; import io.jsonwebtoken.impl.security.ConstantKeyLocator; +import io.jsonwebtoken.impl.security.StandardEncryptionAlgorithms; +import io.jsonwebtoken.impl.security.StandardKeyAlgorithms; +import io.jsonwebtoken.impl.security.StandardSecureDigestAlgorithms; import io.jsonwebtoken.io.CompressionAlgorithm; import io.jsonwebtoken.io.Decoder; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.io.Deserializer; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Collections; -import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.lang.NestedCollection; +import io.jsonwebtoken.lang.Registry; import io.jsonwebtoken.security.AeadAlgorithm; import io.jsonwebtoken.security.InvalidKeyException; import io.jsonwebtoken.security.KeyAlgorithm; @@ -45,9 +52,7 @@ import java.security.Key; import java.security.PrivateKey; import java.security.Provider; import java.security.PublicKey; -import java.util.Collection; import java.util.Date; -import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; @@ -79,17 +84,18 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder { @SuppressWarnings("deprecation") //TODO: remove for 1.0 private SigningKeyResolver signingKeyResolver = null; - private final Collection extraEncAlgs = new LinkedHashSet<>(); + private Registry encAlgs = Jwts.ENC.get(); - private final Collection> extraKeyAlgs = new LinkedHashSet<>(); + private Registry> keyAlgs = Jwts.KEY.get(); - private final Collection> extraSigAlgs = new LinkedHashSet<>(); + private Registry> sigAlgs = Jwts.SIG.get(); - private final Collection extraZipAlgs = new LinkedHashSet<>(); + private Registry zipAlgs = Jwts.ZIP.get(); @SuppressWarnings("deprecation") private CompressionCodecResolver compressionCodecResolver; + @SuppressWarnings("deprecation") private Decoder decoder = new DelegateStringDecoder(Decoders.BASE64URL); private Deserializer> deserializer; @@ -162,7 +168,7 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder { @Override public JwtParserBuilder requireAudience(String audience) { - expectedClaims.audience(audience); + expectedClaims.audience().add(audience).and(); return this; } @@ -211,20 +217,14 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder { } @Override - public JwtParserBuilder critical(String crit) { - if (Strings.hasText(crit)) { - Set existing = Collections.nullSafe(this.critical); - Set newSet = new LinkedHashSet<>(existing); - newSet.add(crit); - critical(newSet); - } - return this; - } - - @Override - public JwtParserBuilder critical(Set crit) { - this.critical = Collections.immutable(new LinkedHashSet<>(Collections.nullSafe(crit))); - return this; + public NestedCollection critical() { + return new DefaultNestedCollection(this, this.critical) { + @Override + public JwtParserBuilder and() { + critical = Collections.asSet(getCollection()); + return super.and(); + } + }; } @Override @@ -301,31 +301,47 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder { } @Override - public JwtParserBuilder addCompressionAlgorithms(Collection algs) { - Assert.notEmpty(algs, "Additional CompressionAlgorithm collection cannot be null or empty."); - this.extraZipAlgs.addAll(algs); - return this; + public NestedCollection zip() { + return new DefaultNestedCollection(this, this.zipAlgs.values()) { + @Override + public JwtParserBuilder and() { + zipAlgs = new IdRegistry<>(StandardCompressionAlgorithms.NAME, getCollection()); + return super.and(); + } + }; } @Override - public JwtParserBuilder addEncryptionAlgorithms(Collection algs) { - Assert.notEmpty(algs, "Additional AeadAlgorithm collection cannot be null or empty."); - this.extraEncAlgs.addAll(algs); - return this; + public NestedCollection enc() { + return new DefaultNestedCollection(this, this.encAlgs.values()) { + @Override + public JwtParserBuilder and() { + encAlgs = new IdRegistry<>(StandardEncryptionAlgorithms.NAME, getCollection()); + return super.and(); + } + }; } @Override - public JwtParserBuilder addSignatureAlgorithms(Collection> algs) { - Assert.notEmpty(algs, "Additional SignatureAlgorithm collection cannot be null or empty."); - this.extraSigAlgs.addAll(algs); - return this; + public NestedCollection, JwtParserBuilder> sig() { + return new DefaultNestedCollection, JwtParserBuilder>(this, this.sigAlgs.values()) { + @Override + public JwtParserBuilder and() { + sigAlgs = new IdRegistry<>(StandardSecureDigestAlgorithms.NAME, getCollection()); + return super.and(); + } + }; } @Override - public JwtParserBuilder addKeyAlgorithms(Collection> algs) { - Assert.notEmpty(algs, "Additional KeyAlgorithm collection cannot be null or empty."); - this.extraKeyAlgs.addAll(algs); - return this; + public NestedCollection, JwtParserBuilder> key() { + return new DefaultNestedCollection, JwtParserBuilder>(this, this.keyAlgs.values()) { + @Override + public JwtParserBuilder and() { + keyAlgs = new IdRegistry<>(StandardKeyAlgorithms.NAME, getCollection()); + return super.and(); + } + }; } @SuppressWarnings("deprecation") //TODO: remove for 1.0 @@ -385,9 +401,9 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder { "due to their security implications."; throw new IllegalStateException(msg); } - if (this.compressionCodecResolver != null && !Collections.isEmpty(extraZipAlgs)) { - String msg = "Both 'addCompressionAlgorithms' and 'compressionCodecResolver' " + - "cannot be specified. Choose either."; + if (this.compressionCodecResolver != null && !Jwts.ZIP.get().equals(this.zipAlgs)) { + String msg = "Both 'zip()' and 'compressionCodecResolver' " + + "cannot be configured. Choose either."; throw new IllegalStateException(msg); } @@ -409,10 +425,10 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder { decoder, deserializer, compressionCodecResolver, - extraZipAlgs, - extraSigAlgs, - extraKeyAlgs, - extraEncAlgs + zipAlgs, + sigAlgs, + keyAlgs, + encAlgs ); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DelegateAudienceCollection.java b/impl/src/main/java/io/jsonwebtoken/impl/DelegateAudienceCollection.java new file mode 100644 index 00000000..64ea44bd --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/DelegateAudienceCollection.java @@ -0,0 +1,69 @@ +/* + * Copyright © 2023 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; + +import io.jsonwebtoken.ClaimsMutator; +import io.jsonwebtoken.lang.Assert; + +import java.util.Collection; + +public class DelegateAudienceCollection

implements ClaimsMutator.AudienceCollection

{ + + private final ClaimsMutator.AudienceCollection delegate; + + private final P parent; + + public DelegateAudienceCollection(P parent, ClaimsMutator.AudienceCollection delegate) { + this.parent = Assert.notNull(parent, "Parent cannot be null."); + this.delegate = Assert.notNull(delegate, "Delegate cannot be null."); + } + + @Override + public P single(String aud) { + delegate.single(aud); + return parent; + } + + @Override + public ClaimsMutator.AudienceCollection

add(String s) { + delegate.add(s); + return this; + } + + @Override + public ClaimsMutator.AudienceCollection

add(Collection c) { + delegate.add(c); + return this; + } + + @Override + public ClaimsMutator.AudienceCollection

clear() { + delegate.clear(); + return this; + } + + @Override + public ClaimsMutator.AudienceCollection

remove(String s) { + delegate.remove(s); + return this; + } + + @Override + public P and() { + delegate.and(); // allow any cleanup/finalization + return parent; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DelegatingClaimsMutator.java b/impl/src/main/java/io/jsonwebtoken/impl/DelegatingClaimsMutator.java index ff8da9a0..bde1b34c 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DelegatingClaimsMutator.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DelegatingClaimsMutator.java @@ -23,9 +23,7 @@ import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.MapMutator; import io.jsonwebtoken.lang.Strings; -import java.util.Collection; import java.util.Date; -import java.util.LinkedHashSet; import java.util.Set; /** @@ -74,7 +72,8 @@ public class DelegatingClaimsMutator & C @Override public T setAudience(String aud) { - return audienceSingle(aud); + //noinspection deprecation + return audience().single(aud); } private Set getAudience() { @@ -83,51 +82,36 @@ public class DelegatingClaimsMutator & C String existing = get(AUDIENCE_STRING); remove(AUDIENCE_STRING.getId()); // clear out any canonical/idiomatic values since we're replacing setDelegate(this.DELEGATE.replace(DefaultClaims.AUDIENCE)); - if (Strings.hasText(existing)) { - put(DefaultClaims.AUDIENCE, Collections.setOf(existing)); // replace as Set - } + put(DefaultClaims.AUDIENCE, Collections.setOf(existing)); // replace as Set } return get(DefaultClaims.AUDIENCE); } - @Override - public T audienceSingle(String aud) { + private T audienceSingle(String aud) { if (!Strings.hasText(aud)) { return put(DefaultClaims.AUDIENCE, null); } // otherwise it's an actual single string, we need to ensure that we can represent it as a single - // string by swapping out the AUDIENCE param if necessary: - if (this.DELEGATE.PARAMS.get(AUDIENCE_STRING.getId()).supports(Collections.emptySet())) { // need to swap: - remove(AUDIENCE_STRING.getId()); //remove any existing value, as conversion will throw an exception - setDelegate(this.DELEGATE.replace(AUDIENCE_STRING)); - } + // string by swapping out the AUDIENCE param: + remove(AUDIENCE_STRING.getId()); //remove any existing value, as conversion will throw an exception + setDelegate(this.DELEGATE.replace(AUDIENCE_STRING)); return put(AUDIENCE_STRING, aud); } @Override - public T audience(String aud) { - aud = Strings.clean(aud); - if (Strings.hasText(aud)) { - audience(java.util.Collections.singleton(aud)); - } - return self(); - } - - @Override - public T audience(Collection aud) { - if (!Collections.isEmpty(aud)) { - Set existing = Collections.nullSafe(getAudience()); - Set set = new LinkedHashSet<>(existing.size() + aud.size()); - set.addAll(existing); - for (String s : aud) { - s = Strings.clean(s); - if (s != null) { - set.add(s); - } + public AudienceCollection audience() { + return new AbstractAudienceCollection(self(), getAudience()) { + @Override + public T single(String audience) { + return audienceSingle(audience); } - put(DefaultClaims.AUDIENCE, set); - } - return self(); + + @Override + public T and() { + put(DefaultClaims.AUDIENCE, Collections.asSet(getCollection())); + return super.and(); + } + }; } @Override diff --git a/impl/src/main/java/io/jsonwebtoken/impl/IdLocator.java b/impl/src/main/java/io/jsonwebtoken/impl/IdLocator.java index c5824dec..32161715 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/IdLocator.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/IdLocator.java @@ -23,16 +23,11 @@ import io.jsonwebtoken.Locator; import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.UnsupportedJwtException; import io.jsonwebtoken.impl.lang.Function; -import io.jsonwebtoken.impl.lang.IdRegistry; import io.jsonwebtoken.impl.lang.Parameter; import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.Registry; import io.jsonwebtoken.lang.Strings; -import java.util.Collection; -import java.util.LinkedHashSet; - public class IdLocator implements Locator, Function { private final Parameter param; @@ -41,28 +36,12 @@ public class IdLocator implements Loca private final Registry registry; - public IdLocator(Parameter param, Registry registry, Collection extras, String requiredExceptionMessage) { + public IdLocator(Parameter param, Registry registry, String requiredExceptionMessage) { this.param = Assert.notNull(param, "Header param cannot be null."); this.requiredMsg = Strings.clean(requiredExceptionMessage); this.valueRequired = Strings.hasText(this.requiredMsg); Assert.notEmpty(registry, "Registry cannot be null or empty."); - Collection all = new LinkedHashSet<>(Collections.size(registry) + Collections.size(extras)); - all.addAll(registry.values()); // defaults MUST come before extras to allow extras to override if necessary - all.addAll(extras); - - // The registry requires CaSe-SeNsItIvE keys on purpose - all JWA standard algorithm identifiers - // (JWS 'alg', JWE 'enc', JWK 'kty', etc) are all case-sensitive per via the following RFC language: - // - // This name is a case-sensitive ASCII string. Names may not match other registered names in a - // case-insensitive manner unless the Designated Experts state that there is a compelling reason to - // allow an exception. - // - // References: - // - JWS/JWE alg and JWE enc 'Algorithm Name': https://www.rfc-editor.org/rfc/rfc7518.html#section-7.1.1 - // - JWE zip 'Compression Algorithm Value': https://www.rfc-editor.org/rfc/rfc7518.html#section-7.3.1 - // - JWK '"kty" Parameter Value': https://www.rfc-editor.org/rfc/rfc7518.html#section-7.4.1 - - this.registry = new IdRegistry<>(param.getName(), all); // do not use the caseSensitive ctor argument - must be false + this.registry = registry; } private static String type(Header header) { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/io/StandardCompressionAlgorithms.java b/impl/src/main/java/io/jsonwebtoken/impl/io/StandardCompressionAlgorithms.java index 5ae80ccc..fc6fc867 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/io/StandardCompressionAlgorithms.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/io/StandardCompressionAlgorithms.java @@ -17,18 +17,19 @@ package io.jsonwebtoken.impl.io; import io.jsonwebtoken.impl.compression.DeflateCompressionAlgorithm; import io.jsonwebtoken.impl.compression.GzipCompressionAlgorithm; -import io.jsonwebtoken.impl.lang.DelegatingRegistry; import io.jsonwebtoken.impl.lang.IdRegistry; import io.jsonwebtoken.io.CompressionAlgorithm; import io.jsonwebtoken.lang.Collections; @SuppressWarnings("unused") // used via reflection in io.jsonwebtoken.Jwts.ZIP -public final class StandardCompressionAlgorithms extends DelegatingRegistry { +public final class StandardCompressionAlgorithms extends IdRegistry { + + public static final String NAME = "Compression Algorithm"; public StandardCompressionAlgorithms() { - super(new IdRegistry<>("Compression Algorithm", Collections.of( + super(NAME, Collections.of( new DeflateCompressionAlgorithm(), new GzipCompressionAlgorithm() - ), false)); + )); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultCollectionMutator.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultCollectionMutator.java new file mode 100644 index 00000000..9a64d7e6 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultCollectionMutator.java @@ -0,0 +1,75 @@ +/* + * Copyright © 2023 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.lang; + +import io.jsonwebtoken.Identifiable; +import io.jsonwebtoken.lang.CollectionMutator; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.lang.Objects; +import io.jsonwebtoken.lang.Strings; + +import java.util.Collection; +import java.util.LinkedHashSet; + +public class DefaultCollectionMutator> implements CollectionMutator { + + private final Collection collection; + + public DefaultCollectionMutator(Collection seed) { + this.collection = new LinkedHashSet<>(Collections.nullSafe(seed)); + } + + @SuppressWarnings("unchecked") + protected final M self() { + return (M) this; + } + + @Override + public M add(E e) { + if (Objects.isEmpty(e)) return self(); + 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 self(); + } + + @Override + public M remove(E e) { + this.collection.remove(e); + return self(); + } + + @Override + public M add(Collection c) { + for (E element : Collections.nullSafe(c)) { + add(element); + } + return self(); + } + + @Override + public M clear() { + this.collection.clear(); + return self(); + } + + protected Collection getCollection() { + return Collections.immutable(this.collection); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/DelegatingRegistry.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultNestedCollection.java similarity index 52% rename from impl/src/main/java/io/jsonwebtoken/impl/lang/DelegatingRegistry.java rename to impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultNestedCollection.java index 913087c7..620f2ad5 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/DelegatingRegistry.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultNestedCollection.java @@ -16,24 +16,22 @@ package io.jsonwebtoken.impl.lang; import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.Registry; +import io.jsonwebtoken.lang.NestedCollection; -/** - * {@code Registry} implementation that delegates all calls to an internal Registry instance. - * - * @param Registry key type - * @param Registry value type - * @since JJWT_RELEASE_VERSION - */ -public class DelegatingRegistry extends DelegatingMap> implements Registry { +import java.util.Collection; - protected DelegatingRegistry(Registry registry) { - super(registry); - this.DELEGATE = Assert.notEmpty(registry, "Delegate registry cannot be null or empty."); +public class DefaultNestedCollection extends DefaultCollectionMutator> + implements NestedCollection { + + private final P parent; + + public DefaultNestedCollection(P parent, Collection seed) { + super(seed); + this.parent = Assert.notNull(parent, "Parent cannot be null."); } @Override - public V forKey(K key) throws IllegalArgumentException { - return DELEGATE.forKey(key); + public P and() { + return this.parent; } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultRegistry.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultRegistry.java index 601eb878..98cee83e 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultRegistry.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultRegistry.java @@ -84,4 +84,25 @@ public class DefaultRegistry extends DelegatingMap> implem public void clear() { immutable(); } + + @Override + public int hashCode() { + return DELEGATE.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj instanceof DefaultRegistry) { + DefaultRegistry other = (DefaultRegistry) obj; + return this.qualifiedKeyName.equals(other.qualifiedKeyName) && + this.DELEGATE.equals(other.DELEGATE); + } + return false; + } + + @Override + public String toString() { + return DELEGATE.toString(); + } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/IdRegistry.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/IdRegistry.java index 87b8a789..d49c570f 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/IdRegistry.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/IdRegistry.java @@ -37,7 +37,18 @@ public class IdRegistry extends StringRegistry { } public IdRegistry(String name, Collection instances) { - this(name, instances, true); + // Each registry requires CaSe-SeNsItIvE keys by default purpose - all JWA standard algorithm identifiers + // (JWS 'alg', JWE 'enc', JWK 'kty', etc) are all case-sensitive per via the following RFC language: + // + // This name is a case-sensitive ASCII string. Names may not match other registered names in a + // case-insensitive manner unless the Designated Experts state that there is a compelling reason to + // allow an exception. + // + // References: + // - JWS/JWE alg and JWE enc 'Algorithm Name': https://www.rfc-editor.org/rfc/rfc7518.html#section-7.1.1 + // - JWE zip 'Compression Algorithm Value': https://www.rfc-editor.org/rfc/rfc7518.html#section-7.3.1 + // - JWK '"kty" Parameter Value': https://www.rfc-editor.org/rfc/rfc7518.html#section-7.4.1 + this(name, instances, true); // <--- } public IdRegistry(String name, Collection instances, boolean caseSensitive) { 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 51cd62ae..5ac3b4e5 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java @@ -15,12 +15,13 @@ */ package io.jsonwebtoken.impl.security; +import io.jsonwebtoken.impl.lang.DefaultNestedCollection; import io.jsonwebtoken.impl.lang.DelegatingMapMutator; import io.jsonwebtoken.impl.lang.IdRegistry; import io.jsonwebtoken.impl.lang.Parameter; import io.jsonwebtoken.impl.lang.Parameters; import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.lang.NestedCollection; import io.jsonwebtoken.lang.Registry; import io.jsonwebtoken.security.HashAlgorithm; import io.jsonwebtoken.security.Jwk; @@ -37,7 +38,6 @@ import java.security.Key; import java.security.Provider; import java.security.SecureRandom; import java.util.Collection; -import java.util.LinkedHashSet; import java.util.Set; abstract class AbstractJwkBuilder, T extends JwkBuilder> @@ -108,18 +108,16 @@ abstract class AbstractJwkBuilder, T extends Jwk } @Override - public T operation(KeyOperation operation) throws IllegalArgumentException { - return operation != null ? operations(Collections.setOf(operation)) : self(); - } - - @Override - public T operations(Collection ops) { - Set set = new LinkedHashSet<>(Collections.nullSafe(ops)); // new ones override existing ones - Set existing = Collections.nullSafe(this.DELEGATE.getOperations()); - set.addAll(existing); - this.opsPolicy.validate(set); - this.DELEGATE.setOperations(set); - return self(); + public NestedCollection operations() { + return new DefaultNestedCollection(self(), this.DELEGATE.getOperations()) { + @Override + public T and() { + Collection c = getCollection(); + opsPolicy.validate(c); + DELEGATE.setOperations(c); + return super.and(); + } + }; } @Override diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java index 1d929aa4..0a1f56e4 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java @@ -195,7 +195,7 @@ public class DefaultJwkContext extends AbstractX509Context setOperations(Collection ops) { + public JwkContext setOperations(Collection ops) { put(AbstractJwk.KEY_OPS, ops); return this; } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyOperationPolicy.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyOperationPolicy.java index 9ee136ef..d39b5a39 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyOperationPolicy.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyOperationPolicy.java @@ -41,7 +41,7 @@ final class DefaultKeyOperationPolicy implements KeyOperationPolicy { } @Override - public void validate(Collection ops) { + public void validate(Collection ops) { if (allowUnrelated || Collections.isEmpty(ops)) return; for (KeyOperation operation : ops) { for (KeyOperation inner : ops) { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyOperationPolicyBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyOperationPolicyBuilder.java index 8d963699..1126bf0e 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyOperationPolicyBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyOperationPolicyBuilder.java @@ -15,24 +15,20 @@ */ package io.jsonwebtoken.impl.security; -import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.impl.lang.DefaultCollectionMutator; import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.security.Jwks; import io.jsonwebtoken.security.KeyOperation; import io.jsonwebtoken.security.KeyOperationPolicy; import io.jsonwebtoken.security.KeyOperationPolicyBuilder; -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.Map; +public class DefaultKeyOperationPolicyBuilder extends DefaultCollectionMutator + implements KeyOperationPolicyBuilder { -public class DefaultKeyOperationPolicyBuilder implements KeyOperationPolicyBuilder { - - private final Map ops; private boolean unrelated = false; public DefaultKeyOperationPolicyBuilder() { - this.ops = new LinkedHashMap<>(Jwks.OP.get()); + super(Jwks.OP.get().values()); } @Override @@ -41,28 +37,8 @@ public class DefaultKeyOperationPolicyBuilder implements KeyOperationPolicyBuild return this; } - @Override - public KeyOperationPolicyBuilder add(KeyOperation op) { - if (op != null) { - String id = Assert.hasText(op.getId(), "KeyOperation id cannot be null or empty."); - this.ops.remove(id); - this.ops.put(id, op); - } - return this; - } - - @Override - public KeyOperationPolicyBuilder add(Collection ops) { - if (!Collections.isEmpty(ops)) { - for (KeyOperation op : ops) { - add(op); - } - } - return this; - } - @Override public KeyOperationPolicy build() { - return new DefaultKeyOperationPolicy(Collections.immutable(this.ops.values()), this.unrelated); + return new DefaultKeyOperationPolicy(Collections.immutable(getCollection()), this.unrelated); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/JwkContext.java b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkContext.java index 1d0e75ef..336309bc 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/JwkContext.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkContext.java @@ -48,7 +48,7 @@ public interface JwkContext extends Identifiable, Map getOperations(); - JwkContext setOperations(Collection operations); + JwkContext setOperations(Collection operations); String getAlgorithm(); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/StandardCurves.java b/impl/src/main/java/io/jsonwebtoken/impl/security/StandardCurves.java index de148f5d..f9853b67 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/StandardCurves.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/StandardCurves.java @@ -15,17 +15,16 @@ */ package io.jsonwebtoken.impl.security; -import io.jsonwebtoken.impl.lang.DelegatingRegistry; import io.jsonwebtoken.impl.lang.IdRegistry; import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.security.Curve; import java.security.Key; -public final class StandardCurves extends DelegatingRegistry { +public final class StandardCurves extends IdRegistry { public StandardCurves() { - super(new IdRegistry<>("Elliptic Curve", Collections.of( + super("Elliptic Curve", Collections.of( ECCurve.P256, ECCurve.P384, ECCurve.P521, @@ -33,7 +32,7 @@ public final class StandardCurves extends DelegatingRegistry { EdwardsCurve.X448, EdwardsCurve.Ed25519, EdwardsCurve.Ed448 - ), false)); + )); } public static Curve findByKey(Key key) { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/StandardEncryptionAlgorithms.java b/impl/src/main/java/io/jsonwebtoken/impl/security/StandardEncryptionAlgorithms.java index 51cef294..5ee23616 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/StandardEncryptionAlgorithms.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/StandardEncryptionAlgorithms.java @@ -15,21 +15,22 @@ */ package io.jsonwebtoken.impl.security; -import io.jsonwebtoken.impl.lang.DelegatingRegistry; import io.jsonwebtoken.impl.lang.IdRegistry; import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.security.AeadAlgorithm; @SuppressWarnings("unused") // used via reflection in io.jsonwebtoken.Jwts.ENC -public final class StandardEncryptionAlgorithms extends DelegatingRegistry { +public final class StandardEncryptionAlgorithms extends IdRegistry { + + public static final String NAME = "JWE Encryption Algorithm"; public StandardEncryptionAlgorithms() { - super(new IdRegistry<>("JWE Encryption Algorithm", Collections.of( + super(NAME, Collections.of( (AeadAlgorithm) new HmacAesAeadAlgorithm(128), new HmacAesAeadAlgorithm(192), new HmacAesAeadAlgorithm(256), new GcmAesAeadAlgorithm(128), new GcmAesAeadAlgorithm(192), - new GcmAesAeadAlgorithm(256)), false)); + new GcmAesAeadAlgorithm(256))); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/StandardHashAlgorithms.java b/impl/src/main/java/io/jsonwebtoken/impl/security/StandardHashAlgorithms.java index 0cbe6f6e..aaa3934b 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/StandardHashAlgorithms.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/StandardHashAlgorithms.java @@ -15,7 +15,6 @@ */ package io.jsonwebtoken.impl.security; -import io.jsonwebtoken.impl.lang.DelegatingRegistry; import io.jsonwebtoken.impl.lang.IdRegistry; import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.security.HashAlgorithm; @@ -26,10 +25,10 @@ import io.jsonwebtoken.security.HashAlgorithm; * @since JJWT_RELEASE_VERSION */ @SuppressWarnings("unused") // used via reflection in io.jsonwebtoken.security.Jwks.HASH -public class StandardHashAlgorithms extends DelegatingRegistry { +public final class StandardHashAlgorithms extends IdRegistry { public StandardHashAlgorithms() { - super(new IdRegistry<>("IANA Hash Algorithm", Collections.of( + super("IANA Hash Algorithm", Collections.of( // We don't include DefaultHashAlgorithm.SHA1 here on purpose because 1) it's not in the JWK IANA // registry so we don't need to expose it anyway, and 2) we don't want to expose a less-safe algorithm. // The SHA1 instance only exists in JJWT's codebase to support RFC-required `x5t` @@ -40,6 +39,6 @@ public class StandardHashAlgorithms extends DelegatingRegistry> { +public final class StandardKeyAlgorithms extends IdRegistry> { + + public static final String NAME = "JWE Key Management Algorithm"; private static final String RSA1_5_ID = "RSA1_5"; private static final String RSA1_5_TRANSFORMATION = "RSA/ECB/PKCS1Padding"; @@ -42,7 +43,7 @@ public final class StandardKeyAlgorithms extends DelegatingRegistry("JWE Key Management Algorithm", Collections.>of( + super(NAME, Collections.>of( new DirectKeyAlgorithm(), new AesWrapKeyAlgorithm(128), new AesWrapKeyAlgorithm(192), @@ -60,7 +61,7 @@ public final class StandardKeyAlgorithms extends DelegatingRegistry { +public final class StandardKeyOperations extends IdRegistry { public StandardKeyOperations() { - super(new IdRegistry<>("JSON Web Key Operation", Collections.of( + super("JSON Web Key Operation", Collections.of( DefaultKeyOperation.SIGN, DefaultKeyOperation.VERIFY, DefaultKeyOperation.ENCRYPT, @@ -32,6 +31,6 @@ public class StandardKeyOperations extends DelegatingRegistry> { +public final class StandardSecureDigestAlgorithms extends IdRegistry> { + + public static final String NAME = "JWS Digital Signature or MAC"; public StandardSecureDigestAlgorithms() { - super(new IdRegistry<>("JWS Digital Signature or MAC", Collections.of( + super(NAME, Collections.of( NoneSignatureAlgorithm.INSTANCE, DefaultMacAlgorithm.HS256, DefaultMacAlgorithm.HS384, @@ -44,7 +45,7 @@ public final class StandardSecureDigestAlgorithms extends DelegatingRegistry request) { - return realAlg.digest(request) - } - - @Override - boolean verify(VerifySecureDigestRequest request) { - return realAlg.verify(request) - } - - @Override - String getId() { - return id - } - } + def alg = new TestMacAlgorithm(id: id, delegate: realAlg) def jws = Jwts.builder().setSubject("joe").signWith(key, alg).compact() assertEquals 'joe', Jwts.parser() - .addSignatureAlgorithms([alg]) + .sig().add(alg).and() .setSigningKey(key) .build() .parseClaimsJws(jws).payload.getSubject() @@ -1129,7 +1104,7 @@ class JwtsTest { def jwe = Jwts.builder().setSubject("joe").encryptWith(key, encAlg).compact() assertEquals 'joe', Jwts.parser() - .addEncryptionAlgorithms([encAlg]) + .enc().add(encAlg).and() .decryptWith(key) .build() .parseClaimsJwe(jwe).payload.getSubject() @@ -1168,7 +1143,7 @@ class JwtsTest { try { Jwts.parser() .keyLocator(new ConstantKeyLocator(TestKeys.HS256, TestKeys.A128GCM)) - .addKeyAlgorithms([badKeyAlg]) // <-- add bad alg here + .key().add(badKeyAlg).and() // <-- add bad alg here .build() .parseClaimsJwe(compact) fail() diff --git a/impl/src/test/groovy/io/jsonwebtoken/RFC7797Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/RFC7797Test.groovy index 23877461..f9cc6176 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/RFC7797Test.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/RFC7797Test.groovy @@ -73,7 +73,7 @@ class RFC7797Test { InputStream asBufferedInputStream = new BufferedInputStream(new ByteArrayInputStream(content)) for (def payload : [content, asByteInputStream, asBufferedInputStream]) { - def parser = Jwts.parser().verifyWith(key).critical(DefaultJwsHeader.B64.id).build() + def parser = Jwts.parser().verifyWith(key).build() def jws if (payload instanceof byte[]) { jws = parser.parseContentJws(s, (byte[]) payload) @@ -111,7 +111,7 @@ class RFC7797Test { InputStream asBufferedInputStream = new BufferedInputStream(new ByteArrayInputStream(content)) for (def payload : [content, asByteInputStream, asBufferedInputStream]) { - def parser = Jwts.parser().verifyWith(key).critical(DefaultJwsHeader.B64.id).build() + def parser = Jwts.parser().verifyWith(key).build() def jws if (payload instanceof byte[]) { jws = parser.parseClaimsJws(s, (byte[]) payload) @@ -137,7 +137,7 @@ class RFC7797Test { InputStream asBufferedInputStream = new BufferedInputStream(new ByteArrayInputStream(content)) for (def payload : [content, asByteInputStream, asBufferedInputStream]) { - def parser = Jwts.parser().verifyWith(key).critical(DefaultJwsHeader.B64.id).build() + def parser = Jwts.parser().verifyWith(key).build() def jws if (payload instanceof byte[]) { jws = parser.parseContentJws(s, (byte[]) payload) @@ -176,8 +176,7 @@ class RFC7797Test { String compact = Jwts.builder().content(stream).signWith(key).encodePayload(false).compact() // signature still verified: - def jwt = Jwts.parser().verifyWith(key).critical(DefaultJwsHeader.B64.id) - .build().parseContentJws(compact, data) + def jwt = Jwts.parser().verifyWith(key).build().parseContentJws(compact, data) assertEquals 'HS256', jwt.header.getAlgorithm() assertEquals s, Strings.utf8(jwt.getPayload()) } @@ -191,8 +190,7 @@ class RFC7797Test { String s = Jwts.builder().signWith(key).content(payload).encodePayload(false).compact() - def jws = Jwts.parser().verifyWith(key).critical(DefaultJwsHeader.B64.id) - .build().parseClaimsJws(s, payload) + def jws = Jwts.parser().verifyWith(key).build().parseClaimsJws(s, payload) assertEquals 'me', jws.getPayload().getSubject() } @@ -232,8 +230,7 @@ class RFC7797Test { @Test void testParseContentWithEmptyBytesPayload() { try { - Jwts.parser().verifyWith(TestKeys.HS256).critical(DefaultJwsHeader.B64.id).build() - .parseContentJws('whatever', Bytes.EMPTY) // <-- empty + Jwts.parser().verifyWith(TestKeys.HS256).build().parseContentJws('whatever', Bytes.EMPTY) // <-- empty fail() } catch (IllegalArgumentException expected) { String msg = 'unencodedPayload argument cannot be null or empty.' @@ -244,8 +241,7 @@ class RFC7797Test { @Test void testParseClaimsWithEmptyBytesPayload() { try { - Jwts.parser().verifyWith(TestKeys.HS256).critical(DefaultJwsHeader.B64.id).build() - .parseClaimsJws('whatever', Bytes.EMPTY) // <-- empty + Jwts.parser().verifyWith(TestKeys.HS256).build().parseClaimsJws('whatever', Bytes.EMPTY) // <-- empty fail() } catch (IllegalArgumentException expected) { String msg = 'unencodedPayload argument cannot be null or empty.' @@ -266,7 +262,7 @@ class RFC7797Test { // try to parse it as a 'normal' JWS (without supplying the payload): try { - Jwts.parser().verifyWith(key).critical(DefaultJwsHeader.B64.id).build() + Jwts.parser().verifyWith(key).critical().add(DefaultJwsHeader.B64.id).and().build() .parseContentJws(s) // <-- no payload supplied fail() } catch (io.jsonwebtoken.security.SignatureException expected) { @@ -289,7 +285,7 @@ class RFC7797Test { // try to parse it as a 'normal' JWS (without supplying the payload): try { - Jwts.parser().verifyWith(key).critical(DefaultJwsHeader.B64.id).build() + Jwts.parser().verifyWith(key).critical().add(DefaultJwsHeader.B64.id).and().build() .parseClaimsJws(s) // <-- no payload supplied fail() } catch (io.jsonwebtoken.security.SignatureException expected) { @@ -313,7 +309,7 @@ class RFC7797Test { // create a non-detached unencoded JWS: String s = Jwts.builder().signWith(key).content(payload).encodePayload(false).compact() - def jws = Jwts.parser().verifyWith(key).critical(DefaultJwsHeader.B64.id).build() + def jws = Jwts.parser().verifyWith(key).critical().add(DefaultJwsHeader.B64.id).and().build() .parseContentJws(s) // <--- parse normally, without calling parseContentJws(s, unencodedPayload) assertArrayEquals Strings.utf8(payload), jws.getPayload() @@ -327,7 +323,7 @@ class RFC7797Test { // create a non-detached unencoded JWS: String s = Jwts.builder().signWith(key).subject('me').encodePayload(false).compact() - def jws = Jwts.parser().verifyWith(key).critical(DefaultJwsHeader.B64.id).build() + def jws = Jwts.parser().verifyWith(key).critical().add(DefaultJwsHeader.B64.id).and().build() .parseClaimsJws(s) // <--- parse normally, without calling parseClaimsJws(s, unencodedPayload) assertEquals 'me', jws.getPayload().getSubject() @@ -356,7 +352,7 @@ class RFC7797Test { InputStream asBufferedInputStream = new BufferedInputStream(new ByteArrayInputStream(compressed)) for (def payload : [compressed, asByteInputStream, asBufferedInputStream]) { - def parser = Jwts.parser().verifyWith(key).critical('b64').build() + def parser = Jwts.parser().verifyWith(key).build() def jws if (payload instanceof byte[]) { jws = parser.parseContentJws(s, (byte[]) payload) @@ -396,7 +392,7 @@ class RFC7797Test { // now try to parse a compressed unencoded using a signing key resolver: try { - Jwts.parser().critical(DefaultJwsHeader.B64.id) // enable b64 extension + Jwts.parser() .setSigningKeyResolver(new SigningKeyResolverAdapter() { @Override Key resolveSigningKey(JwsHeader header, byte[] content) { diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/AbstractProtectedHeaderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/AbstractProtectedHeaderTest.groovy index 7b1d2a00..779a72f0 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/AbstractProtectedHeaderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/AbstractProtectedHeaderTest.groovy @@ -214,7 +214,7 @@ class AbstractProtectedHeaderTest { void testCritical() { Set crits = Collections.setOf('foo', 'bar') def header = Jwts.header().add('alg', 'HS256').add('foo', 'value1').add('bar', 'value2') - .critical(crits).build() as DefaultProtectedHeader + .critical().add(crits).and().build() as DefaultProtectedHeader assertEquals crits, header.getCritical() } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy index 4c69ac05..1c6382ed 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy @@ -590,11 +590,12 @@ class DefaultJwtBuilderTest { } } + @SuppressWarnings('GrDeprecatedAPIUsage') @Test void testAudienceSingle() { def key = TestKeys.HS256 String audienceSingleString = 'test' - def jwt = builder.audienceSingle(audienceSingleString).compact() + def jwt = builder.audience().single(audienceSingleString).compact() // can't use the parser here to validate because it coerces the string value into an array automatically, // so we need to check the raw payload: def encoded = new JwtTokenizer().tokenize(jwt).getPayload() @@ -612,7 +613,8 @@ class DefaultJwtBuilderTest { void testAudienceSingleMultiple() { def first = 'first' def second = 'second' - def jwt = builder.audienceSingle(first).audienceSingle(second).compact() + //noinspection GrDeprecatedAPIUsage + def jwt = builder.audience().single(first).audience().single(second).compact() // can't use the parser here to validate because it coerces the string value into an array automatically, // so we need to check the raw payload: def encoded = new JwtTokenizer().tokenize(jwt).getPayload() @@ -629,8 +631,8 @@ class DefaultJwtBuilderTest { @Test void testAudienceSingleThenNull() { def jwt = builder.id('test') - .audienceSingle('single') // set one - .audienceSingle(null) // remove it entirely + .audience().single('single') // set one + .audience().single(null) // remove it entirely .compact() // shouldn't be an audience at all: @@ -646,9 +648,10 @@ class DefaultJwtBuilderTest { def first = 'first' def second = 'second' def expected = [first, second] as Set - def jwt = builder.audienceSingle(first) // sets single value - .audienceSingle(null) // removes entirely - .audience([first, second]) // sets collection + def jwt = builder + .audience().single(first) // sets single value + .audience().single(null) // removes entirely + .audience().add([first, second]).and() // sets collection .compact() def aud = Jwts.parser().unsecured().build().parseClaimsJwt(jwt).payload.getAudience() @@ -659,11 +662,12 @@ class DefaultJwtBuilderTest { * Test to ensure that if we receive a JWT with a single string value, that the parser coerces it to a String array * so we don't have to worry about different data types: */ + @SuppressWarnings('GrDeprecatedAPIUsage') @Test void testParseAudienceSingle() { def key = TestKeys.HS256 String audienceSingleString = 'test' - def jwt = builder.audienceSingle(audienceSingleString).compact() + def jwt = builder.audience().single(audienceSingleString).compact() assertEquals audienceSingleString, Jwts.parser().unsecured().build().parseClaimsJwt(jwt).payload .getAudience().iterator().next() // a collection, not a single string @@ -672,19 +676,19 @@ class DefaultJwtBuilderTest { @Test void testAudience() { def aud = 'fubar' - def jwt = Jwts.builder().audience(aud).compact() + def jwt = Jwts.builder().audience().add(aud).and().compact() assertEquals aud, Jwts.parser().unsecured().build().parseClaimsJwt(jwt).payload.getAudience().iterator().next() } @Test void testAudienceNullString() { - def jwt = Jwts.builder().subject('me').audience(null).compact() + def jwt = Jwts.builder().subject('me').audience().add(null).and().compact() assertNull Jwts.parser().unsecured().build().parseClaimsJwt(jwt).payload.getAudience() } @Test void testAudienceEmptyString() { - def jwt = Jwts.builder().subject('me').audience(' ').compact() + def jwt = Jwts.builder().subject('me').audience().add(' ').and().compact() assertNull Jwts.parser().unsecured().build().parseClaimsJwt(jwt).payload.getAudience() } @@ -692,7 +696,7 @@ class DefaultJwtBuilderTest { void testAudienceMultipleTimes() { def one = 'one' def two = 'two' - def jwt = Jwts.builder().audience(one).audience(two).compact() + def jwt = Jwts.builder().audience().add(one).add(two).and().compact() def aud = Jwts.parser().unsecured().build().parseClaimsJwt(jwt).payload.getAudience() assertTrue aud.contains(one) assertTrue aud.contains(two) @@ -701,14 +705,14 @@ class DefaultJwtBuilderTest { @Test void testAudienceNullCollection() { Collection c = null - def jwt = Jwts.builder().subject('me').audience(c).compact() + def jwt = Jwts.builder().subject('me').audience().add(c).and().compact() assertNull Jwts.parser().unsecured().build().parseClaimsJwt(jwt).payload.getAudience() } @Test void testAudienceEmptyCollection() { Collection c = new ArrayList() - def jwt = Jwts.builder().subject('me').audience(c).compact() + def jwt = Jwts.builder().subject('me').audience().add(c).and().compact() assertNull Jwts.parser().unsecured().build().parseClaimsJwt(jwt).payload.getAudience() } @@ -716,7 +720,7 @@ class DefaultJwtBuilderTest { void testAudienceCollectionWithNullElement() { Collection c = new ArrayList() c.add(null) - def jwt = Jwts.builder().subject('me').audience(c).compact() + def jwt = Jwts.builder().subject('me').audience().add(c).and().compact() assertNull Jwts.parser().unsecured().build().parseClaimsJwt(jwt).payload.getAudience() } @@ -728,7 +732,8 @@ class DefaultJwtBuilderTest { void testAudienceSingleThenAudience() { def one = 'one' def two = 'two' - def jwt = Jwts.builder().audienceSingle(one).audience(two).compact() + //noinspection GrDeprecatedAPIUsage + def jwt = Jwts.builder().audience().single(one).audience().add(two).and().compact() def aud = Jwts.parser().unsecured().build().parseClaimsJwt(jwt).payload.getAudience() assertTrue aud.contains(one) assertTrue aud.contains(two) @@ -743,7 +748,8 @@ class DefaultJwtBuilderTest { void testAudienceThenAudienceSingle() { def one = 'one' def two = 'two' - def jwt = Jwts.builder().audience(one).audienceSingle(two).compact() + //noinspection GrDeprecatedAPIUsage + def jwt = Jwts.builder().audience().add(one).and().audience().single(two).compact() // can't use the parser here to validate because it coerces the string value into an array automatically, // so we need to check the raw payload: @@ -763,7 +769,8 @@ class DefaultJwtBuilderTest { def single = 'one' def collection = ['two', 'three'] as Set def expected = ['one', 'two', 'three'] as Set - def jwt = Jwts.builder().audienceSingle(single).audience(collection).compact() + //noinspection GrDeprecatedAPIUsage + def jwt = Jwts.builder().audience().single(single).audience().add(collection).and().compact() def aud = Jwts.parser().unsecured().build().parseClaimsJwt(jwt).payload.getAudience() assertEquals expected.size(), aud.size() assertTrue aud.contains(single) && aud.containsAll(collection) @@ -779,7 +786,7 @@ class DefaultJwtBuilderTest { def one = 'one' def two = 'two' def three = 'three' - def jwt = Jwts.builder().audience([one, two]).audienceSingle(three).compact() + def jwt = Jwts.builder().audience().add([one, two]).and().audience().single(three).compact() // can't use the parser here to validate because it coerces the string value into an array automatically, // so we need to check the raw payload: diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtHeaderBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtHeaderBuilderTest.groovy index cf43f6b4..29519283 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtHeaderBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtHeaderBuilderTest.groovy @@ -325,7 +325,7 @@ class DefaultJwtHeaderBuilderTest { @Test void testCritical() { def crit = ['foo'] as Set - header = jws().add('foo', 'bar').critical(crit).build() as JwsHeader + header = jws().add('foo', 'bar').critical().add(crit).and().build() as JwsHeader assertTrue header instanceof JwsHeader assertFalse header instanceof JweHeader assertEquals crit, header.getCritical() @@ -502,7 +502,7 @@ class DefaultJwtHeaderBuilderTest { @Test void testCritSingle() { def crit = 'test' - def header = jws().add(crit, 'foo').critical(crit).build() as ProtectedHeader + def header = jws().add(crit, 'foo').critical().add(crit).and().build() as ProtectedHeader def expected = [crit] as Set assertEquals expected, header.getCritical() } @@ -511,34 +511,34 @@ class DefaultJwtHeaderBuilderTest { void testCritSingleNullIgnored() { def crit = 'test' def expected = [crit] as Set - def header = jws().add(crit, 'foo').critical(crit).build() as ProtectedHeader + def header = jws().add(crit, 'foo').critical().add(crit).and().build() as ProtectedHeader assertEquals expected, header.getCritical() - header = builder.critical((String) null).build() as ProtectedHeader // ignored + header = builder.critical().add((String) null).and().build() as ProtectedHeader // ignored assertEquals expected, header.getCritical() // nothing changed } @Test void testCritNullCollectionIgnored() { def crit = ['test'] as Set - def header = jws().add('test', 'foo').critical(crit).build() as ProtectedHeader + def header = jws().add('test', 'foo').critical().add(crit).and().build() as ProtectedHeader assertEquals crit, header.getCritical() - header = builder.critical((Collection) null).build() as ProtectedHeader + header = builder.critical().add((Collection) null).and().build() as ProtectedHeader assertEquals crit, header.getCritical() // nothing changed } @Test void testCritCollectionWithNullElement() { def crit = [null] as Set - def header = jws().add('test', 'foo').critical(crit).build() as ProtectedHeader + def header = jws().add('test', 'foo').critical().add(crit).and().build() as ProtectedHeader assertNull header.getCritical() } @Test void testCritEmptyIgnored() { def crit = ['test'] as Set - def header = jws().add('test', 'foo').critical(crit).build() as ProtectedHeader + ProtectedHeader header = jws().add('test', 'foo').critical().add(crit).and().build() as ProtectedHeader assertEquals crit, header.getCritical() - header = builder.critical([] as Set).build() + header = builder.critical().add([] as Set).and().build() as ProtectedHeader assertEquals crit, header.getCritical() // ignored } @@ -550,7 +550,7 @@ class DefaultJwtHeaderBuilderTest { void testCritRemovedForUnprotectedHeader() { def crit = Collections.setOf('foo', 'bar') // no JWS or JWE params specified: - def header = builder.add('test', 'value').critical(crit).build() + def header = builder.add('test', 'value').critical().add(crit).and().build() assertFalse header.containsKey(DefaultProtectedHeader.CRIT.getId()) } @@ -562,7 +562,7 @@ class DefaultJwtHeaderBuilderTest { void testCritNamesSanitizedWhenHeaderMissingCorrespondingParameter() { def critGiven = ['foo', 'bar'] as Set def critExpected = ['foo'] as Set - def header = jws().add('foo', 'fooVal').critical(critGiven).build() as ProtectedHeader + def header = jws().add('foo', 'fooVal').critical().add(critGiven).and().build() as ProtectedHeader // header didn't set the 'bar' parameter, so 'bar' should not be in the crit values: assertEquals critExpected, header.getCritical() } @@ -570,7 +570,7 @@ class DefaultJwtHeaderBuilderTest { @Test void testCritNamesRemovedWhenHeaderMissingCorrespondingParameter() { def critGiven = ['foo'] as Set - def header = jws().critical(critGiven).build() as ProtectedHeader + ProtectedHeader header = jws().critical().add(critGiven).and().build() as ProtectedHeader // header didn't set the 'foo' parameter, so crit would have been empty, and then removed from the header: assertNull header.getCritical() } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy index b00015a9..931ec58b 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy @@ -17,15 +17,12 @@ package io.jsonwebtoken.impl import com.fasterxml.jackson.databind.ObjectMapper import io.jsonwebtoken.* -import io.jsonwebtoken.impl.security.ConstantKeyLocator -import io.jsonwebtoken.impl.security.LocatingKeyResolver -import io.jsonwebtoken.impl.security.TestKeys +import io.jsonwebtoken.impl.security.* import io.jsonwebtoken.io.* -import io.jsonwebtoken.security.* +import io.jsonwebtoken.security.InvalidKeyException import org.junit.Before import org.junit.Test -import javax.crypto.SecretKey import java.security.Provider import static org.easymock.EasyMock.* @@ -47,7 +44,7 @@ class DefaultJwtParserBuilderTest { @Test void testCriticalEmtpy() { - builder.critical(' ') // shouldn't modify the set + builder.critical().add(' ').and() // shouldn't modify the set assertTrue builder.@critical.isEmpty() } @@ -165,26 +162,26 @@ class DefaultJwtParserBuilderTest { } } def parser = builder.setCompressionCodecResolver(resolver).build() - assertSame resolver, parser.zipAlgFn.resolver + assertSame resolver, parser.zipAlgs.resolver } @Test void testAddCompressionAlgorithms() { def codec = new TestCompressionCodec(id: 'test') - def parser = builder.addCompressionAlgorithms([codec] as Set).build() + def parser = builder.zip().add(codec).and().build() def header = Jwts.header().add('zip', codec.getId()).build() - assertSame codec, parser.zipAlgFn.locate(header) + assertSame codec, parser.zipAlgs.locate(header) } @Test void testAddCompressionAlgorithmsOverrideDefaults() { def header = Jwts.header().add('zip', 'DEF').build() def parser = builder.build() - assertSame Jwts.ZIP.DEF, parser.zipAlgFn.apply(header) // standard implementation default + assertSame Jwts.ZIP.DEF, parser.zipAlgs.apply(header) // standard implementation default def alg = new TestCompressionCodec(id: 'DEF') // custom impl with standard identifier - parser = builder.addCompressionAlgorithms([alg]).build() - assertSame alg, parser.zipAlgFn.apply(header) // custom one, not standard impl + parser = builder.zip().add(alg).and().build() + assertSame alg, parser.zipAlgs.apply(header) // custom one, not standard impl } @Test @@ -192,9 +189,9 @@ class DefaultJwtParserBuilderTest { def standard = Jwts.header().add('zip', 'DEF').build() def nonStandard = Jwts.header().add('zip', 'def').build() def parser = builder.build() - assertSame Jwts.ZIP.DEF, parser.zipAlgFn.apply(standard) // standard implementation default + assertSame Jwts.ZIP.DEF, parser.zipAlgs.apply(standard) // standard implementation default try { - parser.zipAlgFn.apply(nonStandard) + parser.zipAlgs.apply(nonStandard) fail() } catch (UnsupportedJwtException e) { String msg = "Unrecognized JWT ${DefaultHeader.COMPRESSION_ALGORITHM} header value: def" @@ -207,11 +204,11 @@ class DefaultJwtParserBuilderTest { final String standardId = Jwts.ENC.A256GCM.getId() def header = Jwts.header().add('enc', standardId).build() def parser = builder.build() - assertSame Jwts.ENC.A256GCM, parser.encAlgFn.apply(header) // standard implementation default + assertSame Jwts.ENC.A256GCM, parser.encAlgs.apply(header) // standard implementation default def custom = new TestAeadAlgorithm(id: standardId) // custom impl with standard identifier - parser = builder.addEncryptionAlgorithms([custom]).build() - assertSame custom, parser.encAlgFn.apply(header) // custom one, not standard impl + parser = builder.enc().add(custom).and().build() + assertSame custom, parser.encAlgs.apply(header) // custom one, not standard impl } @Test @@ -220,9 +217,9 @@ class DefaultJwtParserBuilderTest { def standard = Jwts.header().add('alg', 'foo').add('enc', alg.id).build() def nonStandard = Jwts.header().add('alg', 'foo').add('enc', alg.id.toLowerCase()).build() def parser = builder.build() - assertSame alg, parser.encAlgFn.apply(standard) // standard id + assertSame alg, parser.encAlgs.apply(standard) // standard id try { - parser.encAlgFn.apply(nonStandard) // non-standard id + parser.encAlgs.apply(nonStandard) // non-standard id fail() } catch (UnsupportedJwtException e) { String msg = "Unrecognized JWE ${DefaultJweHeader.ENCRYPTION_ALGORITHM} header value: ${alg.id.toLowerCase()}" @@ -235,11 +232,11 @@ class DefaultJwtParserBuilderTest { final String standardId = Jwts.KEY.A256GCMKW.id def header = Jwts.header().add('enc', Jwts.ENC.A256GCM.id).add('alg', standardId).build() def parser = builder.build() - assertSame Jwts.KEY.A256GCMKW, parser.keyAlgFn.apply(header) // standard implementation default + assertSame Jwts.KEY.A256GCMKW, parser.keyAlgs.apply(header) // standard implementation default def custom = new TestKeyAlgorithm(id: standardId) // custom impl with standard identifier - parser = builder.addKeyAlgorithms([custom]).build() - assertSame custom, parser.keyAlgFn.apply(header) // custom one, not standard impl + parser = builder.key().add(custom).and().build() + assertSame custom, parser.keyAlgs.apply(header) // custom one, not standard impl } @Test @@ -249,9 +246,9 @@ class DefaultJwtParserBuilderTest { def standard = hb.add('alg', alg.id).build() def nonStandard = hb.add('alg', alg.id.toLowerCase()).build() def parser = builder.build() - assertSame alg, parser.keyAlgFn.apply(standard) // standard id + assertSame alg, parser.keyAlgs.apply(standard) // standard id try { - parser.keyAlgFn.apply(nonStandard) // non-standard id + parser.keyAlgs.apply(nonStandard) // non-standard id fail() } catch (UnsupportedJwtException e) { String msg = "Unrecognized JWE ${DefaultJweHeader.ALGORITHM} header value: ${alg.id.toLowerCase()}" @@ -264,11 +261,11 @@ class DefaultJwtParserBuilderTest { final String standardId = Jwts.SIG.HS256.id def header = Jwts.header().add('alg', standardId).build() def parser = builder.build() - assertSame Jwts.SIG.HS256, parser.sigAlgFn.apply(header) // standard implementation default + assertSame Jwts.SIG.HS256, parser.sigAlgs.apply(header) // standard implementation default def custom = new TestMacAlgorithm(id: standardId) // custom impl with standard identifier - parser = builder.addSignatureAlgorithms([custom]).build() - assertSame custom, parser.sigAlgFn.apply(header) // custom one, not standard impl + parser = builder.sig().add(custom).and().build() + assertSame custom, parser.sigAlgs.apply(header) // custom one, not standard impl } @Test @@ -278,9 +275,9 @@ class DefaultJwtParserBuilderTest { def standard = hb.build() def nonStandard = hb.add('alg', alg.id.toLowerCase()).build() def parser = builder.build() - assertSame alg, parser.sigAlgFn.apply(standard) // standard id + assertSame alg, parser.sigAlgs.apply(standard) // standard id try { - parser.sigAlgFn.apply(nonStandard) // non-standard id + parser.sigAlgs.apply(nonStandard) // non-standard id fail() } catch (UnsupportedJwtException e) { String msg = "Unrecognized JWS ${DefaultJwsHeader.ALGORITHM} header value: ${alg.id.toLowerCase()}" @@ -298,10 +295,10 @@ class DefaultJwtParserBuilderTest { } } try { - builder.setCompressionCodecResolver(resolver).addCompressionAlgorithms([codec] as Set).build() + builder.setCompressionCodecResolver(resolver).zip().add(codec).and().build() fail() } catch (IllegalStateException expected) { - String msg = "Both 'addCompressionAlgorithms' and 'compressionCodecResolver' cannot be specified. Choose either." + String msg = "Both 'zip()' and 'compressionCodecResolver' cannot be configured. Choose either." assertEquals msg, expected.getMessage() } } @@ -410,83 +407,4 @@ class DefaultJwtParserBuilderTest { } } - static class TestAeadAlgorithm implements AeadAlgorithm { - - String id - int keyBitLength = 256 - - @Override - String getId() { - return id - } - - @Override - void encrypt(AeadRequest request, AeadResult result) throws SecurityException { - } - - @Override - void decrypt(DecryptAeadRequest request, OutputStream out) throws SecurityException { - } - - @Override - SecretKeyBuilder key() { - return null - } - - @Override - int getKeyBitLength() { - return keyBitLength - } - } - - static class TestKeyAlgorithm implements KeyAlgorithm { - - String id - int keyBitLength = 256 - - @Override - String getId() { - return id - } - - @Override - KeyResult getEncryptionKey(KeyRequest request) throws SecurityException { - return null - } - - @Override - SecretKey getDecryptionKey(DecryptionKeyRequest request) throws SecurityException { - return null - } - } - - static class TestMacAlgorithm implements MacAlgorithm { - - String id - - @Override - String getId() { - return id - } - - @Override - byte[] digest(SecureRequest request) throws SecurityException { - return new byte[0] - } - - @Override - boolean verify(VerifySecureDigestRequest request) throws SecurityException { - return false - } - - @Override - SecretKeyBuilder key() { - return null - } - - @Override - int getKeyBitLength() { - return 0 - } - } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy index 53089354..6fb9886a 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy @@ -245,11 +245,11 @@ class DefaultJwtParserTest { def key = TestKeys.HS256 def crit = Collections.setOf('whatever') String jws = Jwts.builder() - .header().critical(crit).add('whatever', 42).and() + .header().critical().add(crit).and().add('whatever', 42).and() .subject('me') .signWith(key).compact() - def jwt = Jwts.parser().critical(crit).verifyWith(key).build().parseClaimsJws(jws) + def jwt = Jwts.parser().critical().add(crit).and().verifyWith(key).build().parseClaimsJws(jws) // no exception thrown, as expected, check the header values: def parsedCrit = jwt.getHeader().getCritical() diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtTest.groovy index a32dde75..caba76f0 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtTest.groovy @@ -29,7 +29,7 @@ class DefaultJwtTest { @Test void testToString() { - String compact = Jwts.builder().header().add('foo', 'bar').and().audience('jsmith').compact() + String compact = Jwts.builder().header().add('foo', 'bar').and().audience().add('jsmith').and().compact() Jwt jwt = Jwts.parser().unsecured().build().parseClaimsJwt(compact) assertEquals 'header={foo=bar, alg=none},payload={aud=[jsmith]}', jwt.toString() } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultMutableJweHeaderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultMutableJweHeaderTest.groovy index 23116e6b..ff609061 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultMutableJweHeaderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultMutableJweHeaderTest.groovy @@ -45,6 +45,7 @@ class DefaultMutableJweHeaderTest { switch (propName) { case 'algorithm': header.add('alg', val); break // no setter case 'compressionAlgorithm': header.add('zip', val); break // no setter + case 'critical': header.critical().add(val).and(); break // no setter default: header."$propName"(val) } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DelegateAudienceCollectionTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DelegateAudienceCollectionTest.groovy new file mode 100644 index 00000000..ec1c4315 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DelegateAudienceCollectionTest.groovy @@ -0,0 +1,46 @@ +/* + * Copyright © 2023 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 + +import io.jsonwebtoken.ClaimsMutator +import org.junit.Test + +import static org.easymock.EasyMock.* +import static org.junit.Assert.assertSame + +class DelegateAudienceCollectionTest { + + @Test + void clear() { + ClaimsMutator.AudienceCollection delegate = createMock(ClaimsMutator.AudienceCollection) + expect(delegate.clear()).andReturn(delegate) + replay(delegate) + def c = new DelegateAudienceCollection(this, delegate) + assertSame c, c.clear() + verify delegate + } + + @Test + void remove() { + String val = 'hello' + ClaimsMutator.AudienceCollection delegate = createMock(ClaimsMutator.AudienceCollection) + expect(delegate.remove(same(val))).andReturn(delegate) + replay(delegate) + def c = new DelegateAudienceCollection(this, delegate) + assertSame c, c.remove(val) + verify delegate + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/IdLocatorTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/IdLocatorTest.groovy index 36db37dc..5ce6b2b2 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/IdLocatorTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/IdLocatorTest.groovy @@ -40,12 +40,12 @@ class IdLocatorTest { def a = new StringIdentifiable(value: 'A') def b = new StringIdentifiable(value: 'B') registry = new IdRegistry('Foo', [a, b], false) - locator = new IdLocator(TEST_PARAM, registry, Collections.emptyList(), exMsg) + locator = new IdLocator(TEST_PARAM, registry, exMsg) } @Test void unrequiredHeaderValueTest() { - locator = new IdLocator(TEST_PARAM, registry, Collections.emptyList(), null) + locator = new IdLocator(TEST_PARAM, registry, null) def header = Jwts.header().add('a', 'b').build() assertNull locator.apply(header) } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/DefaultCollectionMutatorTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/DefaultCollectionMutatorTest.groovy new file mode 100644 index 00000000..b5541c52 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/DefaultCollectionMutatorTest.groovy @@ -0,0 +1,106 @@ +/* + * Copyright © 2023 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.lang + +import io.jsonwebtoken.Identifiable +import io.jsonwebtoken.lang.Strings +import org.junit.Before +import org.junit.Test + +import static org.junit.Assert.* + +/** + * @since JJWT_RELEASE_VERSION + */ +class DefaultCollectionMutatorTest { + + private DefaultCollectionMutator m + + @Before + void setUp() { + m = new DefaultCollectionMutator(null) + } + + @Test + void newInstance() { + def c = m.getCollection() + assertNotNull c + assertTrue c.isEmpty() + } + + @Test + void addEmpty() { + m.add(Strings.EMPTY) + assertTrue m.getCollection().isEmpty() // wasn't added + } + + @Test + void add() { + def val = 'hello' + m.add(val) + assertEquals Collections.singleton(val), m.getCollection() + } + + @Test + void addCollection() { + def vals = ['hello', 'world'] + m.add(vals) + assertEquals vals.size(), m.getCollection().size() + assertTrue vals.containsAll(m.getCollection()) + assertTrue m.getCollection().containsAll(vals) + def i = m.getCollection().iterator() // order retained + assertEquals vals[0], i.next() + assertEquals vals[1], i.next() + assertFalse i.hasNext() + } + + @Test(expected = IllegalArgumentException) + void addIdentifiableWithNullId() { + def e = new Identifiable() { + @Override + String getId() { + return null + } + } + m.add(e) + } + + @Test(expected = IllegalArgumentException) + void addIdentifiableWithEmptyId() { + def e = new Identifiable() { + @Override + String getId() { + return ' ' + } + } + m.add(e) + } + + @Test + void remove() { + m.add('hello').add('world') + m.remove('hello') + assertEquals Collections.singleton('world'), m.getCollection() + } + + @Test + void clear() { + m.add('one').add('two').add(['three', 'four']) + assertEquals 4, m.getCollection().size() + m.clear() + assertTrue m.getCollection().isEmpty() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/DefaultRegistryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/DefaultRegistryTest.groovy index 3c97b94d..712d6de1 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/DefaultRegistryTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/DefaultRegistryTest.groovy @@ -18,8 +18,7 @@ package io.jsonwebtoken.impl.lang import org.junit.Before import org.junit.Test -import static org.junit.Assert.assertEquals -import static org.junit.Assert.fail +import static org.junit.Assert.* class DefaultRegistryTest { @@ -53,4 +52,30 @@ class DefaultRegistryTest { def key = 'a' assertEquals reg.apply(key), reg.get(key) } + + @Test + void testHashCode() { + assertEquals reg.@DELEGATE.hashCode(), reg.hashCode() + } + + @Test + void testEqualsIdentity() { + assertEquals reg, reg + } + + @Test + void testEqualsValues() { + def another = new DefaultRegistry<>('test', 'id', ['a', 'b', 'c', 'd'], Functions.identity()) + assertEquals another, reg + } + + @Test + void testNotEquals() { + assertFalse reg.equals(new Object()) + } + + @Test + void testToString() { + assertEquals reg.@DELEGATE.toString(), reg.toString() + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/DelegatingRegistryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/DelegatingRegistryTest.groovy deleted file mode 100644 index 194169bc..00000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/DelegatingRegistryTest.groovy +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (C) 2023 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.lang - - -import io.jsonwebtoken.lang.Registry -import org.junit.Before -import org.junit.Test - -import static org.junit.Assert.assertEquals - -class DelegatingRegistryTest { - - Registry registry - - @Before - void setUp() { - def src = new DefaultRegistry('test', 'id', ['a', 'b', 'c'], Functions.identity()) - this.registry = new DelegatingRegistry(src) - } - - @Test - void testForKey() { - assertEquals 'a', registry.forKey('a') - assertEquals 'b', registry.forKey('b') - assertEquals 'c', registry.forKey('c') - - } - - @Test(expected = IllegalArgumentException) - void testForKeyInvalid() { - registry.forKey('invalid') // any key value that doesn't exist - } -} 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 4147369d..d54ab088 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy @@ -142,7 +142,7 @@ class AbstractJwkBuilderTest { def op = Jwks.OP.get().get(s) def canonical = Collections.setOf(s) def idiomatic = Collections.setOf(op) - def jwk = builder().operation(op).build() + def jwk = builder().operations().add(op).and().build() assertEquals idiomatic, jwk.getOperations() assertEquals canonical, jwk.key_ops } @@ -153,7 +153,7 @@ class AbstractJwkBuilderTest { def op = Jwks.OP.builder().id(s).build() def canonical = Collections.setOf(s) def idiomatic = Collections.setOf(op) - def jwk = builder().operation(op).build() + def jwk = builder().operations().add(op).and().build() assertEquals idiomatic, jwk.getOperations() assertEquals canonical, jwk.key_ops } @@ -164,7 +164,7 @@ class AbstractJwkBuilderTest { def op = Jwks.OP.builder().id(s).related('verify').build() def canonical = Collections.setOf(s) def idiomatic = Collections.setOf(op) - def jwk = builder().operation(op).build() + def jwk = builder().operations().add(op).and().build() assertEquals idiomatic, jwk.getOperations() assertEquals canonical, jwk.key_ops assertSame op, jwk.getOperations().iterator().next() @@ -172,7 +172,7 @@ class AbstractJwkBuilderTest { //now assert that the standard VERIFY operation treats this as related since it has the same ID: canonical = Collections.setOf(s, 'verify') idiomatic = Collections.setOf(op, Jwks.OP.VERIFY) - jwk = builder().operation(op).operation(Jwks.OP.VERIFY).build() as Jwk + jwk = builder().operations().add(op).add(Jwks.OP.VERIFY).and().build() as Jwk assertEquals idiomatic, jwk.getOperations() assertEquals canonical, jwk.key_ops } @@ -183,7 +183,7 @@ class AbstractJwkBuilderTest { def b = 'verify' def canonical = Collections.setOf(a, b) def idiomatic = Collections.setOf(Jwks.OP.SIGN, Jwks.OP.VERIFY) - def jwk = builder().operations(idiomatic).build() + def jwk = builder().operations().add(idiomatic).and().build() assertEquals idiomatic, jwk.getOperations() assertEquals canonical, jwk.key_ops } @@ -192,7 +192,7 @@ class AbstractJwkBuilderTest { void testOperationsUnrelated() { try { // exception thrown on setter, before calling build: - builder().operations(Collections.setOf(Jwks.OP.SIGN, Jwks.OP.ENCRYPT)) + builder().operations().add(Collections.setOf(Jwks.OP.SIGN, Jwks.OP.ENCRYPT)).and() fail() } catch (IllegalArgumentException e) { String msg = 'Unrelated key operations are not allowed. KeyOperation [\'encrypt\' (Encrypt content)] is ' + @@ -240,7 +240,7 @@ class AbstractJwkBuilderTest { def op = Jwks.OP.builder().id('sign').description('Different Description') .related(Jwks.OP.VERIFY.id).build() def builder = builder().operationPolicy(Jwks.OP.policy().add(op).build()) - def jwk = builder.operations(Collections.setOf(op, Jwks.OP.VERIFY)).build() as Jwk + def jwk = builder.operations().add(Collections.setOf(op, Jwks.OP.VERIFY)).and().build() as Jwk assertSame op, jwk.getOperations().find({it.id == 'sign'}) } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/EdSignatureAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EdSignatureAlgorithmTest.groovy index bd88dbc1..ae08bc76 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/EdSignatureAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EdSignatureAlgorithmTest.groovy @@ -124,7 +124,7 @@ class EdSignatureAlgorithmTest { } static void testSig(PrivateKey signing, PublicKey verification) { - String jwt = Jwts.builder().issuer('me').audience('you').signWith(signing, alg).compact() + String jwt = Jwts.builder().issuer('me').audience().add('you').and().signWith(signing, alg).compact() def token = Jwts.parser().verifyWith(verification).build().parseClaimsJws(jwt) assertEquals([alg: alg.getId()], token.header) assertEquals 'me', token.getPayload().getIssuer() 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 e04766b0..16c99f57 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy @@ -147,14 +147,15 @@ class JwksTest { def op = Jwks.OP.ENCRYPT def expected = [op] as Set def canonical = [op.getId()] as Set - def jwk = Jwks.builder().key(TestKeys.A128GCM).operation(op).build() + def jwk = Jwks.builder().key(TestKeys.A128GCM).operations().add(op).and().build() assertEquals expected, jwk.getOperations() assertEquals canonical, jwk.get(AbstractJwk.KEY_OPS.id) } @Test void testSingleOperationNull() { - def jwk = Jwks.builder().key(TestKeys.A128GCM).operation(null).build() //ignored null + def jwk = Jwks.builder().key(TestKeys.A128GCM).operations().add((KeyOperation) null).and().build() + //ignored null assertNull jwk.getOperations() //nothing added assertFalse jwk.containsKey(AbstractJwk.KEY_OPS.id) } @@ -163,7 +164,7 @@ class JwksTest { void testSingleOperationAppends() { def expected = [Jwks.OP.ENCRYPT, Jwks.OP.DECRYPT] as Set def jwk = Jwks.builder().key(TestKeys.A128GCM) - .operation(Jwks.OP.ENCRYPT).operation(Jwks.OP.DECRYPT) + .operations().add(Jwks.OP.ENCRYPT).add(Jwks.OP.DECRYPT).and() .build() assertEquals expected, jwk.getOperations() } @@ -171,20 +172,20 @@ class JwksTest { @Test void testOperations() { def val = [Jwks.OP.SIGN, Jwks.OP.VERIFY] as Set - def jwk = Jwks.builder().key(TestKeys.A128GCM).operations(val).build() + def jwk = Jwks.builder().key(TestKeys.A128GCM).operations().add(val).and().build() assertEquals val, jwk.getOperations() } @Test void testOperationsNull() { - def jwk = Jwks.builder().key(TestKeys.A128GCM).operations(null).build() + def jwk = Jwks.builder().key(TestKeys.A128GCM).operations().add((Collection) null).and().build() assertNull jwk.getOperations() assertFalse jwk.containsKey(AbstractJwk.KEY_OPS.id) } @Test void testOperationsEmpty() { - def jwk = Jwks.builder().key(TestKeys.A128GCM).operations(Collections.emptyList()).build() + def jwk = Jwks.builder().key(TestKeys.A128GCM).operations().add(Collections.emptyList()).and().build() assertNull 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 89254a74..660edf5c 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/SecretJwkFactoryTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/SecretJwkFactoryTest.groovy @@ -48,7 +48,7 @@ class SecretJwkFactoryTest { @Test void testSignOpSetsKeyHmacSHA256() { SecretJwk jwk = Jwks.builder().key(TestKeys.HS256).delete('alg').build() - SecretJwk result = Jwks.builder().add(jwk).operations([Jwks.OP.SIGN]).build() as SecretJwk + SecretJwk result = Jwks.builder().add(jwk).operations().add(Jwks.OP.SIGN).and().build() as SecretJwk assertNull result.getAlgorithm() assertNull result.get('use') assertEquals 'HmacSHA256', result.toKey().getAlgorithm() @@ -64,7 +64,7 @@ class SecretJwkFactoryTest { @Test void testSignOpSetsKeyHmacSHA384() { SecretJwk jwk = Jwks.builder().key(TestKeys.HS384).delete('alg').build() - SecretJwk result = Jwks.builder().add(jwk).operations([Jwks.OP.SIGN]).build() as SecretJwk + SecretJwk result = Jwks.builder().add(jwk).operations().add(Jwks.OP.SIGN).and().build() as SecretJwk assertNull result.getAlgorithm() assertNull result.get('use') assertEquals 'HmacSHA384', result.toKey().getAlgorithm() @@ -80,7 +80,7 @@ class SecretJwkFactoryTest { @Test void testSignOpSetsKeyHmacSHA512() { SecretJwk jwk = Jwks.builder().key(TestKeys.HS512).delete('alg').build() - SecretJwk result = Jwks.builder().add(jwk).operations([Jwks.OP.SIGN]).build() as SecretJwk + SecretJwk result = Jwks.builder().add(jwk).operations().add(Jwks.OP.SIGN).and().build() as SecretJwk assertNull result.getAlgorithm() assertNull result.get('use') assertEquals 'HmacSHA512', result.toKey().getAlgorithm() diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestAeadAlgorithm.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestAeadAlgorithm.groovy new file mode 100644 index 00000000..adc6271e --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestAeadAlgorithm.groovy @@ -0,0 +1,47 @@ +/* + * Copyright © 2023 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.security.* + +class TestAeadAlgorithm implements AeadAlgorithm { + + String id + int keyBitLength = 256 + + @Override + String getId() { + return id + } + + @Override + void encrypt(AeadRequest request, AeadResult result) throws SecurityException { + } + + @Override + void decrypt(DecryptAeadRequest request, OutputStream out) throws SecurityException { + } + + @Override + SecretKeyBuilder key() { + return null + } + + @Override + int getKeyBitLength() { + return keyBitLength + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestKeyAlgorithm.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestKeyAlgorithm.groovy new file mode 100644 index 00000000..a1c25d7d --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestKeyAlgorithm.groovy @@ -0,0 +1,40 @@ +/* + * Copyright © 2023 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.security.* + +import javax.crypto.SecretKey + +class TestKeyAlgorithm implements KeyAlgorithm { + + String id + + @Override + String getId() { + return id + } + + @Override + KeyResult getEncryptionKey(KeyRequest request) throws SecurityException { + return null + } + + @Override + SecretKey getDecryptionKey(DecryptionKeyRequest request) throws SecurityException { + return null + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestMacAlgorithm.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestMacAlgorithm.groovy new file mode 100644 index 00000000..e34fe08e --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestMacAlgorithm.groovy @@ -0,0 +1,51 @@ +/* + * Copyright © 2023 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.security.* + +import javax.crypto.SecretKey + +class TestMacAlgorithm implements MacAlgorithm { + + String id + MacAlgorithm delegate + + @Override + String getId() { + return id + } + + @Override + byte[] digest(SecureRequest request) throws SecurityException { + return delegate.digest(request) + } + + @Override + boolean verify(VerifySecureDigestRequest request) throws SecurityException { + return delegate.verify(request) + } + + @Override + SecretKeyBuilder key() { + return delegate.key() + } + + @Override + int getKeyBitLength() { + return delegate.getKeyBitLength() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/security/EncryptionAlgorithmsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/security/EncryptionAlgorithmsTest.groovy index 251a76ad..aaf6804a 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/security/EncryptionAlgorithmsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/security/EncryptionAlgorithmsTest.groovy @@ -79,13 +79,6 @@ class EncryptionAlgorithmsTest { } } - @Test - void testGetCaseInsensitive() { - for (AeadAlgorithm alg : registry.values()) { - assertSame alg, registry.get(alg.getId().toLowerCase()) - } - } - @Test(expected = IllegalArgumentException) void testForIdWithInvalidId() { //unlike the 'get' paradigm, 'key' requires the value to exist diff --git a/impl/src/test/groovy/io/jsonwebtoken/security/KeyAlgorithmsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/security/KeyAlgorithmsTest.groovy index 254e2fa1..e1ef70d0 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/security/KeyAlgorithmsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/security/KeyAlgorithmsTest.groovy @@ -23,7 +23,7 @@ import java.security.Key import static org.junit.Assert.* /** - * Tests {@link Jwts#KEY} values. + * Tests {@link Jwts.KEY} values. * * @since JJWT_RELEASE_VERSION */ @@ -63,13 +63,6 @@ class KeyAlgorithmsTest { } } - @Test - void testForKeyCaseInsensitive() { - for (KeyAlgorithm alg : Jwts.KEY.get().values()) { - assertSame alg, Jwts.KEY.get().forKey(alg.getId().toLowerCase()) - } - } - @Test(expected = IllegalArgumentException) void testForKeyWithInvalidId() { //unlike the 'get' paradigm, 'key' requires the value to exist @@ -83,13 +76,6 @@ class KeyAlgorithmsTest { } } - @Test - void testGetCaseInsensitive() { - for (KeyAlgorithm alg : Jwts.KEY.get().values()) { - assertSame alg, Jwts.KEY.get().get(alg.getId().toLowerCase()) - } - } - @Test void testGetWithInvalidId() { // 'get' paradigm can return null if not found diff --git a/impl/src/test/groovy/io/jsonwebtoken/security/StandardAlgorithmsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/security/StandardAlgorithmsTest.groovy index b659a386..4d7563cf 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/security/StandardAlgorithmsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/security/StandardAlgorithmsTest.groovy @@ -45,13 +45,6 @@ class StandardAlgorithmsTest { } } - @Test - void testForKeyCaseInsensitive() { - eachRegAlg { reg, alg -> - assertSame alg, reg.forKey(alg.getId().toLowerCase()) - } - } - @Test void testForKeyWithInvalidId() { //unlike the 'get' paradigm, 'forKey' requires the value to exist @@ -72,13 +65,6 @@ class StandardAlgorithmsTest { } } - @Test - void testGetCaseInsensitive() { - eachRegAlg { reg, alg -> - assertSame alg, reg.get(alg.getId().toLowerCase()) - } - } - @Test void testGetWithInvalidId() { // 'get' paradigm can return null if not found diff --git a/tdjar/src/test/java/io/jsonwebtoken/all/JavaReadmeTest.java b/tdjar/src/test/java/io/jsonwebtoken/all/JavaReadmeTest.java index 3cfb4be8..9ed8c7f0 100644 --- a/tdjar/src/test/java/io/jsonwebtoken/all/JavaReadmeTest.java +++ b/tdjar/src/test/java/io/jsonwebtoken/all/JavaReadmeTest.java @@ -75,9 +75,8 @@ public class JavaReadmeTest { .compact(); Jws parsed = Jwts.parser().verifyWith(testKey) // 1 - .critical("b64") // 2 .build() - .parseContentJws(jws, content); // 3 + .parseContentJws(jws, content); // 2 assertArrayEquals(content, parsed.getPayload()); } @@ -98,7 +97,7 @@ public class JavaReadmeTest { .compact(); Jws parsed = Jwts.parser().verifyWith(testKey) // 1 - .critical("b64") // 2 + .critical().add("b64").and() // 2 .build() .parseClaimsJws(jws); // 3 @@ -106,7 +105,6 @@ public class JavaReadmeTest { assert "me".equals(parsed.getPayload().getIssuer()); parsed = Jwts.parser().verifyWith(testKey) - .critical("b64") .build() .parseClaimsJws(jws, claimsString.getBytes(StandardCharsets.UTF_8)); // <--- @@ -237,7 +235,7 @@ public class JavaReadmeTest { AeadAlgorithm enc = Jwts.ENC.A256GCM; //or A192GCM, A128GCM, A256CBC-HS512, etc... // Bob creates the compact JWE with Alice's RSA public key so only she may read it: - String jwe = Jwts.builder().audience("Alice") + String jwe = Jwts.builder().audience().add("Alice").and() .encryptWith(pair.getPublic(), alg, enc) // <-- Alice's RSA public key .compact(); @@ -285,7 +283,7 @@ public class JavaReadmeTest { AeadAlgorithm enc = Jwts.ENC.A256GCM; //or A192GCM, A128GCM, A256CBC-HS512, etc... // Bob creates the compact JWE with Alice's EC public key so only she may read it: - String jwe = Jwts.builder().audience("Alice") + String jwe = Jwts.builder().audience().add("Alice").and() .encryptWith(pair.getPublic(), alg, enc) // <-- Alice's EC public key .compact();