mirror of https://github.com/jwtk/jjwt.git
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
This commit is contained in:
parent
7ed0b772ae
commit
c142fb5c7a
|
@ -62,28 +62,34 @@ public interface HeaderMutator<T extends HeaderMutator<T>> extends MapMutator<St
|
|||
T type(String typ);
|
||||
|
||||
/**
|
||||
* Sets the JWT <a href="https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.10">
|
||||
* <code>cty</code> (Content Type)</a> header parameter value. A {@code null} value will remove the property from
|
||||
* the JSON map.
|
||||
* Sets the compact <a href="https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.10">
|
||||
* <code>cty</code> (Content Type)</a> header parameter value, used by applications to declare the
|
||||
* <a href="https://www.iana.org/assignments/media-types/media-types.xhtml">IANA MediaType</a> of the JWT
|
||||
* payload. A {@code null} value will remove the property from the JSON map.
|
||||
*
|
||||
* <p>The <code>cty</code> (Content Type) Header Parameter is used by applications to declare the
|
||||
* <a href="https://www.iana.org/assignments/media-types/media-types.xhtml">IANA MediaType</a> 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.</p>
|
||||
* <p><b>Compact Media Type Identifier</b></p>
|
||||
*
|
||||
* <p>To keep messages compact in common situations, it is RECOMMENDED that producers omit an
|
||||
* <b><code>application/</code></b> prefix of a media type value in a {@code cty} Header Parameter when
|
||||
* no other '<b>/</b>' appears in the media type value. A recipient using the media type value <em>MUST</em>
|
||||
* treat it as if <b><code>application/</code></b> were prepended to any {@code cty} value not containing a
|
||||
* '<b>/</b>'. For instance, a {@code cty} value of <b><code>example</code></b> <em>SHOULD</em> be used to
|
||||
* represent the <b><code>application/example</code></b> media type, whereas the media type
|
||||
* <b><code>application/example;part="1/2"</code></b> cannot be shortened to
|
||||
* <b><code>example;part="1/2"</code></b>.</p>
|
||||
* <p>This method will automatically remove any <code><b>application/</b></code> prefix from the
|
||||
* {@code cty} string if possible according to the rules defined in the last paragraph of
|
||||
* <a href="https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.10">RFC 7517, Section 4.1.10</a>:</p>
|
||||
* <blockquote><pre>
|
||||
* 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"".</pre></blockquote>
|
||||
*
|
||||
* @param cty the JWT JOSE {@code cty} header value or {@code null} to remove the property from the JSON map.
|
||||
* <p>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 '<code>/</code>' 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.
|
||||
* </p>
|
||||
*
|
||||
* @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);
|
||||
|
|
|
@ -155,7 +155,7 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
|
|||
* {@link #content(byte[], String)} instead.
|
||||
*
|
||||
* <p>This is a wrapper method for:</p>
|
||||
* <blockquote><pre>
|
||||
* <blockquote><pre>s
|
||||
* {@link #content(byte[]) setPayload}(payload.getBytes(StandardCharsets.UTF_8));</pre></blockquote>
|
||||
*
|
||||
* <p>If you want the JWT payload to be JSON, use the {@link #claims()} method instead.</p>
|
||||
|
@ -178,9 +178,6 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
|
|||
/**
|
||||
* Sets the JWT payload to be the specified content byte array.
|
||||
*
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p><b>Content Type Recommendation</b></p>
|
||||
*
|
||||
* <p>Unless you are confident that the JWT recipient will <em>always</em> know how to use
|
||||
|
@ -188,44 +185,57 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
|
|||
* {@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.</p>
|
||||
*
|
||||
* <p>Note that the content and claims properties are mutually exclusive - only one of the two may be used.</p>
|
||||
* <p><b>Mutually Exclusive Claims and Content</b></p>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* <p>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.</p>
|
||||
* <p>This is a convenience method semantically equivalent to:</p>
|
||||
* <blockquote><pre>
|
||||
* {@link #header()}.{@link HeaderMutator#contentType(String) contentType(cty)}.{@link BuilderHeader#and() and()}
|
||||
* {@link #content(byte[]) content(content)}</pre></blockquote>
|
||||
*
|
||||
* <p><b>Compact Media Type Identifier</b></p>
|
||||
*
|
||||
* <p>As a convenience, this method will automatically trim any <code><b>application/</b></code> prefix from the
|
||||
* {@code cty} string if possible according to the
|
||||
* <a href="https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.10">JWT specification recommendations</a>.</p>
|
||||
*
|
||||
* <p>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:</p>
|
||||
*
|
||||
* <p>This method will automatically remove any <code><b>application/</b></code> prefix from the
|
||||
* {@code cty} string if possible according to the rules defined in the last paragraph of
|
||||
* <a href="https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.10">RFC 7517, Section 4.1.10</a>:</p>
|
||||
* <blockquote><pre>
|
||||
* Jwts.builder()
|
||||
* .header().contentType("application/whatever").and()
|
||||
* .content(byteArray)
|
||||
* ...
|
||||
* .build();</pre></blockquote>
|
||||
* 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"".</pre></blockquote>
|
||||
*
|
||||
* <p>If you want the JWT payload to be JSON claims, use the {@link #claim(String, Object)} or
|
||||
* {@link #claims()} methods instead.</p>
|
||||
* <p>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 '<code>/</code>' 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.
|
||||
* </p>
|
||||
*
|
||||
* <p>Note that the content and claims properties are mutually exclusive - only one of the two may be used.</p>
|
||||
* <p><b>Mutually Exclusive Claims and Content</b></p>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*
|
||||
* @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.
|
||||
|
|
|
@ -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<String> TYPE = Fields.string(Header.TYPE, "Type");
|
||||
static final Field<String> CONTENT_TYPE = Fields.string(Header.CONTENT_TYPE, "Content Type");
|
||||
static final Field<String> CONTENT_TYPE = Fields.builder(String.class)
|
||||
.setId(Header.CONTENT_TYPE).setName("Content Type")
|
||||
.setConverter(CompactMediaTypeIdConverter.INSTANCE).build();
|
||||
static final Field<String> ALGORITHM = Fields.string(Header.ALGORITHM, "Algorithm");
|
||||
static final Field<String> COMPRESSION_ALGORITHM = Fields.string(Header.COMPRESSION_ALGORITHM, "Compression Algorithm");
|
||||
@SuppressWarnings("DeprecatedIsStillUsed")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -22,7 +22,9 @@ public final class CompactMediaTypeIdConverter implements Converter<String, Obje
|
|||
|
||||
public static final Converter<String, Object> 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<String, Obje
|
|||
// we can only use the compact form if no other '/' exists in the string
|
||||
for (int i = cty.length() - 1; i >= 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<String, Obje
|
|||
@Override
|
||||
public String applyFrom(Object o) {
|
||||
Assert.notNull(o, "Value cannot be null.");
|
||||
Assert.isInstanceOf(String.class, o, "Value must be a string.");
|
||||
String s = (String) o;
|
||||
return compactIfPossible(s);
|
||||
String s = Assert.isInstanceOf(String.class, o, "Value must be a string.");
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.10:
|
||||
//
|
||||
// A recipient using the media type value MUST treat it as if
|
||||
// "application/" were prepended to any "cty" value not containing a
|
||||
// '/'.
|
||||
//
|
||||
if (s.indexOf(FORWARD_SLASH) < 0) {
|
||||
s = APP_MEDIA_TYPE_PREFIX + s;
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -178,7 +178,10 @@ class JwtsTest {
|
|||
String cty = "application/$subtype"
|
||||
String compact = Jwts.builder().content(s.getBytes(StandardCharsets.UTF_8), cty).compact()
|
||||
def jwt = Jwts.parser().enableUnsecured().build().parseContentJwt(compact)
|
||||
assertEquals subtype, jwt.header.getContentType() // assert that the compact form was used
|
||||
// assert raw value is compact form:
|
||||
assertEquals subtype, jwt.header.get('cty')
|
||||
// assert getter reflects normalized form per https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.10:
|
||||
assertEquals cty, jwt.header.getContentType()
|
||||
assertEquals s, new String(jwt.payload, StandardCharsets.UTF_8)
|
||||
}
|
||||
|
||||
|
|
|
@ -39,8 +39,11 @@ class DefaultHeaderTest {
|
|||
@Test
|
||||
void testContentType() {
|
||||
header = h([cty: 'bar'])
|
||||
assertEquals 'bar', header.getContentType()
|
||||
assertEquals 'bar', header.get('cty')
|
||||
// Per per https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.10, the raw header should have a
|
||||
// compact form, but application developers shouldn't have to check for that all the time, so our getter has
|
||||
// the normalized form:
|
||||
assertEquals 'bar', header.get('cty') // raw compact form
|
||||
assertEquals 'application/bar', header.getContentType() // getter normalized form
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -266,7 +266,10 @@ class DefaultJwtHeaderBuilderTest {
|
|||
@Test
|
||||
void testDeprecatedSetters() { // TODO: remove before 1.0
|
||||
assertEquals 'foo', builder.setType('foo').build().getType()
|
||||
assertEquals 'foo', builder.setContentType('foo').build().getContentType()
|
||||
|
||||
assertEquals 'foo', builder.setContentType('foo').build().get('cty') // compact form
|
||||
assertEquals 'application/foo', builder.build().getContentType() // normalized form
|
||||
|
||||
assertEquals 'foo', builder.setCompressionAlgorithm('foo').build().getCompressionAlgorithm()
|
||||
assertEquals 'foo', builder.setKeyId('foo').build().getKeyId()
|
||||
assertEquals 'foo', builder.setAlgorithm('foo').build().getAlgorithm()
|
||||
|
|
|
@ -52,7 +52,9 @@ class CompactMediaTypeIdConverterTest {
|
|||
void testNonApplicationMediaType() {
|
||||
String cty = 'foo'
|
||||
assertEquals cty, converter.applyTo(cty)
|
||||
assertEquals cty, converter.applyFrom(cty)
|
||||
// must auto-prepend 'application/' if no slash in cty value
|
||||
// per https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.10:
|
||||
assertEquals "application/$cty" as String, converter.applyFrom(cty)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -610,7 +610,10 @@ class RFC7520Section5Test {
|
|||
assertEquals alg.getId(), parsed.header.getAlgorithm()
|
||||
assertEquals FIGURE_99, b64Url(parsed.header.getPbes2Salt())
|
||||
assertEquals p2c, parsed.header.getPbes2Count()
|
||||
assertEquals cty, parsed.header.getContentType()
|
||||
|
||||
assertEquals cty, parsed.header.get('cty') // compact form
|
||||
assertEquals "application/$cty" as String, parsed.header.getContentType() // normalized form
|
||||
|
||||
assertEquals enc.getId(), parsed.header.getEncryptionAlgorithm()
|
||||
assertEquals FIGURE_95, utf8(parsed.payload)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue