From c142fb5c7a74cf18e78a7c1bb0c87e7e8a98aa42 Mon Sep 17 00:00:00 2001 From: lhazlewood <121180+lhazlewood@users.noreply.github.com> Date: Wed, 9 Aug 2023 12:20:10 -0700 Subject: [PATCH] Automatic compact `cty` header (#795) Ensured header 'cty' raw value reflects a compact form but getContentType() return value reflects a normalized value per per https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.10 --- .../java/io/jsonwebtoken/HeaderMutator.java | 44 ++++++++------ .../main/java/io/jsonwebtoken/JwtBuilder.java | 60 +++++++++++-------- .../io/jsonwebtoken/impl/DefaultHeader.java | 5 +- .../jsonwebtoken/impl/DefaultJwtBuilder.java | 5 +- .../lang/CompactMediaTypeIdConverter.java | 22 +++++-- .../groovy/io/jsonwebtoken/JwtsTest.groovy | 5 +- .../impl/DefaultHeaderTest.groovy | 7 ++- .../impl/DefaultJwtHeaderBuilderTest.groovy | 5 +- .../CompactMediaTypeIdConverterTest.groovy | 4 +- .../impl/security/RFC7520Section5Test.groovy | 5 +- 10 files changed, 102 insertions(+), 60 deletions(-) diff --git a/api/src/main/java/io/jsonwebtoken/HeaderMutator.java b/api/src/main/java/io/jsonwebtoken/HeaderMutator.java index ae1ff568..fd07229e 100644 --- a/api/src/main/java/io/jsonwebtoken/HeaderMutator.java +++ b/api/src/main/java/io/jsonwebtoken/HeaderMutator.java @@ -62,28 +62,34 @@ public interface HeaderMutator> extends MapMutator - * cty (Content Type) header parameter value. A {@code null} value will remove the property from - * the JSON map. + * Sets the compact + * cty (Content Type) header parameter value, used by applications to declare the + * IANA MediaType of the JWT + * payload. A {@code null} value will remove the property from the JSON map. * - *

The cty (Content Type) Header Parameter is used by applications to declare the - * IANA MediaType of the content - * (the payload). This is intended for use by the application when more than - * one kind of object could be present in the Payload; the application can use this value to disambiguate among - * the different kinds of objects that might be present. It will typically not be used by applications when - * the kind of object is already known. This parameter is ignored by JWT implementations (like JJWT); any - * processing of this parameter is performed by the JWS application. Use of this Header Parameter is OPTIONAL.

+ *

Compact Media Type Identifier

* - *

To keep messages compact in common situations, it is RECOMMENDED that producers omit an - * application/ prefix of a media type value in a {@code cty} Header Parameter when - * no other '/' appears in the media type value. A recipient using the media type value MUST - * treat it as if application/ were prepended to any {@code cty} value not containing a - * '/'. For instance, a {@code cty} value of example SHOULD be used to - * represent the application/example media type, whereas the media type - * application/example;part="1/2" cannot be shortened to - * example;part="1/2".

+ *

This method will automatically remove any application/ prefix from the + * {@code cty} string if possible according to the rules defined in the last paragraph of + * RFC 7517, Section 4.1.10:

+ *
+     *     To keep messages compact in common situations, it is RECOMMENDED that
+     *     producers omit an "application/" prefix of a media type value in a
+     *     "cty" Header Parameter when no other '/' appears in the media type
+     *     value.  A recipient using the media type value MUST treat it as if
+     *     "application/" were prepended to any "cty" value not containing a
+     *     '/'.  For instance, a "cty" value of "example" SHOULD be used to
+     *     represent the "application/example" media type, whereas the media
+     *     type "application/example;part="1/2"" cannot be shortened to
+     *     "example;part="1/2"".
* - * @param cty the JWT JOSE {@code cty} header value or {@code null} to remove the property from the JSON map. + *

JJWT performs the reverse during JWT parsing: {@link Header#getContentType()} will automatically prepend the + * {@code application/} prefix if the parsed {@code cty} value does not contain a '/' character (as + * mandated by the RFC language above). This ensures application developers can use and read standard IANA Media + * Type identifiers without needing JWT-specific prefix conditional logic in application code. + *

+ * + * @param cty the JWT {@code cty} header value or {@code null} to remove the property from the JSON map. * @return the instance for method chaining. */ T contentType(String cty); diff --git a/api/src/main/java/io/jsonwebtoken/JwtBuilder.java b/api/src/main/java/io/jsonwebtoken/JwtBuilder.java index 2fafd393..53f87cc3 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JwtBuilder.java @@ -155,7 +155,7 @@ public interface JwtBuilder extends ClaimsMutator { * {@link #content(byte[], String)} instead. * *

This is a wrapper method for:

- *
+     * 
s
      * {@link #content(byte[]) setPayload}(payload.getBytes(StandardCharsets.UTF_8));
* *

If you want the JWT payload to be JSON, use the {@link #claims()} method instead.

@@ -178,9 +178,6 @@ public interface JwtBuilder extends ClaimsMutator { /** * Sets the JWT payload to be the specified content byte array. * - *

This method is mutually exclusive of the {@link #claims()} and {@link #claim(String, Object)} - * methods. Either {@code claims} or {@code content} method variants may be used, but not both.

- * *

Content Type Recommendation

* *

Unless you are confident that the JWT recipient will always know how to use @@ -188,44 +185,57 @@ public interface JwtBuilder extends ClaimsMutator { * {@link #content(byte[], String)} method instead of this one. That method ensures that a JWT recipient * can inspect the {@code cty} header to know how to handle the byte array without ambiguity.

* - *

Note that the content and claims properties are mutually exclusive - only one of the two may be used.

+ *

Mutually Exclusive Claims and Content

+ * + *

This method is mutually exclusive of the {@link #claim(String, Object)} and {@link #claims()} + * methods. Either {@code claims} or {@code content} method variants may be used, but not both. If you want the + * JWT payload to be JSON claims, use the {@link #claim(String, Object)} or {@link #claims()} methods instead.

* * @param content the content byte array to use as the JWT payload * @return the builder for method chaining. + * @see #content(byte[], String) * @since JJWT_RELEASE_VERSION */ JwtBuilder content(byte[] content); /** - * Convenience method that sets the JWT payload to be the specified content byte array and also sets the - * {@link BuilderHeader#contentType(String) contentType} header value to a compact {@code cty} media type + * Sets the JWT payload to be the specified content byte array and also sets the + * {@link BuilderHeader#contentType(String) contentType} header value to a compact {@code cty} IANA Media Type * identifier to indicate the data format of the byte array. The JWT recipient can inspect the * {@code cty} value to determine how to convert the byte array to the final content type as desired. * - *

This method is mutually exclusive of the {@link #claim(String, Object)} and {@link #claims()} - * methods. Either {@code claims} or {@code content} method variants may be used, but not both.

+ *

This is a convenience method semantically equivalent to:

+ *
+     *     {@link #header()}.{@link HeaderMutator#contentType(String) contentType(cty)}.{@link BuilderHeader#and() and()}
+     *     {@link #content(byte[]) content(content)}
* *

Compact Media Type Identifier

* - *

As a convenience, this method will automatically trim any application/ prefix from the - * {@code cty} string if possible according to the - * JWT specification recommendations.

- * - *

If for some reason you do not wish to adhere to the JWT specification recommendation, do not call this - * method - instead call {@link #content(byte[])} and set the header's - * {@link BuilderHeader#contentType(String) contentType} independently. For example:

- * + *

This method will automatically remove any application/ prefix from the + * {@code cty} string if possible according to the rules defined in the last paragraph of + * RFC 7517, Section 4.1.10:

*
-     * Jwts.builder()
-     *     .header().contentType("application/whatever").and()
-     *     .content(byteArray)
-     *     ...
-     *     .build();
+ * To keep messages compact in common situations, it is RECOMMENDED that + * producers omit an "application/" prefix of a media type value in a + * "cty" Header Parameter when no other '/' appears in the media type + * value. A recipient using the media type value MUST treat it as if + * "application/" were prepended to any "cty" value not containing a + * '/'. For instance, a "cty" value of "example" SHOULD be used to + * represent the "application/example" media type, whereas the media + * type "application/example;part="1/2"" cannot be shortened to + * "example;part="1/2"".
* - *

If you want the JWT payload to be JSON claims, use the {@link #claim(String, Object)} or - * {@link #claims()} methods instead.

+ *

JJWT performs the reverse during JWT parsing: {@link Header#getContentType()} will automatically prepend the + * {@code application/} prefix if the parsed {@code cty} value does not contain a '/' character (as + * mandated by the RFC language above). This ensures application developers can use and read standard IANA Media + * Type identifiers without needing JWT-specific prefix conditional logic in application code. + *

* - *

Note that the content and claims properties are mutually exclusive - only one of the two may be used.

+ *

Mutually Exclusive Claims and Content

+ * + *

This method is mutually exclusive of the {@link #claim(String, Object)} and {@link #claims()} + * methods. Either {@code claims} or {@code content} method variants may be used, but not both. If you want the + * JWT payload to be JSON claims, use the {@link #claim(String, Object)} or {@link #claims()} methods instead.

* * @param content the content byte array that will be the JWT payload. Cannot be null or empty. * @param cty the content type (media type) identifier attributed to the byte array. Cannot be null or empty. diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java index 86cc3922..5ce3521b 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java @@ -16,6 +16,7 @@ package io.jsonwebtoken.impl; import io.jsonwebtoken.Header; +import io.jsonwebtoken.impl.lang.CompactMediaTypeIdConverter; import io.jsonwebtoken.impl.lang.Field; import io.jsonwebtoken.impl.lang.Fields; import io.jsonwebtoken.lang.Registry; @@ -26,7 +27,9 @@ import java.util.Map; public class DefaultHeader extends FieldMap implements Header { static final Field TYPE = Fields.string(Header.TYPE, "Type"); - static final Field CONTENT_TYPE = Fields.string(Header.CONTENT_TYPE, "Content Type"); + static final Field CONTENT_TYPE = Fields.builder(String.class) + .setId(Header.CONTENT_TYPE).setName("Content Type") + .setConverter(CompactMediaTypeIdConverter.INSTANCE).build(); static final Field ALGORITHM = Fields.string(Header.ALGORITHM, "Algorithm"); static final Field COMPRESSION_ALGORITHM = Fields.string(Header.COMPRESSION_ALGORITHM, "Compression Algorithm"); @SuppressWarnings("DeprecatedIsStillUsed") diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java index b9f6061c..85f64f0f 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java @@ -20,7 +20,6 @@ import io.jsonwebtoken.JweHeader; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.impl.lang.Bytes; -import io.jsonwebtoken.impl.lang.CompactMediaTypeIdConverter; import io.jsonwebtoken.impl.lang.Function; import io.jsonwebtoken.impl.lang.Functions; import io.jsonwebtoken.impl.lang.Services; @@ -325,9 +324,7 @@ public class DefaultJwtBuilder implements JwtBuilder { public JwtBuilder content(byte[] content, String cty) { Assert.notEmpty(content, "content byte array cannot be null or empty."); Assert.hasText(cty, "Content Type String cannot be null or empty."); - cty = CompactMediaTypeIdConverter.INSTANCE.applyFrom(cty); - this.headerBuilder.contentType(cty); - return content(content); + return header().contentType(cty).and().content(content); } @Override diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/CompactMediaTypeIdConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/CompactMediaTypeIdConverter.java index 8b129af3..50841d39 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/CompactMediaTypeIdConverter.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/CompactMediaTypeIdConverter.java @@ -22,7 +22,9 @@ public final class CompactMediaTypeIdConverter implements Converter INSTANCE = new CompactMediaTypeIdConverter(); - private static final String APP_MEDIA_TYPE_PREFIX = "application/"; + private static final char FORWARD_SLASH = '/'; + + private static final String APP_MEDIA_TYPE_PREFIX = "application" + FORWARD_SLASH; static String compactIfPossible(String cty) { Assert.hasText(cty, "Value cannot be null or empty."); @@ -31,7 +33,7 @@ public final class CompactMediaTypeIdConverter implements Converter= APP_MEDIA_TYPE_PREFIX.length(); i--) { char c = cty.charAt(i); - if (c == '/') { + if (c == FORWARD_SLASH) { return cty; // found another '/', can't compact, so just return unmodified } } @@ -49,8 +51,18 @@ public final class CompactMediaTypeIdConverter implements Converter