Support for JWT Header TYP as "at+jwt"
Expected Behavior
Currently, if the JWT is having typ as "at+jwt", the token is rejected with message "Failed to authenticate since the JWT was invalid". Spring Security Oauth2 Resource Server with JWT as bearer token should accept typ as "at+jwt" as well.
Current Behavior
Currently, if the JWT is having typ as "at+jwt", the token is rejected with message "Failed to authenticate since the JWT was invalid".
My Authorization server is issuing JWT access token with typ as "at+jwt" as per the following draft: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-access-token-jwt-11
How has this issue affected you? JWT token is rejected although this is correct as per Authorization server What are you trying to accomplish? Validate JWT Bearer token using Spring Security OAuth2 Resource Server capabilities. What other alternatives have you considered? At this point, in the documentation I didn;t find a way where I can customize the JWT Validator. Although, documentation is around our own Audience or other validations once the JWT is accepted by Spring Security. Are you aware of any workarounds? Not at this point of time.
@jzheaux can you please have a look if this is bug or this needs enhancement in library?
Thanks in advance.
The reason for this behavior is Nimbus defaults to checking that the typ is JWT. I am not certain whether Nimbus yet supports the at+jwt draft - Spring Security would likely rely on Nimbus for this kind of support.
To get your resource server to accept "typ" : "at+jwt", you can configure Nimbus like so:
@Bean
JwtDecoder jwtDecoder() {
DefaultJOSEObjectTypeVerifier<SecurityContext> verifier =
new DefaultJOSEObjectTypeVerifier<>(new JOSEObjectType("at+jwt"));
NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri(this.uri)
.jwtProcessorCustomizer((processor) -> processor.setJWSTypeVerifier(verifier))
.build()
// ... any other decoder settings
return decoder;
}
For passivity reasons, I imagine that Spring Security will continue to use the Nimbus defaults. Once the draft gets finalized, there may be an opportunity to make this configuration simpler.
@jzheaux The draft has now been published as RFC 9068 (and I am running into this problem, thanks for the configuration hint, however :-))
In light of a separate request from @ymajoros, I think it would be good to revisit this decision, now that the spec is finalized.
Allowing the at+jwt type is not sufficient to support RFC 9068. If an application allows at+jwt, it should also validate those tokens as per the at+jwt spec as well.
I think this would fit nicely into JwtValidators#createDefault and JwtValidators#createDefaultForIssuer:
@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation("http://issuer").build();
jwtDecoder.setJwtValidator(JwtValidators.createDefaultForIssuer("http://issuer"));
return jwtDecoder;
}
where these factory methods change to check the typ field first before deciding on what validation to perform. If it is at+jwt, then it performs the at+jwt validations; otherwise, it does what it has always done.
In this case, NimbusJwtDecoder sets JWSTypeVerifier such that at+jwt, jwt, and null are allowed by Nimbus.
This changes how at+jwt is processed by Spring Security; however, I would consider it a bug if an application is accepting at+jwt tokens today without also validating them.
Thanks @jzheaux . What about this pull request, which solves the problem IMO? https://github.com/spring-projects/spring-security/pull/13186
@ymajoros, thank you very much for the PR. I don't think that it should be in JwtDecoders, but NimbusJwtDecoder. Also, it is missing the needed validation. Please see my comment about what needs to be done to add this support.
After spending some more time on it, I'm not convinced we can do more validation for now, according to RFC 9068 (at+jwt) and RFC 7519 (jwt):
- if we strictly follow RFC 9068, we should only accept application/at+jwt or at+jwt (the latter being recommended)
- besides the
typcheck already checked in this pull request, we should check:iss, which we already do- signature, which we already do
- exp, which we already do
aud, which we don't, but I suggest to keep this as a stricter option for later, as we should also check it for plain jwt anyway, according to RFC 7519
So, I think the current implementation is enough. I'd just add the optional application/ prefix.
@ymajoros If it isn't already, I think it should be possible to configure the expected audience values. At least it wouldn't allow tokens leaked by one service (i.e. in a microservice architecture) to be successfully used against another service.
@shartte true, but that seems out of scope for this issue and a different feature that should also be implemented, isn't it?
@jzheaux can you look at it again in light of my previous comments?
Is there any update on "at+jwt" support ?
Is there any recommended workaround?
Any updates on this one @jzheaux?
We've used the following workaround in our application:
@Bean
JwkSetUriJwtDecoderBuilderCustomizer jwkSetUriJwtDecoderBuilderCustomizer() {
final var verifier = new DefaultJOSEObjectTypeVerifier<>(new JOSEObjectType("at+jwt"));
return builder -> builder.jwtProcessorCustomizer((processor) -> processor.setJWSTypeVerifier(verifier));
}
This approach works well when using either resourceserver.jwt.issuer-uri or resourceserver.jwt.jwk-set-uri, both of which rely on the jwkSetUriJwtDecoderBuilder. However, when using resourceserver.jwt.public-key-location, we don't have a way to customize the JWT processor without creating a completely new decoder. This is problematic as it leads to losing access to features like the audience validator, which would otherwise be provided.
Since there is a PR, I'll close this ticket in favor of that and we can continue the conversation over there: #13186