Skip to content

Commit f9fc7d3

Browse files
committed
OAuth2AuthorizedClientProvider implementations load/save OAuth2AuthorizedClient
- spring-projects#59 Redesign OAuth2AuthorizedClientProvider to load/save OAuth2AuthorizedClient - spring-projects#60 ClientCredentialsOAuth2AuthorizedClientProvider should load/save OAuth2AuthorizedClient - spring-projects#61 RefreshTokenOAuth2AuthorizedClientProvider should load/save OAuth2AuthorizedClient - spring-projects#62 Refactor and use redesigned OAuth2AuthorizedClientProvider implementations
1 parent 21d8528 commit f9fc7d3

19 files changed

+957
-342
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@
2222
import org.springframework.core.type.AnnotationMetadata;
2323
import org.springframework.security.oauth2.client.AuthorizationCodeOAuth2AuthorizedClientProvider;
2424
import org.springframework.security.oauth2.client.ClientCredentialsOAuth2AuthorizedClientProvider;
25+
import org.springframework.security.oauth2.client.DefaultOAuth2AuthorizedClientProvider;
2526
import org.springframework.security.oauth2.client.DelegatingOAuth2AuthorizedClientProvider;
26-
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider;
27+
import org.springframework.security.oauth2.client.RefreshTokenOAuth2AuthorizedClientProvider;
2728
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
2829
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
2930
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
@@ -80,8 +81,16 @@ public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentRes
8081
new ClientCredentialsOAuth2AuthorizedClientProvider(
8182
this.clientRegistrationRepository, this.authorizedClientRepository);
8283
clientCredentialsAuthorizedClientProvider.setAccessTokenResponseClient(this.accessTokenResponseClient);
83-
OAuth2AuthorizedClientProvider authorizedClientProvider = new DelegatingOAuth2AuthorizedClientProvider(
84-
new AuthorizationCodeOAuth2AuthorizedClientProvider(), clientCredentialsAuthorizedClientProvider);
84+
AuthorizationCodeOAuth2AuthorizedClientProvider authorizationCodeAuthorizedClientProvider =
85+
new AuthorizationCodeOAuth2AuthorizedClientProvider(
86+
this.clientRegistrationRepository, this.authorizedClientRepository);
87+
RefreshTokenOAuth2AuthorizedClientProvider refreshTokenAuthorizedClientProvider =
88+
new RefreshTokenOAuth2AuthorizedClientProvider(
89+
this.clientRegistrationRepository, this.authorizedClientRepository);
90+
DelegatingOAuth2AuthorizedClientProvider authorizedClientProvider = new DelegatingOAuth2AuthorizedClientProvider(
91+
authorizationCodeAuthorizedClientProvider, refreshTokenAuthorizedClientProvider, clientCredentialsAuthorizedClientProvider);
92+
authorizedClientProvider.setDefaultAuthorizedClientProvider(
93+
new DefaultOAuth2AuthorizedClientProvider(this.clientRegistrationRepository, this.authorizedClientRepository));
8594
authorizedClientArgumentResolver.setAuthorizedClientProvider(authorizedClientProvider);
8695
}
8796
argumentResolvers.add(authorizedClientArgumentResolver);

config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfigurationTests.java

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,6 @@
1515
*/
1616
package org.springframework.security.config.annotation.web.configuration;
1717

18-
import static org.assertj.core.api.Assertions.assertThatThrownBy;
19-
import static org.mockito.ArgumentMatchers.any;
20-
import static org.mockito.ArgumentMatchers.eq;
21-
import static org.mockito.Mockito.mock;
22-
import static org.mockito.Mockito.times;
23-
import static org.mockito.Mockito.verify;
24-
import static org.mockito.Mockito.verifyZeroInteractions;
25-
import static org.mockito.Mockito.when;
26-
import static org.springframework.security.oauth2.client.registration.TestClientRegistrations.clientCredentials;
27-
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
28-
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
29-
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
30-
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
31-
32-
import javax.servlet.http.HttpServletRequest;
3318
import org.junit.Rule;
3419
import org.junit.Test;
3520
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
@@ -53,6 +38,19 @@
5338
import org.springframework.web.bind.annotation.RestController;
5439
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
5540

