Skip to content

Commit e5fca61

Browse files
philsttrjgrandja
authored andcommitted
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 7f9715d commit e5fca61

File tree

26 files changed

+2504
-480
lines changed

26 files changed

+2504
-480
lines changed

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

Lines changed: 94 additions & 9 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,25 +17,55 @@
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.client.web.RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler;
22+
import org.springframework.security.oauth2.client.web.SaveAuthorizedClientReactiveOAuth2AuthorizationSuccessHandler;
23+
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
2024
import org.springframework.util.Assert;
25+
import org.springframework.web.server.ServerWebExchange;
2126
import reactor.core.publisher.Mono;
2227

2328
import java.util.Collections;
2429
import java.util.Map;
2530
import java.util.function.Function;
2631

2732
/**
28-
* An implementation of an {@link ReactiveOAuth2AuthorizedClientManager}
29-
* that is capable of operating outside of a {@code ServerHttpRequest} context,
33+
* An implementation of a {@link ReactiveOAuth2AuthorizedClientManager}
34+
* that is capable of operating outside of the context of a {@link ServerWebExchange},
3035
* e.g. in a scheduled/background thread and/or in the service-tier.
3136
*
32-
* <p>This is a reactive equivalent of {@link org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager}</p>
37+
* <p>(When operating <em>within</em> the context of a {@link ServerWebExchange},
38+
* use {@link DefaultReactiveOAuth2AuthorizedClientManager} instead.)</p>
39+
*
40+
* <p>This is a reactive equivalent of {@link org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager}.</p>
41+
*
42+
* <h2>Authorized Client Persistence</h2>
43+
*
44+
* <p>This client manager utilizes a {@link ReactiveOAuth2AuthorizedClientService}
45+
* to persist {@link OAuth2AuthorizedClient}s.</p>
46+
*
47+
* <p>By default, when an authorization attempt succeeds, the {@link OAuth2AuthorizedClient}
48+
* will be saved in the authorized client service.
49+
* This functionality can be changed by configuring a custom {@link ReactiveOAuth2AuthorizationSuccessHandler}
50+
* via {@link #setAuthorizationSuccessHandler(ReactiveOAuth2AuthorizationSuccessHandler)}.</p>
51+
*
52+
* <p>By default, when an authorization attempt fails due to an
53+
* {@value org.springframework.security.oauth2.core.OAuth2ErrorCodes#INVALID_GRANT} error,
54+
* the previously saved {@link OAuth2AuthorizedClient}
55+
* will be removed from the authorized client service.
56+
* (The {@value org.springframework.security.oauth2.core.OAuth2ErrorCodes#INVALID_GRANT}
57+
* error generally occurs when a refresh token that is no longer valid
58+
* is used to retrieve a new access token.)
59+
* This functionality can be changed by configuring a custom {@link ReactiveOAuth2AuthorizationFailureHandler}
60+
* via {@link #setAuthorizationFailureHandler(ReactiveOAuth2AuthorizationFailureHandler)}.</p>
3361
*
3462
* @author Ankur Pathak
3563
* @author Phil Clay
3664
* @see ReactiveOAuth2AuthorizedClientManager
3765
* @see ReactiveOAuth2AuthorizedClientProvider
3866
* @see ReactiveOAuth2AuthorizedClientService
67+
* @see ReactiveOAuth2AuthorizationSuccessHandler
68+
* @see ReactiveOAuth2AuthorizationFailureHandler
3969
* @since 5.2.2
4070
*/
4171
public final class AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager
@@ -45,6 +75,8 @@ public final class AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager
4575
private final ReactiveOAuth2AuthorizedClientService authorizedClientService;
4676
private ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = context -> Mono.empty();
4777
private Function<OAuth2AuthorizeRequest, Mono<Map<String, Object>>> contextAttributesMapper = new DefaultContextAttributesMapper();
78+
private ReactiveOAuth2AuthorizationSuccessHandler authorizationSuccessHandler;
79+
private ReactiveOAuth2AuthorizationFailureHandler authorizationFailureHandler;
4880

