Skip to content

Commit dcd997e

Browse files
committed
Add support for Resource Owner Password Credentials grant
Fixes gh-6003
1 parent de672e3 commit dcd997e

File tree

38 files changed

+2393
-72
lines changed

38 files changed

+2393
-72
lines changed

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,16 +74,15 @@ public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentRes
7474
OAuth2AuthorizedClientProviderBuilder authorizedClientProviderBuilder =
7575
OAuth2AuthorizedClientProviderBuilder.builder()
7676
.authorizationCode()
77-
.refreshToken();
78-
77+
.refreshToken()
78+
.password();
7979
if (this.accessTokenResponseClient != null) {
8080
authorizedClientProviderBuilder.clientCredentials(configurer ->
8181
configurer.accessTokenResponseClient(this.accessTokenResponseClient));
8282
} else {
8383
authorizedClientProviderBuilder.clientCredentials();
8484
}
8585
OAuth2AuthorizedClientProvider authorizedClientProvider = authorizedClientProviderBuilder.build();
86-
8786
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
8887
this.clientRegistrationRepository, this.authorizedClientRepository);
8988
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) {
7171
.authorizationCode()
7272
.refreshToken()
7373
.clientCredentials()
74+
.password()
7475
.build();
7576
DefaultServerOAuth2AuthorizedClientManager authorizedClientManager = new DefaultServerOAuth2AuthorizedClientManager(
7677
this.clientRegistrationRepository, getAuthorizedClientRepository());

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,21 @@
3636
*/
3737
public final class OAuth2AuthorizationContext {
3838
/**
39-
* The name of the {@link #getAttribute(String) attribute}
40-
* in the {@link OAuth2AuthorizationContext context}
41-
* associated to the value for the "request scope(s)".
42-
* The value of the attribute is a {@code String[]} of scope(s) to be requested
43-
* by the {@link OAuth2AuthorizationContext#getClientRegistration() client}.
39+
* The name of the {@link #getAttribute(String) attribute} in the context associated to the value for the "request scope(s)".
40+
* The value of the attribute is a {@code String[]} of scope(s) to be requested by the {@link #getClientRegistration() client}.
4441
*/
4542
public static final String REQUEST_SCOPE_ATTRIBUTE_NAME = OAuth2AuthorizationContext.class.getName().concat(".REQUEST_SCOPE");
4643

44+
/**
45+
* The name of the {@link #getAttribute(String) attribute} in the context associated to the value for the resource owner's username.
46+
*/
47+
public static final String USERNAME_ATTRIBUTE_NAME = OAuth2AuthorizationContext.class.getName().concat(".USERNAME");
48+
49+
/**
50+
* The name of the {@link #getAttribute(String) attribute} in the context associated to the value for the resource owner's password.
51+
*/
52+
public static final String PASSWORD_ATTRIBUTE_NAME = OAuth2AuthorizationContext.class.getName().concat(".PASSWORD");
53+
4754
private ClientRegistration clientRegistration;
4855
private OAuth2AuthorizedClient authorizedClient;
4956
private Authentication principal;

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

Lines changed: 96 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,25 @@
1717

1818
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
1919
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
20+
import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
2021
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
2122
import org.springframework.util.Assert;
2223

2324
import java.time.Clock;
2425
import java.time.Duration;
2526
import java.time.Instant;
26-
import java.util.Map;
27-
import java.util.List;
28-
import java.util.LinkedHashMap;
2927
import java.util.ArrayList;
28+
import java.util.LinkedHashMap;
29+
import java.util.List;
30+
import java.util.Map;
3031
import java.util.function.Consumer;
3132

3233
/**
3334
* A builder that builds a {@link DelegatingOAuth2AuthorizedClientProvider} composed of
3435
* one or more {@link OAuth2AuthorizedClientProvider}(s) that implement specific authorization grants.
3536
* The supported authorization grants are {@link #authorizationCode() authorization_code},
36-
* {@link #refreshToken() refresh_token} and {@link #clientCredentials() client_credentials}.
37+
* {@link #refreshToken() refresh_token}, {@link #clientCredentials() client_credentials}
38+
* and {@link #password() password}.
3739
* In addition to the standard authorization grants, an implementation of an extension grant
3840
* may be supplied via {@link #provider(OAuth2AuthorizedClientProvider)}.
3941
*
@@ -43,6 +45,7 @@
4345
* @see AuthorizationCodeOAuth2AuthorizedClientProvider
4446
* @see RefreshTokenOAuth2AuthorizedClientProvider
4547
* @see ClientCredentialsOAuth2AuthorizedClientProvider
48+
* @see PasswordOAuth2AuthorizedClientProvider
4649
* @see DelegatingOAuth2AuthorizedClientProvider
4750
*/
4851
public final class OAuth2AuthorizedClientProviderBuilder {
@@ -279,6 +282,95 @@ public OAuth2AuthorizedClientProvider build() {
279282
}
280283
}
281284

285+
/**
286+
* Configures support for the {@code password} grant.
287+
*
288+
* @return the {@link OAuth2AuthorizedClientProviderBuilder}
289+
*/
290+
public OAuth2AuthorizedClientProviderBuilder password() {
291+
this.builders.computeIfAbsent(PasswordOAuth2AuthorizedClientProvider.class, k -> new PasswordGrantBuilder());
292+
return OAuth2AuthorizedClientProviderBuilder.this;
293+
}
294+
295+
/**
296+
* Configures support for the {@code password} grant.
297+
*
298+
* @param builderConsumer a {@code Consumer} of {@link PasswordGrantBuilder} used for further configuration
299+
* @return the {@link OAuth2AuthorizedClientProviderBuilder}
300+
*/
301+
public OAuth2AuthorizedClientProviderBuilder password(Consumer<PasswordGrantBuilder> builderConsumer) {
302+
PasswordGrantBuilder builder = (PasswordGrantBuilder) this.builders.computeIfAbsent(
303+
PasswordOAuth2AuthorizedClientProvider.class, k -> new PasswordGrantBuilder());
304+
builderConsumer.accept(builder);
305+
return OAuth2AuthorizedClientProviderBuilder.this;
306+
}
307+
308+
/**
309+
* A builder for the {@code password} grant.
310+
*/
311+
public class PasswordGrantBuilder implements Builder {
312+
private OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient;
313+
private Duration clockSkew;
314+
private Clock clock;
315+
316+
private PasswordGrantBuilder() {
317+
}
318+
319+
/**
320+
* Sets the client used when requesting an access token credential at the Token Endpoint.
321+
*
322+
* @param accessTokenResponseClient the client used when requesting an access token credential at the Token Endpoint
323+
* @return the {@link PasswordGrantBuilder}
324+
*/
325+
public PasswordGrantBuilder accessTokenResponseClient(OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient) {
326+
this.accessTokenResponseClient = accessTokenResponseClient;
327+
return this;
328+
}
329+
330+
/**
331+
* Sets the maximum acceptable clock skew, which is used when checking the access token expiry.
332+
* An access token is considered expired if it's before {@code Instant.now(this.clock) - clockSkew}.
333+
*
334+
* @param clockSkew the maximum acceptable clock skew
335+
* @return the {@link PasswordGrantBuilder}
336+
*/
337+
public PasswordGrantBuilder clockSkew(Duration clockSkew) {
338+
this.clockSkew = clockSkew;
339+
return this;
340+
}
341+
342+
/**
343+
* Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the access token expiry.
344+
*
345+
* @param clock the clock
346+
* @return the {@link PasswordGrantBuilder}
347+
*/
348+
public PasswordGrantBuilder clock(Clock clock) {
349+
this.clock = clock;
350+
return this;
351+
}
352+
353+
/**
354+
* Builds an instance of {@link PasswordOAuth2AuthorizedClientProvider}.
355+
*
356+
* @return the {@link PasswordOAuth2AuthorizedClientProvider}
357+
*/
358+
@Override
359+
public OAuth2AuthorizedClientProvider build() {
360+
PasswordOAuth2AuthorizedClientProvider authorizedClientProvider = new PasswordOAuth2AuthorizedClientProvider();
361+
if (this.accessTokenResponseClient != null) {
362+
authorizedClientProvider.setAccessTokenResponseClient(this.accessTokenResponseClient);
363+
}
364+
if (this.clockSkew != null) {
365+
authorizedClientProvider.setClockSkew(this.clockSkew);
366+
}
367+
if (this.clock != null) {
368+
authorizedClientProvider.setClock(this.clock);
369+
}
370+
return authorizedClientProvider;
371+
}
372+
}
373+
282374
/**
283375
* Builds an instance of {@link DelegatingOAuth2AuthorizedClientProvider}
284376
* composed of one or more {@link OAuth2AuthorizedClientProvider}(s).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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.DefaultPasswordTokenResponseClient;
20+
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
21+
import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
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+
import org.springframework.util.StringUtils;
28+
29+
import java.time.Clock;
30+
import java.time.Duration;
31+
import java.time.Instant;
32+
33+
/**
34+
* An implementation of an {@link OAuth2AuthorizedClientProvider}
35+
* for the {@link AuthorizationGrantType#PASSWORD password} grant.
36+
*
37+
* @author Joe Grandja
38+
* @since 5.2
39+
* @see OAuth2AuthorizedClientProvider
40+
* @see DefaultPasswordTokenResponseClient
41+
*/
42+
public final class PasswordOAuth2AuthorizedClientProvider implements OAuth2AuthorizedClientProvider {
43+
private OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient =
44+
new DefaultPasswordTokenResponseClient();
45+
private Duration clockSkew = Duration.ofSeconds(60);
46+
private Clock clock = Clock.systemUTC();
47+
48+
/**
49+
* Attempt to authorize (or re-authorize) the {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided {@code context}.
50+
* Returns {@code null} if authorization (or re-authorization) is not supported,
51+
* e.g. the client's {@link ClientRegistration#getAuthorizationGrantType() authorization grant type}
52+
* is not {@link AuthorizationGrantType#PASSWORD password} OR
53+
* the {@link OAuth2AuthorizationContext#USERNAME_ATTRIBUTE_NAME username} and/or
54+
* {@link OAuth2AuthorizationContext#PASSWORD_ATTRIBUTE_NAME password} attributes
55+
* are not available in the provided {@code context} OR
56+
* the {@link OAuth2AuthorizedClient#getAccessToken() access token} is not expired.
57+
*
58+
* <p>
59+
* The following {@link OAuth2AuthorizationContext#getAttributes() context attributes} are supported:
60+
* <ol>
61+
* <li>{@link OAuth2AuthorizationContext#USERNAME_ATTRIBUTE_NAME} (required) - a {@code String} value for the resource owner's username</li>
62+
* <li>{@link OAuth2AuthorizationContext#PASSWORD_ATTRIBUTE_NAME} (required) - a {@code String} value for the resource owner's password</li>
63+
* </ol>
64+
*
65+
* @param context the context that holds authorization-specific state for the client
66+
* @return the {@link OAuth2AuthorizedClient} or {@code null} if authorization (or re-authorization) is not supported
67+
*/
68+
@Override
69+
@Nullable
70+
public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) {
71+
Assert.notNull(context, "context cannot be null");
72+
73+
ClientRegistration clientRegistration = context.getClientRegistration();
74+
OAuth2AuthorizedClient authorizedClient = context.getAuthorizedClient();
75+
76+
if (!AuthorizationGrantType.PASSWORD.equals(clientRegistration.getAuthorizationGrantType())) {
77+
return null;
78+
}
79+
80+
String username = context.getAttribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME);
81+
String password = context.getAttribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME);
82+
if (!StringUtils.hasText(username) || !StringUtils.hasText(password)) {
83+
return null;
84+
}
85+
86+
if (authorizedClient != null && !hasTokenExpired(authorizedClient.getAccessToken())) {
87+
// If client is already authorized and access token is NOT expired than no need for re-authorization
88+
return null;
89+
}
90+
91+
if (authorizedClient != null && hasTokenExpired(authorizedClient.getAccessToken()) && authorizedClient.getRefreshToken() != null) {
92+
// If client is already authorized and access token is expired and a refresh token is available,
93+
// than return and allow RefreshTokenOAuth2AuthorizedClientProvider to handle the refresh
94+
return null;
95+
}
96+
97+
OAuth2PasswordGrantRequest passwordGrantRequest =
98+
new OAuth2PasswordGrantRequest(clientRegistration, username, password);
99+
OAuth2AccessTokenResponse tokenResponse =
100+
this.accessTokenResponseClient.getTokenResponse(passwordGrantRequest);
101+
102+
return new OAuth2AuthorizedClient(clientRegistration, context.getPrincipal().getName(),
103+
tokenResponse.getAccessToken(), tokenResponse.getRefreshToken());
104+
}
105+
106+
private boolean hasTokenExpired(AbstractOAuth2Token token) {
107+
return token.getExpiresAt().isBefore(Instant.now(this.clock).minus(this.clockSkew));
108+
}
109+
110+
/**
111+
* Sets the client used when requesting an access token credential at the Token Endpoint for the {@code password} grant.
112+
*
113+
* @param accessTokenResponseClient the client used when requesting an access token credential at the Token Endpoint for the {@code password} grant
114+
*/
115+
public void setAccessTokenResponseClient(OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient) {
116+
Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");
117+
this.accessTokenResponseClient = accessTokenResponseClient;
118+
}
119+
120+
/**
121+
* Sets the maximum acceptable clock skew, which is used when checking the
122+
* {@link OAuth2AuthorizedClient#getAccessToken() access token} expiry. The default is 60 seconds.
123+
* An access token is considered expired if it's before {@code Instant.now(this.clock) - clockSkew}.
124+
*
125+
* @param clockSkew the maximum acceptable clock skew
126+
*/
127+
public void setClockSkew(Duration clockSkew) {
128+
Assert.notNull(clockSkew, "clockSkew cannot be null");
129+
Assert.isTrue(clockSkew.getSeconds() >= 0, "clockSkew must be >= 0");
130+
this.clockSkew = clockSkew;
131+
}
132+
133+
/**
134+
* Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the access token expiry.
135+
*
136+
* @param clock the clock
137+
*/
138+
public void setClock(Clock clock) {
139+
Assert.notNull(clock, "clock cannot be null");
140+
this.clock = clock;
141+
}
142+
}

0 commit comments

Comments
 (0)