Skip to content

Commit 4a5bade

Browse files
committed
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
1 parent f1f158b commit 4a5bade

File tree

26 files changed

+2486
-473
lines changed

26 files changed

+2486
-473
lines changed

oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager.java

Lines changed: 89 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,7 +17,10 @@
1717

1818
import org.springframework.security.core.Authentication;
1919
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
20+
import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager;
21+
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
2022
import org.springframework.util.Assert;
23+
import org.springframework.web.server.ServerWebExchange;
2124
import reactor.core.publisher.Mono;
2225

2326
import java.util.Collections;
@@ -26,10 +29,33 @@
2629

2730
/**
2831
* An implementation of an {@link ReactiveOAuth2AuthorizedClientManager}
29-
* that is capable of operating outside of a {@code ServerHttpRequest} context,
32+
* that is capable of operating outside of the context of a {@link ServerWebExchange},
3033
* e.g. in a scheduled/background thread and/or in the service-tier.
3134
*
32-
* <p>This is a reactive equivalent of {@link org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager}</p>
35+
* <p>(When operating <em>within</em> the context of a {@link ServerWebExchange},
36+
* use {@link DefaultReactiveOAuth2AuthorizedClientManager} instead.)</p>
37+
*
38+
* <p>This is a reactive equivalent of {@link org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager}.</p>
39+
*
40+
* <h2>Authorized Client Persistence</h2>
41+
*
42+
* <p>This client manager utilizes a {@link ReactiveOAuth2AuthorizedClientService}
43+
* to persist {@link OAuth2AuthorizedClient}s.</p>
44+
*
45+
* <p>By default, when an authorization attempt succeeds, the {@link OAuth2AuthorizedClient}
46+
* will be saved in the authorized client service.
47+
* This functionality can be changed by configuring a custom {@link ReactiveOAuth2AuthorizationSuccessHandler}
48+
* via {@link #setAuthorizationFailureHandler(ReactiveOAuth2AuthorizationFailureHandler)}.</p>
49+
*
50+
* <p>By default, when an authorization attempt fails due to an
51+
* {@value org.springframework.security.oauth2.core.OAuth2ErrorCodes#INVALID_GRANT} error,
52+
* the previously saved {@link OAuth2AuthorizedClient}
53+
* will be removed from the authorized client service.
54+
* (The {@value org.springframework.security.oauth2.core.OAuth2ErrorCodes#INVALID_GRANT}
55+
* error generally occurs when a refresh token that is no longer valid
56+
* is used to retrieve a new access token.)
57+
* This functionality can be changed by configuring a custom {@link ReactiveOAuth2AuthorizationFailureHandler}
58+
* via {@link #setAuthorizationFailureHandler(ReactiveOAuth2AuthorizationFailureHandler)}.</p>
3359
*
3460
* @author Ankur Pathak
3561
* @author Phil Clay
@@ -45,6 +71,8 @@ public final class AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager
4571
private final ReactiveOAuth2AuthorizedClientService authorizedClientService;
4672
private ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = context -> Mono.empty();
4773
private Function<OAuth2AuthorizeRequest, Mono<Map<String, Object>>> contextAttributesMapper = new DefaultContextAttributesMapper();
74+
private ReactiveOAuth2AuthorizationSuccessHandler authorizationSuccessHandler;
75+
private ReactiveOAuth2AuthorizationFailureHandler authorizationFailureHandler;
4876

4977
/**
5078
* Constructs an {@code AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager} using the provided parameters.
@@ -59,14 +87,16 @@ public AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
5987
Assert.notNull(authorizedClientService, "authorizedClientService cannot be null");
6088
this.clientRegistrationRepository = clientRegistrationRepository;
6189
this.authorizedClientService = authorizedClientService;
90+
this.authorizationSuccessHandler = new SaveAuthorizedClientReactiveOAuth2AuthorizationSuccessHandler(authorizedClientService);
91+
this.authorizationFailureHandler = new RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler(authorizedClientService);
6292
}
6393

6494
@Override
6595
public Mono<OAuth2AuthorizedClient> authorize(OAuth2AuthorizeRequest authorizeRequest) {
6696
Assert.notNull(authorizeRequest, "authorizeRequest cannot be null");
6797

6898
return createAuthorizationContext(authorizeRequest)
69-
.flatMap(this::authorizeAndSave);
99+
.flatMap(authorizationContext -> authorize(authorizationContext, authorizeRequest.getPrincipal()));
70100
}
71101

72102
private Mono<OAuth2AuthorizationContext> createAuthorizationContext(OAuth2AuthorizeRequest authorizeRequest) {
@@ -90,12 +120,33 @@ private Mono<OAuth2AuthorizationContext> createAuthorizationContext(OAuth2Author
90120
}));
91121
}
92122

93-
private Mono<OAuth2AuthorizedClient> authorizeAndSave(OAuth2AuthorizationContext authorizationContext) {
123+
/**
124+
* Performs authorization, and notifies either the {@link #authorizationSuccessHandler}
125+
* or {@link #authorizationFailureHandler}, depending on the authorization result.
126+
*
127+
* @param authorizationContext the context to authorize
128+
* @param principal the principle to authorize
129+
* @return a {@link Mono} that emits the authorized client after the authorization attempt succeeds
130+
* and the {@link #authorizationSuccessHandler} has completed,
131+
* or completes with an exception after the authorization attempt fails
132+
* and the {@link #authorizationFailureHandler} has completed
133+
*/
134+
private Mono<OAuth2AuthorizedClient> authorize(
135+
OAuth2AuthorizationContext authorizationContext,
136+
Authentication principal) {
94137
return this.authorizedClientProvider.authorize(authorizationContext)
95-
.flatMap(authorizedClient -> this.authorizedClientService.saveAuthorizedClient(
138+
// Notify the authorizationSuccessHandler of the successful authorization
139+
.flatMap(authorizedClient -> authorizationSuccessHandler.onAuthorizationSuccess(
96140
authorizedClient,
97-
authorizationContext.getPrincipal())
141+
principal,
142+
Collections.emptyMap())
98143
.thenReturn(authorizedClient))
144+
// Notify the authorizationFailureHandler of the failed authorization
145+
.onErrorResume(OAuth2AuthorizationException.class, authorizationException -> authorizationFailureHandler.onAuthorizationFailure(
146+
authorizationException,
147+
principal,
148+
Collections.emptyMap())
149+
.then(Mono.error(authorizationException)))
99150
.switchIfEmpty(Mono.defer(()-> Mono.justOrEmpty(authorizationContext.getAuthorizedClient())));
100151
}
101152

@@ -121,6 +172,36 @@ public void setContextAttributesMapper(Function<OAuth2AuthorizeRequest, Mono<Map
121172
this.contextAttributesMapper = contextAttributesMapper;
122173
}
123174

175+
/**
176+
* Sets the handler that handles successful authorizations.
177+
*
178+
* <p>A {@link SaveAuthorizedClientReactiveOAuth2AuthorizationSuccessHandler}
179+
* is used by default.</p>
180+
*
181+
* @param authorizationSuccessHandler the handler that handles successful authorizations.
182+
* @see SaveAuthorizedClientReactiveOAuth2AuthorizationSuccessHandler
183+
* @since 5.3
184+
*/
185+
public void setAuthorizationSuccessHandler(ReactiveOAuth2AuthorizationSuccessHandler authorizationSuccessHandler) {
186+
Assert.notNull(authorizationSuccessHandler, "authorizationSuccessHandler cannot be null");
187+
this.authorizationSuccessHandler = authorizationSuccessHandler;
188+
}
189+
190+
/**
191+
* Sets the handler that handles authorization failures.
192+
*
193+
* <p>A {@link RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler}
194+
* is used by default.</p>
195+
*
196+
* @param authorizationFailureHandler the handler that handles authorization failures.
197+
* @see RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler
198+
* @since 5.3
199+
*/
200+
public void setAuthorizationFailureHandler(ReactiveOAuth2AuthorizationFailureHandler authorizationFailureHandler) {
201+
Assert.notNull(authorizationFailureHandler, "authorizationFailureHandler cannot be null");
202+
this.authorizationFailureHandler = authorizationFailureHandler;
203+
}
204+
124205
/**
125206
* The default implementation of the {@link #setContextAttributesMapper(Function) contextAttributesMapper}.
126207
*/
@@ -134,4 +215,5 @@ public Mono<Map<String, Object>> apply(OAuth2AuthorizeRequest authorizeRequest)
134215
return Mono.fromCallable(() -> mapper.apply(authorizeRequest));
135216
}
136217
}
218+
137219
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright 2002-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.security.oauth2.client;
17+
18+
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
19+
import org.springframework.security.oauth2.core.OAuth2Error;
20+
import org.springframework.util.Assert;
21+
22+
/**
23+
* This exception is thrown on the client side when an attempt to authenticate
24+
* or authorize an OAuth 2.0 client fails.
25+
*
26+
* @author Phil Clay
27+
* @since 5.3
28+
* @see OAuth2AuthorizedClient
29+
*/
30+
public class ClientAuthorizationException extends OAuth2AuthorizationException {
31+
32+
private final String clientRegistrationId;
33+
34+
/**
35+
* Constructs a {@code ClientAuthorizationException} using the provided parameters.
36+
*
37+
* @param error the {@link OAuth2Error OAuth 2.0 Error}
38+
* @param clientRegistrationId the identifier for the client's registration
39+
*/
40+
public ClientAuthorizationException(OAuth2Error error, String clientRegistrationId) {
41+
this(error, clientRegistrationId, error.toString());
42+
}
43+
/**
44+
* Constructs a {@code ClientAuthorizationException} using the provided parameters.
45+
*
46+
* @param error the {@link OAuth2Error OAuth 2.0 Error}
47+
* @param clientRegistrationId the identifier for the client's registration
48+
* @param message the exception message
49+
*/
50+
public ClientAuthorizationException(OAuth2Error error, String clientRegistrationId, String message) {
51+
super(error, message);
52+
Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty");
53+
this.clientRegistrationId = clientRegistrationId;
54+
}
55+
56+
/**
57+
* Constructs a {@code ClientAuthorizationException} using the provided parameters.
58+
*
59+
* @param error the {@link OAuth2Error OAuth 2.0 Error}
60+
* @param clientRegistrationId the identifier for the client's registration
61+
* @param cause the root cause
62+
*/
63+
public ClientAuthorizationException(OAuth2Error error, String clientRegistrationId, Throwable cause) {
64+
this(error, clientRegistrationId, error.toString(), cause);
65+
}
66+
67+
/**
68+
* Constructs a {@code ClientAuthorizationException} using the provided parameters.
69+
*
70+
* @param error the {@link OAuth2Error OAuth 2.0 Error}
71+
* @param clientRegistrationId the identifier for the client's registration
72+
* @param message the exception message
73+
* @param cause the root cause
74+
*/
75+
public ClientAuthorizationException(OAuth2Error error, String clientRegistrationId, String message, Throwable cause) {
76+
super(error, message, cause);
77+
Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty");
78+
this.clientRegistrationId = clientRegistrationId;
79+
}
80+
81+
/**
82+
* Returns the identifier for the client's registration.
83+
*
84+
* @return the identifier for the client's registration
85+
*/
86+
public String getClientRegistrationId() {
87+
return this.clientRegistrationId;
88+
}
89+
}
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -15,9 +15,7 @@
1515
*/
1616
package org.springframework.security.oauth2.client;
1717

