jwtk/jjwt

AUD as only array breaks legacy/existing implementations

ccanning opened this issue · 5 comments

When you fixed issue: #77, you changed the functionality to only use arrays. In this issue, #787, you contemplated having a single audience aka aud parameter remain backwards compatible and not an array (its even stated that the spec supports both).

I think its a bug to not remain backwards compatible because a lot of existing services that consume JWT's don't support audience being an array.

Expected:

{ "aud": "audience", "exp": 1715888139211, "sub": "subject", "iat": 1715884539211, "jti": "77c6e241-79a1-435a-8a33-0d87f0d419d5", "iss": "issuer" }

Now Get:

{ "aud": [ "audience" ], "exp": 1715888139211, "sub": "subject", "iat": 1715884539211, "jti": "77c6e241-79a1-435a-8a33-0d87f0d419d5", "iss": "issuer" }

Hi there!

Now Get:

{ "aud": [ "audience" ], "exp": 1715888139211, "sub": "subject", "iat": 1715884539211, "jti": "77c6e241-79a1-435a-8a33-0d87f0d419d5", "iss": "issuer" }

How was that output produced?

It's not clear to me what you mean by backwards compatible - are you talking how the JJWT API changes impact your application Java code directly? Or do you mean how compact JWT strings produced by JJWT now do not produce single-string aud values by default?

The changes related to #77 ensure that an aud value will be created as a JSON Array of Strings by default. However, when creating a new JWT, you are able to set a single string aud value if you choose to do so, so you can retain backwards compatibility for JWT recipients that do not know how to process a set of strings. For example:

Jwts.builder()
    .audience().single("singleAudValue").and()
    ... etc ...
    .compact();

This is documented for the JwtBuilder's audience() builder and its single(String) method supporting single-string values.

When parsing a JWT however, the aud JSON value is always converted in the Java representation to a Set<String>, even for a single JSON string value for two intentional reasons:

  1. Your Java code never needs to change when receiving a JWT that uses a single-string-aud-value vs set-of-string-values - in both cases, JJWT normalizes the value to a Set<String>. It is generally a good philosophy to reduce cyclomatic complexity in a codebase by avoiding 'instanceof' checks in code with multiple branch conditions; your Java code can stay the same regardless of how a JWT issuer formats the aud value.

  2. The RFC intentionally indicates that a set-of-strings should be the default for most applications so applications don't have do do the "if a string, do this, if a set of strings, do that" annoying logic. From https://tools.ietf.org/html/rfc7519#section-4.1.3:

    In the general case, the "aud" value is an array of case-
    sensitive strings, each containing a StringOrURI value. In the
    special case when the JWT has one audience, the "aud" value MAY be a
    single case-sensitive string containing a StringOrURI value. The
    interpretation of audience values is generally application specific.

So JJWT 0.12.0 is not backwards compatible in Java code as to how you receive an aud value, and this was very intentional as described above, and we tried to be clear about this in the 0.12.0 release.

However, you still have the ability to produce a single-string audience value for JWT recipients that do not process aud set-of-string values. This is backwards compatible for JWT recipients.

Does this help?

Thanks for the quick response - this sort of helps but our code normally passes in the claims as a map.

Jwts.builder().claims().add(claims).and().signWith(key).compact()

for re-use.

As for your question, our JWT parsing code works fine, it's when we send JWT's to 3rd-parties.

Is there a way to get the single() when using this way or do we need to use the .audience().single() style?

FYI: this is much better than us using content() and creating the payload json ourselves.

What JJWT version are you using?

#891 was merged and released in 0.12.4 and this should retain aud single string values, even when using a Map:

public Object put(String key, Object value) {
if (AUDIENCE_STRING.getId().equals(key)) { // https://github.com/jwtk/jjwt/issues/890
if (value instanceof String) {
Object existing = get(key);
//noinspection deprecation
audience().single((String) value);
return existing;
}
// otherwise ensure that the Parameter type is the RFC-default data type (JSON Array of Strings):
getAudience();
}
// otherwise retain expected behavior:
return super.put(key, value);
}

Latest version is 0.12.5.

Ah, we are using 0.12.3 which was the default version for used by our version of spring boot/security. Thank you.

Great to hear! Thanks for the update 👍