jwtk/jjwt

Deprecation of support for unsecured JWT token parsing with algo != 'none'

joyfulnikhil opened this issue · 3 comments

Describe the bug
After migrating from jjwt 0.11.5 to 0.12.3, we have observed that the support for unsecured JWT(with algo not defined as 'none') parsing is deprecated.

We had a use case of performing unsecured JWT parsing. When the JWT arrived with a valid algo in the header such as "ES256", we removed the signature section and passed the header & payload to the JWT parse method.

JWT Token arrived: xxxx.yyyy.zzzz
The algo in the header section was "ES256".
We removed the zzzz part and passed the remaining string to the parseClaim method.

Jwts.parser().parseClaimsJwt("xxxx.yyyy");

We used to get the headers and claims parsed. After this, we moved to the 0.12.3 version and since then our implementation has been breaking.
The method parseClaimsJwt has been marked as deprecated and we moved to the new method as per the code documentation.
Jwts.parser().unsecured().build().parseUnsecuredClaims("xxxx.yyyy");

This implementation fails to parse the JWT token stating that io.jsonwebtoken.UnsupportedJwtException: Cannot verify JWS signature: unable to locate signature verification key for JWS with header: {alg=ES256, typ=JWT}

The workaround that we figured out was to pass algo as 'none' for an unsecured JWT parsing.

We would like to understand why this sudden shift towards enforcing algo as 'none' if we want to perform unsecured JWT parsing.? Is there any update in JWT RFC to enforce such checks for unsecured JWTs?

To Reproduce
Steps to reproduce the behavior:

  1. Generate a JWT token with any algo except 'none'.
  2. Remove the signature portion of the JWT token (xxx.yyyy.zzzz)
  3. Try to parse the JWT token in an unsecured way using Jwts.parser().unsecured().build().parseUnsecuredClaims("xxxx.yyyy");
  4. You will receive an exception while parsing the token io.jsonwebtoken.UnsupportedJwtException: Cannot verify JWS signature: unable to locate signature verification key for JWS with header: {alg=ES256, typ=JWT}

Expected behavior
We would have expected the new method to work the same way as previous one, i.e. to be able to parse the JWT in an unsecured manner even if the algo is set to ES256 or RS256.

Screenshots
If applicable, add screenshots to help explain your problem.

TL;DR The new way is more secure (and spec compliant).

If you are interested in bypassing the security mechanisms of a JWT, see this post, though I'd recommend against parsing a JWT you are not able to validate

We had a use case of performing unsecured JWT parsing.

I don't know if your use case is similar to those discussed in other tickets, but I've addressed some of them in other replies that you might find useful:

#86 (comment)

and

#86 (comment)

TLDR; if you need information in a JWS after its initial signature verification and use case (e.g. authenticating an HTTP request), create a new JWT that contains what you need for access later, with appropriate exp times if/as necessary. This new JWT can be unsecured (not recommended) or secured with a key specific to the server/application so it may be verified unmodified later as needed.

Is there any update in JWT RFC to enforce such checks for unsecured JWTs?

Surprisingly, yes. JJWT's original implementation of this logic was created years ago, before the RFCs were finalized (they were very nearly finalized, in draft status, when JJWT was created). Before the RFC was finalized, they added this:

https://datatracker.ietf.org/doc/html/rfc7518#section-8.5

Given the other backwards-incompatible changes slated for 0.12.0, it was also a good time to introduce this other change to represent the (finalized) RFC. Consequently, having a signature algorithm other than none, but not having a signature in the token indicates that the JWS was either malformed or unsafely manipulated, so the JWT library needs to indicate the token is invalid.

Hopefully that helps!

P.S. thank you for such a detailed and well-written issue, we really do appreciate it.

For what it's worth, the code below will achieve your expected behavior, but it's really, really not a good idea. Because of its security implications, it will never be added to JJWT, but for those who live dangerously:

KeyPair keyPair = Jwts.SIG.ES256.keyPair().build();
final String jws = Jwts.builder().subject("Alice").signWith(keyPair.getPrivate()).compact();

// HERE BE DRAGONS. Thou art forewarned:
int i = jws.indexOf('.');
String b64UrlHeader = jws.substring(0, i);
String b64UrlPayload = jws.substring(i + 1, jws.lastIndexOf('.'));
ObjectMapper om = new ObjectMapper();
Map headerMap = om.readValue(Decoders.BASE64URL.decode(b64UrlHeader), Map.class);
headerMap.put("alg", "none");
b64UrlHeader = Encoders.BASE64URL.encode(om.writeValueAsBytes(headerMap));
String unsecured = b64UrlHeader + '.' + b64UrlPayload + '.';
Jwt<Header, Claims> unsafe = Jwts.parser().unsecured().build().parseUnsecuredClaims(unsecured);