18-
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
1918
import org.springframework.security.oauth2.core.OAuth2Error;
20-
import org.springframework.util.Assert;
2119

2220
/**
2321
* This exception is thrown when an OAuth 2.0 Client is required
@@ -27,9 +25,8 @@
2725
* @since 5.1
2826
* @see OAuth2AuthorizedClient
2927
*/
30-
public class ClientAuthorizationRequiredException extends OAuth2AuthorizationException {
28+
public class ClientAuthorizationRequiredException extends ClientAuthorizationException {
3129
private static final String CLIENT_AUTHORIZATION_REQUIRED_ERROR_CODE = "client_authorization_required";
32-
private final String clientRegistrationId;
3330

3431
/**
3532
* Constructs a {@code ClientAuthorizationRequiredException} using the provided parameters.
@@ -38,17 +35,7 @@ public class ClientAuthorizationRequiredException extends OAuth2AuthorizationExc
3835
*/
3936
public ClientAuthorizationRequiredException(String clientRegistrationId) {
4037
super(new OAuth2Error(CLIENT_AUTHORIZATION_REQUIRED_ERROR_CODE,
41-
"Authorization required for Client Registration Id: " + clientRegistrationId, null));
42-
Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty");
43-
this.clientRegistrationId = clientRegistrationId;
44-
}
45-
46-
/**
47-
* Returns the identifier for the client's registration.
48-
*
49-
* @return the identifier for the client's registration
50-
*/
51-
public String getClientRegistrationId() {
52-
return this.clientRegistrationId;
38+
"Authorization required for Client Registration Id: " + clientRegistrationId, null),
39+
clientRegistrationId);
5340
}
5441
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2002-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.security.oauth2.client;
17+
18+
import org.springframework.security.core.Authentication;
19+
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
20+
import reactor.core.publisher.Mono;
21+
22+
import java.util.Map;
23+
24+
/**
25+
* Handles when an OAuth 2.0 Authorized Client
26+
* fails to authorize (or re-authorize)
27+
* via the authorization server or resource server.
28+
*
29+
* @author Phil Clay
30+
* @since 5.3
31+
*/
32+
@FunctionalInterface
33+
public interface ReactiveOAuth2AuthorizationFailureHandler {
34+
35+
/**
36+
* Called when an OAuth 2.0 Authorized Client
37+
* fails to authorize (or re-authorize)
38+
* via the authorization server or resource server.
39+
*
40+
* @param authorizationException the exception that contains details about what failed
41+
* @param principal the {@code Principal} that was attempted to be authorized
42+
* @param attributes an immutable {@code Map} of extra optional attributes present under certain conditions.
43+
* For example, this might contain a {@link org.springframework.web.server.ServerWebExchange ServerWebExchange}
44+
* if the authorization was performed within the context of a {@code ServerWebExchange}.
45+
* @return an empty {@link Mono} that completes after this handler has finished handling the event.
46+
*/
47+
Mono<Void> onAuthorizationFailure(
48+
OAuth2AuthorizationException authorizationException,
49+
Authentication principal,
50+
Map<String, Object> attributes);
51+
}

0 commit comments

Comments
 (0)