16
16
17
17
package org .springframework .security .oauth2 .client .oidc .authentication ;
18
18
19
+ import java .time .Duration ;
19
20
import java .util .Collection ;
21
+ import java .util .HashSet ;
22
+ import java .util .List ;
20
23
import java .util .Map ;
24
+ import java .util .Set ;
21
25
22
26
import org .springframework .context .ApplicationEventPublisher ;
23
27
import org .springframework .context .ApplicationEventPublisherAware ;
@@ -67,6 +71,8 @@ public final class OidcAuthorizedClientRefreshedEventListener
67
71
68
72
private static final String INVALID_NONCE_ERROR_CODE = "invalid_nonce" ;
69
73
74
+ private static final String REFRESH_TOKEN_RESPONSE_ERROR_URI = "https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse" ;
75
+
70
76
private OAuth2UserService <OidcUserRequest , OidcUser > userService = new OidcUserService ();
71
77
72
78
private JwtDecoderFactory <ClientRegistration > jwtDecoderFactory = new OidcIdTokenDecoderFactory ();
@@ -78,6 +84,8 @@ public final class OidcAuthorizedClientRefreshedEventListener
78
84
79
85
private ApplicationEventPublisher applicationEventPublisher ;
80
86
87
+ private Duration clockSkew = Duration .ofSeconds (60 );
88
+
81
89
@ Override
82
90
public void onApplicationEvent (OAuth2AuthorizedClientRefreshedEvent event ) {
83
91
if (this .applicationEventPublisher == null ) {
@@ -119,7 +127,7 @@ public void onApplicationEvent(OAuth2AuthorizedClientRefreshedEvent event) {
119
127
120
128
// Refresh the OidcUser and send a user refreshed event
121
129
OidcIdToken idToken = createOidcToken (clientRegistration , accessTokenResponse );
122
- validateNonce (existingOidcUser , idToken );
130
+ validateIdToken (existingOidcUser , idToken );
123
131
OidcUserRequest userRequest = new OidcUserRequest (clientRegistration , accessTokenResponse .getAccessToken (),
124
132
idToken , additionalParameters );
125
133
OidcUser oidcUser = this .userService .loadUser (userRequest );
@@ -187,6 +195,17 @@ public void setApplicationEventPublisher(ApplicationEventPublisher applicationEv
187
195
this .applicationEventPublisher = applicationEventPublisher ;
188
196
}
189
197
198
+ /**
199
+ * Sets the maximum acceptable clock skew, which is used when checking the
200
+ * {@link OidcIdToken#getIssuedAt() issuedAt} time. The default is 60 seconds.
201
+ * @param clockSkew the maximum acceptable clock skew
202
+ */
203
+ public void setClockSkew (Duration clockSkew ) {
204
+ Assert .notNull (clockSkew , "clockSkew cannot be null" );
205
+ Assert .isTrue (clockSkew .getSeconds () >= 0 , "clockSkew must be >= 0" );
206
+ this .clockSkew = clockSkew ;
207
+ }
208
+
190
209
private OidcIdToken createOidcToken (ClientRegistration clientRegistration ,
191
210
OAuth2AccessTokenResponse accessTokenResponse ) {
192
211
JwtDecoder jwtDecoder = this .jwtDecoderFactory .createDecoder (clientRegistration );
@@ -205,13 +224,97 @@ private Jwt getJwt(OAuth2AccessTokenResponse accessTokenResponse, JwtDecoder jwt
205
224
}
206
225
}
207
226
227
+ private void validateIdToken (OidcUser existingOidcUser , OidcIdToken idToken ) {
228
+ // OpenID Connect Core 1.0 - Section 12.2 Successful Refresh Response
229
+ // If an ID Token is returned as a result of a token refresh request, the
230
+ // following requirements apply:
231
+ // its iss Claim Value MUST be the same as in the ID Token issued when the
232
+ // original authentication occurred,
233
+ validateIssuer (existingOidcUser , idToken );
234
+ // its sub Claim Value MUST be the same as in the ID Token issued when the
235
+ // original authentication occurred,
236
+ validateSubject (existingOidcUser , idToken );
237
+ // its iat Claim MUST represent the time that the new ID Token is issued,
238
+ validateIssuedAt (existingOidcUser , idToken );
239
+ // its aud Claim Value MUST be the same as in the ID Token issued when the
240
+ // original authentication occurred,
241
+ validateAudience (existingOidcUser , idToken );
242
+ // if the ID Token contains an auth_time Claim, its value MUST represent the time
243
+ // of the original authentication - not the time that the new ID token is issued,
244
+ validateAuthenticatedAt (existingOidcUser , idToken );
245
+ // it SHOULD NOT have a nonce Claim, even when the ID Token issued at the time of
246
+ // the original authentication contained nonce; however, if it is present, its
247
+ // value MUST be the same as in the ID Token issued at the time of the original
248
+ // authentication,
249
+ validateNonce (existingOidcUser , idToken );
250
+ }
251
+
252
+ private void validateIssuer (OidcUser existingOidcUser , OidcIdToken idToken ) {
253
+ if (!idToken .getIssuer ().toString ().equals (existingOidcUser .getIdToken ().getIssuer ().toString ())) {
254
+ OAuth2Error oauth2Error = new OAuth2Error (INVALID_ID_TOKEN_ERROR_CODE , "Invalid issuer" ,
255
+ REFRESH_TOKEN_RESPONSE_ERROR_URI );
256
+ throw new OAuth2AuthenticationException (oauth2Error , oauth2Error .toString ());
257
+ }
258
+ }
259
+
260
+ private void validateSubject (OidcUser existingOidcUser , OidcIdToken idToken ) {
261
+ if (!idToken .getSubject ().equals (existingOidcUser .getIdToken ().getSubject ())) {
262
+ OAuth2Error oauth2Error = new OAuth2Error (INVALID_ID_TOKEN_ERROR_CODE , "Invalid subject" ,
263
+ REFRESH_TOKEN_RESPONSE_ERROR_URI );
264
+ throw new OAuth2AuthenticationException (oauth2Error , oauth2Error .toString ());
265
+ }
266
+ }
267
+
268
+ private void validateIssuedAt (OidcUser existingOidcUser , OidcIdToken idToken ) {
269
+ if (!idToken .getIssuedAt ().isAfter (existingOidcUser .getIdToken ().getIssuedAt ().minus (this .clockSkew ))) {
270
+ OAuth2Error oauth2Error = new OAuth2Error (INVALID_ID_TOKEN_ERROR_CODE , "Invalid issued at time" ,
271
+ REFRESH_TOKEN_RESPONSE_ERROR_URI );
272
+ throw new OAuth2AuthenticationException (oauth2Error , oauth2Error .toString ());
273
+ }
274
+ }
275
+
276
+ private void validateAudience (OidcUser existingOidcUser , OidcIdToken idToken ) {
277
+ if (!isValidAudience (existingOidcUser , idToken )) {
278
+ OAuth2Error oauth2Error = new OAuth2Error (INVALID_ID_TOKEN_ERROR_CODE , "Invalid audience" ,
279
+ REFRESH_TOKEN_RESPONSE_ERROR_URI );
280
+ throw new OAuth2AuthenticationException (oauth2Error , oauth2Error .toString ());
281
+ }
282
+ }
283
+
284
+ private boolean isValidAudience (OidcUser existingOidcUser , OidcIdToken idToken ) {
285
+ List <String > idTokenAudiences = idToken .getAudience ();
286
+ Set <String > oidcUserAudiences = new HashSet <>(existingOidcUser .getIdToken ().getAudience ());
287
+ if (idTokenAudiences .size () != oidcUserAudiences .size ()) {
288
+ return false ;
289
+ }
290
+ for (String audience : idTokenAudiences ) {
291
+ if (!oidcUserAudiences .contains (audience )) {
292
+ return false ;
293
+ }
294
+ }
295
+ return true ;
296
+ }
297
+
298
+ private void validateAuthenticatedAt (OidcUser existingOidcUser , OidcIdToken idToken ) {
299
+ if (idToken .getAuthenticatedAt () == null ) {
300
+ return ;
301
+ }
302
+
303
+ if (!idToken .getAuthenticatedAt ().equals (existingOidcUser .getIdToken ().getAuthenticatedAt ())) {
304
+ OAuth2Error oauth2Error = new OAuth2Error (INVALID_ID_TOKEN_ERROR_CODE , "Invalid authenticated at time" ,
305
+ REFRESH_TOKEN_RESPONSE_ERROR_URI );
306
+ throw new OAuth2AuthenticationException (oauth2Error , oauth2Error .toString ());
307
+ }
308
+ }
309
+
208
310
private void validateNonce (OidcUser existingOidcUser , OidcIdToken idToken ) {
209
311
if (!StringUtils .hasText (idToken .getNonce ())) {
210
312
return ;
211
313
}
212
314
213
- if (!idToken .getNonce ().equals (existingOidcUser .getNonce ())) {
214
- OAuth2Error oauth2Error = new OAuth2Error (INVALID_NONCE_ERROR_CODE );
315
+ if (!idToken .getNonce ().equals (existingOidcUser .getIdToken ().getNonce ())) {
316
+ OAuth2Error oauth2Error = new OAuth2Error (INVALID_NONCE_ERROR_CODE , "Invalid nonce" ,
317
+ REFRESH_TOKEN_RESPONSE_ERROR_URI );
215
318
throw new OAuth2AuthenticationException (oauth2Error , oauth2Error .toString ());
216
319
}
217
320
}
0 commit comments