2121import java .util .HashMap ;
2222import java .util .Map ;
2323import java .util .Set ;
24+ import java .util .function .Consumer ;
2425
25- import com .nimbusds .jose .jwk .JWK ;
2626import org .apache .commons .logging .Log ;
2727import org .apache .commons .logging .LogFactory ;
2828
3131import org .springframework .security .core .Authentication ;
3232import org .springframework .security .core .AuthenticationException ;
3333import org .springframework .security .oauth2 .core .AuthorizationGrantType ;
34- import org .springframework .security .oauth2 .core .ClaimAccessor ;
35- import org .springframework .security .oauth2 .core .ClientAuthenticationMethod ;
3634import org .springframework .security .oauth2 .core .OAuth2AccessToken ;
3735import org .springframework .security .oauth2 .core .OAuth2AuthenticationException ;
3836import org .springframework .security .oauth2 .core .OAuth2Error ;
5250import org .springframework .security .oauth2 .server .authorization .token .OAuth2TokenContext ;
5351import org .springframework .security .oauth2 .server .authorization .token .OAuth2TokenGenerator ;
5452import org .springframework .util .Assert ;
55- import org .springframework .util .CollectionUtils ;
5653
5754/**
5855 * An {@link AuthenticationProvider} implementation for the OAuth 2.0 Refresh Token Grant.
5956 *
6057 * @author Alexey Nesterov
6158 * @author Joe Grandja
6259 * @author Anoop Garlapati
60+ * @author Andrey Litvitski
6361 * @since 7.0
6462 * @see OAuth2RefreshTokenAuthenticationToken
6563 * @see OAuth2AccessTokenAuthenticationToken
@@ -84,6 +82,8 @@ public final class OAuth2RefreshTokenAuthenticationProvider implements Authentic
8482
8583 private final OAuth2TokenGenerator <? extends OAuth2Token > tokenGenerator ;
8684
85+ private Consumer <OAuth2RefreshTokenAuthenticationContext > authenticationValidator = new OAuth2RefreshTokenAuthenticationValidator ();
86+ 8787 /**
8888 * Constructs an {@code OAuth2RefreshTokenAuthenticationProvider} using the provided
8989 * parameters.
@@ -164,13 +164,14 @@ public Authentication authenticate(Authentication authentication) throws Authent
164164 // Verify the DPoP Proof (if available)
165165 Jwt dPoPProof = DPoPProofVerifier .verifyIfAvailable (refreshTokenAuthentication );
166166
167- if (dPoPProof != null
168- && clientPrincipal .getClientAuthenticationMethod ().equals (ClientAuthenticationMethod .NONE )) {
169- // For public clients, verify the DPoP Proof public key is same as (current)
170- // access token public key binding
171- Map <String , Object > accessTokenClaims = authorization .getAccessToken ().getClaims ();
172- verifyDPoPProofPublicKey (dPoPProof , () -> accessTokenClaims );
173- }
167+ OAuth2RefreshTokenAuthenticationContext context = OAuth2RefreshTokenAuthenticationContext
168+ .with (refreshTokenAuthentication )
169+ .authorization (authorization )
170+ .clientPrincipal (clientPrincipal )
171+ .dPoPProof (dPoPProof )
172+ .build ();
173+ 174+ this .authenticationValidator .accept (context );
174175
175176 if (this .logger .isTraceEnabled ()) {
176177 this .logger .trace ("Validated token request parameters" );
@@ -292,45 +293,15 @@ public boolean supports(Class<?> authentication) {
292293 return OAuth2RefreshTokenAuthenticationToken .class .isAssignableFrom (authentication );
293294 }
294295
295- private static void verifyDPoPProofPublicKey (Jwt dPoPProof , ClaimAccessor accessTokenClaims ) {
296- JWK jwk = null ;
297- @ SuppressWarnings ("unchecked" )
298- Map <String , Object > jwkJson = (Map <String , Object >) dPoPProof .getHeaders ().get ("jwk" );
299- try {
300- jwk = JWK .parse (jwkJson );
301- }
302- catch (Exception ignored ) {
303- }
304- if (jwk == null ) {
305- OAuth2Error error = new OAuth2Error (OAuth2ErrorCodes .INVALID_DPOP_PROOF ,
306- "jwk header is missing or invalid." , null );
307- throw new OAuth2AuthenticationException (error );
308- }
309- 310- String jwkThumbprint ;
311- try {
312- jwkThumbprint = jwk .computeThumbprint ().toString ();
313- }
314- catch (Exception ex ) {
315- OAuth2Error error = new OAuth2Error (OAuth2ErrorCodes .INVALID_DPOP_PROOF ,
316- "Failed to compute SHA-256 Thumbprint for jwk." , null );
317- throw new OAuth2AuthenticationException (error );
318- }
319- 320- String jwkThumbprintClaim = null ;
321- Map <String , Object > confirmationMethodClaim = accessTokenClaims .getClaimAsMap ("cnf" );
322- if (!CollectionUtils .isEmpty (confirmationMethodClaim ) && confirmationMethodClaim .containsKey ("jkt" )) {
323- jwkThumbprintClaim = (String ) confirmationMethodClaim .get ("jkt" );
324- }
325- if (jwkThumbprintClaim == null ) {
326- OAuth2Error error = new OAuth2Error (OAuth2ErrorCodes .INVALID_DPOP_PROOF , "jkt claim is missing." , null );
327- throw new OAuth2AuthenticationException (error );
328- }
329- 330- if (!jwkThumbprint .equals (jwkThumbprintClaim )) {
331- OAuth2Error error = new OAuth2Error (OAuth2ErrorCodes .INVALID_DPOP_PROOF , "jwk header is invalid." , null );
332- throw new OAuth2AuthenticationException (error );
333- }
296+ /**
297+ * Sets the {@code Consumer} responsible for validating the OAuth 2.0 Refresh Token
298+ * Grant Request using the provided {@link OAuth2RefreshTokenAuthenticationContext}.
299+ * <p>
300+ * The default validator performs DPoP proof verification if present.
301+ */
302+ public void setAuthenticationValidator (Consumer <OAuth2RefreshTokenAuthenticationContext > authenticationValidator ) {
303+ Assert .notNull (authenticationValidator , "authenticationValidator cannot be null" );
304+ this .authenticationValidator = authenticationValidator ;
334305 }
335306
336307}
0 commit comments