Skip to content

Commit

Permalink
Add RFC 9068 Support
Browse files Browse the repository at this point in the history
Closes gh-13185
  • Loading branch information
jzheaux committed Feb 27, 2025
1 parent 81e2fd2 commit ab43a66
Show file tree
Hide file tree
Showing 10 changed files with 751 additions and 13 deletions.
172 changes: 172 additions & 0 deletions docs/modules/ROOT/pages/migration/oauth2.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
= OAuth 2.0 Changes

== Validate `typ` Header with `JwtTypeValidator`

`NimbusJwtDecoder` in Spring Security 7 will move `typ` header validation to `JwtTypeValidator` intsead of relying on Nimbus.
This brings it in line with `NimbusJwtDecoder` validating claims instead of relying on Nimbus to validate them.

If you are changing Nimbus's default type validation in a `jwtProcessorCustomizer` method, then you should move that to `JwtTypeValidator` or an implementation of `OAuth2TokenValidator` of your own.

To check if you are prepared for this change, add the default `JwtTypeValidator` to your list of validators, as this will be included by default in 7:

[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(location)
.validateTypes(false) <1>
// ... your remaining configuration
.build();
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithValidators(
new JwtIssuerValidator(location), JwtTypeValidator.jwt())); <2>
return jwtDecoder;
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun jwtDecoder(): JwtDecoder {
val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(location)
.validateTypes(false) <1>
// ... your remaining configuration
.build()
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithValidators(
JwtIssuerValidator(location), JwtTypeValidator.jwt())) <2>
return jwtDecoder
}
----
======
<1> - Switch off Nimbus verifying the `typ` (this will be off by default in 7)
<2> - Add the default `typ` validator (this will be included by default in 7)

Note the default value verifies that the `typ` value either be `JWT` or not present, which is the same as the Nimbus default.
It is also aligned with https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.9[RFC 7515] which states that `typ` is optional.


=== I'm Using A `DefaultJOSEObjectTypeVerifier`

If you have something like the following in your configuration:

[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(location)
.jwtProcessorCustomizer((c) -> c
.setJWSTypeVerifier(new DefaultJOSEObjectTypeVerifier<>("JOSE"))
)
.build();
return jwtDecoder;
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun jwtDecoder(): JwtDecoder {
val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(location)
.jwtProcessorCustomizer {
it.setJWSTypeVerifier(DefaultJOSEObjectTypeVerifier("JOSE"))
}
.build()
return jwtDecoder
}
----
======

Then change this to:

[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(location)
.validateTypes(false)
.build();
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithValidators(
new JwtIssuerValidator(location), new JwtTypeValidator("JOSE")));
return jwtDecoder;
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun jwtDecoder(): JwtDecoder {
val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(location)
.validateTypes(false)
.build()
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithValidators(
JwtIssuerValidator(location), JwtTypeValidator("JOSE")))
return jwtDecoder
}
----
======

To indicate that the `typ` header is optional, use `#setAllowEmpty(true)` (this is the equivalent of including `null` in the list of allowed types in `DefaultJOSEObjectTypeVerifier`).

=== I want to opt-out

If you want to keep doing things the way that you are, then the steps are similar, just in reverse:

[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(location)
.validateTypes(true) <1>
.jwtProcessorCustomizer((c) -> c
.setJWSTypeVerifier(new DefaultJOSEObjectTypeVerifier<>("JOSE"))
)
.build();
jwtDecoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(
new JwtTimestampValidator(), new JwtIssuerValidator(location))); <2>
return jwtDecoder;
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun jwtDecoder(): JwtDecoder {
val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(location)
.validateTypes(true) <1>
.jwtProcessorCustomizer {
it.setJWSTypeVerifier(DefaultJOSEObjectTypeVerifier("JOSE"))
}
.build()
jwtDecoder.setJwtValidator(DelegatingOAuth2TokenValidator(
JwtTimestampValidator(), JwtIssuerValidator(location))) <2>
return jwtDecoder
}
----
======
<1> - leave Nimbus type verification on
<2> - specify the list of validators you need, excluding `JwtTypeValidator`

For additional guidance, please see the xref:servlet/oauth2/resource-server/jwt.adoc#oauth2resourceserver-jwt-validation[JwtDecoder Validators] section in the reference.
40 changes: 40 additions & 0 deletions docs/modules/ROOT/pages/reactive/oauth2/resource-server/jwt.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -936,6 +936,46 @@ fun jwtDecoder(): ReactiveJwtDecoder {
By default, Resource Server configures a clock skew of 60 seconds.
====

[[webflux-oauth2resourceserver-validation-rfc9068]]
=== Configuring RFC 9068 Validation

If you need to require tokens that meet https://datatracker.ietf.org/doc/rfc9068/[RFC 9068], you can configure validation in the following way:

[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
JwtDecoder jwtDecoder() {
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuerUri)
.validateTypes(false).build();
jwtDecoder.setJwtValidator(JwtValidators.createAtJwtValidator()
.audience("https://audience.example.org")
.clientId("client-identifier")
.issuer("https://issuer.example.org").build());
return jwtDecoder;
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun jwtDecoder(): JwtDecoder {
val jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuerUri)
.validateTypes(false).build()
jwtDecoder.setJwtValidator(JwtValidators.createAtJwtValidator()
.audience("https://audience.example.org")
.clientId("client-identifier")
.issuer("https://issuer.example.org").build())
return jwtDecoder
}
----
======

[[webflux-oauth2resourceserver-validation-custom]]
==== Configuring a Custom Validator

Expand Down
40 changes: 40 additions & 0 deletions docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1213,6 +1213,46 @@ fun jwtDecoder(): JwtDecoder {
[NOTE]
By default, Resource Server configures a clock skew of 60 seconds.

[[oauth2resourceserver-jwt-validation-rfc9068]]
=== Configuring RFC 9068 Validation

If you need to require tokens that meet https://datatracker.ietf.org/doc/rfc9068/[RFC 9068], you can configure validation in the following way:

[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuerUri)
.validateTypes(false).build();
jwtDecoder.setJwtValidator(JwtValidators.createAtJwtValidator()
.audience("https://audience.example.org")
.clientId("client-identifier")
.issuer("https://issuer.example.org").build());
return jwtDecoder;
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun jwtDecoder(): JwtDecoder {
val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuerUri)
.validateTypes(false).build()
jwtDecoder.setJwtValidator(JwtValidators.createAtJwtValidator()
.audience("https://audience.example.org")
.clientId("client-identifier")
.issuer("https://issuer.example.org").build())
return jwtDecoder
}
----
======

[[oauth2resourceserver-jwt-validation-custom]]
=== Configuring a Custom Validator

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
*/
public final class JwtTypeValidator implements OAuth2TokenValidator<Jwt> {

private Collection<String> validTypes;
private final Collection<String> validTypes;

private boolean allowEmpty;

Expand All @@ -45,6 +45,10 @@ public JwtTypeValidator(Collection<String> validTypes) {
this.validTypes = new ArrayList<>(validTypes);
}

public JwtTypeValidator(String... validTypes) {
this(List.of(validTypes));
}

/**
* Require that the {@code typ} header be {@code JWT} or absent
*/
Expand Down
Loading

0 comments on commit ab43a66

Please sign in to comment.