1616
1717package org .springframework .security .oauth2 .client .web .reactive .function .client ;
1818
19+ import org .springframework .http .HttpHeaders ;
1920import org .springframework .http .HttpStatus ;
20- import org .springframework .lang .Nullable ;
2121import org .springframework .security .authentication .AnonymousAuthenticationToken ;
2222import org .springframework .security .core .Authentication ;
2323import org .springframework .security .core .authority .AuthorityUtils ;
3434import org .springframework .security .oauth2 .client .ReactiveOAuth2AuthorizedClientProvider ;
3535import org .springframework .security .oauth2 .client .ReactiveOAuth2AuthorizedClientProviderBuilder ;
3636import org .springframework .security .oauth2 .client .RefreshTokenReactiveOAuth2AuthorizedClientProvider ;
37- import org .springframework .security .oauth2 .client .web .RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler ;
38- import org .springframework .security .oauth2 .client .web .SaveAuthorizedClientReactiveOAuth2AuthorizationSuccessHandler ;
3937import org .springframework .security .oauth2 .client .authentication .OAuth2AuthenticationToken ;
4038import org .springframework .security .oauth2 .client .endpoint .OAuth2ClientCredentialsGrantRequest ;
4139import org .springframework .security .oauth2 .client .endpoint .ReactiveOAuth2AccessTokenResponseClient ;
4240import org .springframework .security .oauth2 .client .registration .ClientRegistration ;
4341import org .springframework .security .oauth2 .client .registration .ReactiveClientRegistrationRepository ;
4442import org .springframework .security .oauth2 .client .web .DefaultReactiveOAuth2AuthorizedClientManager ;
43+ import org .springframework .security .oauth2 .client .web .RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler ;
44+ import org .springframework .security .oauth2 .client .web .SaveAuthorizedClientReactiveOAuth2AuthorizationSuccessHandler ;
4545import org .springframework .security .oauth2 .client .web .server .ServerOAuth2AuthorizedClientRepository ;
4646import org .springframework .security .oauth2 .client .web .server .UnAuthenticatedServerOAuth2AuthorizedClientRepository ;
4747import org .springframework .security .oauth2 .core .OAuth2AuthorizationException ;
4848import org .springframework .security .oauth2 .core .OAuth2Error ;
4949import org .springframework .security .oauth2 .core .OAuth2ErrorCodes ;
50+ import org .springframework .security .oauth2 .core .endpoint .OAuth2ParameterNames ;
5051import org .springframework .util .Assert ;
52+ import org .springframework .util .StringUtils ;
5153import org .springframework .web .reactive .function .client .ClientRequest ;
5254import org .springframework .web .reactive .function .client .ClientResponse ;
5355import org .springframework .web .reactive .function .client .ExchangeFilterFunction ;
6264import java .util .Map ;
6365import java .util .Optional ;
6466import java .util .function .Consumer ;
67+ import java .util .stream .Collectors ;
68+ import java .util .stream .Stream ;
6569
6670/**
6771 * Provides an easy mechanism for using an {@link OAuth2AuthorizedClient} to make OAuth2 requests by including the
@@ -614,32 +618,84 @@ private AuthorizationFailureForwarder(ReactiveOAuth2AuthorizationFailureHandler
614618 }
615619
616620 @ Override
617- public Mono <ClientResponse > handleResponse (
618- ClientRequest request ,
619- Mono <ClientResponse > responseMono ) {
620-
621+ public Mono <ClientResponse > handleResponse (ClientRequest request , Mono <ClientResponse > responseMono ) {
621622 return responseMono
622- .flatMap (response -> handleHttpStatus (request , response . rawStatusCode (), null )
623+ .flatMap (response -> handleResponse (request , response )
623624 .thenReturn (response ))
624- .onErrorResume (WebClientResponseException .class , e -> handleHttpStatus (request , e . getRawStatusCode () , e )
625+ .onErrorResume (WebClientResponseException .class , e -> handleWebClientResponseException (request , e )
625626 .then (Mono .error (e )))
626627 .onErrorResume (OAuth2AuthorizationException .class , e -> handleAuthorizationException (request , e )
627628 .then (Mono .error (e )));
628629 }
629630
631+ private Mono <Void > handleResponse (ClientRequest request , ClientResponse response ) {
632+ return Mono .justOrEmpty (resolveErrorIfPossible (response ))
633+ .flatMap (oauth2Error -> {
634+ Mono <Optional <ServerWebExchange >> serverWebExchange = effectiveServerWebExchange (request );
635+
636+ Mono <String > clientRegistrationId = effectiveClientRegistrationId (request );
637+
638+ return Mono .zip (currentAuthenticationMono , serverWebExchange , clientRegistrationId )
639+ .flatMap (tuple3 -> handleAuthorizationFailure (
640+ tuple3 .getT1 (), // Authentication principal
641+ tuple3 .getT2 ().orElse (null ), // ServerWebExchange exchange
642+ new ClientAuthorizationException (
643+ oauth2Error ,
644+ tuple3 .getT3 ()))); // String clientRegistrationId
645+ });
646+ }
647+
648+ private OAuth2Error resolveErrorIfPossible (ClientResponse response ) {
649+ // Try to resolve from 'WWW-Authenticate' header
650+ if (!response .headers ().header (HttpHeaders .WWW_AUTHENTICATE ).isEmpty ()) {
651+ String wwwAuthenticateHeader = response .headers ().header (HttpHeaders .WWW_AUTHENTICATE ).get (0 );
652+ Map <String , String > authParameters = parseAuthParameters (wwwAuthenticateHeader );
653+ if (authParameters .containsKey (OAuth2ParameterNames .ERROR )) {
654+ return new OAuth2Error (
655+ authParameters .get (OAuth2ParameterNames .ERROR ),
656+ authParameters .get (OAuth2ParameterNames .ERROR_DESCRIPTION ),
657+ authParameters .get (OAuth2ParameterNames .ERROR_URI ));
658+ }
659+ }
660+ return resolveErrorIfPossible (response .rawStatusCode ());
661+ }
662+
663+ private OAuth2Error resolveErrorIfPossible (int statusCode ) {
664+ if (this .httpStatusToOAuth2ErrorCodeMap .containsKey (statusCode )) {
665+ return new OAuth2Error (
666+ this .httpStatusToOAuth2ErrorCodeMap .get (statusCode ),
667+ null ,
668+ "https://tools.ietf.org/html/rfc6750#section-3.1" );
669+ }
670+ return null ;
671+ }
672+
673+ private Map <String , String > parseAuthParameters (String wwwAuthenticateHeader ) {
674+ return Stream .of (wwwAuthenticateHeader )
675+ .filter (header -> !StringUtils .isEmpty (header ))
676+ .filter (header -> header .toLowerCase ().startsWith ("bearer" ))
677+ .map (header -> header .substring ("bearer" .length ()))
678+ .map (header -> header .split ("," ))
679+ .flatMap (Stream ::of )
680+ .map (parameter -> parameter .split ("=" ))
681+ .filter (parameter -> parameter .length > 1 )
682+ .collect (Collectors .toMap (
683+ parameters -> parameters [0 ].trim (),
684+ parameters -> parameters [1 ].trim ().replace ("\" " , "" )));
685+ }
686+
630687 /**
631688 * Handles the given http status code returned from a resource server
632689 * by notifying the authorization failure handler if the http status
633690 * code is in the {@link #httpStatusToOAuth2ErrorCodeMap}.
634691 *
635692 * @param request the request being processed
636- * @param httpStatusCode the http status returned by the resource server
637- * @param exception The root cause exception for the failure (nullable)
693+ * @param exception The root cause exception for the failure
638694 * @return a {@link Mono} that completes empty after the authorization failure handler completes.
639695 */
640- private Mono <Void > handleHttpStatus (ClientRequest request , int httpStatusCode , @ Nullable Exception exception ) {
641- return Mono .justOrEmpty (this . httpStatusToOAuth2ErrorCodeMap . get ( httpStatusCode ))
642- .flatMap (oauth2ErrorCode -> {
696+ private Mono <Void > handleWebClientResponseException (ClientRequest request , WebClientResponseException exception ) {
697+ return Mono .justOrEmpty (resolveErrorIfPossible ( exception . getRawStatusCode () ))
698+ .flatMap (oauth2Error -> {
643699 Mono <Optional <ServerWebExchange >> serverWebExchange = effectiveServerWebExchange (request );
644700
645701 Mono <String > clientRegistrationId = effectiveClientRegistrationId (request );
@@ -648,9 +704,9 @@ private Mono<Void> handleHttpStatus(ClientRequest request, int httpStatusCode, @
648704 .flatMap (tuple3 -> handleAuthorizationFailure (
649705 tuple3 .getT1 (), // Authentication principal
650706 tuple3 .getT2 ().orElse (null ), // ServerWebExchange exchange
651- createAuthorizationException (
707+ new ClientAuthorizationException (
708+ oauth2Error ,
652709 tuple3 .getT3 (), // String clientRegistrationId
653- oauth2ErrorCode ,
654710 exception )));
655711 });
656712 }
@@ -673,28 +729,6 @@ private Mono<Void> handleAuthorizationException(ClientRequest request, OAuth2Aut
673729 exception ));
674730 }
675731
676- /**
677- * Creates an authorization exception using the given parameters.
678- *
679- * @param clientRegistrationId the client registration id of the client that failed authentication/authorization.
680- * @param oauth2ErrorCode the OAuth 2.0 error code to use in the authorization failure event
681- * @param exception The root cause exception for the failure (nullable)
682- * @return an authorization exception using the given parameters.
683- */
684- private ClientAuthorizationException createAuthorizationException (
685- String clientRegistrationId ,
686- String oauth2ErrorCode ,
687- @ Nullable Exception exception ) {
688- return new ClientAuthorizationException (
689- new OAuth2Error (
690- oauth2ErrorCode ,
691- null ,
692- "https://tools.ietf.org/html/rfc6750#section-3.1" ),
693- clientRegistrationId ,
694- exception );
695- }
696-
697-
698732 /**
699733 * Delegates to the authorization failure handler of the failed authorization.
700734 *
0 commit comments