Skip to content

Commit d5e4325

Browse files
committed
Introduce Reactive OAuth2AuthorizedClient Manager/Provider
Fixes gh-7116
1 parent 4ca9e15 commit d5e4325

File tree

27 files changed

+2923
-664
lines changed

27 files changed

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

0 commit comments

Comments
 (0)