4981
/**
5082
* Constructs an {@code AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager} using the provided parameters.
@@ -59,14 +91,16 @@ public AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
5991
Assert.notNull(authorizedClientService, "authorizedClientService cannot be null");
6092
this.clientRegistrationRepository = clientRegistrationRepository;
6193
this.authorizedClientService = authorizedClientService;
94+
this.authorizationSuccessHandler = new SaveAuthorizedClientReactiveOAuth2AuthorizationSuccessHandler(authorizedClientService);
95+
this.authorizationFailureHandler = new RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler(authorizedClientService);
6296
}
6397

6498
@Override
6599
public Mono<OAuth2AuthorizedClient> authorize(OAuth2AuthorizeRequest authorizeRequest) {
66100
Assert.notNull(authorizeRequest, "authorizeRequest cannot be null");
67101

68102
return createAuthorizationContext(authorizeRequest)
69-
.flatMap(this::authorizeAndSave);
103+
.flatMap(authorizationContext -> authorize(authorizationContext, authorizeRequest.getPrincipal()));
70104
}
71105

72106
private Mono<OAuth2AuthorizationContext> createAuthorizationContext(OAuth2AuthorizeRequest authorizeRequest) {
@@ -90,13 +124,34 @@ private Mono<OAuth2AuthorizationContext> createAuthorizationContext(OAuth2Author
90124
}));
91125
}
92126

93-
private Mono<OAuth2AuthorizedClient> authorizeAndSave(OAuth2AuthorizationContext authorizationContext) {
127+
/**
128+
* Performs authorization and then delegates to either the {@link #authorizationSuccessHandler}
129+
* or {@link #authorizationFailureHandler}, depending on the authorization result.
130+
*
131+
* @param authorizationContext the context to authorize
132+
* @param principal the principle to authorize
133+
* @return a {@link Mono} that emits the authorized client after the authorization attempt succeeds
134+
* and the {@link #authorizationSuccessHandler} has completed,
135+
* or completes with an exception after the authorization attempt fails
136+
* and the {@link #authorizationFailureHandler} has completed
137+
*/
138+
private Mono<OAuth2AuthorizedClient> authorize(
139+
OAuth2AuthorizationContext authorizationContext,
140+
Authentication principal) {
94141
return this.authorizedClientProvider.authorize(authorizationContext)
95-
.flatMap(authorizedClient -> this.authorizedClientService.saveAuthorizedClient(
142+
// Delegate to the authorizationSuccessHandler of the successful authorization
143+
.flatMap(authorizedClient -> this.authorizationSuccessHandler.onAuthorizationSuccess(
96144
authorizedClient,
97-
authorizationContext.getPrincipal())
145+
principal,
146+
Collections.emptyMap())
98147
.thenReturn(authorizedClient))
99-
.switchIfEmpty(Mono.defer(()-> Mono.justOrEmpty(authorizationContext.getAuthorizedClient())));
148+
// Delegate to the authorizationFailureHandler of the failed authorization
149+
.onErrorResume(OAuth2AuthorizationException.class, authorizationException -> this.authorizationFailureHandler.onAuthorizationFailure(
150+
authorizationException,
151+
principal,
152+
Collections.emptyMap())
153+
.then(Mono.error(authorizationException)))
154+
.switchIfEmpty(Mono.defer(() -> Mono.justOrEmpty(authorizationContext.getAuthorizedClient())));
100155
}
101156

102157
/**
@@ -121,6 +176,36 @@ public void setContextAttributesMapper(Function<OAuth2AuthorizeRequest, Mono<Map
121176
this.contextAttributesMapper = contextAttributesMapper;
122177
}
123178

179+
/**
180+
* Sets the handler that handles successful authorizations.
181+
*
182+
* <p>A {@link SaveAuthorizedClientReactiveOAuth2AuthorizationSuccessHandler}
183+
* is used by default.</p>
184+
*
185+
* @param authorizationSuccessHandler the handler that handles successful authorizations.
186+
* @see SaveAuthorizedClientReactiveOAuth2AuthorizationSuccessHandler
187+
* @since 5.3
188+
*/
189+
public void setAuthorizationSuccessHandler(ReactiveOAuth2AuthorizationSuccessHandler authorizationSuccessHandler) {
190+
Assert.notNull(authorizationSuccessHandler, "authorizationSuccessHandler cannot be null");
191+
this.authorizationSuccessHandler = authorizationSuccessHandler;
192+
}
193+
194+
/**
195+
* Sets the handler that handles authorization failures.
196+
*
197+
* <p>A {@link RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler}
198+
* is used by default.</p>
199+
*
200+
* @param authorizationFailureHandler the handler that handles authorization failures.
201+
* @see RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler
202+
* @since 5.3
203+
*/
204+
public void setAuthorizationFailureHandler(ReactiveOAuth2AuthorizationFailureHandler authorizationFailureHandler) {
205+
Assert.notNull(authorizationFailureHandler, "authorizationFailureHandler cannot be null");
206+
this.authorizationFailureHandler = authorizationFailureHandler;
207+
}
208+
124209
/**
125210
* The default implementation of the {@link #setContextAttributesMapper(Function) contextAttributesMapper}.
126211
*/
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 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 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)