Skip to content

Commit c05b076

Browse files
committed
Introduce OAuth2AuthorizedClient Manager/Provider
Fixes gh-6845
1 parent 7e84540 commit c05b076

File tree

31 files changed

+3562
-364
lines changed

31 files changed

+3562
-364
lines changed

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

+17-9
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,26 @@
1515
*/
1616
package org.springframework.security.config.annotation.web.configuration;
1717

18-
import java.util.List;
19-
import java.util.Optional;
2018
import org.springframework.beans.factory.annotation.Autowired;
2119
import org.springframework.context.annotation.Configuration;
2220
import org.springframework.context.annotation.Import;
2321
import org.springframework.context.annotation.ImportSelector;
2422
import org.springframework.core.type.AnnotationMetadata;
23+
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider;
24+
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder;
2525
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
2626
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
2727
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
28+
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
2829
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
2930
import org.springframework.security.oauth2.client.web.method.annotation.OAuth2AuthorizedClientArgumentResolver;
3031
import org.springframework.util.ClassUtils;
3132
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
3233
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
3334

35+
import java.util.List;
36+
import java.util.Optional;
37+
3438
/**
3539
* {@link Configuration} for OAuth 2.0 Client support.
3640
*
@@ -67,13 +71,17 @@ static class OAuth2ClientWebMvcSecurityConfiguration implements WebMvcConfigurer
6771
@Override
6872
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
6973
if (this.clientRegistrationRepository != null && this.authorizedClientRepository != null) {
70-
OAuth2AuthorizedClientArgumentResolver authorizedClientArgumentResolver =
71-
new OAuth2AuthorizedClientArgumentResolver(
72-
this.clientRegistrationRepository, this.authorizedClientRepository);
73-
if (this.accessTokenResponseClient != null) {
74-
authorizedClientArgumentResolver.setClientCredentialsTokenResponseClient(this.accessTokenResponseClient);
75-
}
76-
argumentResolvers.add(authorizedClientArgumentResolver);
74+
OAuth2AuthorizedClientProvider authorizedClientProvider =
75+
OAuth2AuthorizedClientProviderBuilder.builder()
76+
.authorizationCode()
77+
.refreshToken()
78+
.clientCredentials(configurer ->
79+
Optional.ofNullable(this.accessTokenResponseClient).ifPresent(configurer::accessTokenResponseClient))
80+
.build();
81+
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
82+
this.clientRegistrationRepository, this.authorizedClientRepository);
83+
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
84+
argumentResolvers.add(new OAuth2AuthorizedClientArgumentResolver(authorizedClientManager));
7785
}
7886
}
7987

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

+17-15
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,8 +70,12 @@ 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);
78+
when(authorizedClient.getClientRegistration()).thenReturn(clientRegistration);
7779
when(authorizedClientRepository.loadAuthorizedClient(
7880
eq(clientRegistrationId), eq(authentication), any(HttpServletRequest.class)))
7981
.thenReturn(authorizedClient);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2002-2019 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.lang.Nullable;
19+
import org.springframework.security.oauth2.client.registration.ClientRegistration;
20+
import org.springframework.security.oauth2.core.AuthorizationGrantType;
21+
import org.springframework.util.Assert;
22+
23+
/**
24+
* An implementation of an {@link OAuth2AuthorizedClientProvider}
25+
* for the {@link AuthorizationGrantType#AUTHORIZATION_CODE authorization_code} grant.
26+
*
27+
* @author Joe Grandja
28+
* @since 5.2
29+
* @see OAuth2AuthorizedClientProvider
30+
*/
31+
public final class AuthorizationCodeOAuth2AuthorizedClientProvider implements OAuth2AuthorizedClientProvider {
32+
33+
/**
34+
* Attempt to authorize the {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided {@code context}.
35+
* Returns {@code null} if authorization is not supported,
36+
* e.g. the client's {@link ClientRegistration#getAuthorizationGrantType() authorization grant type}
37+
* is not {@link AuthorizationGrantType#AUTHORIZATION_CODE authorization_code} OR the client is already authorized.
38+
*
39+
* @param context the context that holds authorization-specific state for the client
40+
* @return the {@link OAuth2AuthorizedClient} or {@code null} if authorization is not supported
41+
*/
42+
@Override
43+
@Nullable
44+
public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) {
45+
Assert.notNull(context, "context cannot be null");
46+
47+
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(context.getClientRegistration().getAuthorizationGrantType()) &&
48+
context.getAuthorizedClient() == null) {
49+
// ClientAuthorizationRequiredException is caught by OAuth2AuthorizationRequestRedirectFilter which initiates authorization
50+
throw new ClientAuthorizationRequiredException(context.getClientRegistration().getRegistrationId());
51+
}
52+
return null;
53+
}
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright 2002-2019 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.lang.Nullable;
19+
import org.springframework.security.oauth2.client.endpoint.DefaultClientCredentialsTokenResponseClient;
20+
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
21+
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
22+
import org.springframework.security.oauth2.client.registration.ClientRegistration;
23+
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
24+
import org.springframework.security.oauth2.core.AuthorizationGrantType;
25+
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
26+
import org.springframework.util.Assert;
27+
28+
import java.time.Duration;
29+
import java.time.Instant;
30+
31+
/**
32+
* An implementation of an {@link OAuth2AuthorizedClientProvider}
33+
* for the {@link AuthorizationGrantType#CLIENT_CREDENTIALS client_credentials} grant.
34+
*
35+
* @author Joe Grandja
36+
* @since 5.2
37+
* @see OAuth2AuthorizedClientProvider
38+
* @see DefaultClientCredentialsTokenResponseClient
39+
*/
40+
public final class ClientCredentialsOAuth2AuthorizedClientProvider implements OAuth2AuthorizedClientProvider {
41+
private OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient =
42+
new DefaultClientCredentialsTokenResponseClient();
43+
private Duration clockSkew = Duration.ofSeconds(60);
44+
45+
/**
46+
* Attempt to authorize (or re-authorize) the {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided {@code context}.
47+
* Returns {@code null} if authorization (or re-authorization) is not supported,
48+
* e.g. the client's {@link ClientRegistration#getAuthorizationGrantType() authorization grant type}
49+
* is not {@link AuthorizationGrantType#CLIENT_CREDENTIALS client_credentials} OR
50+
* the {@link OAuth2AuthorizedClient#getAccessToken() access token} is not expired.
51+
*
52+
* @param context the context that holds authorization-specific state for the client
53+
* @return the {@link OAuth2AuthorizedClient} or {@code null} if authorization (or re-authorization) is not supported
54+
*/
55+
@Override
56+
@Nullable
57+
public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) {
58+
Assert.notNull(context, "context cannot be null");
59+
60+
ClientRegistration clientRegistration = context.getClientRegistration();
61+
if (!AuthorizationGrantType.CLIENT_CREDENTIALS.equals(clientRegistration.getAuthorizationGrantType())) {
62+
return null;
63+
}
64+
65+
OAuth2AuthorizedClient authorizedClient = context.getAuthorizedClient();
66+
if (authorizedClient != null && !hasTokenExpired(authorizedClient.getAccessToken())) {
67+
// If client is already authorized but access token is NOT expired than no need for re-authorization
68+
return null;
69+
}
70+
71+
// As per spec, in section 4.4.3 Access Token Response
72+
// https://tools.ietf.org/html/rfc6749#section-4.4.3
73+
// A refresh token SHOULD NOT be included.
74+
//
75+
// Therefore, renewing an expired access token (re-authorization)
76+
// is the same as acquiring a new access token (authorization).
77+
78+
OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest =
79+
new OAuth2ClientCredentialsGrantRequest(clientRegistration);
80+
OAuth2AccessTokenResponse tokenResponse =
81+
this.accessTokenResponseClient.getTokenResponse(clientCredentialsGrantRequest);
82+
83+
return new OAuth2AuthorizedClient(clientRegistration, context.getPrincipal().getName(), tokenResponse.getAccessToken());
84+
}
85+
86+
private boolean hasTokenExpired(AbstractOAuth2Token token) {
87+
return token.getExpiresAt().isBefore(Instant.now().minus(this.clockSkew));
88+
}
89+
90+
/**
91+
* Sets the client used when requesting an access token credential at the Token Endpoint for the {@code client_credentials} grant.
92+
*
93+
* @param accessTokenResponseClient the client used when requesting an access token credential at the Token Endpoint for the {@code client_credentials} grant
94+
*/
95+
public void setAccessTokenResponseClient(OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient) {
96+
Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");
97+
this.accessTokenResponseClient = accessTokenResponseClient;
98+
}
99+
100+
/**
101+
* Sets the maximum acceptable clock skew, which is used when checking the
102+
* {@link OAuth2AuthorizedClient#getAccessToken() access token} expiry. The default is 60 seconds.
103+
* An access token is considered expired if it's before {@code Instant.now() - clockSkew}.
104+
*
105+
* @param clockSkew the maximum acceptable clock skew
106+
*/
107+
public void setClockSkew(Duration clockSkew) {
108+
Assert.notNull(clockSkew, "clockSkew cannot be null");
109+
Assert.isTrue(clockSkew.getSeconds() >= 0, "clockSkew must be >= 0");
110+
this.clockSkew = clockSkew;
111+
}
112+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2002-2019 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.lang.Nullable;
19+
import org.springframework.util.Assert;
20+
21+
import java.util.ArrayList;
22+
import java.util.Arrays;
23+
import java.util.Collections;
24+
import java.util.List;
25+
import java.util.Objects;
26+
27+
/**
28+
* An implementation of an {@link OAuth2AuthorizedClientProvider} that simply delegates
29+
* to it's internal {@code List} of {@link OAuth2AuthorizedClientProvider}(s).
30+
* <p>
31+
* Each provider is given a chance to
32+
* {@link OAuth2AuthorizedClientProvider#authorize(OAuth2AuthorizationContext) authorize}
33+
* the {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided context
34+
* with the first {@code non-null} {@link OAuth2AuthorizedClient} being returned.
35+
*
36+
* @author Joe Grandja
37+
* @since 5.2
38+
* @see OAuth2AuthorizedClientProvider
39+
*/
40+
public final class DelegatingOAuth2AuthorizedClientProvider implements OAuth2AuthorizedClientProvider {
41+
private final List<OAuth2AuthorizedClientProvider> authorizedClientProviders;
42+
43+
/**
44+
* Constructs a {@code DelegatingOAuth2AuthorizedClientProvider} using the provided parameters.
45+
*
46+
* @param authorizedClientProviders a list of {@link OAuth2AuthorizedClientProvider}(s)
47+
*/
48+
public DelegatingOAuth2AuthorizedClientProvider(OAuth2AuthorizedClientProvider... authorizedClientProviders) {
49+
Assert.notEmpty(authorizedClientProviders, "authorizedClientProviders cannot be empty");
50+
this.authorizedClientProviders = Collections.unmodifiableList(Arrays.asList(authorizedClientProviders));
51+
}
52+
53+
/**
54+
* Constructs a {@code DelegatingOAuth2AuthorizedClientProvider} using the provided parameters.
55+
*
56+
* @param authorizedClientProviders a {@code List} of {@link OAuth2AuthorizedClientProvider}(s)
57+
*/
58+
public DelegatingOAuth2AuthorizedClientProvider(List<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
59+
Assert.notEmpty(authorizedClientProviders, "authorizedClientProviders cannot be empty");
60+
this.authorizedClientProviders = Collections.unmodifiableList(new ArrayList<>(authorizedClientProviders));
61+
}
62+
63+
@Override
64+
@Nullable
65+
public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) {
66+
Assert.notNull(context, "context cannot be null");
67+
return this.authorizedClientProviders.stream()
68+
.map(authorizedClientProvider -> authorizedClientProvider.authorize(context))
69+
.filter(Objects::nonNull)
70+
.findFirst()
71+
.orElse(null);
72+
}
73+
}

0 commit comments

Comments
 (0)