Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentRes
.refreshToken()
.clientCredentials(configurer ->
Optional.ofNullable(this.accessTokenResponseClient).ifPresent(configurer::accessTokenResponseClient))
.password()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
this.clientRegistrationRepository, this.authorizedClientRepository);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) {
.authorizationCode()
.refreshToken()
.clientCredentials()
.password()
.build();
DefaultServerOAuth2AuthorizedClientManager authorizedClientManager = new DefaultServerOAuth2AuthorizedClientManager(
this.clientRegistrationRepository, getAuthorizedClientRepository());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,21 @@
*/
public final class OAuth2AuthorizationContext {
/**
* The name of the {@link #getAttribute(String) attribute}
* in the {@link OAuth2AuthorizationContext context}
* associated to the value for the "request scope(s)".
* The value of the attribute is a {@code String[]} of scope(s) to be requested
* by the {@link OAuth2AuthorizationContext#getClientRegistration() client}.
* The name of the {@link #getAttribute(String) attribute} in the context associated to the value for the "request scope(s)".
* The value of the attribute is a {@code String[]} of scope(s) to be requested by the {@link #getClientRegistration() client}.
*/
public static final String REQUEST_SCOPE_ATTRIBUTE_NAME = OAuth2AuthorizationContext.class.getName().concat(".REQUEST_SCOPE");

/**
* The name of the {@link #getAttribute(String) attribute} in the context associated to the value for the resource owner's username.
*/
public static final String USERNAME_ATTRIBUTE_NAME = OAuth2AuthorizationContext.class.getName().concat(".USERNAME");

/**
* The name of the {@link #getAttribute(String) attribute} in the context associated to the value for the resource owner's password.
*/
public static final String PASSWORD_ATTRIBUTE_NAME = OAuth2AuthorizationContext.class.getName().concat(".PASSWORD");

private ClientRegistration clientRegistration;
private OAuth2AuthorizedClient authorizedClient;
private Authentication principal;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
import org.springframework.util.Assert;

Expand All @@ -31,7 +32,8 @@
* A builder that builds a {@link DelegatingOAuth2AuthorizedClientProvider} composed of
* one or more {@link OAuth2AuthorizedClientProvider}(s) that implement specific authorization grants.
* The supported authorization grants are {@link #authorizationCode() authorization_code},
* {@link #refreshToken() refresh_token} and {@link #clientCredentials() client_credentials}.
* {@link #refreshToken() refresh_token}, {@link #clientCredentials() client_credentials}
* and {@link #password() password}.
* In addition to the standard authorization grants, an implementation of an extension grant
* may be supplied via {@link #provider(OAuth2AuthorizedClientProvider)}.
*
Expand All @@ -41,6 +43,7 @@
* @see AuthorizationCodeOAuth2AuthorizedClientProvider
* @see RefreshTokenOAuth2AuthorizedClientProvider
* @see ClientCredentialsOAuth2AuthorizedClientProvider
* @see PasswordOAuth2AuthorizedClientProvider
* @see DelegatingOAuth2AuthorizedClientProvider
*/
public final class OAuth2AuthorizedClientProviderBuilder {
Expand Down Expand Up @@ -247,6 +250,64 @@ public OAuth2AuthorizedClientProvider build() {
}
}

/**
* Configures support for the {@code password} grant.
*
* @return the {@link OAuth2AuthorizedClientProviderBuilder}
*/
public OAuth2AuthorizedClientProviderBuilder password() {
this.builders.computeIfAbsent(PasswordOAuth2AuthorizedClientProvider.class, k -> new PasswordGrantBuilder());
return OAuth2AuthorizedClientProviderBuilder.this;
}

/**
* Configures support for the {@code password} grant.
*
* @param builderConsumer a {@code Consumer} of {@link PasswordGrantBuilder} used for further configuration
* @return the {@link OAuth2AuthorizedClientProviderBuilder}
*/
public OAuth2AuthorizedClientProviderBuilder password(Consumer<PasswordGrantBuilder> builderConsumer) {
PasswordGrantBuilder builder = (PasswordGrantBuilder) this.builders.computeIfAbsent(
PasswordOAuth2AuthorizedClientProvider.class, k -> new PasswordGrantBuilder());
builderConsumer.accept(builder);
return OAuth2AuthorizedClientProviderBuilder.this;
}

/**
* A builder for the {@code password} grant.
*/
public class PasswordGrantBuilder implements Builder {
private OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient;

private PasswordGrantBuilder() {
}

/**
* Sets the client used when requesting an access token credential at the Token Endpoint.
*
* @param accessTokenResponseClient the client used when requesting an access token credential at the Token Endpoint
* @return the {@link PasswordGrantBuilder}
*/
public PasswordGrantBuilder accessTokenResponseClient(OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient) {
this.accessTokenResponseClient = accessTokenResponseClient;
return this;
}

/**
* Builds an instance of {@link PasswordOAuth2AuthorizedClientProvider}.
*
* @return the {@link PasswordOAuth2AuthorizedClientProvider}
*/
@Override
public OAuth2AuthorizedClientProvider build() {
PasswordOAuth2AuthorizedClientProvider authorizedClientProvider = new PasswordOAuth2AuthorizedClientProvider();
if (this.accessTokenResponseClient != null) {
authorizedClientProvider.setAccessTokenResponseClient(this.accessTokenResponseClient);
}
return authorizedClientProvider;
}
}

/**
* Builds an instance of {@link DelegatingOAuth2AuthorizedClientProvider}
* composed of one or more {@link OAuth2AuthorizedClientProvider}(s).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;

import org.springframework.lang.Nullable;
import org.springframework.security.oauth2.client.endpoint.DefaultPasswordTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
* An implementation of an {@link OAuth2AuthorizedClientProvider}
* for the {@link AuthorizationGrantType#PASSWORD password} grant.
*
* @author Joe Grandja
* @since 5.2
* @see OAuth2AuthorizedClientProvider
* @see DefaultPasswordTokenResponseClient
*/
public final class PasswordOAuth2AuthorizedClientProvider implements OAuth2AuthorizedClientProvider {
private OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient =
new DefaultPasswordTokenResponseClient();

/**
* Attempt to authorize the {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided {@code context}.
* Returns {@code null} if authorization is not supported,
* e.g. the client's {@link ClientRegistration#getAuthorizationGrantType() authorization grant type}
* is not {@link AuthorizationGrantType#PASSWORD password} OR the client is already authorized OR
* the {@link OAuth2AuthorizationContext#USERNAME_ATTRIBUTE_NAME username} and/or
* {@link OAuth2AuthorizationContext#PASSWORD_ATTRIBUTE_NAME password} attributes
* are not available in the provided {@code context}.
*
* <p>
* The following {@link OAuth2AuthorizationContext#getAttributes() context attributes} are supported:
* <ol>
* <li>{@link OAuth2AuthorizationContext#USERNAME_ATTRIBUTE_NAME} (required) - a {@code String} value for the resource owner's username</li>
* <li>{@link OAuth2AuthorizationContext#PASSWORD_ATTRIBUTE_NAME} (required) - a {@code String} value for the resource owner's password</li>
* </ol>
*
* @param context the context that holds authorization-specific state for the client
* @return the {@link OAuth2AuthorizedClient} or {@code null} if authorization is not supported
*/
@Override
@Nullable
public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) {
Assert.notNull(context, "context cannot be null");

ClientRegistration clientRegistration = context.getClientRegistration();
OAuth2AuthorizedClient authorizedClient = context.getAuthorizedClient();
if (!AuthorizationGrantType.PASSWORD.equals(clientRegistration.getAuthorizationGrantType()) ||
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it retrieve the OAuth2AuthorizedClient again if the token is expired?

Copy link
Contributor Author

@jgrandja jgrandja Aug 23, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The RefreshTokenOAuth2AuthorizedClientProvider will take care of this as long as it's configured/composed within the DefaultOAuth2AuthorizedClientManager - which by default it is configured.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the grant type is PASSWORD doesn't it need to use a OAuth2PasswordGrantRequest to request a new token?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does use a OAuth2PasswordGrantRequest when calling the OAuth2AccessTokenResponseClient- see code just below this logic.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry that was in context of my previous comment. If the token expires shouldn't a new token be requested with a OAuth2PasswordGrantRequest

Copy link
Contributor Author

@jgrandja jgrandja Aug 28, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the token expires shouldn't a new token be requested with a OAuth2PasswordGrantRequest

No. For expired access tokens, the RefreshTokenOAuth2AuthorizedClientProvider handles this. The PasswordOAuth2AuthorizedClientProvider only handles getting a new access token (if client is not authorized)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, looks like I missed a condition here. I'll update shortly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rwinch Thanks for catching this. I've pushed the updates.

authorizedClient != null) {
return null;
}

String username = context.getAttribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME);
String password = context.getAttribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME);
if (!StringUtils.hasText(username) || !StringUtils.hasText(password)) {
return null;
}

OAuth2PasswordGrantRequest passwordGrantRequest =
new OAuth2PasswordGrantRequest(clientRegistration, username, password);
OAuth2AccessTokenResponse tokenResponse =
this.accessTokenResponseClient.getTokenResponse(passwordGrantRequest);

return new OAuth2AuthorizedClient(clientRegistration, context.getPrincipal().getName(),
tokenResponse.getAccessToken(), tokenResponse.getRefreshToken());
}

/**
* Sets the client used when requesting an access token credential at the Token Endpoint for the {@code password} grant.
*
* @param accessTokenResponseClient the client used when requesting an access token credential at the Token Endpoint for the {@code password} grant
*/
public void setAccessTokenResponseClient(OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient) {
Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");
this.accessTokenResponseClient = accessTokenResponseClient;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;

import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.WebClientReactivePasswordTokenResponseClient;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Mono;

/**
* An implementation of a {@link ReactiveOAuth2AuthorizedClientProvider}
* for the {@link AuthorizationGrantType#PASSWORD password} grant.
*
* @author Joe Grandja
* @since 5.2
* @see ReactiveOAuth2AuthorizedClientProvider
* @see WebClientReactivePasswordTokenResponseClient
*/
public final class PasswordReactiveOAuth2AuthorizedClientProvider implements ReactiveOAuth2AuthorizedClientProvider {
private ReactiveOAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient =
new WebClientReactivePasswordTokenResponseClient();

/**
* Attempt to authorize the {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided {@code context}.
* Returns an empty {@code Mono} if authorization is not supported,
* e.g. the client's {@link ClientRegistration#getAuthorizationGrantType() authorization grant type}
* is not {@link AuthorizationGrantType#PASSWORD password} OR the client is already authorized OR
* the {@link OAuth2AuthorizationContext#USERNAME_ATTRIBUTE_NAME username} and/or
* {@link OAuth2AuthorizationContext#PASSWORD_ATTRIBUTE_NAME password} attributes
* are not available in the provided {@code context}.
*
* <p>
* The following {@link OAuth2AuthorizationContext#getAttributes() context attributes} are supported:
* <ol>
* <li>{@link OAuth2AuthorizationContext#USERNAME_ATTRIBUTE_NAME} (required) - a {@code String} value for the resource owner's username</li>
* <li>{@link OAuth2AuthorizationContext#PASSWORD_ATTRIBUTE_NAME} (required) - a {@code String} value for the resource owner's password</li>
* </ol>
*
* @param context the context that holds authorization-specific state for the client
* @return the {@link OAuth2AuthorizedClient} or an empty {@code Mono} if authorization is not supported
*/
@Override
public Mono<OAuth2AuthorizedClient> authorize(OAuth2AuthorizationContext context) {
Assert.notNull(context, "context cannot be null");

ClientRegistration clientRegistration = context.getClientRegistration();
OAuth2AuthorizedClient authorizedClient = context.getAuthorizedClient();
if (!AuthorizationGrantType.PASSWORD.equals(clientRegistration.getAuthorizationGrantType()) ||
authorizedClient != null) {
return Mono.empty();
}

String username = context.getAttribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME);
String password = context.getAttribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME);
if (!StringUtils.hasText(username) || !StringUtils.hasText(password)) {
return Mono.empty();
}

OAuth2PasswordGrantRequest passwordGrantRequest =
new OAuth2PasswordGrantRequest(clientRegistration, username, password);

return Mono.just(passwordGrantRequest)
.flatMap(this.accessTokenResponseClient::getTokenResponse)
.map(tokenResponse -> new OAuth2AuthorizedClient(clientRegistration, context.getPrincipal().getName(),
tokenResponse.getAccessToken(), tokenResponse.getRefreshToken()));
}

/**
* Sets the client used when requesting an access token credential at the Token Endpoint for the {@code password} grant.
*
* @param accessTokenResponseClient the client used when requesting an access token credential at the Token Endpoint for the {@code password} grant
*/
public void setAccessTokenResponseClient(ReactiveOAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient) {
Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");
this.accessTokenResponseClient = accessTokenResponseClient;
}
}
Loading