41+
import javax.servlet.http.HttpServletRequest;
42+
43+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
44+
import static org.mockito.ArgumentMatchers.any;
45+
import static org.mockito.ArgumentMatchers.eq;
46+
import static org.mockito.Mockito.*;
47+
import static org.springframework.security.oauth2.client.registration.TestClientRegistrations.clientCredentials;
48+
import static org.springframework.security.oauth2.client.registration.TestClientRegistrations.clientRegistration;
49+
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
50+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
51+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
52+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
53+
5654
/**
5755
* Tests for {@link OAuth2ClientConfiguration}.
5856
*
@@ -72,6 +70,9 @@ public void requestWhenAuthorizedClientFoundThenMethodArgumentResolved() throws
7270
TestingAuthenticationToken authentication = new TestingAuthenticationToken(principalName, "password");
7371

7472
ClientRegistrationRepository clientRegistrationRepository = mock(ClientRegistrationRepository.class);
73+
ClientRegistration clientRegistration = clientRegistration().registrationId(clientRegistrationId).build();
74+
when(clientRegistrationRepository.findByRegistrationId(eq(clientRegistrationId))).thenReturn(clientRegistration);
75+
7576
OAuth2AuthorizedClientRepository authorizedClientRepository = mock(OAuth2AuthorizedClientRepository.class);
7677
OAuth2AuthorizedClient authorizedClient = mock(OAuth2AuthorizedClient.class);
7778
when(authorizedClientRepository.loadAuthorizedClient(

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

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,15 @@
1616
package org.springframework.security.oauth2.client;
1717

1818
import org.springframework.lang.Nullable;
19+
import org.springframework.security.oauth2.client.registration.ClientRegistration;
20+
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
21+
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
1922
import org.springframework.security.oauth2.core.AuthorizationGrantType;
2023
import org.springframework.util.Assert;
2124

25+
import javax.servlet.http.HttpServletRequest;
26+
import javax.servlet.http.HttpServletResponse;
27+
2228
/**
2329
* An implementation of an {@link OAuth2AuthorizedClientProvider}
2430
* for the {@link AuthorizationGrantType#AUTHORIZATION_CODE authorization_code} grant.
@@ -28,16 +34,66 @@
2834
* @see OAuth2AuthorizedClientProvider
2935
*/
3036
public final class AuthorizationCodeOAuth2AuthorizedClientProvider implements OAuth2AuthorizedClientProvider {
37+
private static final String HTTP_SERVLET_REQUEST_ATTRIBUTE_NAME = HttpServletRequest.class.getName();
38+
private static final String HTTP_SERVLET_RESPONSE_ATTRIBUTE_NAME = HttpServletResponse.class.getName();
39+
private final ClientRegistrationRepository clientRegistrationRepository;
40+
private final OAuth2AuthorizedClientRepository authorizedClientRepository;
41+
42+
/**
43+
* Constructs an {@code AuthorizationCodeOAuth2AuthorizedClientProvider} using the provided parameters.
44+
*
45+
* @param clientRegistrationRepository the repository of client registrations
46+
* @param authorizedClientRepository the repository of authorized clients
47+
*/
48+
public AuthorizationCodeOAuth2AuthorizedClientProvider(ClientRegistrationRepository clientRegistrationRepository,
49+
OAuth2AuthorizedClientRepository authorizedClientRepository) {
50+
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
51+
Assert.notNull(authorizedClientRepository, "authorizedClientRepository cannot be null");
52+
this.clientRegistrationRepository = clientRegistrationRepository;
53+
this.authorizedClientRepository = authorizedClientRepository;
54+
}
3155

56+
/**
57+
* Attempt to authorize the {@link OAuth2AuthorizationContext#getClientRegistrationId() client} in the provided {@code context}.
58+
* Returns {@code null} if authorization is not supported,
59+
* e.g. the client's {@link ClientRegistration#getAuthorizationGrantType() authorization grant type}
60+
* is not {@link AuthorizationGrantType#AUTHORIZATION_CODE authorization_code} OR the client is already authorized.
61+
*
62+
* <p>
63+
* The following {@link OAuth2AuthorizationContext#getAttributes() context attributes} are supported:
64+
* <ol>
65+
* <li>{@code "javax.servlet.http.HttpServletRequest"} (required) - the {@code HttpServletRequest}</li>
66+
* <li>{@code "javax.servlet.http.HttpServletResponse"} (required) - the {@code HttpServletResponse}</li>
67+
* </ol>
68+
*
69+
* @param context the context that holds authorization-specific state for the client
70+
* @return the {@link OAuth2AuthorizedClient} or {@code null} if authorization is not supported
71+
*/
3272
@Override
3373
@Nullable
3474
public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) {
3575
Assert.notNull(context, "context cannot be null");
36-
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(context.getClientRegistration().getAuthorizationGrantType()) &&
37-
context.authorizationRequested()) {
76+
77+
HttpServletRequest request = context.getAttribute(HTTP_SERVLET_REQUEST_ATTRIBUTE_NAME);
78+
HttpServletResponse response = context.getAttribute(HTTP_SERVLET_RESPONSE_ATTRIBUTE_NAME);
79+
Assert.notNull(request, "The context attribute cannot be null '" + HTTP_SERVLET_REQUEST_ATTRIBUTE_NAME + "'");
80+
Assert.notNull(response, "The context attribute cannot be null '" + HTTP_SERVLET_RESPONSE_ATTRIBUTE_NAME + "'");
81+
82+
String clientRegistrationId = context.getClientRegistrationId();
83+
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId);
84+
Assert.notNull(clientRegistration, "Could not find ClientRegistration with id '" + clientRegistrationId + "'");
85+
86+
if (!AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) {
87+
return null;
88+
}
89+
90+
OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository.loadAuthorizedClient(
91+
clientRegistrationId, context.getPrincipal(), request);
92+
if (authorizedClient == null) {
3893
// ClientAuthorizationRequiredException is caught by OAuth2AuthorizationRequestRedirectFilter which initiates authorization
39-
throw new ClientAuthorizationRequiredException(context.getClientRegistration().getRegistrationId());
94+
throw new ClientAuthorizationRequiredException(clientRegistrationId);
4095
}
96+
4197
return null;
4298
}
4399
}

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

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,15 @@
2222
import org.springframework.security.oauth2.client.registration.ClientRegistration;
2323
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
2424
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
25+
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
2526
import org.springframework.security.oauth2.core.AuthorizationGrantType;
2627
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
2728
import org.springframework.util.Assert;
2829

