Skip to content

Commit ab43a66

Browse files
committed
Add RFC 9068 Support
Closes gh-13185
1 parent 81e2fd2 commit ab43a66

File tree

10 files changed

+751
-13
lines changed

10 files changed

+751
-13
lines changed
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
= OAuth 2.0 Changes
2+
3+
== Validate `typ` Header with `JwtTypeValidator`
4+
5+
`NimbusJwtDecoder` in Spring Security 7 will move `typ` header validation to `JwtTypeValidator` intsead of relying on Nimbus.
6+
This brings it in line with `NimbusJwtDecoder` validating claims instead of relying on Nimbus to validate them.
7+
8+
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.
9+
10+
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:
11+
12+
[tabs]
13+
======
14+
Java::
15+
+
16+
[source,java,role="primary"]
17+
----
18+
@Bean
19+
JwtDecoder jwtDecoder() {
20+
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(location)
21+
.validateTypes(false) <1>
22+
// ... your remaining configuration
23+
.build();
24+
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithValidators(
25+
new JwtIssuerValidator(location), JwtTypeValidator.jwt())); <2>
26+
return jwtDecoder;
27+
}
28+
----
29+
30+
Kotlin::
31+
+
32+
[source,kotlin,role="secondary"]
33+
----
34+
@Bean
35+
fun jwtDecoder(): JwtDecoder {
36+
val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(location)
37+
.validateTypes(false) <1>
38+
// ... your remaining configuration
39+
.build()
40+
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithValidators(
41+
JwtIssuerValidator(location), JwtTypeValidator.jwt())) <2>
42+
return jwtDecoder
43+
}
44+
----
45+
======
46+
<1> - Switch off Nimbus verifying the `typ` (this will be off by default in 7)
47+
<2> - Add the default `typ` validator (this will be included by default in 7)
48+
49+
Note the default value verifies that the `typ` value either be `JWT` or not present, which is the same as the Nimbus default.
50+
It is also aligned with https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.9[RFC 7515] which states that `typ` is optional.
51+
52+
53+
=== I'm Using A `DefaultJOSEObjectTypeVerifier`
54+
55+
If you have something like the following in your configuration:
56+
57+
[tabs]
58+
======
59+
Java::
60+
+
61+
[source,java,role="primary"]
62+
----
63+
@Bean
64+
JwtDecoder jwtDecoder() {
65+
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(location)
66+
.jwtProcessorCustomizer((c) -> c
67+
.setJWSTypeVerifier(new DefaultJOSEObjectTypeVerifier<>("JOSE"))
68+
)
69+
.build();
70+
return jwtDecoder;
71+
}
72+
----
73+
74+
Kotlin::
75+
+
76+
[source,kotlin,role="secondary"]
77+
----
78+
@Bean
79+
fun jwtDecoder(): JwtDecoder {
80+
val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(location)
81+
.jwtProcessorCustomizer {
82+
it.setJWSTypeVerifier(DefaultJOSEObjectTypeVerifier("JOSE"))
83+
}
84+
.build()
85+
return jwtDecoder
86+
}
87+
----
88+
======
89+
90+
Then change this to:
91+
92+
[tabs]
93+
======
94+
Java::
95+
+
96+
[source,java,role="primary"]
97+
----
98+
@Bean
99+
JwtDecoder jwtDecoder() {
100+
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(location)
101+
.validateTypes(false)
102+
.build();
103+
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithValidators(
104+
new JwtIssuerValidator(location), new JwtTypeValidator("JOSE")));
105+
return jwtDecoder;
106+
}
107+
----
108+
109+
Kotlin::
110+
+
111+
[source,kotlin,role="secondary"]
112+
----
113+
@Bean
114+
fun jwtDecoder(): JwtDecoder {
115+
val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(location)
116+
.validateTypes(false)
117+
.build()
118+
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithValidators(
119+
JwtIssuerValidator(location), JwtTypeValidator("JOSE")))
120+
return jwtDecoder
121+
}
122+
----
123+
======
124+
125+
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`).
126+
127+
=== I want to opt-out
128+
129+
If you want to keep doing things the way that you are, then the steps are similar, just in reverse:
130+
131+
[tabs]
132+
======
133+
Java::
134+
+
135+
[source,java,role="primary"]
136+
----
137+
@Bean
138+
JwtDecoder jwtDecoder() {
139+
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(location)
140+
.validateTypes(true) <1>
141+
.jwtProcessorCustomizer((c) -> c
142+
.setJWSTypeVerifier(new DefaultJOSEObjectTypeVerifier<>("JOSE"))
143+
)
144+
.build();
145+
jwtDecoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(
146+
new JwtTimestampValidator(), new JwtIssuerValidator(location))); <2>
147+
return jwtDecoder;
148+
}
149+
----
150+
151+
Kotlin::
152+
+
153+
[source,kotlin,role="secondary"]
154+
----
155+
@Bean
156+
fun jwtDecoder(): JwtDecoder {
157+
val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(location)
158+
.validateTypes(true) <1>
159+
.jwtProcessorCustomizer {
160+
it.setJWSTypeVerifier(DefaultJOSEObjectTypeVerifier("JOSE"))
161+
}
162+
.build()
163+
jwtDecoder.setJwtValidator(DelegatingOAuth2TokenValidator(
164+
JwtTimestampValidator(), JwtIssuerValidator(location))) <2>
165+
return jwtDecoder
166+
}
167+
----
168+
======
169+
<1> - leave Nimbus type verification on
170+
<2> - specify the list of validators you need, excluding `JwtTypeValidator`
171+
172+
For additional guidance, please see the xref:servlet/oauth2/resource-server/jwt.adoc#oauth2resourceserver-jwt-validation[JwtDecoder Validators] section in the reference.

docs/modules/ROOT/pages/reactive/oauth2/resource-server/jwt.adoc

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -936,6 +936,46 @@ fun jwtDecoder(): ReactiveJwtDecoder {
936936
By default, Resource Server configures a clock skew of 60 seconds.
937937
====
938938

939+
[[webflux-oauth2resourceserver-validation-rfc9068]]
940+
=== Configuring RFC 9068 Validation
941+
942+
If you need to require tokens that meet https://datatracker.ietf.org/doc/rfc9068/[RFC 9068], you can configure validation in the following way:
943+
944+
[tabs]
945+
======
946+
Java::
947+
+
948+
[source,java,role="primary"]
949+
----
950+
@Bean
951+
JwtDecoder jwtDecoder() {
952+
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuerUri)
953+
.validateTypes(false).build();
954+
jwtDecoder.setJwtValidator(JwtValidators.createAtJwtValidator()
955+
.audience("https://audience.example.org")
956+
.clientId("client-identifier")
957+
.issuer("https://issuer.example.org").build());
958+
return jwtDecoder;
959+
}
960+
----
961+
962+
Kotlin::
963+
+
964+
[source,kotlin,role="secondary"]
965+
----
966+
@Bean
967+
fun jwtDecoder(): JwtDecoder {
968+
val jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuerUri)
969+
.validateTypes(false).build()
970+
jwtDecoder.setJwtValidator(JwtValidators.createAtJwtValidator()
971+
.audience("https://audience.example.org")
972+
.clientId("client-identifier")
973+
.issuer("https://issuer.example.org").build())
974+
return jwtDecoder
975+
}
976+
----
977+
======
978+
939979
[[webflux-oauth2resourceserver-validation-custom]]
940980
==== Configuring a Custom Validator
941981

docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1213,6 +1213,46 @@ fun jwtDecoder(): JwtDecoder {
12131213
[NOTE]
12141214
By default, Resource Server configures a clock skew of 60 seconds.
12151215

1216+
[[oauth2resourceserver-jwt-validation-rfc9068]]
1217+
=== Configuring RFC 9068 Validation
1218+
1219+
If you need to require tokens that meet https://datatracker.ietf.org/doc/rfc9068/[RFC 9068], you can configure validation in the following way:
1220+
1221+
[tabs]
1222+
======
1223+
Java::
1224+
+
1225+
[source,java,role="primary"]
1226+
----
1227+
@Bean
1228+
JwtDecoder jwtDecoder() {
1229+
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuerUri)
1230+
.validateTypes(false).build();
1231+
jwtDecoder.setJwtValidator(JwtValidators.createAtJwtValidator()
1232+
.audience("https://audience.example.org")
1233+
.clientId("client-identifier")
1234+
.issuer("https://issuer.example.org").build());
1235+
return jwtDecoder;
1236+
}
1237+
----
1238+
1239+
Kotlin::
1240+
+
1241+
[source,kotlin,role="secondary"]
1242+
----
1243+
@Bean
1244+
fun jwtDecoder(): JwtDecoder {
1245+
val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuerUri)
1246+
.validateTypes(false).build()
1247+
jwtDecoder.setJwtValidator(JwtValidators.createAtJwtValidator()
1248+
.audience("https://audience.example.org")
1249+
.clientId("client-identifier")
1250+
.issuer("https://issuer.example.org").build())
1251+
return jwtDecoder
1252+
}
1253+
----
1254+
======
1255+
12161256
[[oauth2resourceserver-jwt-validation-custom]]
12171257
=== Configuring a Custom Validator
12181258

oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtTypeValidator.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
*/
3737
public final class JwtTypeValidator implements OAuth2TokenValidator<Jwt> {
3838

39-
private Collection<String> validTypes;
39+
private final Collection<String> validTypes;
4040

4141
private boolean allowEmpty;
4242

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

48+
public JwtTypeValidator(String... validTypes) {
49+
this(List.of(validTypes));
50+
}
51+
4852
/**
4953
* Require that the {@code typ} header be {@code JWT} or absent
5054
*/

0 commit comments

Comments
 (0)