From 4a5bade35da2eec9fe3db824b0004a2c76f48ac3 Mon Sep 17 00:00:00 2001
From: Phil Clay
Date: Mon, 16 Dec 2019 20:19:22 -0800
Subject: [PATCH] Introduce Reactive OAuth2Authorization success/failure
handlers
All ReactiveOAuth2AuthorizedClientManagers now have authorization success/failure handlers.
A success handler is provided to save authorized clients for future requests.
A failure handler is provided to remove previously saved authorized clients.
ServerOAuth2AuthorizedClientExchangeFilterFunction also makes use of a
failure handler in the case of unauthorized or forbidden http status code.
The main use cases now handled are
- remove authorized client when an authorization server indicates that a refresh token is no longer valid (when authorization server returns invalid_grant)
- remove authorized client when a resource server indicates that an access token is no longer valid (when resource server returns invalid_token)
Introduced ClientAuthorizationException to capture details needed when removing an authorized client.
All ReactiveOAuth2AccessTokenResponseClients now throw a ClientAuthorizationException on failures.
Created AbstractWebClientReactiveOAuth2AccessTokenResponseClient to unify common logic between all ReactiveOAuth2AccessTokenResponseClients.
Fixes gh-7699
---
...ReactiveOAuth2AuthorizedClientManager.java | 96 ++++-
.../client/ClientAuthorizationException.java | 89 ++++
.../ClientAuthorizationRequiredException.java | 21 +-
...tiveOAuth2AuthorizationFailureHandler.java | 51 +++
...tiveOAuth2AuthorizationSuccessHandler.java | 51 +++
...tiveOAuth2AuthorizationFailureHandler.java | 169 ++++++++
...tiveOAuth2AuthorizationSuccessHandler.java | 77 ++++
...activeOAuth2AccessTokenResponseClient.java | 229 +++++++++++
...eAuthorizationCodeTokenResponseClient.java | 76 +---
...eClientCredentialsTokenResponseClient.java | 94 +----
...ntReactivePasswordTokenResponseClient.java | 103 +----
...activeRefreshTokenTokenResponseClient.java | 110 ++---
...ReactiveOAuth2AuthorizedClientManager.java | 177 ++++++--
...uthorizedClientExchangeFilterFunction.java | 384 +++++++++++++++---
...iveOAuth2AuthorizedClientManagerTests.java | 234 ++++++++++-
...orizationCodeTokenResponseClientTests.java | 12 +-
...ntCredentialsTokenResponseClientTests.java | 16 +-
...ctivePasswordTokenResponseClientTests.java | 24 +-
...eRefreshTokenTokenResponseClientTests.java | 22 +-
...iveOAuth2AuthorizedClientManagerTests.java | 228 +++++++++++
...zedClientExchangeFilterFunctionITests.java | 330 +++++++++++++++
...izedClientExchangeFilterFunctionTests.java | 271 +++++++++++-
.../core/OAuth2AuthorizationException.java | 33 +-
.../oauth2/core/OAuth2ErrorCodes.java | 23 +-
...Auth2AccessTokenResponseBodyExtractor.java | 17 +-
.../function/OAuth2BodyExtractorsTests.java | 22 +-
26 files changed, 2486 insertions(+), 473 deletions(-)
create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ClientAuthorizationException.java
create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ReactiveOAuth2AuthorizationFailureHandler.java
create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ReactiveOAuth2AuthorizationSuccessHandler.java
create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler.java
create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/SaveAuthorizedClientReactiveOAuth2AuthorizationSuccessHandler.java
create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/AbstractWebClientReactiveOAuth2AccessTokenResponseClient.java
create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunctionITests.java
diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager.java
index 16400dc922c..fdbe3f828ab 100644
--- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager.java
+++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,7 +17,10 @@
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
+import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager;
+import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.util.Assert;
+import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Collections;
@@ -26,10 +29,33 @@
/**
* An implementation of an {@link ReactiveOAuth2AuthorizedClientManager}
- * that is capable of operating outside of a {@code ServerHttpRequest} context,
+ * that is capable of operating outside of the context of a {@link ServerWebExchange},
* e.g. in a scheduled/background thread and/or in the service-tier.
*
- * This is a reactive equivalent of {@link org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager}
+ * (When operating within the context of a {@link ServerWebExchange},
+ * use {@link DefaultReactiveOAuth2AuthorizedClientManager} instead.)
+ *
+ * This is a reactive equivalent of {@link org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager}.
+ *
+ * Authorized Client Persistence
+ *
+ * This client manager utilizes a {@link ReactiveOAuth2AuthorizedClientService}
+ * to persist {@link OAuth2AuthorizedClient}s.
+ *
+ * By default, when an authorization attempt succeeds, the {@link OAuth2AuthorizedClient}
+ * will be saved in the authorized client service.
+ * This functionality can be changed by configuring a custom {@link ReactiveOAuth2AuthorizationSuccessHandler}
+ * via {@link #setAuthorizationFailureHandler(ReactiveOAuth2AuthorizationFailureHandler)}.
+ *
+ * By default, when an authorization attempt fails due to an
+ * {@value org.springframework.security.oauth2.core.OAuth2ErrorCodes#INVALID_GRANT} error,
+ * the previously saved {@link OAuth2AuthorizedClient}
+ * will be removed from the authorized client service.
+ * (The {@value org.springframework.security.oauth2.core.OAuth2ErrorCodes#INVALID_GRANT}
+ * error generally occurs when a refresh token that is no longer valid
+ * is used to retrieve a new access token.)
+ * This functionality can be changed by configuring a custom {@link ReactiveOAuth2AuthorizationFailureHandler}
+ * via {@link #setAuthorizationFailureHandler(ReactiveOAuth2AuthorizationFailureHandler)}.
*
* @author Ankur Pathak
* @author Phil Clay
@@ -45,6 +71,8 @@ public final class AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager
private final ReactiveOAuth2AuthorizedClientService authorizedClientService;
private ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = context -> Mono.empty();
private Function>> contextAttributesMapper = new DefaultContextAttributesMapper();
+ private ReactiveOAuth2AuthorizationSuccessHandler authorizationSuccessHandler;
+ private ReactiveOAuth2AuthorizationFailureHandler authorizationFailureHandler;
/**
* Constructs an {@code AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager} using the provided parameters.
@@ -59,6 +87,8 @@ public AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
Assert.notNull(authorizedClientService, "authorizedClientService cannot be null");
this.clientRegistrationRepository = clientRegistrationRepository;
this.authorizedClientService = authorizedClientService;
+ this.authorizationSuccessHandler = new SaveAuthorizedClientReactiveOAuth2AuthorizationSuccessHandler(authorizedClientService);
+ this.authorizationFailureHandler = new RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler(authorizedClientService);
}
@Override
@@ -66,7 +96,7 @@ public Mono authorize(OAuth2AuthorizeRequest authorizeRe
Assert.notNull(authorizeRequest, "authorizeRequest cannot be null");
return createAuthorizationContext(authorizeRequest)
- .flatMap(this::authorizeAndSave);
+ .flatMap(authorizationContext -> authorize(authorizationContext, authorizeRequest.getPrincipal()));
}
private Mono createAuthorizationContext(OAuth2AuthorizeRequest authorizeRequest) {
@@ -90,12 +120,33 @@ private Mono createAuthorizationContext(OAuth2Author
}));
}
- private Mono authorizeAndSave(OAuth2AuthorizationContext authorizationContext) {
+ /**
+ * Performs authorization, and notifies either the {@link #authorizationSuccessHandler}
+ * or {@link #authorizationFailureHandler}, depending on the authorization result.
+ *
+ * @param authorizationContext the context to authorize
+ * @param principal the principle to authorize
+ * @return a {@link Mono} that emits the authorized client after the authorization attempt succeeds
+ * and the {@link #authorizationSuccessHandler} has completed,
+ * or completes with an exception after the authorization attempt fails
+ * and the {@link #authorizationFailureHandler} has completed
+ */
+ private Mono authorize(
+ OAuth2AuthorizationContext authorizationContext,
+ Authentication principal) {
return this.authorizedClientProvider.authorize(authorizationContext)
- .flatMap(authorizedClient -> this.authorizedClientService.saveAuthorizedClient(
+ // Notify the authorizationSuccessHandler of the successful authorization
+ .flatMap(authorizedClient -> authorizationSuccessHandler.onAuthorizationSuccess(
authorizedClient,
- authorizationContext.getPrincipal())
+ principal,
+ Collections.emptyMap())
.thenReturn(authorizedClient))
+ // Notify the authorizationFailureHandler of the failed authorization
+ .onErrorResume(OAuth2AuthorizationException.class, authorizationException -> authorizationFailureHandler.onAuthorizationFailure(
+ authorizationException,
+ principal,
+ Collections.emptyMap())
+ .then(Mono.error(authorizationException)))
.switchIfEmpty(Mono.defer(()-> Mono.justOrEmpty(authorizationContext.getAuthorizedClient())));
}
@@ -121,6 +172,36 @@ public void setContextAttributesMapper(FunctionA {@link SaveAuthorizedClientReactiveOAuth2AuthorizationSuccessHandler}
+ * is used by default.
+ *
+ * @param authorizationSuccessHandler the handler that handles successful authorizations.
+ * @see SaveAuthorizedClientReactiveOAuth2AuthorizationSuccessHandler
+ * @since 5.3
+ */
+ public void setAuthorizationSuccessHandler(ReactiveOAuth2AuthorizationSuccessHandler authorizationSuccessHandler) {
+ Assert.notNull(authorizationSuccessHandler, "authorizationSuccessHandler cannot be null");
+ this.authorizationSuccessHandler = authorizationSuccessHandler;
+ }
+
+ /**
+ * Sets the handler that handles authorization failures.
+ *
+ * A {@link RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler}
+ * is used by default.
+ *
+ * @param authorizationFailureHandler the handler that handles authorization failures.
+ * @see RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler
+ * @since 5.3
+ */
+ public void setAuthorizationFailureHandler(ReactiveOAuth2AuthorizationFailureHandler authorizationFailureHandler) {
+ Assert.notNull(authorizationFailureHandler, "authorizationFailureHandler cannot be null");
+ this.authorizationFailureHandler = authorizationFailureHandler;
+ }
+
/**
* The default implementation of the {@link #setContextAttributesMapper(Function) contextAttributesMapper}.
*/
@@ -134,4 +215,5 @@ public Mono