Skip to content

Commit 69156b7

Browse files
committed
Add OAuth2Authorization success/failure handlers
Fixes gh-7840
1 parent 1b68cdb commit 69156b7

15 files changed

+1349
-101
lines changed

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

Lines changed: 80 additions & 5 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.
@@ -19,6 +19,11 @@
1919
import org.springframework.security.core.Authentication;
2020
import org.springframework.security.oauth2.client.registration.ClientRegistration;
2121
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
22+
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
23+
import org.springframework.security.oauth2.client.web.RemoveAuthorizedClientOAuth2AuthorizationFailureHandler;
24+
import org.springframework.security.oauth2.client.web.SaveAuthorizedClientOAuth2AuthorizationSuccessHandler;
25+
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
26+
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
2227
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
2328
import org.springframework.util.Assert;
2429
import org.springframework.util.CollectionUtils;
@@ -31,20 +36,50 @@
3136

3237
/**
3338
* An implementation of an {@link OAuth2AuthorizedClientManager}
34-
* that is capable of operating outside of a {@code HttpServletRequest} context,
39+
* that is capable of operating outside of the context of a {@code HttpServletRequest},
3540
* e.g. in a scheduled/background thread and/or in the service-tier.
3641
*
42+
* <p>
43+
* (When operating <em>within</em> the context of a {@code HttpServletRequest},
44+
* use {@link DefaultOAuth2AuthorizedClientManager} instead.)
45+
*
46+
* <h2>Authorized Client Persistence</h2>
47+
*
48+
* <p>
49+
* This manager utilizes an {@link OAuth2AuthorizedClientService}
50+
* to persist {@link OAuth2AuthorizedClient}s.
51+
*
52+
* <p>
53+
* By default, when an authorization attempt succeeds, the {@link OAuth2AuthorizedClient}
54+
* will be saved in the {@link OAuth2AuthorizedClientService}.
55+
* This functionality can be changed by configuring a custom {@link OAuth2AuthorizationSuccessHandler}
56+
* via {@link #setAuthorizationSuccessHandler(OAuth2AuthorizationSuccessHandler)}.
57+
*
58+
* <p>
59+
* By default, when an authorization attempt fails due to an
60+
* {@value OAuth2ErrorCodes#INVALID_GRANT} error,
61+
* the previously saved {@link OAuth2AuthorizedClient}
62+
* will be removed from the {@link OAuth2AuthorizedClientService}.
63+
* (The {@value OAuth2ErrorCodes#INVALID_GRANT} error can occur
64+
* when a refresh token that is no longer valid is used to retrieve a new access token.)
65+
* This functionality can be changed by configuring a custom {@link OAuth2AuthorizationFailureHandler}
66+
* via {@link #setAuthorizationFailureHandler(OAuth2AuthorizationFailureHandler)}.
67+
*
3768
* @author Joe Grandja
3869
* @since 5.2
3970
* @see OAuth2AuthorizedClientManager
4071
* @see OAuth2AuthorizedClientProvider
4172
* @see OAuth2AuthorizedClientService
73+
* @see OAuth2AuthorizationSuccessHandler
74+
* @see OAuth2AuthorizationFailureHandler
4275
*/
4376
public final class AuthorizedClientServiceOAuth2AuthorizedClientManager implements OAuth2AuthorizedClientManager {
4477
private final ClientRegistrationRepository clientRegistrationRepository;
4578
private final OAuth2AuthorizedClientService authorizedClientService;
4679
private OAuth2AuthorizedClientProvider authorizedClientProvider = context -> null;
47-
private Function<OAuth2AuthorizeRequest, Map<String, Object>> contextAttributesMapper = new DefaultContextAttributesMapper();
80+
private Function<OAuth2AuthorizeRequest, Map<String, Object>> contextAttributesMapper;
81+
private OAuth2AuthorizationSuccessHandler authorizationSuccessHandler;
82+
private OAuth2AuthorizationFailureHandler authorizationFailureHandler;
4883

4984
/**
5085
* Constructs an {@code AuthorizedClientServiceOAuth2AuthorizedClientManager} using the provided parameters.
@@ -58,6 +93,9 @@ public AuthorizedClientServiceOAuth2AuthorizedClientManager(ClientRegistrationRe
5893
Assert.notNull(authorizedClientService, "authorizedClientService cannot be null");
5994
this.clientRegistrationRepository = clientRegistrationRepository;
6095
this.authorizedClientService = authorizedClientService;
96+
this.contextAttributesMapper = new DefaultContextAttributesMapper();
97+
this.authorizationSuccessHandler = new SaveAuthorizedClientOAuth2AuthorizationSuccessHandler(authorizedClientService);
98+
this.authorizationFailureHandler = new RemoveAuthorizedClientOAuth2AuthorizationFailureHandler(authorizedClientService);
6199
}
62100

63101
@Nullable
@@ -92,9 +130,16 @@ public OAuth2AuthorizedClient authorize(OAuth2AuthorizeRequest authorizeRequest)
92130
})
93131
.build();
94132

95-
authorizedClient = this.authorizedClientProvider.authorize(authorizationContext);
133+
try {
134+
authorizedClient = this.authorizedClientProvider.authorize(authorizationContext);
135+
} catch (OAuth2AuthorizationException ex) {
136+
this.authorizationFailureHandler.onAuthorizationFailure(ex, principal, Collections.emptyMap());
137+
throw ex;
138+
}
139+
96140
if (authorizedClient != null) {
97-
this.authorizedClientService.saveAuthorizedClient(authorizedClient, principal);
141+
this.authorizationSuccessHandler.onAuthorizationSuccess(
142+
authorizedClient, principal, Collections.emptyMap());
98143
} else {
99144
// In the case of re-authorization, the returned `authorizedClient` may be null if re-authorization is not supported.
100145
// For these cases, return the provided `authorizationContext.authorizedClient`.
@@ -128,6 +173,36 @@ public void setContextAttributesMapper(Function<OAuth2AuthorizeRequest, Map<Stri
128173
this.contextAttributesMapper = contextAttributesMapper;
129174
}
130175

176+
/**
177+
* Sets the {@link OAuth2AuthorizationSuccessHandler} that handles successful authorizations.
178+
*
179+
* <p>
180+
* A {@link SaveAuthorizedClientOAuth2AuthorizationSuccessHandler} is used by default.
181+
*
182+
* @param authorizationSuccessHandler the {@link OAuth2AuthorizationSuccessHandler} that handles successful authorizations
183+
* @see SaveAuthorizedClientOAuth2AuthorizationSuccessHandler
184+
* @since 5.3
185+
*/
186+
public void setAuthorizationSuccessHandler(OAuth2AuthorizationSuccessHandler authorizationSuccessHandler) {
187+
Assert.notNull(authorizationSuccessHandler, "authorizationSuccessHandler cannot be null");
188+
this.authorizationSuccessHandler = authorizationSuccessHandler;
189+
}
190+
191+
/**
192+
* Sets the {@link OAuth2AuthorizationFailureHandler} that handles authorization failures.
193+
*
194+
* <p>
195+
* A {@link RemoveAuthorizedClientOAuth2AuthorizationFailureHandler} is used by default.
196+
*
197+
* @param authorizationFailureHandler the {@link OAuth2AuthorizationFailureHandler} that handles authorization failures
198+
* @see RemoveAuthorizedClientOAuth2AuthorizationFailureHandler
199+
* @since 5.3
200+
*/
201+
public void setAuthorizationFailureHandler(OAuth2AuthorizationFailureHandler authorizationFailureHandler) {
202+
Assert.notNull(authorizationFailureHandler, "authorizationFailureHandler cannot be null");
203+
this.authorizationFailureHandler = authorizationFailureHandler;
204+
}
205+
131206
/**
132207
* The default implementation of the {@link #setContextAttributesMapper(Function) contextAttributesMapper}.
133208
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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+
21+
import java.util.Map;
22+
23+
/**
24+
* Handles when an OAuth 2.0 Client fails to authorize (or re-authorize)
25+
* via the Authorization Server or Resource Server.
26+
*
27+
* @author Joe Grandja
28+
* @since 5.3
29+
* @see OAuth2AuthorizedClient
30+
* @see OAuth2AuthorizedClientManager
31+
*/
32+
@FunctionalInterface
33+
public interface OAuth2AuthorizationFailureHandler {
34+
35+
/**
36+
* Called when an OAuth 2.0 Client fails to authorize (or re-authorize)
37+
* via the Authorization Server or Resource Server.
38+
*
39+
* @param authorizationException the exception that contains details about what failed
40+
* @param principal the {@code Principal} associated with the attempted authorization
41+
* @param attributes an immutable {@code Map} of (optional) attributes present under certain conditions.
42+
* For example, this might contain a {@code javax.servlet.http.HttpServletRequest}
43+
* and {@code javax.servlet.http.HttpServletResponse} if the authorization was performed
44+
* within the context of a {@code javax.servlet.ServletContext}.
45+
*/
46+
void onAuthorizationFailure(OAuth2AuthorizationException authorizationException,
47+
Authentication principal, Map<String, Object> attributes);
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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+
20+
import java.util.Map;
21+
22+
/**
23+
* Handles when an OAuth 2.0 Client has been successfully
24+
* authorized (or re-authorized) via the Authorization Server.
25+
*
26+
* @author Joe Grandja
27+
* @since 5.3
28+
* @see OAuth2AuthorizedClient
29+
* @see OAuth2AuthorizedClientManager
30+
*/
31+
@FunctionalInterface
32+
public interface OAuth2AuthorizationSuccessHandler {
33+
34+
/**
35+
* Called when an OAuth 2.0 Client has been successfully
36+
* authorized (or re-authorized) via the Authorization Server.
37+
*
38+
* @param authorizedClient the client that was successfully authorized (or re-authorized)
39+
* @param principal the {@code Principal} associated with the authorized client
40+
* @param attributes an immutable {@code Map} of (optional) attributes present under certain conditions.
41+
* For example, this might contain a {@code javax.servlet.http.HttpServletRequest}
42+
* and {@code javax.servlet.http.HttpServletResponse} if the authorization was performed
43+
* within the context of a {@code javax.servlet.ServletContext}.
44+
*/
45+
void onAuthorizationSuccess(OAuth2AuthorizedClient authorizedClient,
46+
Authentication principal, Map<String, Object> attributes);
47+
}

oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultAuthorizationCodeTokenResponseClient.java

Lines changed: 19 additions & 5 deletions
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.
@@ -20,16 +20,17 @@
2020
import org.springframework.http.ResponseEntity;
2121
import org.springframework.http.converter.FormHttpMessageConverter;
2222
import org.springframework.http.converter.HttpMessageConverter;
23+
import org.springframework.security.oauth2.client.ClientAuthorizationException;
2324
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
2425
import org.springframework.security.oauth2.core.AuthorizationGrantType;
25-
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
2626
import org.springframework.security.oauth2.core.OAuth2Error;
2727
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
2828
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
2929
import org.springframework.util.Assert;
3030
import org.springframework.util.CollectionUtils;
3131
import org.springframework.web.client.ResponseErrorHandler;
3232
import org.springframework.web.client.RestClientException;
33+
import org.springframework.web.client.RestClientResponseException;
3334
import org.springframework.web.client.RestOperations;
3435
import org.springframework.web.client.RestTemplate;
3536

@@ -74,9 +75,22 @@ public OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizationCodeGrantRe
7475
try {
7576
response = this.restOperations.exchange(request, OAuth2AccessTokenResponse.class);
7677
} catch (RestClientException ex) {
77-
OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE,
78-
"An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: " + ex.getMessage(), null);
79-
throw new OAuth2AuthorizationException(oauth2Error, ex);
78+
int statusCode = 500;
79+
if (ex instanceof RestClientResponseException) {
80+
statusCode = ((RestClientResponseException) ex).getRawStatusCode();
81+
}
82+
OAuth2Error oauth2Error = new OAuth2Error(
83+
INVALID_TOKEN_RESPONSE_ERROR_CODE,
84+
"An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: " + ex.getMessage(),
85+
null);
86+
String message = String.format("Error retrieving OAuth 2.0 Access Token (HTTP Status Code: %s) %s",
87+
statusCode,
88+
oauth2Error);
89+
throw new ClientAuthorizationException(
90+
oauth2Error,
91+
authorizationCodeGrantRequest.getClientRegistration().getRegistrationId(),
92+
message,
93+
ex);
8094
}
8195

8296
OAuth2AccessTokenResponse tokenResponse = response.getBody();

oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultClientCredentialsTokenResponseClient.java

Lines changed: 19 additions & 5 deletions
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.
@@ -20,16 +20,17 @@
2020
import org.springframework.http.ResponseEntity;
2121
import org.springframework.http.converter.FormHttpMessageConverter;
2222
import org.springframework.http.converter.HttpMessageConverter;
23+
import org.springframework.security.oauth2.client.ClientAuthorizationException;
2324
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
2425
import org.springframework.security.oauth2.core.AuthorizationGrantType;
25-
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
2626
import org.springframework.security.oauth2.core.OAuth2Error;
2727
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
2828
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
2929
import org.springframework.util.Assert;
3030
import org.springframework.util.CollectionUtils;
3131
import org.springframework.web.client.ResponseErrorHandler;
3232
import org.springframework.web.client.RestClientException;
33+
import org.springframework.web.client.RestClientResponseException;
3334
import org.springframework.web.client.RestOperations;
3435
import org.springframework.web.client.RestTemplate;
3536

@@ -74,9 +75,22 @@ public OAuth2AccessTokenResponse getTokenResponse(OAuth2ClientCredentialsGrantRe
7475
try {
7576
response = this.restOperations.exchange(request, OAuth2AccessTokenResponse.class);
7677
} catch (RestClientException ex) {
77-
OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE,
78-
"An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: " + ex.getMessage(), null);
79-
throw new OAuth2AuthorizationException(oauth2Error, ex);
78+
int statusCode = 500;
79+
if (ex instanceof RestClientResponseException) {
80+
statusCode = ((RestClientResponseException) ex).getRawStatusCode();
81+
}
82+
OAuth2Error oauth2Error = new OAuth2Error(
83+
INVALID_TOKEN_RESPONSE_ERROR_CODE,
84+
"An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: " + ex.getMessage(),
85+
null);
86+
String message = String.format("Error retrieving OAuth 2.0 Access Token (HTTP Status Code: %s) %s",
87+
statusCode,
88+
oauth2Error);
89+
throw new ClientAuthorizationException(
90+
oauth2Error,
91+
clientCredentialsGrantRequest.getClientRegistration().getRegistrationId(),
92+
message,
93+
ex);
8094
}
8195

8296
OAuth2AccessTokenResponse tokenResponse = response.getBody();

oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultPasswordTokenResponseClient.java

Lines changed: 19 additions & 5 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.
@@ -20,16 +20,17 @@
2020
import org.springframework.http.ResponseEntity;
2121
import org.springframework.http.converter.FormHttpMessageConverter;
2222
import org.springframework.http.converter.HttpMessageConverter;
23+
import org.springframework.security.oauth2.client.ClientAuthorizationException;
2324
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
2425
import org.springframework.security.oauth2.core.AuthorizationGrantType;
25-
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
2626
import org.springframework.security.oauth2.core.OAuth2Error;
2727
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
2828
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
2929
import org.springframework.util.Assert;
3030
import org.springframework.util.CollectionUtils;
3131
import org.springframework.web.client.ResponseErrorHandler;
3232
import org.springframework.web.client.RestClientException;
33+
import org.springframework.web.client.RestClientResponseException;
3334
import org.springframework.web.client.RestOperations;
3435
import org.springframework.web.client.RestTemplate;
3536

@@ -74,9 +75,22 @@ public OAuth2AccessTokenResponse getTokenResponse(OAuth2PasswordGrantRequest pas
7475
try {
7576
response = this.restOperations.exchange(request, OAuth2AccessTokenResponse.class);
7677
} catch (RestClientException ex) {
77-
OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE,
78-
"An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: " + ex.getMessage(), null);
79-
throw new OAuth2AuthorizationException(oauth2Error, ex);
78+
int statusCode = 500;
79+
if (ex instanceof RestClientResponseException) {
80+
statusCode = ((RestClientResponseException) ex).getRawStatusCode();
81+
}
82+
OAuth2Error oauth2Error = new OAuth2Error(
83+
INVALID_TOKEN_RESPONSE_ERROR_CODE,
84+
"An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: " + ex.getMessage(),
85+
null);
86+
String message = String.format("Error retrieving OAuth 2.0 Access Token (HTTP Status Code: %s) %s",
87+
statusCode,
88+
oauth2Error);
89+
throw new ClientAuthorizationException(
90+
oauth2Error,
91+
passwordGrantRequest.getClientRegistration().getRegistrationId(),
92+
message,
93+
ex);
8094
}
8195

8296
OAuth2AccessTokenResponse tokenResponse = response.getBody();

0 commit comments

Comments
 (0)