1616
1717package org .springframework .security .oauth2 .client .oidc .authentication ;
1818
19+ import java .time .Duration ;
1920import java .util .Collection ;
21+ import java .util .HashSet ;
22+ import java .util .List ;
2023import java .util .Map ;
24+ import java .util .Set ;
2125
2226import org .springframework .context .ApplicationEventPublisher ;
2327import org .springframework .context .ApplicationEventPublisherAware ;
@@ -67,6 +71,8 @@ public final class OidcAuthorizedClientRefreshedEventListener
6771
6872 private static final String INVALID_NONCE_ERROR_CODE = "invalid_nonce" ;
6973
74+ private static final String REFRESH_TOKEN_RESPONSE_ERROR_URI = "https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse" ;
75+
7076 private OAuth2UserService <OidcUserRequest , OidcUser > userService = new OidcUserService ();
7177
7278 private JwtDecoderFactory <ClientRegistration > jwtDecoderFactory = new OidcIdTokenDecoderFactory ();
@@ -78,6 +84,8 @@ public final class OidcAuthorizedClientRefreshedEventListener
7884
7985 private ApplicationEventPublisher applicationEventPublisher ;
8086
87+ private Duration clockSkew = Duration .ofSeconds (60 );
88+
8189 @ Override
8290 public void onApplicationEvent (OAuth2AuthorizedClientRefreshedEvent event ) {
8391 if (this .applicationEventPublisher == null ) {
@@ -119,7 +127,7 @@ public void onApplicationEvent(OAuth2AuthorizedClientRefreshedEvent event) {
119127
120128 // Refresh the OidcUser and send a user refreshed event
121129 OidcIdToken idToken = createOidcToken (clientRegistration , accessTokenResponse );
122- validateNonce (existingOidcUser , idToken );
130+ validateIdToken (existingOidcUser , idToken );
123131 OidcUserRequest userRequest = new OidcUserRequest (clientRegistration , accessTokenResponse .getAccessToken (),
124132 idToken , additionalParameters );
125133 OidcUser oidcUser = this .userService .loadUser (userRequest );
@@ -187,6 +195,17 @@ public void setApplicationEventPublisher(ApplicationEventPublisher applicationEv
187195 this .applicationEventPublisher = applicationEventPublisher ;
188196 }
189197
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+
190209 private OidcIdToken createOidcToken (ClientRegistration clientRegistration ,
191210 OAuth2AccessTokenResponse accessTokenResponse ) {
192211 JwtDecoder jwtDecoder = this .jwtDecoderFactory .createDecoder (clientRegistration );
@@ -205,13 +224,97 @@ private Jwt getJwt(OAuth2AccessTokenResponse accessTokenResponse, JwtDecoder jwt
205224 }
206225 }
207226
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+
208310 private void validateNonce (OidcUser existingOidcUser , OidcIdToken idToken ) {
209311 if (!StringUtils .hasText (idToken .getNonce ())) {
210312 return ;
211313 }
212314
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 );
215318 throw new OAuth2AuthenticationException (oauth2Error , oauth2Error .toString ());
216319 }
217320 }
0 commit comments