Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unable to extract claims when cty specified in JWS header (>0.12.0) #897

Open
icecreamhead opened this issue Jan 18, 2024 · 19 comments
Open
Milestone

Comments

@icecreamhead
Copy link

Describe the bug
In 0.11.5 and below, the Claims object can be extracted from JWS regardless of whether the cty field is set on header or not.
From 0.12.0 onwards, if the cty header is set, an exception is thrown when attempting to extract the Claims 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 the cty field set results in io.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)

image

New, unexpected behaviour (0.12.3)

image

@lhazlewood
Copy link
Contributor

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:

  1. parse(compact) which returns a Jwt<?,?> instance, and then

  2. immediately 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

    1. a Jws instance (not just a Jwt or Jwe), and
    2. that the payload is a Claims instance (and not any random payload).

    and if not, throws the exception you see.

So, in JJWT's current form, you have two choices:

  1. 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.

  2. 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".

@lhazlewood
Copy link
Contributor

@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.

@lhazlewood
Copy link
Contributor

lhazlewood commented Jan 18, 2024

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.

@icecreamhead
Copy link
Author

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.

@icecreamhead
Copy link
Author

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.

@lhazlewood
Copy link
Contributor

lhazlewood commented Jan 20, 2024

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.

@icecreamhead
Copy link
Author

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?

@lhazlewood
Copy link
Contributor

Out of curiosity, what is the media type you see being used for most of these?

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.

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:

  1. A handler would need things given to it, for example, the Header, maybe the configured Deserializer<Map<String<?>> and I'm not sure if anything else.
  2. The other thing we might want to work on in the same PR is the parsing 'hint' concept. The parse/visitor thing is fine, but I'm not sure if this can be made better. At the least, perhaps improve the exception messages thrown by the visitors to more clearly indicated what was received vs what was expected.
  3. The big one: right now, a JJWT payload that is returned to the application is only one of two things: a 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.

@lhazlewood
Copy link
Contributor

@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!

@icecreamhead
Copy link
Author

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.

@icecreamhead
Copy link
Author

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.
image

@bdemers
Copy link
Member

bdemers commented Jan 23, 2024

@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:

  • The verifier must validate that the typ header if specified has the value JOSE.
  • The verifier must validate that the cty header to ensure that the payload is of the expected mime type.
  • The verifier must ensure that the specified alg is one of the algorithms specified by OBIE.
  • The verifier must ensure that the specified kid is valid and a public key with the specified key Id can be retrieved from the Trust Anchor.
  • The verifier must ensure that the b64 claim is set to false.
  • The verifier must ensure that the http://openbanking.org.uk/iat claim has a date-time value set in the past.
  • The verifier must ensure that PSP bound to the http://openbanking.org.uk/iss claim matches the expected PSP.
  • The verifier must ensure that http://openbanking.org.uk/tan claim contains the DNS name of a Trust Anchor that it trusts.
  • The verifier must ensure that the 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?

@icecreamhead
Copy link
Author

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

@bdemers
Copy link
Member

bdemers commented Jan 23, 2024

Do you have an example with the cty value set?

@lhazlewood
Copy link
Contributor

lhazlewood commented Jan 23, 2024

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.

@lhazlewood
Copy link
Contributor

  1. 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.

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!

@lhazlewood
Copy link
Contributor

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.

@icecreamhead
Copy link
Author

Do you have an example with the cty value set?