2930
import javax.servlet.http.HttpServletRequest;
3031
import javax.servlet.http.HttpServletResponse;
32+
import java.time.Duration;
33+
import java.time.Instant;
3134

3235
/**
3336
* An implementation of an {@link OAuth2AuthorizedClientProvider}
@@ -45,6 +48,7 @@ public final class ClientCredentialsOAuth2AuthorizedClientProvider implements OA
4548
private final OAuth2AuthorizedClientRepository authorizedClientRepository;
4649
private OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient =
4750
new DefaultClientCredentialsTokenResponseClient();
51+
private Duration clockSkew = Duration.ofSeconds(60);
4852

4953
/**
5054
* Constructs a {@code ClientCredentialsOAuth2AuthorizedClientProvider} using the provided parameters.
@@ -61,10 +65,11 @@ public ClientCredentialsOAuth2AuthorizedClientProvider(ClientRegistrationReposit
6165
}
6266

6367
/**
64-
* Attempt to authorize (or re-authorize) the {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided {@code context}.
68+
* Attempt to authorize (or re-authorize) the {@link OAuth2AuthorizationContext#getClientRegistrationId() client} in the provided {@code context}.
6569
* Returns {@code null} if authorization (or re-authorization) is not supported,
6670
* e.g. the client's {@link ClientRegistration#getAuthorizationGrantType() authorization grant type}
67-
* is not {@link AuthorizationGrantType#CLIENT_CREDENTIALS client_credentials}.
71+
* is not {@link AuthorizationGrantType#CLIENT_CREDENTIALS client_credentials} OR
72+
* the {@link OAuth2AuthorizedClient#getAccessToken() access token} is not expired.
6873
*
6974
* <p>
7075
* The following {@link OAuth2AuthorizationContext#getAttributes() context attributes} are supported:
@@ -80,15 +85,26 @@ public ClientCredentialsOAuth2AuthorizedClientProvider(ClientRegistrationReposit
8085
@Nullable
8186
public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) {
8287
Assert.notNull(context, "context cannot be null");
83-
if (!AuthorizationGrantType.CLIENT_CREDENTIALS.equals(context.getClientRegistration().getAuthorizationGrantType())) {
84-
return null;
85-
}
8688

8789
HttpServletRequest request = context.getAttribute(HTTP_SERVLET_REQUEST_ATTRIBUTE_NAME);
8890
HttpServletResponse response = context.getAttribute(HTTP_SERVLET_RESPONSE_ATTRIBUTE_NAME);
8991
Assert.notNull(request, "The context attribute cannot be null '" + HTTP_SERVLET_REQUEST_ATTRIBUTE_NAME + "'");
9092
Assert.notNull(response, "The context attribute cannot be null '" + HTTP_SERVLET_RESPONSE_ATTRIBUTE_NAME + "'");
9193

94+
String clientRegistrationId = context.getClientRegistrationId();
95+
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId);
96+
Assert.notNull(clientRegistration, "Could not find ClientRegistration with id '" + clientRegistrationId + "'");
97+
98+
if (!AuthorizationGrantType.CLIENT_CREDENTIALS.equals(clientRegistration.getAuthorizationGrantType())) {
99+
return null;
100+
}
101+
102+
OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository.loadAuthorizedClient(
103+
clientRegistrationId, context.getPrincipal(), request);
104+
if (authorizedClient != null && !hasTokenExpired(authorizedClient.getAccessToken())) {
105+
return null;
106+
}
107+
92108
// As per spec, in section 4.4.3 Access Token Response
93109
// https://tools.ietf.org/html/rfc6749#section-4.4.3
94110
// A refresh token SHOULD NOT be included.
@@ -97,24 +113,23 @@ public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) {
97113
// is the same as acquiring a new access token (authorization).
98114

99115
OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest =
100-
new OAuth2ClientCredentialsGrantRequest(context.getClientRegistration());
116+
new OAuth2ClientCredentialsGrantRequest(clientRegistration);
101117
OAuth2AccessTokenResponse tokenResponse =
102118
this.accessTokenResponseClient.getTokenResponse(clientCredentialsGrantRequest);
103119

104-
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(
105-
context.getClientRegistration(),
106-
context.getPrincipal().getName(),
107-
tokenResponse.getAccessToken());
120+
authorizedClient = new OAuth2AuthorizedClient(
121+
clientRegistration, context.getPrincipal().getName(), tokenResponse.getAccessToken());
108122

109123
this.authorizedClientRepository.saveAuthorizedClient(
110-
authorizedClient,
111-
context.getPrincipal(),
112-
request,
113-
response);
124+
authorizedClient, context.getPrincipal(), request, response);
114125

115126
return authorizedClient;
116127
}
117128

129+
private boolean hasTokenExpired(AbstractOAuth2Token token) {
130+
return token.getExpiresAt().isBefore(Instant.now().minus(this.clockSkew));
131+
}
132+
118133
/**
119134
* Sets the client used when requesting an access token credential at the Token Endpoint for the {@code client_credentials} grant.
120135
*
@@ -124,4 +139,17 @@ public void setAccessTokenResponseClient(OAuth2AccessTokenResponseClient<OAuth2C
124139
Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");
125140
this.accessTokenResponseClient = accessTokenResponseClient;
126141
}
142+
143+
/**
144+
* Sets the maximum acceptable clock skew, which is used when checking the
145+
* {@link OAuth2AuthorizedClient#getAccessToken() access token} expiry. The default is 60 seconds.
146+
* An access token is considered expired if it's before {@code Instant.now() - clockSkew}.
147+
*
148+
* @param clockSkew the maximum acceptable clock skew
149+
*/
150+
public void setClockSkew(Duration clockSkew) {
151+
Assert.notNull(clockSkew, "clockSkew cannot be null");
152+
Assert.isTrue(clockSkew.getSeconds() >= 0, "clockSkew must be >= 0");
153+
this.clockSkew = clockSkew;
154+
}
127155
}

0 commit comments

Comments
 (0)