Skip to content

Commit 46756d2

Browse files
committed
Introduce Reactive OAuth2AuthorizedClient Manager/Provider
Fixes gh-7116
1 parent a377581 commit 46756d2

File tree

25 files changed

+2911
-269
lines changed

25 files changed

+2911
-269
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2ClientImportSelector.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,14 @@
2020
import org.springframework.context.annotation.Configuration;
2121
import org.springframework.context.annotation.ImportSelector;
2222
import org.springframework.core.type.AnnotationMetadata;
23+
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProvider;
24+
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProviderBuilder;
2325
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService;
2426
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
27+
import org.springframework.security.oauth2.client.web.reactive.result.method.annotation.OAuth2AuthorizedClientArgumentResolver;
2528
import org.springframework.security.oauth2.client.web.server.AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository;
29+
import org.springframework.security.oauth2.client.web.server.DefaultServerOAuth2AuthorizedClientManager;
2630
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
27-
import org.springframework.security.oauth2.client.web.reactive.result.method.annotation.OAuth2AuthorizedClientArgumentResolver;
2831
import org.springframework.util.ClassUtils;
2932
import org.springframework.web.reactive.config.WebFluxConfigurer;
3033
import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer;
@@ -63,7 +66,16 @@ static class OAuth2ClientWebFluxSecurityConfiguration implements WebFluxConfigur
6366
@Override
6467
public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) {
6568
if (this.authorizedClientRepository != null && this.clientRegistrationRepository != null) {
66-
configurer.addCustomResolver(new OAuth2AuthorizedClientArgumentResolver(this.clientRegistrationRepository, getAuthorizedClientRepository()));
69+
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
70+
ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
71+
.authorizationCode()
72+
.refreshToken()
73+
.clientCredentials()
74+
.build();
75+
DefaultServerOAuth2AuthorizedClientManager authorizedClientManager = new DefaultServerOAuth2AuthorizedClientManager(
76+
this.clientRegistrationRepository, getAuthorizedClientRepository());
77+
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
78+
configurer.addCustomResolver(new OAuth2AuthorizedClientArgumentResolver(authorizedClientManager));
6779
}
6880
}
6981

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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.security.oauth2.client.registration.ClientRegistration;
19+
import org.springframework.security.oauth2.core.AuthorizationGrantType;
20+
import org.springframework.util.Assert;
21+
import reactor.core.publisher.Mono;
22+
23+
/**
24+
* An implementation of a {@link ReactiveOAuth2AuthorizedClientProvider}
25+
* for the {@link AuthorizationGrantType#AUTHORIZATION_CODE authorization_code} grant.
26+
*
27+
* @author Joe Grandja
28+
* @since 5.2
29+
* @see ReactiveOAuth2AuthorizedClientProvider
30+
*/
31+
public final class AuthorizationCodeReactiveOAuth2AuthorizedClientProvider implements ReactiveOAuth2AuthorizedClientProvider {
32+
33+
/**
34+
* Attempt to authorize the {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided {@code context}.
35+
* Returns an empty {@code Mono} 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 an empty {@code Mono} if authorization is not supported
41+
*/
42+
@Override
43+
public Mono<OAuth2AuthorizedClient> authorize(OAuth2AuthorizationContext context) {
44+
Assert.notNull(context, "context cannot be null");
45+
46+
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(context.getClientRegistration().getAuthorizationGrantType()) &&
47+
context.getAuthorizedClient() == null) {
48+
// ClientAuthorizationRequiredException is caught by OAuth2AuthorizationRequestRedirectWebFilter which initiates authorization
49+
return Mono.error(() -> new ClientAuthorizationRequiredException(context.getClientRegistration().getRegistrationId()));
50+
}
51+
return Mono.empty();
52+
}
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
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.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
19+
import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
20+
import org.springframework.security.oauth2.client.endpoint.WebClientReactiveClientCredentialsTokenResponseClient;
21+
import org.springframework.security.oauth2.client.registration.ClientRegistration;
22+
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
23+
import org.springframework.security.oauth2.core.AuthorizationGrantType;
24+
import org.springframework.util.Assert;
25+
import reactor.core.publisher.Mono;
26+
27+
import java.time.Duration;
28+
import java.time.Instant;
29+
30+
/**
31+
* An implementation of a {@link ReactiveOAuth2AuthorizedClientProvider}
32+
* for the {@link AuthorizationGrantType#CLIENT_CREDENTIALS client_credentials} grant.
33+
*
34+
* @author Joe Grandja
35+
* @since 5.2
36+
* @see ReactiveOAuth2AuthorizedClientProvider
37+
* @see WebClientReactiveClientCredentialsTokenResponseClient
38+
*/
39+
public final class ClientCredentialsReactiveOAuth2AuthorizedClientProvider implements ReactiveOAuth2AuthorizedClientProvider {
40+
private ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient =
41+
new WebClientReactiveClientCredentialsTokenResponseClient();
42+
private Duration clockSkew = Duration.ofSeconds(60);
43+
44+
/**
45+
* Attempt to authorize (or re-authorize) the {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided {@code context}.
46+
* Returns an empty {@code Mono} if authorization (or re-authorization) is not supported,
47+
* e.g. the client's {@link ClientRegistration#getAuthorizationGrantType() authorization grant type}
48+
* is not {@link AuthorizationGrantType#CLIENT_CREDENTIALS client_credentials} OR
49+
* the {@link OAuth2AuthorizedClient#getAccessToken() access token} is not expired.
50+
*
51+
* @param context the context that holds authorization-specific state for the client
52+
* @return the {@link OAuth2AuthorizedClient} or an empty {@code Mono} if authorization (or re-authorization) is not supported
53+
*/
54+
@Override
55+
public Mono<OAuth2AuthorizedClient> authorize(OAuth2AuthorizationContext context) {
56+
Assert.notNull(context, "context cannot be null");
57+
58+
ClientRegistration clientRegistration = context.getClientRegistration();
59+
if (!AuthorizationGrantType.CLIENT_CREDENTIALS.equals(clientRegistration.getAuthorizationGrantType())) {
60+
return Mono.empty();
61+
}
62+
63+
OAuth2AuthorizedClient authorizedClient = context.getAuthorizedClient();
64+
if (authorizedClient != null && !hasTokenExpired(authorizedClient.getAccessToken())) {
65+
// If client is already authorized but access token is NOT expired than no need for re-authorization
66+
return Mono.empty();
67+
}
68+
69+
// As per spec, in section 4.4.3 Access Token Response
70+
// https://tools.ietf.org/html/rfc6749#section-4.4.3
71+
// A refresh token SHOULD NOT be included.
72+
//
73+
// Therefore, renewing an expired access token (re-authorization)
74+
// is the same as acquiring a new access token (authorization).
75+
76+
return Mono.just(new OAuth2ClientCredentialsGrantRequest(clientRegistration))
77+
.flatMap(this.accessTokenResponseClient::getTokenResponse)
78+
.map(tokenResponse -> new OAuth2AuthorizedClient(
79+
clientRegistration, context.getPrincipal().getName(), tokenResponse.getAccessToken()));
80+
}
81+
82+
private boolean hasTokenExpired(AbstractOAuth2Token token) {
83+
return token.getExpiresAt().isBefore(Instant.now().minus(this.clockSkew));
84+
}
85+
86+
/**
87+
* Sets the client used when requesting an access token credential at the Token Endpoint for the {@code client_credentials} grant.
88+
*
89+
* @param accessTokenResponseClient the client used when requesting an access token credential at the Token Endpoint for the {@code client_credentials} grant
90+
*/
91+
public void setAccessTokenResponseClient(ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient) {
92+
Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");
93+
this.accessTokenResponseClient = accessTokenResponseClient;
94+
}
95+
96+
/**
97+
* Sets the maximum acceptable clock skew, which is used when checking the
98+
* {@link OAuth2AuthorizedClient#getAccessToken() access token} expiry. The default is 60 seconds.
99+
* An access token is considered expired if it's before {@code Instant.now() - clockSkew}.
100+
*
101+
* @param clockSkew the maximum acceptable clock skew
102+
*/
103+
public void setClockSkew(Duration clockSkew) {
104+
Assert.notNull(clockSkew, "clockSkew cannot be null");
105+
Assert.isTrue(clockSkew.getSeconds() >= 0, "clockSkew must be >= 0");
106+
this.clockSkew = clockSkew;
107+
}
108+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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.util.Assert;
19+
import reactor.core.publisher.Flux;
20+
import reactor.core.publisher.Mono;
21+
22+
import java.util.ArrayList;
23+
import java.util.Arrays;
24+
import java.util.Collections;
25+
import java.util.List;
26+
27+
/**
28+
* An implementation of a {@link ReactiveOAuth2AuthorizedClientProvider} that simply delegates
29+
* to it's internal {@code List} of {@link ReactiveOAuth2AuthorizedClientProvider}(s).
30+
* <p>
31+
* Each provider is given a chance to
32+
* {@link ReactiveOAuth2AuthorizedClientProvider#authorize(OAuth2AuthorizationContext) authorize}
33+
* the {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided context
34+
* with the first available {@link OAuth2AuthorizedClient} being returned.
35+
*
36+
* @author Joe Grandja
37+
* @since 5.2
38+
* @see ReactiveOAuth2AuthorizedClientProvider
39+
*/
40+
public final class DelegatingReactiveOAuth2AuthorizedClientProvider implements ReactiveOAuth2AuthorizedClientProvider {
41+
private final List<ReactiveOAuth2AuthorizedClientProvider> authorizedClientProviders;
42+
43+
/**
44+
* Constructs a {@code DelegatingReactiveOAuth2AuthorizedClientProvider} using the provided parameters.
45+
*
46+
* @param authorizedClientProviders a list of {@link ReactiveOAuth2AuthorizedClientProvider}(s)
47+
*/
48+
public DelegatingReactiveOAuth2AuthorizedClientProvider(ReactiveOAuth2AuthorizedClientProvider... authorizedClientProviders) {
49+
Assert.notEmpty(authorizedClientProviders, "authorizedClientProviders cannot be empty");
50+
this.authorizedClientProviders = Collections.unmodifiableList(Arrays.asList(authorizedClientProviders));
51+
}
52+
53+
/**
54+
* Constructs a {@code DelegatingReactiveOAuth2AuthorizedClientProvider} using the provided parameters.
55+
*
56+
* @param authorizedClientProviders a {@code List} of {@link OAuth2AuthorizedClientProvider}(s)
57+
*/
58+
public DelegatingReactiveOAuth2AuthorizedClientProvider(List<ReactiveOAuth2AuthorizedClientProvider> authorizedClientProviders) {
59+
Assert.notEmpty(authorizedClientProviders, "authorizedClientProviders cannot be empty");
60+
this.authorizedClientProviders = Collections.unmodifiableList(new ArrayList<>(authorizedClientProviders));
61+
}
62+
63+
@Override
64+
public Mono<OAuth2AuthorizedClient> authorize(OAuth2AuthorizationContext context) {
65+
Assert.notNull(context, "context cannot be null");
66+
return Flux.fromIterable(this.authorizedClientProviders)
67+
.concatMap(authorizedClientProvider -> authorizedClientProvider.authorize(context))
68+
.next();
69+
}
70+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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.security.oauth2.client.registration.ClientRegistration;
19+
import org.springframework.security.oauth2.core.AuthorizationGrantType;
20+
import reactor.core.publisher.Mono;
21+
22+
/**
23+
* A strategy for authorizing (or re-authorizing) an OAuth 2.0 Client.
24+
* Implementations will typically implement a specific {@link AuthorizationGrantType authorization grant} type.
25+
*
26+
* @author Joe Grandja
27+
* @since 5.2
28+
* @see OAuth2AuthorizedClient
29+
* @see OAuth2AuthorizationContext
30+
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.3">Section 1.3 Authorization Grant</a>
31+
*/
32+
public interface ReactiveOAuth2AuthorizedClientProvider {
33+
34+
/**
35+
* Attempt to authorize (or re-authorize) the {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided context.
36+
* Implementations must return an empty {@code Mono} if authorization is not supported for the specified client,
37+
* e.g. the provider doesn't support the {@link ClientRegistration#getAuthorizationGrantType() authorization grant} type configured for the client.
38+
*
39+
* @param context the context that holds authorization-specific state for the client
40+
* @return the {@link OAuth2AuthorizedClient} or an empty {@code Mono} if authorization is not supported for the specified client
41+
*/
42+
Mono<OAuth2AuthorizedClient> authorize(OAuth2AuthorizationContext context);
43+
44+
}

0 commit comments

Comments
 (0)