eyJraWQiOiJlSTFZRF96c2ZURGliSTN5aHdzbGJQNVVHT2MiLCJjdHkiOiJqc29uIiwidHlwIjoiSldUIiwiYWxnIjoiUFMyNTYifQ.eyJncmFudF90eXBlcyI6WyJjbGllbnRfY3JlZGVudGlhbHMiLCJhdXRob3JpemF0aW9uX2NvZGUiLCJyZWZyZXNoX3Rva2VuIl0sImFwcGxpY2F0aW9uX3R5cGUiOiJ3ZWIiLCJpc3MiOiJtMUxpUzNxTDVZM0FuTnpxT2pESDd0IiwidGxzX2NsaWVudF9hdXRoX3N1YmplY3RfZG4iOiJDTj0wMDE1ODAwMDAxNmk0NFZBQVEsMi41LjQuOTc9UFNER0ItRkNBLTczMDE2NixPPVN0YXJsaW5nIEJhbmsgTGltaXRlZCxDPUdCIiwicmVkaXJlY3RfdXJpcyI6WyJodHRwczovL3RlYXBvdHByb2R1Y3Rpb25zLnRlc3QvcmVkaXJlY3QxL2Zsb3ciXSwidG9rZW5fZW5kcG9pbnRfYXV0aF9tZXRob2QiOiJ0bHNfY2xpZW50X2F1dGgiLCJhdWQiOiIwMDE1ODAwMDAxNmk0NFZBQVEiLCJzb2Z0d2FyZV9pZCI6Im0xTGlTM3FMNVkzQW5OenFPakRIN3QiLCJzb2Z0d2FyZV9zdGF0ZW1lbnQiOiJleUpoYkdjaU9pSlFVekkxTmlJc0ltdHBaQ0k2SWtWalNFUllhV2xtVFZsWmIwRnpNRUZQWjJsdmJXOTJhMHd3YzBwa2RVRk1lRmx3VTFNM1pUbFBWVms5SWl3aWRIbHdJam9pU2xkVUluMC5leUpwYzNNaU9pSlBjR1Z1UW1GdWEybHVaeUJNZEdRaUxDSnBZWFFpT2pFM01EWXdPVEF3TURJc0ltcDBhU0k2SWpNME1HWmxNbVEyTW1JME1EUTBNakFpTENKemIyWjBkMkZ5WlY5bGJuWnBjbTl1YldWdWRDSTZJbk5oYm1SaWIzZ2lMQ0p6YjJaMGQyRnlaVjl0YjJSbElqb2lWR1Z6ZENJc0luTnZablIzWVhKbFgybGtJam9pYlRGTWFWTXpjVXcxV1ROQmJrNTZjVTlxUkVnM2RDSXNJbk52Wm5SM1lYSmxYMk5zYVdWdWRGOXBaQ0k2SW0weFRHbFRNM0ZNTlZrelFXNU9lbkZQYWtSSU4zUWlMQ0p6YjJaMGQyRnlaVjlqYkdsbGJuUmZibUZ0WlNJNklrUmxiVzh2YzJGdVpHSnZlQ0JwYm5SbGNtNWhiQ0IwWlhOMGFXNW5JaXdpYzI5bWRIZGhjbVZmWTJ4cFpXNTBYMlJsYzJOeWFYQjBhVzl1SWpvaVQyNXNlU0IxYzJWa0lHbHVkR1Z5Ym1Gc2JIa3VJaXdpYzI5bWRIZGhjbVZmZG1WeWMybHZiaUk2SWpBdU1TSXNJbk52Wm5SM1lYSmxYMk5zYVdWdWRGOTFjbWtpT2lKb2RIUndjem92TDNOMFlYSnNhVzVuWW1GdWF5NWpiMjBpTENKemIyWjBkMkZ5WlY5eVpXUnBjbVZqZEY5MWNtbHpJanBiSW1oMGRIQnpPaTh2YzNSaGNteHBibWRpWVc1ckxtTnZiU0lzSW1oMGRIQnpPaTh2ZEdWaGNHOTBjSEp2WkhWamRHbHZibk11ZEdWemRDSXNJbWgwZEhCek9pOHZkR1ZoY0c5MGNISnZaSFZqZEdsdmJuTXVkR1Z6ZEM5eVpXUnBjbVZqZERFdlpteHZkeUpkTENKemIyWjBkMkZ5WlY5eWIyeGxjeUk2V3lKUVNWTlFJaXdpUVVsVFVDSmRMQ0p2Y21kaGJtbHpZWFJwYjI1ZlkyOXRjR1YwWlc1MFgyRjFkR2h2Y21sMGVWOWpiR0ZwYlhNaU9uc2lZWFYwYUc5eWFYUjVYMmxrSWpvaVJrTkJSMEpTSWl3aWNtVm5hWE4wY21GMGFXOXVYMmxrSWpvaU56TXdNVFkySWl3aWMzUmhkSFZ6SWpvaVFXTjBhWFpsSWl3aVlYVjBhRzl5YVhOaGRHbHZibk1pT2x0N0ltMWxiV0psY2w5emRHRjBaU0k2SWtkQ0lpd2ljbTlzWlhNaU9sc2lRVWxUVUNJc0lrRlRVRk5RSWl3aVVFbFRVQ0pkZlN4N0ltMWxiV0psY2w5emRHRjBaU0k2SWtsRklpd2ljbTlzWlhNaU9sc2lVRWxUVUNJc0lrRkpVMUFpTENKQlUxQlRVQ0pkZlN4N0ltMWxiV0psY2w5emRHRjBaU0k2SWs1TUlpd2ljbTlzWlhNaU9sc2lRVWxUVUNJc0lsQkpVMUFpTENKQlUxQlRVQ0pkZlYxOUxDSnpiMlowZDJGeVpWOXNiMmR2WDNWeWFTSTZJbWgwZEhCek9pOHZjM1JoY214cGJtZGlZVzVyTG1OdmJTSXNJbTl5WjE5emRHRjBkWE1pT2lKQlkzUnBkbVVpTENKdmNtZGZhV1FpT2lJd01ERTFPREF3TURBeE5tazBORlpCUVZFaUxDSnZjbWRmYm1GdFpTSTZJbE4wWVhKc2FXNW5JRUpoYm1zZ1RHbHRhWFJsWkNJc0ltOXlaMTlqYjI1MFlXTjBjeUk2VzNzaWJtRnRaU0k2SWxSbFkyaHVhV05oYkNJc0ltVnRZV2xzSWpvaVpHVjJaV3h2Y0dWeVFITjBZWEpzYVc1blltRnVheTVqYjIwaUxDSndhRzl1WlNJNklpczBOREl3SURNNE5UY2dOemN4T1NJc0luUjVjR1VpT2lKVVpXTm9ibWxqWVd3aWZTeDdJbTVoYldVaU9pSkNkWE5wYm1WemN5SXNJbVZ0WVdsc0lqb2lhR1ZzY0VCemRHRnliR2x1WjJKaGJtc3VZMjl0SWl3aWNHaHZibVVpT2lJck5EUXlNQ0F6T0RVM0lEYzNNVGtpTENKMGVYQmxJam9pUW5WemFXNWxjM01pZlYwc0ltOXlaMTlxZDJ0elgyVnVaSEJ2YVc1MElqb2lhSFIwY0hNNkx5OXJaWGx6ZEc5eVpTNXZjR1Z1WW1GdWEybHVaM1JsYzNRdWIzSm5MblZyTHpBd01UVTRNREF3TURFMmFUUTBWa0ZCVVM4d01ERTFPREF3TURBeE5tazBORlpCUVZFdWFuZHJjeUlzSW05eVoxOXFkMnR6WDNKbGRtOXJaV1JmWlc1a2NHOXBiblFpT2lKb2RIUndjem92TDJ0bGVYTjBiM0psTG05d1pXNWlZVzVyYVc1bmRHVnpkQzV2Y21jdWRXc3ZNREF4TlRnd01EQXdNVFpwTkRSV1FVRlJMM0psZG05clpXUXZNREF4TlRnd01EQXdNVFpwTkRSV1FVRlJMbXAzYTNNaUxDSnpiMlowZDJGeVpWOXFkMnR6WDJWdVpIQnZhVzUwSWpvaWFIUjBjSE02THk5clpYbHpkRzl5WlM1dmNHVnVZbUZ1YTJsdVozUmxjM1F1YjNKbkxuVnJMekF3TVRVNE1EQXdNREUyYVRRMFZrRkJVUzl0TVV4cFV6TnhURFZaTTBGdVRucHhUMnBFU0RkMExtcDNhM01pTENKemIyWjBkMkZ5WlY5cWQydHpYM0psZG05clpXUmZaVzVrY0c5cGJuUWlPaUpvZEhSd2N6b3ZMMnRsZVhOMGIzSmxMbTl3Wlc1aVlXNXJhVzVuZEdWemRDNXZjbWN1ZFdzdk1EQXhOVGd3TURBd01UWnBORFJXUVVGUkwzSmxkbTlyWldRdmJURk1hVk16Y1V3MVdUTkJiazU2Y1U5cVJFZzNkQzVxZDJ0eklpd2ljMjltZEhkaGNtVmZjRzlzYVdONVgzVnlhU0k2SW1oMGRIQnpPaTh2YzNSaGNteHBibWRpWVc1ckxtTnZiU0lzSW5OdlpuUjNZWEpsWDNSdmMxOTFjbWtpT2lKb2RIUndjem92TDNOMFlYSnNhVzVuWW1GdWF5NWpiMjBpTENKemIyWjBkMkZ5WlY5dmJsOWlaV2hoYkdaZmIyWmZiM0puSWpwdWRXeHNmUS5LYlF1am1oU0xsQ1dYUWNZMGU5c29UY2NDNHBqRDYwVk01UWd6UVBmYmJFMU5WV3hVU084am9sekYtc1Z1OU0xZ0pLcm14T3hPMVFqMTBjX1V4UWpNenJnQVQ1OHVFNEZBRXUzVlZnY0hxcXVzVTY0RzZUOGxPQXdmS1JndENpSDJVVnYxY3pwaTJYTE52MWxjSmt6akRUZXUwWkhGcndDTzMyVFdiUTZub2Qycl95aEp2cUh4a0VBRTM3dTlPUjBOVl96Tk43d20xVmZpeWh0UzJmcDNDZ3QtVjRYemZSajk3SENjY0ItUWVvektFeVp2S3pxV2xuTC00U3JvVzdIOHFfVS1UYTZrRXZ1TWdRczJrelI4eWc2Y2NXakw1emgxT2JzTm1OWTVpcnB0d3I4R2xpbGZLUlh6dVpkYmluM003M09YRVJoYi1BMzV1YmMyWjNxaVEiLCJzY29wZSI6Im9wZW5pZCBhY2NvdW50cyBwYXltZW50cyIsInJlcXVlc3Rfb2JqZWN0X3NpZ25pbmdfYWxnIjoiUFMyNTYiLCJleHAiOjE3MDYwOTA2MDIsImlhdCI6MTcwNjA5MDAwMiwianRpIjoiMGIyZDA2OTEtZDQ1MC00NzAwLWJlNGYtYzFiYTgzZTMwMGVkIiwicmVzcG9uc2VfdHlwZXMiOlsiY29kZSBpZF90b2tlbiJdLCJpZF90b2tlbl9zaWduZWRfcmVzcG9uc2VfYWxnIjoiUFMyNTYifQ.MODepc6rfSy174yAmrmd_eOwvPE0Hp4iSYNxB6bSPQIl9odpybmUFxpX5stfgO6ERqIMyxhv3jaafzmb4Oq_r0kG587GforAmak28nMYAt6xLYh1LTrGAWVkS3AMe75cofZ06Tr2BP2wsK70VCqGN2qLuFKJqcBamOGahPR3SjSHGvlPyJE7KQNm7FHXkYsHI7OlFiPHWNv0GDrS_bCGYNCVacYTJsjLT8_bcPYtMuLHBlEwMpLrz7ONFMLXdR4C7swsbHoBCCaN9tB8PTuHEP9UJ6ijybG66IpgjW8kGw-LyIK_lUwyvvIdtDSVBdEyoOhvGoypYTpEQjHBiTBJ4Q

@icecreamhead
Copy link
Author

icecreamhead commented Jan 24, 2024

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.

@lhazlewood lhazlewood added this to the 0.13.0 milestone Jan 28, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants