Skip to content

Commit 42f1e20

Browse files
committed
Add support for Resource Owner Password Credentials grant
Fixes gh-6003
1 parent 46756d2 commit 42f1e20

File tree

38 files changed

+2054
-46
lines changed

38 files changed

+2054
-46
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentRes
7777
.refreshToken()
7878
.clientCredentials(configurer ->
7979
Optional.ofNullable(this.accessTokenResponseClient).ifPresent(configurer::accessTokenResponseClient))
80+
.password()
8081
.build();
8182
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
8283
this.clientRegistrationRepository, this.authorizedClientRepository);

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: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
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

@@ -31,7 +32,8 @@
3132
* A builder that builds a {@link DelegatingOAuth2AuthorizedClientProvider} composed of
3233
* one or more {@link OAuth2AuthorizedClientProvider}(s) that implement specific authorization grants.
3334
* The supported authorization grants are {@link #authorizationCode() authorization_code},
34-
* {@link #refreshToken() refresh_token} and {@link #clientCredentials() client_credentials}.
35+
* {@link #refreshToken() refresh_token}, {@link #clientCredentials() client_credentials}
36+
* and {@link #password() password}.
3537
* In addition to the standard authorization grants, an implementation of an extension grant
3638
* may be supplied via {@link #provider(OAuth2AuthorizedClientProvider)}.
3739
*
@@ -41,6 +43,7 @@
4143
* @see AuthorizationCodeOAuth2AuthorizedClientProvider
4244
* @see RefreshTokenOAuth2AuthorizedClientProvider
4345
* @see ClientCredentialsOAuth2AuthorizedClientProvider
46+
* @see PasswordOAuth2AuthorizedClientProvider
4447
* @see DelegatingOAuth2AuthorizedClientProvider
4548
*/
4649
public final class OAuth2AuthorizedClientProviderBuilder {
@@ -247,6 +250,64 @@ public OAuth2AuthorizedClientProvider build() {
247250
}
248251
}
249252

253+
/**
254+
* Configures support for the {@code password} grant.
255+
*
256+
* @return the {@link OAuth2AuthorizedClientProviderBuilder}
257+
*/
258+
public OAuth2AuthorizedClientProviderBuilder password() {
259+
this.builders.computeIfAbsent(PasswordOAuth2AuthorizedClientProvider.class, k -> new PasswordGrantBuilder());
260+
return OAuth2AuthorizedClientProviderBuilder.this;
261+
}
262+
263+
/**
264+
* Configures support for the {@code password} grant.
265+
*
266+
* @param builderConsumer a {@code Consumer} of {@link PasswordGrantBuilder} used for further configuration
267+
* @return the {@link OAuth2AuthorizedClientProviderBuilder}
268+
*/
269+
public OAuth2AuthorizedClientProviderBuilder password(Consumer<PasswordGrantBuilder> builderConsumer) {
270+
PasswordGrantBuilder builder = (PasswordGrantBuilder) this.builders.computeIfAbsent(
271+
PasswordOAuth2AuthorizedClientProvider.class, k -> new PasswordGrantBuilder());
272+
builderConsumer.accept(builder);
273+
return OAuth2AuthorizedClientProviderBuilder.this;
274+
}
275+
276+
/**
277+
* A builder for the {@code password} grant.
278+
*/
279+
public class PasswordGrantBuilder implements Builder {
280+
private OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient;
281+
282+
private PasswordGrantBuilder() {
283+
}
284+
285+
/**
286+
* Sets the client used when requesting an access token credential at the Token Endpoint.
287+
*
288+
* @param accessTokenResponseClient the client used when requesting an access token credential at the Token Endpoint
289+
* @return the {@link PasswordGrantBuilder}
290+
*/
291+
public PasswordGrantBuilder accessTokenResponseClient(OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient) {
292+
this.accessTokenResponseClient = accessTokenResponseClient;
293+
return this;
294+
}
295+
296+
/**
297+
* Builds an instance of {@link PasswordOAuth2AuthorizedClientProvider}.
298+
*
299+
* @return the {@link PasswordOAuth2AuthorizedClientProvider}
300+
*/
301+
@Override
302+
public OAuth2AuthorizedClientProvider build() {
303+
PasswordOAuth2AuthorizedClientProvider authorizedClientProvider = new PasswordOAuth2AuthorizedClientProvider();
304+
if (this.accessTokenResponseClient != null) {
305+
authorizedClientProvider.setAccessTokenResponseClient(this.accessTokenResponseClient);
306+
}
307+
return authorizedClientProvider;
308+
}
309+
}
310+
250311
/**
251312
* Builds an instance of {@link DelegatingOAuth2AuthorizedClientProvider}
252313
* composed of one or more {@link OAuth2AuthorizedClientProvider}(s).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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.AuthorizationGrantType;
24+
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
25+
import org.springframework.util.Assert;
26+
import org.springframework.util.StringUtils;
27+
28+
/**
29+
* An implementation of an {@link OAuth2AuthorizedClientProvider}
30+
* for the {@link AuthorizationGrantType#PASSWORD password} grant.
31+
*
32+
* @author Joe Grandja
33+
* @since 5.2
34+
* @see OAuth2AuthorizedClientProvider
35+
* @see DefaultPasswordTokenResponseClient
36+
*/
37+
public final class PasswordOAuth2AuthorizedClientProvider implements OAuth2AuthorizedClientProvider {
38+
private OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient =
39+
new DefaultPasswordTokenResponseClient();
40+
41+
/**
42+
* Attempt to authorize the {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided {@code context}.
43+
* Returns {@code null} if authorization is not supported,
44+
* e.g. the client's {@link ClientRegistration#getAuthorizationGrantType() authorization grant type}
45+
* is not {@link AuthorizationGrantType#PASSWORD password} OR the client is already authorized OR
46+
* the {@link OAuth2AuthorizationContext#USERNAME_ATTRIBUTE_NAME username} and/or
47+
* {@link OAuth2AuthorizationContext#PASSWORD_ATTRIBUTE_NAME password} attributes
48+
* are not available in the provided {@code context}.
49+
*
50+
* <p>
51+
* The following {@link OAuth2AuthorizationContext#getAttributes() context attributes} are supported:
52+
* <ol>
53+
* <li>{@link OAuth2AuthorizationContext#USERNAME_ATTRIBUTE_NAME} (required) - a {@code String} value for the resource owner's username</li>
54+
* <li>{@link OAuth2AuthorizationContext#PASSWORD_ATTRIBUTE_NAME} (required) - a {@code String} value for the resource owner's password</li>
55+
* </ol>
56+
*
57+
* @param context the context that holds authorization-specific state for the client
58+
* @return the {@link OAuth2AuthorizedClient} or {@code null} if authorization is not supported
59+
*/
60+
@Override
61+
@Nullable
62+
public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) {
63+
Assert.notNull(context, "context cannot be null");
64+
65+
ClientRegistration clientRegistration = context.getClientRegistration();
66+
OAuth2AuthorizedClient authorizedClient = context.getAuthorizedClient();
67+
if (!AuthorizationGrantType.PASSWORD.equals(clientRegistration.getAuthorizationGrantType()) ||
68+
authorizedClient != null) {
69+
return null;
70+
}
71+
72+
String username = context.getAttribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME);
73+
String password = context.getAttribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME);
74+
if (!StringUtils.hasText(username) || !StringUtils.hasText(password)) {
75+
return null;
76+
}
77+
78+
OAuth2PasswordGrantRequest passwordGrantRequest =
79+
new OAuth2PasswordGrantRequest(clientRegistration, username, password);
80+
OAuth2AccessTokenResponse tokenResponse =
81+
this.accessTokenResponseClient.getTokenResponse(passwordGrantRequest);
82+
83+
return new OAuth2AuthorizedClient(clientRegistration, context.getPrincipal().getName(),
84+
tokenResponse.getAccessToken(), tokenResponse.getRefreshToken());
85+
}
86+
87+
/**
88+
* Sets the client used when requesting an access token credential at the Token Endpoint for the {@code password} grant.
89+
*
90+
* @param accessTokenResponseClient the client used when requesting an access token credential at the Token Endpoint for the {@code password} grant
91+
*/
92+
public void setAccessTokenResponseClient(OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient) {
93+
Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");
94+
this.accessTokenResponseClient = accessTokenResponseClient;
95+
}
96+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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.OAuth2PasswordGrantRequest;
19+
import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
20+
import org.springframework.security.oauth2.client.endpoint.WebClientReactivePasswordTokenResponseClient;
21+
import org.springframework.security.oauth2.client.registration.ClientRegistration;
22+
import org.springframework.security.oauth2.core.AuthorizationGrantType;
23+
import org.springframework.util.Assert;
24+
import org.springframework.util.StringUtils;
25+
import reactor.core.publisher.Mono;
26+
27+
/**
28+
* An implementation of a {@link ReactiveOAuth2AuthorizedClientProvider}
29+
* for the {@link AuthorizationGrantType#PASSWORD password} grant.
30+
*
31+
* @author Joe Grandja
32+
* @since 5.2
33+
* @see ReactiveOAuth2AuthorizedClientProvider
34+
* @see WebClientReactivePasswordTokenResponseClient
35+
*/
36+
public final class PasswordReactiveOAuth2AuthorizedClientProvider implements ReactiveOAuth2AuthorizedClientProvider {
37+
private ReactiveOAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient =
38+
new WebClientReactivePasswordTokenResponseClient();
39+
40+
/**
41+
* Attempt to authorize the {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided {@code context}.
42+
* Returns an empty {@code Mono} if authorization is not supported,
43+
* e.g. the client's {@link ClientRegistration#getAuthorizationGrantType() authorization grant type}
44+
* is not {@link AuthorizationGrantType#PASSWORD password} OR the client is already authorized OR
45+
* the {@link OAuth2AuthorizationContext#USERNAME_ATTRIBUTE_NAME username} and/or
46+
* {@link OAuth2AuthorizationContext#PASSWORD_ATTRIBUTE_NAME password} attributes
47+
* are not available in the provided {@code context}.
48+
*
49+
* <p>
50+
* The following {@link OAuth2AuthorizationContext#getAttributes() context attributes} are supported:
51+
* <ol>
52+
* <li>{@link OAuth2AuthorizationContext#USERNAME_ATTRIBUTE_NAME} (required) - a {@code String} value for the resource owner's username</li>
53+
* <li>{@link OAuth2AuthorizationContext#PASSWORD_ATTRIBUTE_NAME} (required) - a {@code String} value for the resource owner's password</li>
54+
* </ol>
55+
*
56+
* @param context the context that holds authorization-specific state for the client
57+
* @return the {@link OAuth2AuthorizedClient} or an empty {@code Mono} if authorization is not supported
58+
*/
59+
@Override
60+
public Mono<OAuth2AuthorizedClient> authorize(OAuth2AuthorizationContext context) {
61+
Assert.notNull(context, "context cannot be null");
62+
63+
ClientRegistration clientRegistration = context.getClientRegistration();
64+
OAuth2AuthorizedClient authorizedClient = context.getAuthorizedClient();
65+
if (!AuthorizationGrantType.PASSWORD.equals(clientRegistration.getAuthorizationGrantType()) ||
66+
authorizedClient != null) {
67+
return Mono.empty();
68+
}
69+
70+
String username = context.getAttribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME);
71+
String password = context.getAttribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME);
72+
if (!StringUtils.hasText(username) || !StringUtils.hasText(password)) {
73+
return Mono.empty();
74+
}
75+
76+
OAuth2PasswordGrantRequest passwordGrantRequest =
77+
new OAuth2PasswordGrantRequest(clientRegistration, username, password);
78+
79+
return Mono.just(passwordGrantRequest)
80+
.flatMap(this.accessTokenResponseClient::getTokenResponse)
81+
.map(tokenResponse -> new OAuth2AuthorizedClient(clientRegistration, context.getPrincipal().getName(),
82+
tokenResponse.getAccessToken(), tokenResponse.getRefreshToken()));
83+
}
84+
85+
/**
86+
* Sets the client used when requesting an access token credential at the Token Endpoint for the {@code password} grant.
87+
*
88+
* @param accessTokenResponseClient the client used when requesting an access token credential at the Token Endpoint for the {@code password} grant
89+
*/
90+
public void setAccessTokenResponseClient(ReactiveOAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient) {
91+
Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");
92+
this.accessTokenResponseClient = accessTokenResponseClient;
93+
}
94+
}

0 commit comments

Comments
 (0)