Open icecreamhead opened 8 months ago
Hi @icecreamhead!
Thank you for reaching out! Let me explain what's happening, and see if we can find a workaround for you.
As you noted, this change is purely driven by the RFC, and the RFC is quite clear that libraries (e.g. JJWT) are not to interpret payloads that have an assigned content type, and it must be the application (i.e. your code). Per RFC 7515, Section 4.1.10, "cty" (Content Type) Header Parameter:
This is intended for use by the application when more than one kind of object could be present in the JWS Payload; the application can use this value to disambiguate among the different kinds of objects that might be present.
and, most important for JJWT:
This parameter is ignored by JWS implementations; any processing of this parameter is performed by the JWS application.
So the spec is implicitly saying:
"If the cty
header is set, the payload can be literally anything, and it's not reasonable for a library to know how to process the payload because it's application-specific. Because of this, the application is responsible for handling payload themselves".
Consequently, if JJWT >= 0.12.0
sees that the cty
header has a non-empty value, it won't parse the payload at all (completely skips that), because JJWT has no way (currently) to figure out how to handle the payload.
BUT! It doesn't mean that JJWT ignores security. If found to be a JWS, JJWT does indeed perform signature verification, and if successful, constructs a Jws
object, but the payload returned in that Jws
instance will be a byte[]
because JJWT can't parse it as JSON; it instead 'passes it along' to your application code.
Consequently, the reason you're seeing that exception is that because, per the parseSignedClaims
JavaDoc, that method is a convenience that simply delegates to:
parse(compact)
which returns a Jwt<?,?>
instance, and thenimmediately calls the type-asserting Jws.CLAIMS
Visitor to ensure the parsed Jwt
is what you expect it to be. Jws.CLAIMS
is a simple visitor that merely asserts that what was parsed was
Jws
instance (not just a Jwt
or Jwe
), andClaims
instance (and not any random payload).and if not, throws the exception you see.
So, in JJWT's current form, you have two choices:
Don't call parseSignedClaims
because of the type validation in step 2 above. You can call just parse
, get back a Jwt<?, ?>
yourself. If the instance is a Jws
and the payload is a Claims
, then no cty
header was set. If the instance is a Jws
and the payload is a byte[]
, then the cty
header was set, and you'll need to parse it yourself based on the cty
value. You could theoretically then parse it via a JSON parser, and pass the resulting Map<String,?>
to a Jwts.claims()
builder if desired to get a final Claims
instance.
Communicate with your upstream jwt token issuer that what they're doing is causing problems because of the RFC. Just because you don't control how they issue the token doesn't mean they won't listen to a bug report you file, especially when they hear that they're violating the spec. Resolving RFC non-compliance is usually a much bigger driving force for prioritization than a customer/client/recipient just saying "can you please change how this works".
@icecreamhead my immediately preceding comment was to explain why things are the way they are, and how to potentially address your needs today, without JJWT code changes. This comment addresses a discussion of how we might support cty
behavior in the future.
When working towards the 0.12.0 release, and its associated cty
logic per the RFC, I did actually envision enabling a custom ContentTypeHandler
(or similar) concept where, if a cty
header was encountered, the header and payload byte[]
would be delegated to the handler for conversion. The handler would return an Object
of whatever the byte[]
became, and that would be set as the Jws
payload instance, e.g. new DefaultJws(JwsHeader, whateverTheHandlerReturned);
And the application could register various type handlers with the JwtParserBuilder
to perform this logic automatically during parsing before a complete Jwt
instance was returned.
The reason this wasn't done for 0.12.0 was due to time constraints and complexity. The JWE and JWK stuff was so large and comprehensive, and delayed for quite a while, we just had to get it out. And considering that JJWT is spec-compliant with regard to the RFC, it made sense.
The other reason was content-type/media-type identifier parsing, and registration of various handler types. If you've ever seen the Spring Framework's (very thorough and robust) MimeType
and MediaType
pluggable support, you know how complex this could end up being, and it didn't make sense replicating a similar framework for JJWT, especially when the JWT RFC is clear that the application should be handling this stuff to begin with.
Perhaps a middle ground could be made, at least initially, where JJWT has such a cty
handler concept as a single simple interface, and, the application is free to plug in its own implementation to do whatever it wants. Over time this could be made more robust by providing out-of-the-box MediaType
converters (a la Spring), but I'm not sure that'd be necessary, nor would I be sure that we'd even want JJWT to be in that business of maintaining such a thing when it's an application-specific concern to begin with.
Anyway, I hope these comments have been helpful in understanding why we made those various decisions.
but if they specify json, then I think we should continue to parse the claims as if the field weren't specified at all.
Just noting in the general case, this doesn't appear to be a viable solution: If the cty
indicates JSON, JSON can be any structure, even non-Object structures (string or number literals, arrays, etc). Claims are not only JSON, but they are JSON with an additional set of parsing/parameter constraints.
In other words, a payload can definitely be JSON, and any kind of JSON at all. Claims are a further restriction on JSON structure/parameters, so there needs to be additional information in a media type identifier to indicate "not only is this JSON, but it is JWT Claims JSON" in order to (correctly) parse it into Claims.
There is no IANA-registered media type for Claims JSON. If there was it'd be something like application/joseclaims+json
(or similar). This is why if the payload is Claims JSON, the cty
header should be omitted entirely, because there's no standard media type to indicate Claims.
Hi @lhazlewood, thank you for a speedy, comprehensive and well-reasoned response. It's genuinely appreciated!
Unfortunately, asking the upstream token issuers to fix their JWSs is out of the question for us. My use-case is for the UK Open Banking & payee confirmation ecosystem, of which there are hundreds of independent participants. We've observed that the majority actually are setting the cty field on their tokens.
We actually have implemented your first suggestion of capturing the byte[] and parsing the claims ourselves, but this feels like an unnecessary overhead when the library can (and has previously) done the legwork for us.
I think the aspect of the behaviour that doesn't make sense to me is that by invoking parseSignedClaims
, we're signalling to the library that we expect a claims payload to be present, so the fact that the library explicitly doesn't attempt to read the payload when we've indicated what it should be just seems a bit odd.
My request would be to always try to decode the claims when parseSignedClaims
is invoked. If deserialisation fails then it's my problem as an application developer to handle that. Alternatively, a forceParse
option on the parser builder could be added to explicitly indicate that I want to decode the payload, regardless of the content type.
The other minor smell is that DefaultJws
is in the impl
package which suggests that we shouldn't construct instances of it ourselves. This means to construct an instance of Jws<Claims>
containing our manually-deserialized claims payload, we need to implement our own subclass of the Jws
interface, which is basically a duplicate of DefaultJws
.
Hi @lhazlewood, thank you for a speedy, comprehensive and well-reasoned response. It's genuinely appreciated!
Happy to help! Let's see if we can keep working on a better solution...
We've observed that the majority actually are setting the
cty
field on their tokens.
That's frustrating to hear that they do that; it is in direct conflict with the RFC. It explicitly says, if the payload is JSON Claims, that cty
is NOT RECOMMENDED (RFC emphasis, not mine). Upstream providers definitely should be told they're causing downstream problems. I know they may not listen to your team, but no one will try to fix it if they're not told 🤷 .
We actually have implemented your first suggestion of capturing the byte[] and parsing the claims ourselves,
Out of curiosity, what is the media type you see being used for most of these? Is it always application/json
(or just shortened json
per RFC shortening syntax recommendations)? Or anything different that those?
but this feels like an unnecessary overhead when the library can (and has previously) done the legwork for us.
Agreed, this is a pain, I'd like to have a more elegant solution now that I understand what you're experiencing.
I think the aspect of the behaviour that doesn't make sense to me is that by invoking
parseSignedClaims
, we're signalling to the library that we expect a claims payload to be present
Ahah, I think we're making progress 😄. Yes, at the moment, those methods are purely conveniences. parseSignedClaims
is just an alias for:
parse(compact).accept(Jws.CLAIMS);
So it is parsed, per normal RFC parsing rules (and cty
rules/expectations), and only then asserted that the resulting Jwt
instance is a Jws
and has a Claims
payload. There are no 'hints' conveyed from parseSignedClaims
to the general-purpose parse(compact)
method call.
Even so, let's assume we did convey such a hint to the parse
implementation.
What if the cty
header is application/octet-stream
(for example)? It is often not safe to blindly parse content as JSON, especially if the header indicates that it contains something else. Substitution attacks have been prevalent in other JWT libraries with naive parsing approaches, so we tend to think really cautiously about these kinds of things.
I suppose at least after verifying the signature, the risks for blind parsing are lessened, but still we have to be really careful about security implications.
Alternatively, a
forceParse
option on the parser builder could be added to explicitly indicate that I want to decode the payload, regardless of the content type.
I'd be hesitant to add that due to the security implications.
But I'm wondering if a simple ContentTypeHandler
concept (or similar) might allow a reasonable solution. For example:
Jwts.parser().cty(handler)...
And the handler would be called when detecting a cty
value. Then the application developer configuring their own handler implementation can do whatever logic they like, from blindly ignoring the value to converting payloads to Claims
or images/documents/etc, or anything in between.
At least that way it would be quite intentional by the app developer, at least assuming they understood the security repercussions of purposefully ignoring any indicated cty
value. Implementing a callback handler like this requires slightly more forethought/insight than a blind 'forceParse' option which could be (very) easily abused and always set to true
when an app dev just wants to 'get on with testing, and come back to this later' (and never actually do it 😉 ).
Anyway, I'm just thinking 'out loud', and still very interested in finding a clean solution that makes things simple for people while still enabling strong security by default.
Out of curiosity, what is the media type you see being used for most of these? Is it always application/json (or just shortened json per RFC shortening syntax recommendations)? Or anything different that those?
We've observed a mix of json
and application/json
for tokens we've actually investigated, but we don't bother to capture it programmatically so it's possible there have been others.
But I'm wondering if a simple ContentTypeHandler concept (or similar) might allow a reasonable solution.
I think this would be ideal for us. I'd be more than happy to throw a PR together if it helps?
Out of curiosity, what is the media type you see being used for most of these?
We've observed a mix of
json
andapplication/json
for tokens we've actually investigated, but we don't bother to capture it programmatically so it's possible there have been others.
That's helpful, thanks.
But I'm wondering if a simple ContentTypeHandler concept (or similar) might allow a reasonable solution.
I think this would be ideal for us. I'd be more than happy to throw a PR together if it helps?
That would be appreciated, but as I dug into this potential change more, it's not exactly simple:
Header
, maybe the configured Deserializer<Map<String<?>>
and I'm not sure if anything else.Claims
instance (if a compact JWT and a cty
header isn't set), or a byte[]
instance if the cty
header is set. If we introduce such a thing as a content type handler, the return type from the various .parse*
methods should reflect the already-converted data type, for example:Jws<?> parseSignedContent(CharSequence jws);
which is in conflict with the existing (0.12.x
) signature of Jws<byte[]> parseSignedContent(jws);
In other words, a ContentTypeHandler
(or whatever it's called) should perform the type-conversion and there would never be a Jws<byte[]>
method signature on the parser.
I don't know how difficult a change that would/will be, but it's not exactly trivial.
@icecreamhead in thinking of the Visitor
pattern more, I think that actually can be the 'handler' concept. A Visitor
allows logic, it's just that the default only asserts type expectations. But it can do anything really.
You could just implement the JwtVisitor
interface yourself (or subclass SupportedJwtVisitor
) and do what you want in your visit(jws);
implementation. For example (assuming Jackson):
public class AlwaysClaimsVisitor extends SupportedJwtVisitor<Claims> {
@Override
public Claims onVerifiedContent(Jws<byte[]> jws) {
// if cty header is set, application needs to convert as necessary:
byte[] payload = jws.getPayload();
ObjectMapper objectMapper = new ObjectMapper(); // or get other application singleton
Map<String,?> map = (Map<String,?>)objectMapper.readValue(payload, Map.class);
return Jwts.claims().add(map).build();
}
@Override
public Claims onVerifiedClaims(Jws<Claims> jws) {
return jws.getPayload();
}
}
and then use that visitor:
AlwaysClaimsVisitor visitor = new AlwaysClaimsVisitor();
Jwt<?, ?> jwt = Jwts.parser().build()/*... */.parse(jws);
Claims claims = jwt.accept(visitor);
This seems like a pretty clean workaround, no? The visitor is your 'handler' implementation, and the existing API already supports this use case.
This seems pretty clean, but I could be missing something. Please let me know your thoughts!
I managed to get the handler pattern working (and correctly inferring the output type) but the code isn't pleasant. I'll stick it on a branch.
I think you're right that the visitor pattern is the right solution (it's what we've switched to right now in lieu of parseSignedClaims()
) but I have two problems with it:
1) We have to write extra code to achieve functionality that's already baked into the library.
2) I think we lose the Jws
object? i.e. Jws<byte[]>
is passed into the visitor and Claims
are returned. There's no way for us to end up with Jws<Claims>
because we don't have an easy way to reconstruct the Jws
.
As an aside, I've just found this in the spec we're using for one of the services. The spec explicitly says clients are allowed to set cty
.
@icecreamhead What does a header and payload look like for a openbanking JWS (use fake data)? From a quick look at the docs, it seems some registered claims from the JWT spec are part of the openbanking JOSE header? e.g.
{
"alg": "RS512",
"kid": "90210ABAD",
"b64": false,
"http://openbanking.org.uk/iat": 1501497671,
"http://openbanking.org.uk/iss": "C=UK, ST=England, L=London, O=Acme Ltd.",
"http://openbanking.org.uk/tan": "openbanking.org.uk",
"crit": [ "b64", "http://openbanking.org.uk/iat", "http://openbanking.org.uk/iss", "http://openbanking.org.uk/tan"]
}
This seems to imply there are other validation steps that should happen:
typ
header if specified has the value JOSE.cty
header to ensure that the payload is of the expected mime type.alg
is one of the algorithms specified by OBIE.kid
is valid and a public key with the specified key Id can be retrieved from the Trust Anchor.b64
claim is set to false.http://openbanking.org.uk/iat
claim has a date-time value set in the past.http://openbanking.org.uk/iss
claim matches the expected PSP.http://openbanking.org.uk/tan
claim contains the DNS name of a Trust Anchor that it trusts.crit
claim does not contain additional critical elements.Personally, I'm a fan of this logic being moved into the header vs the payload, but that is a little off spec (per the JWT/JWS rfcs)
We could make sure this functionality is exposed in JJWT though for these types of use cases (as @lhazlewood mentioned above ContentTypeHandler
). 🤔 Potentially creating a module that wraps the open banking jjwt-openbanking
(I know know enough about the openbanking world to know if that would be useful), but either way, we should create a doc with an example. Any chance you can help with an example?
I think dynamic client registration is the only place we actually receive fully-fledged JWSs.
Here's an example of a registration request:
eyJraWQiOiJlSTFZRF96c2ZURGliSTN5aHdzbGJQNVVHT2MiLCJ0eXAiOiJKV1QiLCJhbGciOiJQUzI1NiJ9.eyJncmFudF90eXBlcyI6WyJjbGllbnRfY3JlZGVudGlhbHMiLCJhdXRob3JpemF0aW9uX2NvZGUiLCJyZWZyZXNoX3Rva2VuIl0sImFwcGxpY2F0aW9uX3R5cGUiOiJ3ZWIiLCJpc3MiOiJtMUxpUzNxTDVZM0FuTnpxT2pESDd0IiwidGxzX2NsaWVudF9hdXRoX3N1YmplY3RfZG4iOiJDTj0wMDE1ODAwMDAxNmk0NFZBQVEsMi41LjQuOTc9UFNER0ItRkNBLTczMDE2NixPPVN0YXJsaW5nIEJhbmsgTGltaXRlZCxDPUdCIiwicmVkaXJlY3RfdXJpcyI6WyJodHRwczovL3RlYXBvdHByb2R1Y3Rpb25zLnRlc3QvcmVkaXJlY3QxL2Zsb3ciXSwidG9rZW5fZW5kcG9pbnRfYXV0aF9tZXRob2QiOiJ0bHNfY2xpZW50X2F1dGgiLCJhdWQiOiIwMDE1ODAwMDAxNmk0NFZBQVEiLCJzb2Z0d2FyZV9pZCI6Im0xTGlTM3FMNVkzQW5OenFPakRIN3QiLCJzb2Z0d2FyZV9zdGF0ZW1lbnQiOiJleUpoYkdjaU9pSlFVekkxTmlJc0ltdHBaQ0k2SWtWalNFUllhV2xtVFZsWmIwRnpNRUZQWjJsdmJXOTJhMHd3YzBwa2RVRk1lRmx3VTFNM1pUbFBWVms5SWl3aWRIbHdJam9pU2xkVUluMC5leUpwYzNNaU9pSlBjR1Z1UW1GdWEybHVaeUJNZEdRaUxDSnBZWFFpT2pFM01EWXdNamM0TVRJc0ltcDBhU0k2SWpWak9ERTJOemxsWkRBM05UUmlZalVpTENKemIyWjBkMkZ5WlY5bGJuWnBjbTl1YldWdWRDSTZJbk5oYm1SaWIzZ2lMQ0p6YjJaMGQyRnlaVjl0YjJSbElqb2lWR1Z6ZENJc0luTnZablIzWVhKbFgybGtJam9pYlRGTWFWTXpjVXcxV1ROQmJrNTZjVTlxUkVnM2RDSXNJbk52Wm5SM1lYSmxYMk5zYVdWdWRGOXBaQ0k2SW0weFRHbFRNM0ZNTlZrelFXNU9lbkZQYWtSSU4zUWlMQ0p6YjJaMGQyRnlaVjlqYkdsbGJuUmZibUZ0WlNJNklrUmxiVzh2YzJGdVpHSnZlQ0JwYm5SbGNtNWhiQ0IwWlhOMGFXNW5JaXdpYzI5bWRIZGhjbVZmWTJ4cFpXNTBYMlJsYzJOeWFYQjBhVzl1SWpvaVQyNXNlU0IxYzJWa0lHbHVkR1Z5Ym1Gc2JIa3VJaXdpYzI5bWRIZGhjbVZmZG1WeWMybHZiaUk2SWpBdU1TSXNJbk52Wm5SM1lYSmxYMk5zYVdWdWRGOTFjbWtpT2lKb2RIUndjem92TDNOMFlYSnNhVzVuWW1GdWF5NWpiMjBpTENKemIyWjBkMkZ5WlY5eVpXUnBjbVZqZEY5MWNtbHpJanBiSW1oMGRIQnpPaTh2YzNSaGNteHBibWRpWVc1ckxtTnZiU0lzSW1oMGRIQnpPaTh2ZEdWaGNHOTBjSEp2WkhWamRHbHZibk11ZEdWemRDSXNJbWgwZEhCek9pOHZkR1ZoY0c5MGNISnZaSFZqZEdsdmJuTXVkR1Z6ZEM5eVpXUnBjbVZqZERFdlpteHZkeUpkTENKemIyWjBkMkZ5WlY5eWIyeGxjeUk2V3lKUVNWTlFJaXdpUVVsVFVDSmRMQ0p2Y21kaGJtbHpZWFJwYjI1ZlkyOXRjR1YwWlc1MFgyRjFkR2h2Y21sMGVWOWpiR0ZwYlhNaU9uc2lZWFYwYUc5eWFYUjVYMmxrSWpvaVJrTkJSMEpTSWl3aWNtVm5hWE4wY21GMGFXOXVYMmxrSWpvaU56TXdNVFkySWl3aWMzUmhkSFZ6SWpvaVFXTjBhWFpsSWl3aVlYVjBhRzl5YVhOaGRHbHZibk1pT2x0N0ltMWxiV0psY2w5emRHRjBaU0k2SWtkQ0lpd2ljbTlzWlhNaU9sc2lRVWxUVUNJc0lrRlRVRk5RSWl3aVVFbFRVQ0pkZlN4N0ltMWxiV0psY2w5emRHRjBaU0k2SWtsRklpd2ljbTlzWlhNaU9sc2lVRWxUVUNJc0lrRkpVMUFpTENKQlUxQlRVQ0pkZlN4N0ltMWxiV0psY2w5emRHRjBaU0k2SWs1TUlpd2ljbTlzWlhNaU9sc2lRVWxUVUNJc0lsQkpVMUFpTENKQlUxQlRVQ0pkZlYxOUxDSnpiMlowZDJGeVpWOXNiMmR2WDNWeWFTSTZJbWgwZEhCek9pOHZjM1JoY214cGJtZGlZVzVyTG1OdmJTSXNJbTl5WjE5emRHRjBkWE1pT2lKQlkzUnBkbVVpTENKdmNtZGZhV1FpT2lJd01ERTFPREF3TURBeE5tazBORlpCUVZFaUxDSnZjbWRmYm1GdFpTSTZJbE4wWVhKc2FXNW5JRUpoYm1zZ1RHbHRhWFJsWkNJc0ltOXlaMTlqYjI1MFlXTjBjeUk2VzNzaWJtRnRaU0k2SWxSbFkyaHVhV05oYkNJc0ltVnRZV2xzSWpvaVpHVjJaV3h2Y0dWeVFITjBZWEpzYVc1blltRnVheTVqYjIwaUxDSndhRzl1WlNJNklpczBOREl3SURNNE5UY2dOemN4T1NJc0luUjVjR1VpT2lKVVpXTm9ibWxqWVd3aWZTeDdJbTVoYldVaU9pSkNkWE5wYm1WemN5SXNJbVZ0WVdsc0lqb2lhR1ZzY0VCemRHRnliR2x1WjJKaGJtc3VZMjl0SWl3aWNHaHZibVVpT2lJck5EUXlNQ0F6T0RVM0lEYzNNVGtpTENKMGVYQmxJam9pUW5WemFXNWxjM01pZlYwc0ltOXlaMTlxZDJ0elgyVnVaSEJ2YVc1MElqb2lhSFIwY0hNNkx5OXJaWGx6ZEc5eVpTNXZjR1Z1WW1GdWEybHVaM1JsYzNRdWIzSm5MblZyTHpBd01UVTRNREF3TURFMmFUUTBWa0ZCVVM4d01ERTFPREF3TURBeE5tazBORlpCUVZFdWFuZHJjeUlzSW05eVoxOXFkMnR6WDNKbGRtOXJaV1JmWlc1a2NHOXBiblFpT2lKb2RIUndjem92TDJ0bGVYTjBiM0psTG05d1pXNWlZVzVyYVc1bmRHVnpkQzV2Y21jdWRXc3ZNREF4TlRnd01EQXdNVFpwTkRSV1FVRlJMM0psZG05clpXUXZNREF4TlRnd01EQXdNVFpwTkRSV1FVRlJMbXAzYTNNaUxDSnpiMlowZDJGeVpWOXFkMnR6WDJWdVpIQnZhVzUwSWpvaWFIUjBjSE02THk5clpYbHpkRzl5WlM1dmNHVnVZbUZ1YTJsdVozUmxjM1F1YjNKbkxuVnJMekF3TVRVNE1EQXdNREUyYVRRMFZrRkJVUzl0TVV4cFV6TnhURFZaTTBGdVRucHhUMnBFU0RkMExtcDNhM01pTENKemIyWjBkMkZ5WlY5cWQydHpYM0psZG05clpXUmZaVzVrY0c5cGJuUWlPaUpvZEhSd2N6b3ZMMnRsZVhOMGIzSmxMbTl3Wlc1aVlXNXJhVzVuZEdWemRDNXZjbWN1ZFdzdk1EQXhOVGd3TURBd01UWnBORFJXUVVGUkwzSmxkbTlyWldRdmJURk1hVk16Y1V3MVdUTkJiazU2Y1U5cVJFZzNkQzVxZDJ0eklpd2ljMjltZEhkaGNtVmZjRzlzYVdONVgzVnlhU0k2SW1oMGRIQnpPaTh2YzNSaGNteHBibWRpWVc1ckxtTnZiU0lzSW5OdlpuUjNZWEpsWDNSdmMxOTFjbWtpT2lKb2RIUndjem92TDNOMFlYSnNhVzVuWW1GdWF5NWpiMjBpTENKemIyWjBkMkZ5WlY5dmJsOWlaV2hoYkdaZmIyWmZiM0puSWpwdWRXeHNmUS51UDJTNzdNT1NFbFhseDdfOVE3U0h2QlpXbVVCVEsyaV90LWZoYkN0eHpsUWM0V281NXJ5RmVwQXpSNU45ejl6LU81WV9oMUZtTzAtUE5PaURJbHE3QllWYlpwb3pXTkJfLWlwSGJIcF9FZ1M0UFNmUDRsVFBHMS1SMVgyMFdoMDZMSThvVjVlYjlDSlNHWnhUYTBURFRvVE53ZVZtMF9XSkFvdVRCZWhaMFdjeXRKcUtiN0JzOVNMcjhrclhBWHpKd2VPNjBJOXRPOTlHQXlSS0ZPekFwd1pXZ1ctdGc2Q3BwZEk4M21kbHE1SUdjVmNqbU5Cb1BXUnFVUWFCVlFyZUlJaWM4VU9yYXl4WjlUckl5d0dXN0JZQkhnQmxEbzA4N3g1TFE3dzlXYklsRHpIelpKYUtOUVZwZmZxcHJCQkZkb1BYZHdXWHZoVDh2OHJXdURxLUEiLCJzY29wZSI6Im9wZW5pZCBhY2NvdW50cyBwYXltZW50cyIsInJlcXVlc3Rfb2JqZWN0X3NpZ25pbmdfYWxnIjoiUFMyNTYiLCJleHAiOjE3MDYwMjg0MTIsImlhdCI6MTcwNjAyNzgxMiwianRpIjoiOTM4YmFmZjgtYjc4NC00MTQyLTkwODItMjk3MDhhODU2OTI5IiwicmVzcG9uc2VfdHlwZXMiOlsiY29kZSBpZF90b2tlbiJdLCJpZF90b2tlbl9zaWduZWRfcmVzcG9uc2VfYWxnIjoiUFMyNTYifQ.w6r4rCWs0TycXrdWTV1IstaWdSTEmC6ge8kjeM3m_C6QDencmme5OvRQLjEZVLd6QRe50C-DH0KvsoYxnnEjZfWTdHZj9Fki6fumDCMBkbccvSjDauSus4cQS9JPIK0HP7ILbFiyeCGaSuu-hoxPUxylel4Fp23YHVHrSc5Dtm0LvSB2AgxRYZAlOPM0CXLJE_nZwRpnMc_obZrPQXGBYzhGdHMdVWh7RgC3zqmDYbOLsby9iG2HbWMpt49iMLiOT60Un5uJ2Ja9znkHCmy77dsjSLOfVjiqfd4J2dsgSLzjrDPHfCjFPzaOizktwDRpMyXKwcf9FrZTo6hZoUA5jw
The jose header is generally sent as a detached http header with the body stripped out. The payload is sent as the raw http body (and is vanilla json).
eyJhbGciOiJQUzI1NiIsImtpZCI6ImVJMVlEX3pzZlREaWJJM3lod3NsYlA1VUdPYyIsImh0dHA6Ly9vcGVuYmFua2luZy5vcmcudWsvaWF0IjoxNzA2MDI4NDY3LCJodHRwOi8vb3BlbmJhbmtpbmcub3JnLnVrL2lzcyI6IjAwMTU4MDAwMDE2aTQ0VkFBUS9tMUxpUzNxTDVZM0FuTnpxT2pESDd0IiwiaHR0cDovL29wZW5iYW5raW5nLm9yZy51ay90YW4iOiJvcGVuYmFua2luZy5vcmcudWsiLCJjcml0IjpbImh0dHA6Ly9vcGVuYmFua2luZy5vcmcudWsvaWF0IiwiaHR0cDovL29wZW5iYW5raW5nLm9yZy51ay9pc3MiLCJodHRwOi8vb3BlbmJhbmtpbmcub3JnLnVrL3RhbiJdfQ..UQI4xzvZlxUJGg_yq-QZicNjux9m4J61whEte9ZRPFaKVi-vtfphkh3fQ1olH_FLmYM5Ii-fF59cKjv7GGi1F73ArJH6D7J1GbAey3nIztl07wVh0nD-VatZCYqtpPyQ_a4Woqjrnfe4E1u0VuvDh9AtIROiwda1uz0H_M1ZlC2uPGKvjxbOqu54vADgfB1FaVsV4xSs4yfh3AEyzadC7rOjgB8aOESMj7kn_ylUyE4RTPaAS2RC_FOVauZOghByjLf4xGElIY6MSCVVP2-8xttxaumpl7MYiM4Uz447ZK3qVrtHGQKw_yt_LOaRRuyWS_RhD8Eh_FeWrTy2QZK1qw
Do you have an example with the cty
value set?
As an aside, I've just found this in the spec we're using for one of the services. The spec explicitly says clients are allowed to set
cty
.
I'm assuming you're referring to the OpenBanking spec?
If so, that spec directly conflicts with the recommendations in JWT RFC 7519 (Section 5.2) that the cty
header is only intended to be used to support nested JWTs:
In the normal case in which nested signing or encryption operations are not employed, the use of this Header Parameter is NOT RECOMMENDED. In the case that nested signing or encryption is employed, this Header Parameter MUST be present; in this case, the value MUST be "JWT", to indicate that a Nested JWT is carried in this JWT.
In fact, it is the very absence of a cty
header that indicates to JWT libraries per RFC 7519 that the payload is expected to be Claims JSON.
Otherwise, the cty
header must be handled/processed by the application (not the JWT library). Per JWS RFC 7515, Section 4.1.10:
This parameter is ignored by JWS implementations; any processing of this parameter is performed by the JWS application.
The reason for this is that there is no media type for Claims JSON (e.g. application/jose.claims+json
or similar); it is impossible for a JWT library to see a media type of json
or application/json
and 'know' that it should be parsed not only as JSON, but also interpret/validate that JSON as Claims name/value pairs per the JWT RFCs.
In other words, if a JWT payload was application/json
and that JSON represented an application data model (e.g. a JSON document that represented an invoice, purchase order, etc), a JWT library can't (and shouldn't) process that as Claims JSON.
So how does a JWT library then 'know' when to treat the payload as Claims vs any other JSON data structure? The only mechanism identified in the JWT RFC 7519 is the absence of the cty
header when the payload is not Claims or a Nested JWT.
Someone should definitely bring this up to the OpenBanking spec committee because the very presence of their cty
recommendation contradicts RFC 7519.
JJWT's primary responsibility is to implement the RFC behavior specified by the various specifications managed by the JOSE Working Group. Additional specifications beyond that (e.g. OpenID Connect, OpenBanking, etc) is not currently a design goal (but could be in the future), as anything built on top of JOSE RFCs are expected to be symbiotic, not conflict with them.
BUT! That doesn't mean we can't/won't implement enhancements that can allow this additional behavior, or try to help to find workarounds, etc. We'll do our best, I'm just stating that the JOSE WG RFCs are the primary goal and anything in conflict or contradictory to them are secondary goals.
- I think we lose the
Jws
object? i.e.Jws<byte[]>
is passed into the visitor andClaims
are returned. There's no way for us to end up withJws<Claims>
because we don't have an easy way to reconstruct theJws
.
That's true at the moment, but it's a trivial exercise to implement the Jws<Claims>
interface directly in your project and return that from a utility method so the rest of your application code doesn't need to know about these underlying details. For example:
...
Jwt<?, ?> jwt = Jwts.parser().build()/*... */.parse(jws);
Claims claims = jwt.accept(visitor);
Jws<?> jws = (Jws<?>)jwt;
// ClaimsJws in your project implements Jws<Claims>
return new ClaimsJws(jws.header(), claims, jws.getSignature(), jws.getDigest());
I think that's a reasonable (albeit not ideal) workaround until we can finalize a ContentTypeHandler
concept. I hope that helps!
The (OpenBanking) spec explicitly says clients are allowed to set
cty
.
FWIW, I contacted the secretary of the OpenBanking Working Group to indicate there is a conflict between the two specifications. We'll see if/how there might be a process to modify their specification to be congruent.
Do you have an example with the cty value set?
eyJraWQiOiJlSTFZRF96c2ZURGliSTN5aHdzbGJQNVVHT2MiLCJjdHkiOiJqc29uIiwidHlwIjoiSldUIiwiYWxnIjoiUFMyNTYifQ.eyJncmFudF90eXBlcyI6WyJjbGllbnRfY3JlZGVudGlhbHMiLCJhdXRob3JpemF0aW9uX2NvZGUiLCJyZWZyZXNoX3Rva2VuIl0sImFwcGxpY2F0aW9uX3R5cGUiOiJ3ZWIiLCJpc3MiOiJtMUxpUzNxTDVZM0FuTnpxT2pESDd0IiwidGxzX2NsaWVudF9hdXRoX3N1YmplY3RfZG4iOiJDTj0wMDE1ODAwMDAxNmk0NFZBQVEsMi41LjQuOTc9UFNER0ItRkNBLTczMDE2NixPPVN0YXJsaW5nIEJhbmsgTGltaXRlZCxDPUdCIiwicmVkaXJlY3RfdXJpcyI6WyJodHRwczovL3RlYXBvdHByb2R1Y3Rpb25zLnRlc3QvcmVkaXJlY3QxL2Zsb3ciXSwidG9rZW5fZW5kcG9pbnRfYXV0aF9tZXRob2QiOiJ0bHNfY2xpZW50X2F1dGgiLCJhdWQiOiIwMDE1ODAwMDAxNmk0NFZBQVEiLCJzb2Z0d2FyZV9pZCI6Im0xTGlTM3FMNVkzQW5OenFPakRIN3QiLCJzb2Z0d2FyZV9zdGF0ZW1lbnQiOiJleUpoYkdjaU9pSlFVekkxTmlJc0ltdHBaQ0k2SWtWalNFUllhV2xtVFZsWmIwRnpNRUZQWjJsdmJXOTJhMHd3YzBwa2RVRk1lRmx3VTFNM1pUbFBWVms5SWl3aWRIbHdJam9pU2xkVUluMC5leUpwYzNNaU9pSlBjR1Z1UW1GdWEybHVaeUJNZEdRaUxDSnBZWFFpT2pFM01EWXdPVEF3TURJc0ltcDBhU0k2SWpNME1HWmxNbVEyTW1JME1EUTBNakFpTENKemIyWjBkMkZ5WlY5bGJuWnBjbTl1YldWdWRDSTZJbk5oYm1SaWIzZ2lMQ0p6YjJaMGQyRnlaVjl0YjJSbElqb2lWR1Z6ZENJc0luTnZablIzWVhKbFgybGtJam9pYlRGTWFWTXpjVXcxV1ROQmJrNTZjVTlxUkVnM2RDSXNJbk52Wm5SM1lYSmxYMk5zYVdWdWRGOXBaQ0k2SW0weFRHbFRNM0ZNTlZrelFXNU9lbkZQYWtSSU4zUWlMQ0p6YjJaMGQyRnlaVjlqYkdsbGJuUmZibUZ0WlNJNklrUmxiVzh2YzJGdVpHSnZlQ0JwYm5SbGNtNWhiQ0IwWlhOMGFXNW5JaXdpYzI5bWRIZGhjbVZmWTJ4cFpXNTBYMlJsYzJOeWFYQjBhVzl1SWpvaVQyNXNlU0IxYzJWa0lHbHVkR1Z5Ym1Gc2JIa3VJaXdpYzI5bWRIZGhjbVZmZG1WeWMybHZiaUk2SWpBdU1TSXNJbk52Wm5SM1lYSmxYMk5zYVdWdWRGOTFjbWtpT2lKb2RIUndjem92TDNOMFlYSnNhVzVuWW1GdWF5NWpiMjBpTENKemIyWjBkMkZ5WlY5eVpXUnBjbVZqZEY5MWNtbHpJanBiSW1oMGRIQnpPaTh2YzNSaGNteHBibWRpWVc1ckxtTnZiU0lzSW1oMGRIQnpPaTh2ZEdWaGNHOTBjSEp2WkhWamRHbHZibk11ZEdWemRDSXNJbWgwZEhCek9pOHZkR1ZoY0c5MGNISnZaSFZqZEdsdmJuTXVkR1Z6ZEM5eVpXUnBjbVZqZERFdlpteHZkeUpkTENKemIyWjBkMkZ5WlY5eWIyeGxjeUk2V3lKUVNWTlFJaXdpUVVsVFVDSmRMQ0p2Y21kaGJtbHpZWFJwYjI1ZlkyOXRjR1YwWlc1MFgyRjFkR2h2Y21sMGVWOWpiR0ZwYlhNaU9uc2lZWFYwYUc5eWFYUjVYMmxrSWpvaVJrTkJSMEpTSWl3aWNtVm5hWE4wY21GMGFXOXVYMmxrSWpvaU56TXdNVFkySWl3aWMzUmhkSFZ6SWpvaVFXTjBhWFpsSWl3aVlYVjBhRzl5YVhOaGRHbHZibk1pT2x0N0ltMWxiV0psY2w5emRHRjBaU0k2SWtkQ0lpd2ljbTlzWlhNaU9sc2lRVWxUVUNJc0lrRlRVRk5RSWl3aVVFbFRVQ0pkZlN4N0ltMWxiV0psY2w5emRHRjBaU0k2SWtsRklpd2ljbTlzWlhNaU9sc2lVRWxUVUNJc0lrRkpVMUFpTENKQlUxQlRVQ0pkZlN4N0ltMWxiV0psY2w5emRHRjBaU0k2SWs1TUlpd2ljbTlzWlhNaU9sc2lRVWxUVUNJc0lsQkpVMUFpTENKQlUxQlRVQ0pkZlYxOUxDSnpiMlowZDJGeVpWOXNiMmR2WDNWeWFTSTZJbWgwZEhCek9pOHZjM1JoY214cGJtZGlZVzVyTG1OdmJTSXNJbTl5WjE5emRHRjBkWE1pT2lKQlkzUnBkbVVpTENKdmNtZGZhV1FpT2lJd01ERTFPREF3TURBeE5tazBORlpCUVZFaUxDSnZjbWRmYm1GdFpTSTZJbE4wWVhKc2FXNW5JRUpoYm1zZ1RHbHRhWFJsWkNJc0ltOXlaMTlqYjI1MFlXTjBjeUk2VzNzaWJtRnRaU0k2SWxSbFkyaHVhV05oYkNJc0ltVnRZV2xzSWpvaVpHVjJaV3h2Y0dWeVFITjBZWEpzYVc1blltRnVheTVqYjIwaUxDSndhRzl1WlNJNklpczBOREl3SURNNE5UY2dOemN4T1NJc0luUjVjR1VpT2lKVVpXTm9ibWxqWVd3aWZTeDdJbTVoYldVaU9pSkNkWE5wYm1WemN5SXNJbVZ0WVdsc0lqb2lhR1ZzY0VCemRHRnliR2x1WjJKaGJtc3VZMjl0SWl3aWNHaHZibVVpT2lJck5EUXlNQ0F6T0RVM0lEYzNNVGtpTENKMGVYQmxJam9pUW5WemFXNWxjM01pZlYwc0ltOXlaMTlxZDJ0elgyVnVaSEJ2YVc1MElqb2lhSFIwY0hNNkx5OXJaWGx6ZEc5eVpTNXZjR1Z1WW1GdWEybHVaM1JsYzNRdWIzSm5MblZyTHpBd01UVTRNREF3TURFMmFUUTBWa0ZCVVM4d01ERTFPREF3TURBeE5tazBORlpCUVZFdWFuZHJjeUlzSW05eVoxOXFkMnR6WDNKbGRtOXJaV1JmWlc1a2NHOXBiblFpT2lKb2RIUndjem92TDJ0bGVYTjBiM0psTG05d1pXNWlZVzVyYVc1bmRHVnpkQzV2Y21jdWRXc3ZNREF4TlRnd01EQXdNVFpwTkRSV1FVRlJMM0psZG05clpXUXZNREF4TlRnd01EQXdNVFpwTkRSV1FVRlJMbXAzYTNNaUxDSnpiMlowZDJGeVpWOXFkMnR6WDJWdVpIQnZhVzUwSWpvaWFIUjBjSE02THk5clpYbHpkRzl5WlM1dmNHVnVZbUZ1YTJsdVozUmxjM1F1YjNKbkxuVnJMekF3TVRVNE1EQXdNREUyYVRRMFZrRkJVUzl0TVV4cFV6TnhURFZaTTBGdVRucHhUMnBFU0RkMExtcDNhM01pTENKemIyWjBkMkZ5WlY5cWQydHpYM0psZG05clpXUmZaVzVrY0c5cGJuUWlPaUpvZEhSd2N6b3ZMMnRsZVhOMGIzSmxMbTl3Wlc1aVlXNXJhVzVuZEdWemRDNXZjbWN1ZFdzdk1EQXhOVGd3TURBd01UWnBORFJXUVVGUkwzSmxkbTlyWldRdmJURk1hVk16Y1V3MVdUTkJiazU2Y1U5cVJFZzNkQzVxZDJ0eklpd2ljMjltZEhkaGNtVmZjRzlzYVdONVgzVnlhU0k2SW1oMGRIQnpPaTh2YzNSaGNteHBibWRpWVc1ckxtTnZiU0lzSW5OdlpuUjNZWEpsWDNSdmMxOTFjbWtpT2lKb2RIUndjem92TDNOMFlYSnNhVzVuWW1GdWF5NWpiMjBpTENKemIyWjBkMkZ5WlY5dmJsOWlaV2hoYkdaZmIyWmZiM0puSWpwdWRXeHNmUS5LYlF1am1oU0xsQ1dYUWNZMGU5c29UY2NDNHBqRDYwVk01UWd6UVBmYmJFMU5WV3hVU084am9sekYtc1Z1OU0xZ0pLcm14T3hPMVFqMTBjX1V4UWpNenJnQVQ1OHVFNEZBRXUzVlZnY0hxcXVzVTY0RzZUOGxPQXdmS1JndENpSDJVVnYxY3pwaTJYTE52MWxjSmt6akRUZXUwWkhGcndDTzMyVFdiUTZub2Qycl95aEp2cUh4a0VBRTM3dTlPUjBOVl96Tk43d20xVmZpeWh0UzJmcDNDZ3QtVjRYemZSajk3SENjY0ItUWVvektFeVp2S3pxV2xuTC00U3JvVzdIOHFfVS1UYTZrRXZ1TWdRczJrelI4eWc2Y2NXakw1emgxT2JzTm1OWTVpcnB0d3I4R2xpbGZLUlh6dVpkYmluM003M09YRVJoYi1BMzV1YmMyWjNxaVEiLCJzY29wZSI6Im9wZW5pZCBhY2NvdW50cyBwYXltZW50cyIsInJlcXVlc3Rfb2JqZWN0X3NpZ25pbmdfYWxnIjoiUFMyNTYiLCJleHAiOjE3MDYwOTA2MDIsImlhdCI6MTcwNjA5MDAwMiwianRpIjoiMGIyZDA2OTEtZDQ1MC00NzAwLWJlNGYtYzFiYTgzZTMwMGVkIiwicmVzcG9uc2VfdHlwZXMiOlsiY29kZSBpZF90b2tlbiJdLCJpZF90b2tlbl9zaWduZWRfcmVzcG9uc2VfYWxnIjoiUFMyNTYifQ.MODepc6rfSy174yAmrmd_eOwvPE0Hp4iSYNxB6bSPQIl9odpybmUFxpX5stfgO6ERqIMyxhv3jaafzmb4Oq_r0kG587GforAmak28nMYAt6xLYh1LTrGAWVkS3AMe75cofZ06Tr2BP2wsK70VCqGN2qLuFKJqcBamOGahPR3SjSHGvlPyJE7KQNm7FHXkYsHI7OlFiPHWNv0GDrS_bCGYNCVacYTJsjLT8_bcPYtMuLHBlEwMpLrz7ONFMLXdR4C7swsbHoBCCaN9tB8PTuHEP9UJ6ijybG66IpgjW8kGw-LyIK_lUwyvvIdtDSVBdEyoOhvGoypYTpEQjHBiTBJ4Q
I should clarify that the open banking DCR spec does not reference cty
at all, which I think is what you'd expect. It's the Confirmation of Payee (CoP) DCR spec that I shared above which references cty. CoP is a different service to open banking but they currently share the same ecosystem for onboarding. I don't think OBIE are responsible for the CoP specs though.
Describe the bug In 0.11.5 and below, the
Claims
object can be extracted from JWS regardless of whether thecty
field is set on header or not. From 0.12.0 onwards, if thecty
header is set, an exception is thrown when attempting to extract theClaims
object, even when the content type is json. The behaviour appears to have changed in this PR. The change is clearly driven by the RFC but I don't believe the jjwt library should automatically throw an exception in this scenario. We have no control over whether the client has included the cty field or not, but if they specify json, then I think we should continue to parse the claims as if the field weren't specified at all.To Reproduce Any attempt to invoke
DefaultJwtParser#parseSignedClaims()
when the supplied JWS has thecty
field set results inio.jsonwebtoken.UnsupportedJwtException: Unexpected content JWS.
. This occurs even if the specified content type is json.Expected behavior The claims are parsed successfully and returned from the method.
Screenshots Old, expected behaviour (0.11.5)
New, unexpected behaviour (0.12.3)