Skip to content

Add support for Resource Owner Password Credentials grant #7013

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
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,10 +17,13 @@

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;

import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -31,7 +34,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 +45,7 @@
* @see AuthorizationCodeOAuth2AuthorizedClientProvider
* @see RefreshTokenOAuth2AuthorizedClientProvider
* @see ClientCredentialsOAuth2AuthorizedClientProvider
* @see PasswordOAuth2AuthorizedClientProvider
* @see DelegatingOAuth2AuthorizedClientProvider
*/
public final class OAuth2AuthorizedClientProviderBuilder {
Expand Down Expand Up @@ -247,6 +252,95 @@ 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 Duration clockSkew;
private Clock clock;

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;
}

/**
* Sets the maximum acceptable clock skew, which is used when checking the access token expiry.
* An access token is considered expired if it's before {@code Instant.now(this.clock) - clockSkew}.
*
* @param clockSkew the maximum acceptable clock skew
* @return the {@link PasswordGrantBuilder}
*/
public PasswordGrantBuilder clockSkew(Duration clockSkew) {
this.clockSkew = clockSkew;
return this;
}

/**
* Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the access token expiry.
*
* @param clock the clock
* @return the {@link PasswordGrantBuilder}
*/
public PasswordGrantBuilder clock(Clock clock) {
this.clock = clock;
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);
}
if (this.clockSkew != null) {
authorizedClientProvider.setClockSkew(this.clockSkew);
}
if (this.clock != null) {
authorizedClientProvider.setClock(this.clock);
}
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,142 @@
/*
* 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.AbstractOAuth2Token;
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;

import java.time.Clock;
import java.time.Duration;
import java.time.Instant;

/**
* 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();
private Duration clockSkew = Duration.ofSeconds(60);
private Clock clock = Clock.systemUTC();

/**
* Attempt to authorize (or re-authorize) the {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided {@code context}.
* Returns {@code null} if authorization (or re-authorization) is not supported,
* e.g. the client's {@link ClientRegistration#getAuthorizationGrantType() authorization grant type}
* is not {@link AuthorizationGrantType#PASSWORD password} 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} OR
* the {@link OAuth2AuthorizedClient#getAccessToken() access token} is not expired.
*
* <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 (or re-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())) {
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;
}

if (authorizedClient != null && !hasTokenExpired(authorizedClient.getAccessToken())) {
// If client is already authorized and access token is NOT expired than no need for re-authorization
return null;
}

if (authorizedClient != null && hasTokenExpired(authorizedClient.getAccessToken()) && authorizedClient.getRefreshToken() != null) {
// If client is already authorized and access token is expired and a refresh token is available,
// than return and allow RefreshTokenOAuth2AuthorizedClientProvider to handle the refresh
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());
}

private boolean hasTokenExpired(AbstractOAuth2Token token) {
return token.getExpiresAt().isBefore(Instant.now(this.clock).minus(this.clockSkew));
}

/**
* 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;
}

/**
* Sets the maximum acceptable clock skew, which is used when checking the
* {@link OAuth2AuthorizedClient#getAccessToken() access token} expiry. The default is 60 seconds.
* An access token is considered expired if it's before {@code Instant.now(this.clock) - clockSkew}.
*
* @param clockSkew the maximum acceptable clock skew
*/
public void setClockSkew(Duration clockSkew) {
Assert.notNull(clockSkew, "clockSkew cannot be null");
Assert.isTrue(clockSkew.getSeconds() >= 0, "clockSkew must be >= 0");
this.clockSkew = clockSkew;
}

/**
* Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the access token expiry.
*
* @param clock the clock
*/
public void setClock(Clock clock) {
Assert.notNull(clock, "clock cannot be null");
this.clock = clock;
}
}
Loading