Bladeren bron

Add support for Resource Owner Password Credentials grant

Fixes gh-6003
Joe Grandja 6 jaren geleden
bovenliggende
commit
dcd997ea43
38 gewijzigde bestanden met toevoegingen van 2393 en 72 verwijderingen
  1. 2 3
      config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java
  2. 1 0
      config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2ClientImportSelector.java
  3. 12 5
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizationContext.java
  4. 96 4
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilder.java
  5. 142 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/PasswordOAuth2AuthorizedClientProvider.java
  6. 140 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/PasswordReactiveOAuth2AuthorizedClientProvider.java
  7. 93 1
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ReactiveOAuth2AuthorizedClientProviderBuilder.java
  8. 124 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultPasswordTokenResponseClient.java
  9. 81 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/OAuth2PasswordGrantRequest.java
  10. 89 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/OAuth2PasswordGrantRequestEntityConverter.java
  11. 134 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactivePasswordTokenResponseClient.java
  12. 17 7
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java
  13. 8 3
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizedClientManager.java
  14. 2 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolver.java
  15. 2 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunction.java
  16. 2 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java
  17. 1 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2AuthorizedClientArgumentResolver.java
  18. 32 20
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizedClientManager.java
  19. 36 0
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilderTests.java
  20. 190 0
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/PasswordOAuth2AuthorizedClientProviderTests.java
  21. 191 0
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/PasswordReactiveOAuth2AuthorizedClientProviderTests.java
  22. 50 0
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/ReactiveOAuth2AuthorizedClientProviderBuilderTests.java
  23. 220 0
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultPasswordTokenResponseClientTests.java
  24. 76 0
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/OAuth2PasswordGrantRequestEntityConverterTests.java
  25. 81 0
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/OAuth2PasswordGrantRequestTests.java
  26. 212 0
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactivePasswordTokenResponseClientTests.java
  27. 84 0
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java
  28. 11 0
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/TestClientRegistrations.java
  29. 29 11
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizedClientManagerTests.java
  30. 50 1
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolverTests.java
  31. 71 2
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunctionTests.java
  32. 43 0
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunctionTests.java
  33. 50 12
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizedClientManagerTests.java
  34. 2 1
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthorizationGrantType.java
  35. 11 1
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2ParameterNames.java
  36. 6 1
      oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/AuthorizationGrantTypeTests.java
  37. 1 0
      samples/boot/oauth2webclient-webflux/src/main/java/sample/config/WebClientConfig.java
  38. 1 0
      samples/boot/oauth2webclient/src/main/java/sample/config/WebClientConfig.java

+ 2 - 3
config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java

@@ -74,8 +74,8 @@ final class OAuth2ClientConfiguration {
 				OAuth2AuthorizedClientProviderBuilder authorizedClientProviderBuilder =
 						OAuth2AuthorizedClientProviderBuilder.builder()
 								.authorizationCode()
-								.refreshToken();
-
+								.refreshToken()
+								.password();
 				if (this.accessTokenResponseClient != null) {
 					authorizedClientProviderBuilder.clientCredentials(configurer ->
 									configurer.accessTokenResponseClient(this.accessTokenResponseClient));
@@ -83,7 +83,6 @@ final class OAuth2ClientConfiguration {
 					authorizedClientProviderBuilder.clientCredentials();
 				}
 				OAuth2AuthorizedClientProvider authorizedClientProvider = authorizedClientProviderBuilder.build();
-
 				DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
 						this.clientRegistrationRepository, this.authorizedClientRepository);
 				authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

+ 1 - 0
config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2ClientImportSelector.java

@@ -71,6 +71,7 @@ final class ReactiveOAuth2ClientImportSelector implements ImportSelector {
 								.authorizationCode()
 								.refreshToken()
 								.clientCredentials()
+								.password()
 								.build();
 				DefaultServerOAuth2AuthorizedClientManager authorizedClientManager = new DefaultServerOAuth2AuthorizedClientManager(
 						this.clientRegistrationRepository, getAuthorizedClientRepository());

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

@@ -36,14 +36,21 @@ import java.util.Map;
  */
 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;

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

@@ -17,23 +17,25 @@ package org.springframework.security.oauth2.client;
 
 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.Map;
-import java.util.List;
-import java.util.LinkedHashMap;
 import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
 import java.util.function.Consumer;
 
 /**
  * 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)}.
  *
@@ -43,6 +45,7 @@ import java.util.function.Consumer;
  * @see AuthorizationCodeOAuth2AuthorizedClientProvider
  * @see RefreshTokenOAuth2AuthorizedClientProvider
  * @see ClientCredentialsOAuth2AuthorizedClientProvider
+ * @see PasswordOAuth2AuthorizedClientProvider
  * @see DelegatingOAuth2AuthorizedClientProvider
  */
 public final class OAuth2AuthorizedClientProviderBuilder {
@@ -279,6 +282,95 @@ public final class OAuth2AuthorizedClientProviderBuilder {
 		}
 	}
 
+	/**
+	 * 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).

+ 142 - 0
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/PasswordOAuth2AuthorizedClientProvider.java

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

+ 140 - 0
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/PasswordReactiveOAuth2AuthorizedClientProvider.java

@@ -0,0 +1,140 @@
+/*
+ * 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.AbstractOAuth2Token;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+import reactor.core.publisher.Mono;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+
+/**
+ * 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();
+	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 an empty {@code Mono} 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 an empty {@code Mono} if authorization (or re-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())) {
+			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();
+		}
+
+		if (authorizedClient != null && !hasTokenExpired(authorizedClient.getAccessToken())) {
+			// If client is already authorized and access token is NOT expired than no need for re-authorization
+			return Mono.empty();
+		}
+
+		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 RefreshTokenReactiveOAuth2AuthorizedClientProvider to handle the refresh
+			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()));
+	}
+
+	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(ReactiveOAuth2AccessTokenResponseClient<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;
+	}
+}

+ 93 - 1
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ReactiveOAuth2AuthorizedClientProviderBuilder.java

@@ -16,6 +16,7 @@
 package org.springframework.security.oauth2.client;
 
 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.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
 import org.springframework.util.Assert;
@@ -33,7 +34,8 @@ import java.util.stream.Collectors;
  * A builder that builds a {@link DelegatingReactiveOAuth2AuthorizedClientProvider} composed of
  * one or more {@link ReactiveOAuth2AuthorizedClientProvider}(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(ReactiveOAuth2AuthorizedClientProvider)}.
  *
@@ -43,6 +45,7 @@ import java.util.stream.Collectors;
  * @see AuthorizationCodeReactiveOAuth2AuthorizedClientProvider
  * @see RefreshTokenReactiveOAuth2AuthorizedClientProvider
  * @see ClientCredentialsReactiveOAuth2AuthorizedClientProvider
+ * @see PasswordReactiveOAuth2AuthorizedClientProvider
  * @see DelegatingReactiveOAuth2AuthorizedClientProvider
  */
 public final class ReactiveOAuth2AuthorizedClientProviderBuilder {
@@ -279,6 +282,95 @@ public final class ReactiveOAuth2AuthorizedClientProviderBuilder {
 		}
 	}
 
+	/**
+	 * Configures support for the {@code password} grant.
+	 *
+	 * @return the {@link ReactiveOAuth2AuthorizedClientProviderBuilder}
+	 */
+	public ReactiveOAuth2AuthorizedClientProviderBuilder password() {
+		this.builders.computeIfAbsent(PasswordReactiveOAuth2AuthorizedClientProvider.class, k -> new PasswordGrantBuilder());
+		return ReactiveOAuth2AuthorizedClientProviderBuilder.this;
+	}
+
+	/**
+	 * Configures support for the {@code password} grant.
+	 *
+	 * @param builderConsumer a {@code Consumer} of {@link PasswordGrantBuilder} used for further configuration
+	 * @return the {@link ReactiveOAuth2AuthorizedClientProviderBuilder}
+	 */
+	public ReactiveOAuth2AuthorizedClientProviderBuilder password(Consumer<PasswordGrantBuilder> builderConsumer) {
+		PasswordGrantBuilder builder = (PasswordGrantBuilder) this.builders.computeIfAbsent(
+				PasswordReactiveOAuth2AuthorizedClientProvider.class, k -> new PasswordGrantBuilder());
+		builderConsumer.accept(builder);
+		return ReactiveOAuth2AuthorizedClientProviderBuilder.this;
+	}
+
+	/**
+	 * A builder for the {@code password} grant.
+	 */
+	public class PasswordGrantBuilder implements Builder {
+		private ReactiveOAuth2AccessTokenResponseClient<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(ReactiveOAuth2AccessTokenResponseClient<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 PasswordReactiveOAuth2AuthorizedClientProvider}.
+		 *
+		 * @return the {@link PasswordReactiveOAuth2AuthorizedClientProvider}
+		 */
+		@Override
+		public ReactiveOAuth2AuthorizedClientProvider build() {
+			PasswordReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = new PasswordReactiveOAuth2AuthorizedClientProvider();
+			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 DelegatingReactiveOAuth2AuthorizedClientProvider}
 	 * composed of one or more {@link ReactiveOAuth2AuthorizedClientProvider}(s).

+ 124 - 0
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultPasswordTokenResponseClient.java

@@ -0,0 +1,124 @@
+/*
+ * 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.endpoint;
+
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.http.RequestEntity;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.converter.FormHttpMessageConverter;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
+import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+import org.springframework.web.client.ResponseErrorHandler;
+import org.springframework.web.client.RestClientException;
+import org.springframework.web.client.RestOperations;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.Arrays;
+
+/**
+ * The default implementation of an {@link OAuth2AccessTokenResponseClient}
+ * for the {@link AuthorizationGrantType#PASSWORD password} grant.
+ * This implementation uses a {@link RestOperations} when requesting
+ * an access token credential at the Authorization Server's Token Endpoint.
+ *
+ * @author Joe Grandja
+ * @since 5.2
+ * @see OAuth2AccessTokenResponseClient
+ * @see OAuth2PasswordGrantRequest
+ * @see OAuth2AccessTokenResponse
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.3.2">Section 4.3.2 Access Token Request (Resource Owner Password Credentials Grant)</a>
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.3.3">Section 4.3.3 Access Token Response (Resource Owner Password Credentials Grant)</a>
+ */
+public final class DefaultPasswordTokenResponseClient implements OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> {
+	private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response";
+
+	private Converter<OAuth2PasswordGrantRequest, RequestEntity<?>> requestEntityConverter =
+			new OAuth2PasswordGrantRequestEntityConverter();
+
+	private RestOperations restOperations;
+
+	public DefaultPasswordTokenResponseClient() {
+		RestTemplate restTemplate = new RestTemplate(Arrays.asList(
+				new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter()));
+		restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
+		this.restOperations = restTemplate;
+	}
+
+	@Override
+	public OAuth2AccessTokenResponse getTokenResponse(OAuth2PasswordGrantRequest passwordGrantRequest) {
+		Assert.notNull(passwordGrantRequest, "passwordGrantRequest cannot be null");
+
+		RequestEntity<?> request = this.requestEntityConverter.convert(passwordGrantRequest);
+
+		ResponseEntity<OAuth2AccessTokenResponse> response;
+		try {
+			response = this.restOperations.exchange(request, OAuth2AccessTokenResponse.class);
+		} catch (RestClientException ex) {
+			OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE,
+					"An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: " + ex.getMessage(), null);
+			throw new OAuth2AuthorizationException(oauth2Error, ex);
+		}
+
+		OAuth2AccessTokenResponse tokenResponse = response.getBody();
+
+		if (CollectionUtils.isEmpty(tokenResponse.getAccessToken().getScopes())) {
+			// As per spec, in Section 5.1 Successful Access Token Response
+			// https://tools.ietf.org/html/rfc6749#section-5.1
+			// If AccessTokenResponse.scope is empty, then default to the scope
+			// originally requested by the client in the Token Request
+			tokenResponse = OAuth2AccessTokenResponse.withResponse(tokenResponse)
+					.scopes(passwordGrantRequest.getClientRegistration().getScopes())
+					.build();
+		}
+
+		return tokenResponse;
+	}
+
+	/**
+	 * Sets the {@link Converter} used for converting the {@link OAuth2PasswordGrantRequest}
+	 * to a {@link RequestEntity} representation of the OAuth 2.0 Access Token Request.
+	 *
+	 * @param requestEntityConverter the {@link Converter} used for converting to a {@link RequestEntity} representation of the Access Token Request
+	 */
+	public void setRequestEntityConverter(Converter<OAuth2PasswordGrantRequest, RequestEntity<?>> requestEntityConverter) {
+		Assert.notNull(requestEntityConverter, "requestEntityConverter cannot be null");
+		this.requestEntityConverter = requestEntityConverter;
+	}
+
+	/**
+	 * Sets the {@link RestOperations} used when requesting the OAuth 2.0 Access Token Response.
+	 *
+	 * <p>
+	 * <b>NOTE:</b> At a minimum, the supplied {@code restOperations} must be configured with the following:
+	 * <ol>
+	 *  <li>{@link HttpMessageConverter}'s - {@link FormHttpMessageConverter} and {@link OAuth2AccessTokenResponseHttpMessageConverter}</li>
+	 *  <li>{@link ResponseErrorHandler} - {@link OAuth2ErrorResponseErrorHandler}</li>
+	 * </ol>
+	 *
+	 * @param restOperations the {@link RestOperations} used when requesting the Access Token Response
+	 */
+	public void setRestOperations(RestOperations restOperations) {
+		Assert.notNull(restOperations, "restOperations cannot be null");
+		this.restOperations = restOperations;
+	}
+}

+ 81 - 0
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/OAuth2PasswordGrantRequest.java

@@ -0,0 +1,81 @@
+/*
+ * 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.endpoint;
+
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.util.Assert;
+
+/**
+ * An OAuth 2.0 Resource Owner Password Credentials Grant request
+ * that holds the resource owner's credentials.
+ *
+ * @author Joe Grandja
+ * @since 5.2
+ * @see AbstractOAuth2AuthorizationGrantRequest
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.3.3">Section 1.3.3 Resource Owner Password Credentials</a>
+ */
+public class OAuth2PasswordGrantRequest extends AbstractOAuth2AuthorizationGrantRequest {
+	private final ClientRegistration clientRegistration;
+	private final String username;
+	private final String password;
+
+	/**
+	 * Constructs an {@code OAuth2PasswordGrantRequest} using the provided parameters.
+	 *
+	 * @param clientRegistration the client registration
+	 * @param username the resource owner's username
+	 * @param password the resource owner's password
+	 */
+	public OAuth2PasswordGrantRequest(ClientRegistration clientRegistration, String username, String password) {
+		super(AuthorizationGrantType.PASSWORD);
+		Assert.notNull(clientRegistration, "clientRegistration cannot be null");
+		Assert.isTrue(AuthorizationGrantType.PASSWORD.equals(clientRegistration.getAuthorizationGrantType()),
+				"clientRegistration.authorizationGrantType must be AuthorizationGrantType.PASSWORD");
+		Assert.hasText(username, "username cannot be empty");
+		Assert.hasText(password, "password cannot be empty");
+		this.clientRegistration = clientRegistration;
+		this.username = username;
+		this.password = password;
+	}
+
+	/**
+	 * Returns the {@link ClientRegistration client registration}.
+	 *
+	 * @return the {@link ClientRegistration}
+	 */
+	public ClientRegistration getClientRegistration() {
+		return this.clientRegistration;
+	}
+
+	/**
+	 * Returns the resource owner's username.
+	 *
+	 * @return the resource owner's username
+	 */
+	public String getUsername() {
+		return this.username;
+	}
+
+	/**
+	 * Returns the resource owner's password.
+	 *
+	 * @return the resource owner's password
+	 */
+	public String getPassword() {
+		return this.password;
+	}
+}

+ 89 - 0
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/OAuth2PasswordGrantRequestEntityConverter.java

@@ -0,0 +1,89 @@
+/*
+ * 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.endpoint;
+
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.RequestEntity;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.util.StringUtils;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import java.net.URI;
+
+/**
+ * A {@link Converter} that converts the provided {@link OAuth2PasswordGrantRequest}
+ * to a {@link RequestEntity} representation of an OAuth 2.0 Access Token Request
+ * for the Resource Owner Password Credentials Grant.
+ *
+ * @author Joe Grandja
+ * @since 5.2
+ * @see Converter
+ * @see OAuth2PasswordGrantRequest
+ * @see RequestEntity
+ */
+public class OAuth2PasswordGrantRequestEntityConverter implements Converter<OAuth2PasswordGrantRequest, RequestEntity<?>> {
+
+	/**
+	 * Returns the {@link RequestEntity} used for the Access Token Request.
+	 *
+	 * @param passwordGrantRequest the password grant request
+	 * @return the {@link RequestEntity} used for the Access Token Request
+	 */
+	@Override
+	public RequestEntity<?> convert(OAuth2PasswordGrantRequest passwordGrantRequest) {
+		ClientRegistration clientRegistration = passwordGrantRequest.getClientRegistration();
+
+		HttpHeaders headers = OAuth2AuthorizationGrantRequestEntityUtils.getTokenRequestHeaders(clientRegistration);
+		MultiValueMap<String, String> formParameters = buildFormParameters(passwordGrantRequest);
+		URI uri = UriComponentsBuilder.fromUriString(clientRegistration.getProviderDetails().getTokenUri())
+				.build()
+				.toUri();
+
+		return new RequestEntity<>(formParameters, headers, HttpMethod.POST, uri);
+	}
+
+	/**
+	 * Returns a {@link MultiValueMap} of the form parameters used for the Access Token Request body.
+	 *
+	 * @param passwordGrantRequest the password grant request
+	 * @return a {@link MultiValueMap} of the form parameters used for the Access Token Request body
+	 */
+	private MultiValueMap<String, String> buildFormParameters(OAuth2PasswordGrantRequest passwordGrantRequest) {
+		ClientRegistration clientRegistration = passwordGrantRequest.getClientRegistration();
+
+		MultiValueMap<String, String> formParameters = new LinkedMultiValueMap<>();
+		formParameters.add(OAuth2ParameterNames.GRANT_TYPE, passwordGrantRequest.getGrantType().getValue());
+		formParameters.add(OAuth2ParameterNames.USERNAME, passwordGrantRequest.getUsername());
+		formParameters.add(OAuth2ParameterNames.PASSWORD, passwordGrantRequest.getPassword());
+		if (!CollectionUtils.isEmpty(clientRegistration.getScopes())) {
+			formParameters.add(OAuth2ParameterNames.SCOPE,
+					StringUtils.collectionToDelimitedString(clientRegistration.getScopes(), " "));
+		}
+		if (ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod())) {
+			formParameters.add(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());
+			formParameters.add(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret());
+		}
+
+		return formParameters;
+	}
+}

+ 134 - 0
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactivePasswordTokenResponseClient.java

@@ -0,0 +1,134 @@
+/*
+ * 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.endpoint;
+
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.core.io.buffer.DataBufferUtils;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+import org.springframework.web.reactive.function.BodyInserters;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.core.publisher.Mono;
+
+import java.util.Collections;
+import java.util.function.Consumer;
+
+import static org.springframework.security.oauth2.core.web.reactive.function.OAuth2BodyExtractors.oauth2AccessTokenResponse;
+
+/**
+ * An implementation of a {@link ReactiveOAuth2AccessTokenResponseClient}
+ * for the {@link AuthorizationGrantType#PASSWORD password} grant.
+ * This implementation uses {@link WebClient} when requesting
+ * an access token credential at the Authorization Server's Token Endpoint.
+ *
+ * @author Joe Grandja
+ * @since 5.2
+ * @see ReactiveOAuth2AccessTokenResponseClient
+ * @see OAuth2PasswordGrantRequest
+ * @see OAuth2AccessTokenResponse
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.3.2">Section 4.3.2 Access Token Request (Resource Owner Password Credentials Grant)</a>
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.3.3">Section 4.3.3 Access Token Response (Resource Owner Password Credentials Grant)</a>
+ */
+public final class WebClientReactivePasswordTokenResponseClient implements ReactiveOAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> {
+	private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response";
+	private WebClient webClient = WebClient.builder().build();
+
+	@Override
+	public Mono<OAuth2AccessTokenResponse> getTokenResponse(OAuth2PasswordGrantRequest passwordGrantRequest) {
+		Assert.notNull(passwordGrantRequest, "passwordGrantRequest cannot be null");
+		return Mono.defer(() -> {
+			ClientRegistration clientRegistration = passwordGrantRequest.getClientRegistration();
+			return this.webClient.post()
+					.uri(clientRegistration.getProviderDetails().getTokenUri())
+					.headers(tokenRequestHeaders(clientRegistration))
+					.body(tokenRequestBody(passwordGrantRequest))
+					.exchange()
+					.flatMap(response -> {
+						HttpStatus status = HttpStatus.resolve(response.rawStatusCode());
+						if (status == null || !status.is2xxSuccessful()) {
+							OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE,
+									"An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: " +
+											"HTTP Status Code " + response.rawStatusCode(), null);
+							return response
+									.bodyToMono(DataBuffer.class)
+									.map(DataBufferUtils::release)
+									.then(Mono.error(new OAuth2AuthorizationException(oauth2Error)));
+						}
+						return response.body(oauth2AccessTokenResponse());
+					})
+					.map(tokenResponse -> {
+						if (CollectionUtils.isEmpty(tokenResponse.getAccessToken().getScopes())) {
+							// As per spec, in Section 5.1 Successful Access Token Response
+							// https://tools.ietf.org/html/rfc6749#section-5.1
+							// If AccessTokenResponse.scope is empty, then default to the scope
+							// originally requested by the client in the Token Request
+							tokenResponse = OAuth2AccessTokenResponse.withResponse(tokenResponse)
+									.scopes(passwordGrantRequest.getClientRegistration().getScopes())
+									.build();
+						}
+						return tokenResponse;
+					});
+		});
+	}
+
+	private static Consumer<HttpHeaders> tokenRequestHeaders(ClientRegistration clientRegistration) {
+		return headers -> {
+			headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
+			headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
+			if (ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
+				headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret());
+			}
+		};
+	}
+
+	private static BodyInserters.FormInserter<String> tokenRequestBody(OAuth2PasswordGrantRequest passwordGrantRequest) {
+		ClientRegistration clientRegistration = passwordGrantRequest.getClientRegistration();
+		BodyInserters.FormInserter<String> body = BodyInserters.fromFormData(
+				OAuth2ParameterNames.GRANT_TYPE, passwordGrantRequest.getGrantType().getValue());
+		body.with(OAuth2ParameterNames.USERNAME, passwordGrantRequest.getUsername());
+		body.with(OAuth2ParameterNames.PASSWORD, passwordGrantRequest.getPassword());
+		if (!CollectionUtils.isEmpty(passwordGrantRequest.getClientRegistration().getScopes())) {
+			body.with(OAuth2ParameterNames.SCOPE,
+					StringUtils.collectionToDelimitedString(passwordGrantRequest.getClientRegistration().getScopes(), " "));
+		}
+		if (ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod())) {
+			body.with(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());
+			body.with(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret());
+		}
+		return body;
+	}
+
+	/**
+	 * Sets the {@link WebClient} used when requesting the OAuth 2.0 Access Token Response.
+	 *
+	 * @param webClient the {@link WebClient} used when requesting the Access Token Response
+	 */
+	public void setWebClient(WebClient webClient) {
+		Assert.notNull(webClient, "webClient cannot be null");
+		this.webClient = webClient;
+	}
+}

+ 17 - 7
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java

@@ -15,6 +15,13 @@
  */
 package org.springframework.security.oauth2.client.registration;
 
+import org.springframework.security.core.SpringSecurityCoreVersion;
+import org.springframework.security.oauth2.core.AuthenticationMethod;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
 import java.io.Serializable;
 import java.util.Arrays;
 import java.util.Collection;
@@ -24,13 +31,6 @@ import java.util.LinkedHashSet;
 import java.util.Map;
 import java.util.Set;
 
-import org.springframework.security.core.SpringSecurityCoreVersion;
-import org.springframework.security.oauth2.core.AuthenticationMethod;
-import org.springframework.security.oauth2.core.AuthorizationGrantType;
-import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
-import org.springframework.util.Assert;
-import org.springframework.util.StringUtils;
-
 /**
  * A representation of a client registration with an OAuth 2.0 or OpenID Connect 1.0 Provider.
  *
@@ -484,6 +484,8 @@ public final class ClientRegistration implements Serializable {
 			Assert.notNull(this.authorizationGrantType, "authorizationGrantType cannot be null");
 			if (AuthorizationGrantType.CLIENT_CREDENTIALS.equals(this.authorizationGrantType)) {
 				this.validateClientCredentialsGrantType();
+			} else if (AuthorizationGrantType.PASSWORD.equals(this.authorizationGrantType)) {
+				this.validatePasswordGrantType();
 			} else if (AuthorizationGrantType.IMPLICIT.equals(this.authorizationGrantType)) {
 				this.validateImplicitGrantType();
 			} else if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(this.authorizationGrantType)) {
@@ -552,6 +554,14 @@ public final class ClientRegistration implements Serializable {
 			Assert.hasText(this.tokenUri, "tokenUri cannot be empty");
 		}
 
+		private void validatePasswordGrantType() {
+			Assert.isTrue(AuthorizationGrantType.PASSWORD.equals(this.authorizationGrantType),
+					() -> "authorizationGrantType must be " + AuthorizationGrantType.PASSWORD.getValue());
+			Assert.hasText(this.registrationId, "registrationId cannot be empty");
+			Assert.hasText(this.clientId, "clientId cannot be empty");
+			Assert.hasText(this.tokenUri, "tokenUri cannot be empty");
+		}
+
 		private void validateScopes() {
 			if (this.scopes == null) {
 				return;

+ 8 - 3
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizedClientManager.java

@@ -28,7 +28,6 @@ import org.springframework.util.StringUtils;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.function.Function;
@@ -134,13 +133,19 @@ public final class DefaultOAuth2AuthorizedClientManager implements OAuth2Authori
 
 		@Override
 		public Map<String, Object> apply(OAuth2AuthorizeRequest authorizeRequest) {
-			Map<String, Object> contextAttributes = Collections.emptyMap();
+			Map<String, Object> contextAttributes = new HashMap<>();
 			String scope = authorizeRequest.getServletRequest().getParameter(OAuth2ParameterNames.SCOPE);
 			if (StringUtils.hasText(scope)) {
-				contextAttributes = new HashMap<>();
 				contextAttributes.put(OAuth2AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME,
 						StringUtils.delimitedListToStringArray(scope, " "));
 			}
+			String username = authorizeRequest.getServletRequest().getParameter(OAuth2ParameterNames.USERNAME);
+			String password = authorizeRequest.getServletRequest().getParameter(OAuth2ParameterNames.PASSWORD);
+			if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
+				contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username);
+				contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password);
+			}
+
 			return contextAttributes;
 		}
 	}

+ 2 - 0
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolver.java

@@ -105,6 +105,7 @@ public final class OAuth2AuthorizedClientArgumentResolver implements HandlerMeth
 						.authorizationCode()
 						.refreshToken()
 						.clientCredentials()
+						.password()
 						.build();
 		DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
 				clientRegistrationRepository, authorizedClientRepository);
@@ -193,6 +194,7 @@ public final class OAuth2AuthorizedClientArgumentResolver implements HandlerMeth
 						.authorizationCode()
 						.refreshToken()
 						.clientCredentials(configurer -> configurer.accessTokenResponseClient(clientCredentialsTokenResponseClient))
+						.password()
 						.build();
 		((DefaultOAuth2AuthorizedClientManager) this.authorizedClientManager).setAuthorizedClientProvider(authorizedClientProvider);
 	}

+ 2 - 0
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunction.java

@@ -122,6 +122,7 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
 						.authorizationCode()
 						.refreshToken()
 						.clientCredentials()
+						.password()
 						.build();
 		DefaultServerOAuth2AuthorizedClientManager authorizedClientManager = new DefaultServerOAuth2AuthorizedClientManager(
 				clientRegistrationRepository, authorizedClientRepository);
@@ -263,6 +264,7 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
 						.authorizationCode()
 						.refreshToken(configurer -> configurer.clockSkew(this.accessTokenExpiresSkew))
 						.clientCredentials(this::updateClientCredentialsProvider)
+						.password(configurer -> configurer.clockSkew(this.accessTokenExpiresSkew))
 						.build();
 		((DefaultServerOAuth2AuthorizedClientManager) this.authorizedClientManager).setAuthorizedClientProvider(authorizedClientProvider);
 	}

+ 2 - 0
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java

@@ -166,6 +166,7 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction
 						.authorizationCode()
 						.refreshToken()
 						.clientCredentials()
+						.password()
 						.build();
 		DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
 				clientRegistrationRepository, authorizedClientRepository);
@@ -210,6 +211,7 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction
 						.authorizationCode()
 						.refreshToken(configurer -> configurer.clockSkew(this.accessTokenExpiresSkew))
 						.clientCredentials(this::updateClientCredentialsProvider)
+						.password(configurer -> configurer.clockSkew(this.accessTokenExpiresSkew))
 						.build();
 		((DefaultOAuth2AuthorizedClientManager) this.authorizedClientManager).setAuthorizedClientProvider(authorizedClientProvider);
 	}

+ 1 - 0
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2AuthorizedClientArgumentResolver.java

@@ -98,6 +98,7 @@ public final class OAuth2AuthorizedClientArgumentResolver implements HandlerMeth
 						.authorizationCode()
 						.refreshToken()
 						.clientCredentials()
+						.password()
 						.build();
 		DefaultServerOAuth2AuthorizedClientManager authorizedClientManager = new DefaultServerOAuth2AuthorizedClientManager(
 				clientRegistrationRepository, authorizedClientRepository);

+ 32 - 20
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizedClientManager.java

@@ -19,6 +19,7 @@ import org.springframework.security.core.Authentication;
 import org.springframework.security.oauth2.client.OAuth2AuthorizationContext;
 import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
 import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProvider;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
 import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
 import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
 import org.springframework.util.Assert;
@@ -26,7 +27,6 @@ import org.springframework.util.StringUtils;
 import org.springframework.web.server.ServerWebExchange;
 import reactor.core.publisher.Mono;
 
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.function.Function;
@@ -43,7 +43,7 @@ public final class DefaultServerOAuth2AuthorizedClientManager implements ServerO
 	private final ReactiveClientRegistrationRepository clientRegistrationRepository;
 	private final ServerOAuth2AuthorizedClientRepository authorizedClientRepository;
 	private ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = context -> Mono.empty();
-	private Function<ServerOAuth2AuthorizeRequest, Map<String, Object>> contextAttributesMapper = new DefaultContextAttributesMapper();
+	private Function<ServerOAuth2AuthorizeRequest, Mono<Map<String, Object>>> contextAttributesMapper = new DefaultContextAttributesMapper();
 
 	/**
 	 * Constructs a {@code DefaultServerOAuth2AuthorizedClientManager} using the provided parameters.
@@ -72,28 +72,21 @@ public final class DefaultServerOAuth2AuthorizedClientManager implements ServerO
 						this.authorizedClientRepository.loadAuthorizedClient(clientRegistrationId, principal, serverWebExchange)))
 				.flatMap(authorizedClient -> {
 					// Re-authorize
-					OAuth2AuthorizationContext reauthorizationContext =
-							OAuth2AuthorizationContext.withAuthorizedClient(authorizedClient)
-									.principal(principal)
-									.attributes(this.contextAttributesMapper.apply(authorizeRequest))
-									.build();
-					return Mono.just(reauthorizationContext)
+					return authorizationContext(authorizeRequest, authorizedClient)
 							.flatMap(this.authorizedClientProvider::authorize)
 							.doOnNext(reauthorizedClient ->
 									this.authorizedClientRepository.saveAuthorizedClient(
 											reauthorizedClient, principal, serverWebExchange))
-							// Return the `authorizedClient` if `reauthorizedClient` is null, e.g. re-authorization is not supported
-							.defaultIfEmpty(authorizedClient);
+							// Default to the existing authorizedClient if the client was not re-authorized
+							.defaultIfEmpty(authorizeRequest.getAuthorizedClient() != null ?
+									authorizeRequest.getAuthorizedClient() : authorizedClient);
 				})
 				.switchIfEmpty(Mono.defer(() ->
 						// Authorize
 						this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId)
 								.switchIfEmpty(Mono.error(() -> new IllegalArgumentException(
 										"Could not find ClientRegistration with id '" + clientRegistrationId + "'")))
-								.map(clientRegistration -> OAuth2AuthorizationContext.withClientRegistration(clientRegistration)
-										.principal(principal)
-										.attributes(this.contextAttributesMapper.apply(authorizeRequest))
-										.build())
+								.flatMap(clientRegistration -> authorizationContext(authorizeRequest, clientRegistration))
 								.flatMap(this.authorizedClientProvider::authorize)
 								.doOnNext(authorizedClient ->
 										this.authorizedClientRepository.saveAuthorizedClient(
@@ -101,6 +94,26 @@ public final class DefaultServerOAuth2AuthorizedClientManager implements ServerO
 				));
 	}
 
+	private Mono<OAuth2AuthorizationContext> authorizationContext(ServerOAuth2AuthorizeRequest authorizeRequest,
+																	OAuth2AuthorizedClient authorizedClient) {
+		return Mono.just(authorizeRequest)
+				.flatMap(this.contextAttributesMapper::apply)
+				.map(attrs -> OAuth2AuthorizationContext.withAuthorizedClient(authorizedClient)
+						.principal(authorizeRequest.getPrincipal())
+						.attributes(attrs)
+						.build());
+	}
+
+	private Mono<OAuth2AuthorizationContext> authorizationContext(ServerOAuth2AuthorizeRequest authorizeRequest,
+																	ClientRegistration clientRegistration) {
+		return Mono.just(authorizeRequest)
+				.flatMap(this.contextAttributesMapper::apply)
+				.map(attrs -> OAuth2AuthorizationContext.withClientRegistration(clientRegistration)
+						.principal(authorizeRequest.getPrincipal())
+						.attributes(attrs)
+						.build());
+	}
+
 	/**
 	 * Sets the {@link ReactiveOAuth2AuthorizedClientProvider} used for authorizing (or re-authorizing) an OAuth 2.0 Client.
 	 *
@@ -118,7 +131,7 @@ public final class DefaultServerOAuth2AuthorizedClientManager implements ServerO
 	 * @param contextAttributesMapper the {@code Function} used for supplying the {@code Map} of attributes
 	 *                                   to the {@link OAuth2AuthorizationContext#getAttributes() authorization context}
 	 */
-	public void setContextAttributesMapper(Function<ServerOAuth2AuthorizeRequest, Map<String, Object>> contextAttributesMapper) {
+	public void setContextAttributesMapper(Function<ServerOAuth2AuthorizeRequest, Mono<Map<String, Object>>> contextAttributesMapper) {
 		Assert.notNull(contextAttributesMapper, "contextAttributesMapper cannot be null");
 		this.contextAttributesMapper = contextAttributesMapper;
 	}
@@ -126,18 +139,17 @@ public final class DefaultServerOAuth2AuthorizedClientManager implements ServerO
 	/**
 	 * The default implementation of the {@link #setContextAttributesMapper(Function) contextAttributesMapper}.
 	 */
-	public static class DefaultContextAttributesMapper implements Function<ServerOAuth2AuthorizeRequest, Map<String, Object>> {
+	public static class DefaultContextAttributesMapper implements Function<ServerOAuth2AuthorizeRequest, Mono<Map<String, Object>>> {
 
 		@Override
-		public Map<String, Object> apply(ServerOAuth2AuthorizeRequest authorizeRequest) {
-			Map<String, Object> contextAttributes = Collections.emptyMap();
+		public Mono<Map<String, Object>> apply(ServerOAuth2AuthorizeRequest authorizeRequest) {
+			Map<String, Object> contextAttributes = new HashMap<>();
 			String scope = authorizeRequest.getServerWebExchange().getRequest().getQueryParams().getFirst(OAuth2ParameterNames.SCOPE);
 			if (StringUtils.hasText(scope)) {
-				contextAttributes = new HashMap<>();
 				contextAttributes.put(OAuth2AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME,
 						StringUtils.delimitedListToStringArray(scope, " "));
 			}
-			return contextAttributes;
+			return Mono.just(contextAttributes);
 		}
 	}
 }

+ 36 - 0
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilderTests.java

@@ -23,6 +23,7 @@ import org.springframework.http.ResponseEntity;
 import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.oauth2.client.endpoint.DefaultClientCredentialsTokenResponseClient;
+import org.springframework.security.oauth2.client.endpoint.DefaultPasswordTokenResponseClient;
 import org.springframework.security.oauth2.client.endpoint.DefaultRefreshTokenTokenResponseClient;
 import org.springframework.security.oauth2.client.registration.ClientRegistration;
 import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
@@ -48,6 +49,7 @@ public class OAuth2AuthorizedClientProviderBuilderTests {
 	private RestOperations accessTokenClient;
 	private DefaultClientCredentialsTokenResponseClient clientCredentialsTokenResponseClient;
 	private DefaultRefreshTokenTokenResponseClient refreshTokenTokenResponseClient;
+	private DefaultPasswordTokenResponseClient passwordTokenResponseClient;
 	private Authentication principal;
 
 	@SuppressWarnings("unchecked")
@@ -61,6 +63,8 @@ public class OAuth2AuthorizedClientProviderBuilderTests {
 		this.refreshTokenTokenResponseClient.setRestOperations(this.accessTokenClient);
 		this.clientCredentialsTokenResponseClient = new DefaultClientCredentialsTokenResponseClient();
 		this.clientCredentialsTokenResponseClient.setRestOperations(this.accessTokenClient);
+		this.passwordTokenResponseClient = new DefaultPasswordTokenResponseClient();
+		this.passwordTokenResponseClient.setRestOperations(this.accessTokenClient);
 		this.principal = new TestingAuthenticationToken("principal", "password");
 	}
 
@@ -125,6 +129,25 @@ public class OAuth2AuthorizedClientProviderBuilderTests {
 		verify(this.accessTokenClient).exchange(any(RequestEntity.class), eq(OAuth2AccessTokenResponse.class));
 	}
 
+	@Test
+	public void buildWhenPasswordProviderThenProviderAuthorizes() {
+		OAuth2AuthorizedClientProvider authorizedClientProvider =
+				OAuth2AuthorizedClientProviderBuilder.builder()
+						.password(configurer -> configurer.accessTokenResponseClient(this.passwordTokenResponseClient))
+						.build();
+
+		OAuth2AuthorizationContext authorizationContext =
+				OAuth2AuthorizationContext.withClientRegistration(TestClientRegistrations.password().build())
+						.principal(this.principal)
+						.attribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, "username")
+						.attribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, "password")
+						.build();
+		OAuth2AuthorizedClient authorizedClient = authorizedClientProvider.authorize(authorizationContext);
+
+		assertThat(authorizedClient).isNotNull();
+		verify(this.accessTokenClient).exchange(any(RequestEntity.class), eq(OAuth2AccessTokenResponse.class));
+	}
+
 	@Test
 	public void buildWhenAllProvidersThenProvidersAuthorize() {
 		OAuth2AuthorizedClientProvider authorizedClientProvider =
@@ -132,6 +155,7 @@ public class OAuth2AuthorizedClientProviderBuilderTests {
 						.authorizationCode()
 						.refreshToken(configurer -> configurer.accessTokenResponseClient(this.refreshTokenTokenResponseClient))
 						.clientCredentials(configurer -> configurer.accessTokenResponseClient(this.clientCredentialsTokenResponseClient))
+						.password(configurer -> configurer.accessTokenResponseClient(this.passwordTokenResponseClient))
 						.build();
 
 		ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
@@ -172,6 +196,18 @@ public class OAuth2AuthorizedClientProviderBuilderTests {
 
 		assertThat(authorizedClient).isNotNull();
 		verify(this.accessTokenClient, times(2)).exchange(any(RequestEntity.class), eq(OAuth2AccessTokenResponse.class));
+
+		// password
+		OAuth2AuthorizationContext passwordContext =
+				OAuth2AuthorizationContext.withClientRegistration(TestClientRegistrations.password().build())
+						.principal(this.principal)
+						.attribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, "username")
+						.attribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, "password")
+						.build();
+		authorizedClient = authorizedClientProvider.authorize(passwordContext);
+
+		assertThat(authorizedClient).isNotNull();
+		verify(this.accessTokenClient, times(3)).exchange(any(RequestEntity.class), eq(OAuth2AccessTokenResponse.class));
 	}
 
 	@Test

+ 190 - 0
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/PasswordOAuth2AuthorizedClientProviderTests.java

@@ -0,0 +1,190 @@
+/*
+ * 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.junit.Before;
+import org.junit.Test;
+import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.core.Authentication;
+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.client.registration.TestClientRegistrations;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.TestOAuth2RefreshTokens;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
+import org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses;
+
+import java.time.Duration;
+import java.time.Instant;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests for {@link PasswordOAuth2AuthorizedClientProvider}.
+ *
+ * @author Joe Grandja
+ */
+public class PasswordOAuth2AuthorizedClientProviderTests {
+	private PasswordOAuth2AuthorizedClientProvider authorizedClientProvider;
+	private OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient;
+	private ClientRegistration clientRegistration;
+	private Authentication principal;
+
+	@Before
+	public void setup() {
+		this.authorizedClientProvider = new PasswordOAuth2AuthorizedClientProvider();
+		this.accessTokenResponseClient = mock(OAuth2AccessTokenResponseClient.class);
+		this.authorizedClientProvider.setAccessTokenResponseClient(this.accessTokenResponseClient);
+		this.clientRegistration = TestClientRegistrations.password().build();
+		this.principal = new TestingAuthenticationToken("principal", "password");
+	}
+
+	@Test
+	public void setAccessTokenResponseClientWhenClientIsNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> this.authorizedClientProvider.setAccessTokenResponseClient(null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("accessTokenResponseClient cannot be null");
+	}
+
+	@Test
+	public void setClockSkewWhenNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> this.authorizedClientProvider.setClockSkew(null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("clockSkew cannot be null");
+	}
+
+	@Test
+	public void setClockSkewWhenNegativeSecondsThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> this.authorizedClientProvider.setClockSkew(Duration.ofSeconds(-1)))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("clockSkew must be >= 0");
+	}
+
+	@Test
+	public void setClockWhenNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> this.authorizedClientProvider.setClock(null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("clock cannot be null");
+	}
+
+	@Test
+	public void authorizeWhenContextIsNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> this.authorizedClientProvider.authorize(null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("context cannot be null");
+	}
+
+	@Test
+	public void authorizeWhenNotPasswordThenUnableToAuthorize() {
+		ClientRegistration clientRegistration = TestClientRegistrations.clientCredentials().build();
+
+		OAuth2AuthorizationContext authorizationContext =
+				OAuth2AuthorizationContext.withClientRegistration(clientRegistration)
+						.principal(this.principal)
+						.build();
+		assertThat(this.authorizedClientProvider.authorize(authorizationContext)).isNull();
+	}
+
+	@Test
+	public void authorizeWhenPasswordAndNotAuthorizedAndEmptyUsernameThenUnableToAuthorize() {
+		OAuth2AuthorizationContext authorizationContext =
+				OAuth2AuthorizationContext.withClientRegistration(this.clientRegistration)
+						.principal(this.principal)
+						.attribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, null)
+						.attribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, "password")
+						.build();
+		assertThat(this.authorizedClientProvider.authorize(authorizationContext)).isNull();
+	}
+
+	@Test
+	public void authorizeWhenPasswordAndNotAuthorizedAndEmptyPasswordThenUnableToAuthorize() {
+		OAuth2AuthorizationContext authorizationContext =
+				OAuth2AuthorizationContext.withClientRegistration(this.clientRegistration)
+						.principal(this.principal)
+						.attribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, "username")
+						.attribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, null)
+						.build();
+		assertThat(this.authorizedClientProvider.authorize(authorizationContext)).isNull();
+	}
+
+	@Test
+	public void authorizeWhenPasswordAndNotAuthorizedThenAuthorize() {
+		OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build();
+		when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse);
+
+		OAuth2AuthorizationContext authorizationContext =
+				OAuth2AuthorizationContext.withClientRegistration(this.clientRegistration)
+						.principal(this.principal)
+						.attribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, "username")
+						.attribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, "password")
+						.build();
+		OAuth2AuthorizedClient authorizedClient = this.authorizedClientProvider.authorize(authorizationContext);
+
+		assertThat(authorizedClient.getClientRegistration()).isSameAs(this.clientRegistration);
+		assertThat(authorizedClient.getPrincipalName()).isEqualTo(this.principal.getName());
+		assertThat(authorizedClient.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken());
+	}
+
+	@Test
+	public void authorizeWhenPasswordAndAuthorizedWithoutRefreshTokenAndTokenExpiredThenReauthorize() {
+		Instant issuedAt = Instant.now().minus(Duration.ofDays(1));
+		Instant expiresAt = issuedAt.plus(Duration.ofMinutes(60));
+		OAuth2AccessToken accessToken = new OAuth2AccessToken(
+				OAuth2AccessToken.TokenType.BEARER, "access-token-expired", issuedAt, expiresAt);
+		OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(
+				this.clientRegistration, this.principal.getName(), accessToken);	// without refresh token
+
+		OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build();
+		when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse);
+
+		OAuth2AuthorizationContext authorizationContext =
+				OAuth2AuthorizationContext.withAuthorizedClient(authorizedClient)
+						.attribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, "username")
+						.attribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, "password")
+						.principal(this.principal)
+						.build();
+		authorizedClient = this.authorizedClientProvider.authorize(authorizationContext);
+
+		assertThat(authorizedClient.getClientRegistration()).isSameAs(this.clientRegistration);
+		assertThat(authorizedClient.getPrincipalName()).isEqualTo(this.principal.getName());
+		assertThat(authorizedClient.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken());
+
+	}
+
+	@Test
+	public void authorizeWhenPasswordAndAuthorizedWithRefreshTokenAndTokenExpiredThenNotReauthorize() {
+		Instant issuedAt = Instant.now().minus(Duration.ofDays(1));
+		Instant expiresAt = issuedAt.plus(Duration.ofMinutes(60));
+		OAuth2AccessToken accessToken = new OAuth2AccessToken(
+				OAuth2AccessToken.TokenType.BEARER, "access-token-expired", issuedAt, expiresAt);
+		OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(
+				this.clientRegistration, this.principal.getName(),
+				accessToken, TestOAuth2RefreshTokens.refreshToken());	// with refresh token
+
+		OAuth2AuthorizationContext authorizationContext =
+				OAuth2AuthorizationContext.withAuthorizedClient(authorizedClient)
+						.attribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, "username")
+						.attribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, "password")
+						.principal(this.principal)
+						.build();
+		assertThat(this.authorizedClientProvider.authorize(authorizationContext)).isNull();
+	}
+}

+ 191 - 0
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/PasswordReactiveOAuth2AuthorizedClientProviderTests.java

@@ -0,0 +1,191 @@
+/*
+ * 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.junit.Before;
+import org.junit.Test;
+import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
+import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.TestOAuth2RefreshTokens;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
+import org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses;
+import reactor.core.publisher.Mono;
+
+import java.time.Duration;
+import java.time.Instant;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests for {@link PasswordReactiveOAuth2AuthorizedClientProvider}.
+ *
+ * @author Joe Grandja
+ */
+public class PasswordReactiveOAuth2AuthorizedClientProviderTests {
+	private PasswordReactiveOAuth2AuthorizedClientProvider authorizedClientProvider;
+	private ReactiveOAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient;
+	private ClientRegistration clientRegistration;
+	private Authentication principal;
+
+	@Before
+	public void setup() {
+		this.authorizedClientProvider = new PasswordReactiveOAuth2AuthorizedClientProvider();
+		this.accessTokenResponseClient = mock(ReactiveOAuth2AccessTokenResponseClient.class);
+		this.authorizedClientProvider.setAccessTokenResponseClient(this.accessTokenResponseClient);
+		this.clientRegistration = TestClientRegistrations.password().build();
+		this.principal = new TestingAuthenticationToken("principal", "password");
+	}
+
+	@Test
+	public void setAccessTokenResponseClientWhenClientIsNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> this.authorizedClientProvider.setAccessTokenResponseClient(null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("accessTokenResponseClient cannot be null");
+	}
+
+	@Test
+	public void setClockSkewWhenNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> this.authorizedClientProvider.setClockSkew(null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("clockSkew cannot be null");
+	}
+
+	@Test
+	public void setClockSkewWhenNegativeSecondsThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> this.authorizedClientProvider.setClockSkew(Duration.ofSeconds(-1)))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("clockSkew must be >= 0");
+	}
+
+	@Test
+	public void setClockWhenNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> this.authorizedClientProvider.setClock(null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("clock cannot be null");
+	}
+
+	@Test
+	public void authorizeWhenContextIsNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> this.authorizedClientProvider.authorize(null).block())
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("context cannot be null");
+	}
+
+	@Test
+	public void authorizeWhenNotPasswordThenUnableToAuthorize() {
+		ClientRegistration clientRegistration = TestClientRegistrations.clientCredentials().build();
+
+		OAuth2AuthorizationContext authorizationContext =
+				OAuth2AuthorizationContext.withClientRegistration(clientRegistration)
+						.principal(this.principal)
+						.build();
+		assertThat(this.authorizedClientProvider.authorize(authorizationContext).block()).isNull();
+	}
+
+	@Test
+	public void authorizeWhenPasswordAndNotAuthorizedAndEmptyUsernameThenUnableToAuthorize() {
+		OAuth2AuthorizationContext authorizationContext =
+				OAuth2AuthorizationContext.withClientRegistration(this.clientRegistration)
+						.principal(this.principal)
+						.attribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, null)
+						.attribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, "password")
+						.build();
+		assertThat(this.authorizedClientProvider.authorize(authorizationContext).block()).isNull();
+	}
+
+	@Test
+	public void authorizeWhenPasswordAndNotAuthorizedAndEmptyPasswordThenUnableToAuthorize() {
+		OAuth2AuthorizationContext authorizationContext =
+				OAuth2AuthorizationContext.withClientRegistration(this.clientRegistration)
+						.principal(this.principal)
+						.attribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, "username")
+						.attribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, null)
+						.build();
+		assertThat(this.authorizedClientProvider.authorize(authorizationContext).block()).isNull();
+	}
+
+	@Test
+	public void authorizeWhenPasswordAndNotAuthorizedThenAuthorize() {
+		OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build();
+		when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse));
+
+		OAuth2AuthorizationContext authorizationContext =
+				OAuth2AuthorizationContext.withClientRegistration(this.clientRegistration)
+						.principal(this.principal)
+						.attribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, "username")
+						.attribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, "password")
+						.build();
+		OAuth2AuthorizedClient authorizedClient = this.authorizedClientProvider.authorize(authorizationContext).block();
+
+		assertThat(authorizedClient.getClientRegistration()).isSameAs(this.clientRegistration);
+		assertThat(authorizedClient.getPrincipalName()).isEqualTo(this.principal.getName());
+		assertThat(authorizedClient.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken());
+	}
+
+	@Test
+	public void authorizeWhenPasswordAndAuthorizedWithoutRefreshTokenAndTokenExpiredThenReauthorize() {
+		Instant issuedAt = Instant.now().minus(Duration.ofDays(1));
+		Instant expiresAt = issuedAt.plus(Duration.ofMinutes(60));
+		OAuth2AccessToken accessToken = new OAuth2AccessToken(
+				OAuth2AccessToken.TokenType.BEARER, "access-token-expired", issuedAt, expiresAt);
+		OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(
+				this.clientRegistration, this.principal.getName(), accessToken);	// without refresh token
+
+		OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build();
+		when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse));
+
+		OAuth2AuthorizationContext authorizationContext =
+				OAuth2AuthorizationContext.withAuthorizedClient(authorizedClient)
+						.attribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, "username")
+						.attribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, "password")
+						.principal(this.principal)
+						.build();
+		authorizedClient = this.authorizedClientProvider.authorize(authorizationContext).block();
+
+		assertThat(authorizedClient.getClientRegistration()).isSameAs(this.clientRegistration);
+		assertThat(authorizedClient.getPrincipalName()).isEqualTo(this.principal.getName());
+		assertThat(authorizedClient.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken());
+
+	}
+
+	@Test
+	public void authorizeWhenPasswordAndAuthorizedWithRefreshTokenAndTokenExpiredThenNotReauthorize() {
+		Instant issuedAt = Instant.now().minus(Duration.ofDays(1));
+		Instant expiresAt = issuedAt.plus(Duration.ofMinutes(60));
+		OAuth2AccessToken accessToken = new OAuth2AccessToken(
+				OAuth2AccessToken.TokenType.BEARER, "access-token-expired", issuedAt, expiresAt);
+		OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(
+				this.clientRegistration, this.principal.getName(),
+				accessToken, TestOAuth2RefreshTokens.refreshToken());	// with refresh token
+
+		OAuth2AuthorizationContext authorizationContext =
+				OAuth2AuthorizationContext.withAuthorizedClient(authorizedClient)
+						.attribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, "username")
+						.attribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, "password")
+						.principal(this.principal)
+						.build();
+		assertThat(this.authorizedClientProvider.authorize(authorizationContext).block()).isNull();
+	}
+}

+ 50 - 0
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/ReactiveOAuth2AuthorizedClientProviderBuilderTests.java

@@ -149,6 +149,37 @@ public class ReactiveOAuth2AuthorizedClientProviderBuilderTests {
 		assertThat(formParameters).contains("grant_type=client_credentials");
 	}
 
+	@Test
+	public void buildWhenPasswordProviderThenProviderAuthorizes() throws Exception {
+		String accessTokenSuccessResponse = "{\n" +
+				"	\"access_token\": \"access-token-1234\",\n" +
+				"   \"token_type\": \"bearer\",\n" +
+				"   \"expires_in\": \"3600\"\n" +
+				"}\n";
+		this.server.enqueue(jsonResponse(accessTokenSuccessResponse));
+
+		ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
+				ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
+						.password()
+						.build();
+
+		OAuth2AuthorizationContext authorizationContext =
+				OAuth2AuthorizationContext.withClientRegistration(this.clientRegistrationBuilder.authorizationGrantType(AuthorizationGrantType.PASSWORD).build())
+						.principal(this.principal)
+						.attribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, "username")
+						.attribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, "password")
+						.build();
+		OAuth2AuthorizedClient authorizedClient = authorizedClientProvider.authorize(authorizationContext).block();
+
+		assertThat(authorizedClient).isNotNull();
+
+		assertThat(this.server.getRequestCount()).isEqualTo(1);
+
+		RecordedRequest recordedRequest = this.server.takeRequest();
+		String formParameters = recordedRequest.getBody().readUtf8();
+		assertThat(formParameters).contains("grant_type=password");
+	}
+
 	@Test
 	public void buildWhenAllProvidersThenProvidersAuthorize() throws Exception {
 		String accessTokenSuccessResponse = "{\n" +
@@ -158,12 +189,14 @@ public class ReactiveOAuth2AuthorizedClientProviderBuilderTests {
 				"}\n";
 		this.server.enqueue(jsonResponse(accessTokenSuccessResponse));
 		this.server.enqueue(jsonResponse(accessTokenSuccessResponse));
+		this.server.enqueue(jsonResponse(accessTokenSuccessResponse));
 
 		ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
 				ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
 						.authorizationCode()
 						.refreshToken()
 						.clientCredentials()
+						.password()
 						.build();
 
 		// authorization_code
@@ -211,6 +244,23 @@ public class ReactiveOAuth2AuthorizedClientProviderBuilderTests {
 		recordedRequest = this.server.takeRequest();
 		formParameters = recordedRequest.getBody().readUtf8();
 		assertThat(formParameters).contains("grant_type=client_credentials");
+
+		// password
+		OAuth2AuthorizationContext passwordContext =
+				OAuth2AuthorizationContext.withClientRegistration(this.clientRegistrationBuilder.authorizationGrantType(AuthorizationGrantType.PASSWORD).build())
+						.principal(this.principal)
+						.attribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, "username")
+						.attribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, "password")
+						.build();
+		authorizedClient = authorizedClientProvider.authorize(passwordContext).block();
+
+		assertThat(authorizedClient).isNotNull();
+
+		assertThat(this.server.getRequestCount()).isEqualTo(3);
+
+		recordedRequest = this.server.takeRequest();
+		formParameters = recordedRequest.getBody().readUtf8();
+		assertThat(formParameters).contains("grant_type=password");
 	}
 
 	@Test

+ 220 - 0
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultPasswordTokenResponseClientTests.java

@@ -0,0 +1,220 @@
+/*
+ * 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.endpoint;
+
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
+
+import java.time.Instant;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Tests for {@link DefaultPasswordTokenResponseClient}.
+ *
+ * @author Joe Grandja
+ */
+public class DefaultPasswordTokenResponseClientTests {
+	private DefaultPasswordTokenResponseClient tokenResponseClient = new DefaultPasswordTokenResponseClient();
+	private ClientRegistration.Builder clientRegistrationBuilder;
+	private String username = "user1";
+	private String password = "password";
+	private MockWebServer server;
+
+	@Before
+	public void setup() throws Exception {
+		this.server = new MockWebServer();
+		this.server.start();
+		String tokenUri = this.server.url("/oauth2/token").toString();
+		this.clientRegistrationBuilder = TestClientRegistrations.clientRegistration()
+				.authorizationGrantType(AuthorizationGrantType.PASSWORD)
+				.scope("read", "write")
+				.tokenUri(tokenUri);
+	}
+
+	@After
+	public void cleanup() throws Exception {
+		this.server.shutdown();
+	}
+
+	@Test
+	public void setRequestEntityConverterWhenConverterIsNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> this.tokenResponseClient.setRequestEntityConverter(null))
+				.isInstanceOf(IllegalArgumentException.class);
+	}
+
+	@Test
+	public void setRestOperationsWhenRestOperationsIsNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> this.tokenResponseClient.setRestOperations(null))
+				.isInstanceOf(IllegalArgumentException.class);
+	}
+
+	@Test
+	public void getTokenResponseWhenRequestIsNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> this.tokenResponseClient.getTokenResponse(null))
+				.isInstanceOf(IllegalArgumentException.class);
+	}
+
+	@Test
+	public void getTokenResponseWhenSuccessResponseThenReturnAccessTokenResponse() throws Exception {
+		String accessTokenSuccessResponse = "{\n" +
+				"	\"access_token\": \"access-token-1234\",\n" +
+				"   \"token_type\": \"bearer\",\n" +
+				"   \"expires_in\": \"3600\"\n" +
+				"}\n";
+		this.server.enqueue(jsonResponse(accessTokenSuccessResponse));
+
+		Instant expiresAtBefore = Instant.now().plusSeconds(3600);
+
+		ClientRegistration clientRegistration = this.clientRegistrationBuilder.build();
+		OAuth2PasswordGrantRequest passwordGrantRequest = new OAuth2PasswordGrantRequest(
+				clientRegistration, this.username, this.password);
+
+		OAuth2AccessTokenResponse accessTokenResponse = this.tokenResponseClient.getTokenResponse(passwordGrantRequest);
+
+		Instant expiresAtAfter = Instant.now().plusSeconds(3600);
+
+		RecordedRequest recordedRequest = this.server.takeRequest();
+		assertThat(recordedRequest.getMethod()).isEqualTo(HttpMethod.POST.toString());
+		assertThat(recordedRequest.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON_UTF8_VALUE);
+		assertThat(recordedRequest.getHeader(HttpHeaders.CONTENT_TYPE)).isEqualTo(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8");
+
+		String formParameters = recordedRequest.getBody().readUtf8();
+		assertThat(formParameters).contains("grant_type=password");
+		assertThat(formParameters).contains("username=user1");
+		assertThat(formParameters).contains("password=password");
+		assertThat(formParameters).contains("scope=read+write");
+
+		assertThat(accessTokenResponse.getAccessToken().getTokenValue()).isEqualTo("access-token-1234");
+		assertThat(accessTokenResponse.getAccessToken().getTokenType()).isEqualTo(OAuth2AccessToken.TokenType.BEARER);
+		assertThat(accessTokenResponse.getAccessToken().getExpiresAt()).isBetween(expiresAtBefore, expiresAtAfter);
+		assertThat(accessTokenResponse.getAccessToken().getScopes()).containsExactly(clientRegistration.getScopes().toArray(new String[0]));
+		assertThat(accessTokenResponse.getRefreshToken()).isNull();
+	}
+
+	@Test
+	public void getTokenResponseWhenClientAuthenticationPostThenFormParametersAreSent() throws Exception {
+		String accessTokenSuccessResponse = "{\n" +
+				"	\"access_token\": \"access-token-1234\",\n" +
+				"   \"token_type\": \"bearer\",\n" +
+				"   \"expires_in\": \"3600\"\n" +
+				"}\n";
+		this.server.enqueue(jsonResponse(accessTokenSuccessResponse));
+
+		ClientRegistration clientRegistration = this.clientRegistrationBuilder
+				.clientAuthenticationMethod(ClientAuthenticationMethod.POST)
+				.build();
+		OAuth2PasswordGrantRequest passwordGrantRequest = new OAuth2PasswordGrantRequest(
+				clientRegistration, this.username, this.password);
+
+		this.tokenResponseClient.getTokenResponse(passwordGrantRequest);
+
+		RecordedRequest recordedRequest = this.server.takeRequest();
+		assertThat(recordedRequest.getHeader(HttpHeaders.AUTHORIZATION)).isNull();
+
+		String formParameters = recordedRequest.getBody().readUtf8();
+		assertThat(formParameters).contains("client_id=client-id");
+		assertThat(formParameters).contains("client_secret=client-secret");
+	}
+
+	@Test
+	public void getTokenResponseWhenSuccessResponseAndNotBearerTokenTypeThenThrowOAuth2AuthorizationException() {
+		String accessTokenSuccessResponse = "{\n" +
+				"	\"access_token\": \"access-token-1234\",\n" +
+				"   \"token_type\": \"not-bearer\",\n" +
+				"   \"expires_in\": \"3600\"\n" +
+				"}\n";
+		this.server.enqueue(jsonResponse(accessTokenSuccessResponse));
+
+		OAuth2PasswordGrantRequest passwordGrantRequest = new OAuth2PasswordGrantRequest(
+				this.clientRegistrationBuilder.build(), this.username, this.password);
+
+		assertThatThrownBy(() -> this.tokenResponseClient.getTokenResponse(passwordGrantRequest))
+				.isInstanceOf(OAuth2AuthorizationException.class)
+				.hasMessageContaining("[invalid_token_response] An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response")
+				.hasMessageContaining("tokenType cannot be null");
+	}
+
+	@Test
+	public void getTokenResponseWhenSuccessResponseIncludesScopeThenAccessTokenHasResponseScope() throws Exception {
+		String accessTokenSuccessResponse = "{\n" +
+				"	\"access_token\": \"access-token-1234\",\n" +
+				"   \"token_type\": \"bearer\",\n" +
+				"   \"expires_in\": \"3600\",\n" +
+				"   \"scope\": \"read\"\n" +
+				"}\n";
+		this.server.enqueue(jsonResponse(accessTokenSuccessResponse));
+
+		OAuth2PasswordGrantRequest passwordGrantRequest = new OAuth2PasswordGrantRequest(
+				this.clientRegistrationBuilder.build(), this.username, this.password);
+
+		OAuth2AccessTokenResponse accessTokenResponse = this.tokenResponseClient.getTokenResponse(passwordGrantRequest);
+
+		RecordedRequest recordedRequest = this.server.takeRequest();
+		String formParameters = recordedRequest.getBody().readUtf8();
+		assertThat(formParameters).contains("scope=read");
+
+		assertThat(accessTokenResponse.getAccessToken().getScopes()).containsExactly("read");
+	}
+
+	@Test
+	public void getTokenResponseWhenErrorResponseThenThrowOAuth2AuthorizationException() {
+		String accessTokenErrorResponse = "{\n" +
+				"   \"error\": \"unauthorized_client\"\n" +
+				"}\n";
+		this.server.enqueue(jsonResponse(accessTokenErrorResponse).setResponseCode(400));
+
+		OAuth2PasswordGrantRequest passwordGrantRequest = new OAuth2PasswordGrantRequest(
+				this.clientRegistrationBuilder.build(), this.username, this.password);
+
+		assertThatThrownBy(() -> this.tokenResponseClient.getTokenResponse(passwordGrantRequest))
+				.isInstanceOf(OAuth2AuthorizationException.class)
+				.hasMessageContaining("[unauthorized_client]");
+	}
+
+	@Test
+	public void getTokenResponseWhenServerErrorResponseThenThrowOAuth2AuthorizationException() {
+		this.server.enqueue(new MockResponse().setResponseCode(500));
+
+		OAuth2PasswordGrantRequest passwordGrantRequest = new OAuth2PasswordGrantRequest(
+				this.clientRegistrationBuilder.build(), this.username, this.password);
+
+		assertThatThrownBy(() -> this.tokenResponseClient.getTokenResponse(passwordGrantRequest))
+				.isInstanceOf(OAuth2AuthorizationException.class)
+				.hasMessage("[invalid_token_response] An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: 500 Server Error");
+	}
+
+	private MockResponse jsonResponse(String json) {
+		return new MockResponse()
+				.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
+				.setBody(json);
+	}
+}

+ 76 - 0
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/OAuth2PasswordGrantRequestEntityConverterTests.java

@@ -0,0 +1,76 @@
+/*
+ * 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.endpoint;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.http.RequestEntity;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.util.MultiValueMap;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE;
+
+/**
+ * Tests for {@link OAuth2PasswordGrantRequestEntityConverter}.
+ *
+ * @author Joe Grandja
+ */
+public class OAuth2PasswordGrantRequestEntityConverterTests {
+	private OAuth2PasswordGrantRequestEntityConverter converter = new OAuth2PasswordGrantRequestEntityConverter();
+	private OAuth2PasswordGrantRequest passwordGrantRequest;
+
+	@Before
+	public void setup() {
+		ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration()
+				.authorizationGrantType(AuthorizationGrantType.PASSWORD)
+				.scope("read", "write")
+				.build();
+		this.passwordGrantRequest = new OAuth2PasswordGrantRequest(clientRegistration, "user1", "password");
+	}
+
+	@SuppressWarnings("unchecked")
+	@Test
+	public void convertWhenGrantRequestValidThenConverts() {
+		RequestEntity<?> requestEntity = this.converter.convert(this.passwordGrantRequest);
+
+		ClientRegistration clientRegistration = this.passwordGrantRequest.getClientRegistration();
+
+		assertThat(requestEntity.getMethod()).isEqualTo(HttpMethod.POST);
+		assertThat(requestEntity.getUrl().toASCIIString()).isEqualTo(
+				clientRegistration.getProviderDetails().getTokenUri());
+
+		HttpHeaders headers = requestEntity.getHeaders();
+		assertThat(headers.getAccept()).contains(MediaType.APPLICATION_JSON_UTF8);
+		assertThat(headers.getContentType()).isEqualTo(
+				MediaType.valueOf(APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8"));
+		assertThat(headers.getFirst(HttpHeaders.AUTHORIZATION)).startsWith("Basic ");
+
+		MultiValueMap<String, String> formParameters = (MultiValueMap<String, String>) requestEntity.getBody();
+		assertThat(formParameters.getFirst(OAuth2ParameterNames.GRANT_TYPE)).isEqualTo(
+				AuthorizationGrantType.PASSWORD.getValue());
+		assertThat(formParameters.getFirst(OAuth2ParameterNames.USERNAME)).isEqualTo("user1");
+		assertThat(formParameters.getFirst(OAuth2ParameterNames.PASSWORD)).isEqualTo("password");
+		assertThat(formParameters.getFirst(OAuth2ParameterNames.SCOPE)).isEqualTo("read write");
+	}
+
+}

+ 81 - 0
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/OAuth2PasswordGrantRequestTests.java

@@ -0,0 +1,81 @@
+/*
+ * 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.endpoint;
+
+import org.junit.Test;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Tests for {@link OAuth2PasswordGrantRequest}.
+ *
+ * @author Joe Grandja
+ */
+public class OAuth2PasswordGrantRequestTests {
+	private ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration()
+			.authorizationGrantType(AuthorizationGrantType.PASSWORD).build();
+	private String username = "user1";
+	private String password = "password";
+
+	@Test
+	public void constructorWhenClientRegistrationIsNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> new OAuth2PasswordGrantRequest(null, this.username, this.password))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("clientRegistration cannot be null");
+	}
+
+	@Test
+	public void constructorWhenUsernameIsEmptyThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> new OAuth2PasswordGrantRequest(this.clientRegistration, null, this.password))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("username cannot be empty");
+		assertThatThrownBy(() -> new OAuth2PasswordGrantRequest(this.clientRegistration, "", this.password))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("username cannot be empty");
+	}
+
+	@Test
+	public void constructorWhenPasswordIsEmptyThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> new OAuth2PasswordGrantRequest(this.clientRegistration, this.username, null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("password cannot be empty");
+		assertThatThrownBy(() -> new OAuth2PasswordGrantRequest(this.clientRegistration, this.username, ""))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("password cannot be empty");
+	}
+
+	@Test
+	public void constructorWhenClientRegistrationInvalidGrantTypeThenThrowIllegalArgumentException() {
+		ClientRegistration registration = TestClientRegistrations.clientCredentials().build();
+		assertThatThrownBy(() -> new OAuth2PasswordGrantRequest(registration, this.username, this.password))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("clientRegistration.authorizationGrantType must be AuthorizationGrantType.PASSWORD");
+	}
+
+	@Test
+	public void constructorWhenValidParametersProvidedThenCreated() {
+		OAuth2PasswordGrantRequest passwordGrantRequest = new OAuth2PasswordGrantRequest(
+				this.clientRegistration, this.username, this.password);
+		assertThat(passwordGrantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.PASSWORD);
+		assertThat(passwordGrantRequest.getClientRegistration()).isSameAs(this.clientRegistration);
+		assertThat(passwordGrantRequest.getUsername()).isEqualTo(this.username);
+		assertThat(passwordGrantRequest.getPassword()).isEqualTo(this.password);
+	}
+}

+ 212 - 0
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactivePasswordTokenResponseClientTests.java

@@ -0,0 +1,212 @@
+/*
+ * 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.endpoint;
+
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
+
+import java.time.Instant;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Tests for {@link WebClientReactivePasswordTokenResponseClient}.
+ *
+ * @author Joe Grandja
+ */
+public class WebClientReactivePasswordTokenResponseClientTests {
+	private WebClientReactivePasswordTokenResponseClient tokenResponseClient = new WebClientReactivePasswordTokenResponseClient();
+	private ClientRegistration.Builder clientRegistrationBuilder;
+	private String username = "user1";
+	private String password = "password";
+	private MockWebServer server;
+
+	@Before
+	public void setup() throws Exception {
+		this.server = new MockWebServer();
+		this.server.start();
+		String tokenUri = this.server.url("/oauth2/token").toString();
+		this.clientRegistrationBuilder = TestClientRegistrations.password().tokenUri(tokenUri);
+	}
+
+	@After
+	public void cleanup() throws Exception {
+		this.server.shutdown();
+	}
+
+	@Test
+	public void setWebClientWhenClientIsNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> this.tokenResponseClient.setWebClient(null))
+				.isInstanceOf(IllegalArgumentException.class);
+	}
+
+	@Test
+	public void getTokenResponseWhenRequestIsNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> this.tokenResponseClient.getTokenResponse(null).block())
+				.isInstanceOf(IllegalArgumentException.class);
+	}
+
+	@Test
+	public void getTokenResponseWhenSuccessResponseThenReturnAccessTokenResponse() throws Exception {
+		String accessTokenSuccessResponse = "{\n" +
+				"	\"access_token\": \"access-token-1234\",\n" +
+				"   \"token_type\": \"bearer\",\n" +
+				"   \"expires_in\": \"3600\"\n" +
+				"}\n";
+		this.server.enqueue(jsonResponse(accessTokenSuccessResponse));
+
+		Instant expiresAtBefore = Instant.now().plusSeconds(3600);
+
+		ClientRegistration clientRegistration = this.clientRegistrationBuilder.build();
+		OAuth2PasswordGrantRequest passwordGrantRequest = new OAuth2PasswordGrantRequest(
+				clientRegistration, this.username, this.password);
+
+		OAuth2AccessTokenResponse accessTokenResponse = this.tokenResponseClient.getTokenResponse(passwordGrantRequest).block();
+
+		Instant expiresAtAfter = Instant.now().plusSeconds(3600);
+
+		RecordedRequest recordedRequest = this.server.takeRequest();
+		assertThat(recordedRequest.getMethod()).isEqualTo(HttpMethod.POST.toString());
+		assertThat(recordedRequest.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON_VALUE);
+		assertThat(recordedRequest.getHeader(HttpHeaders.CONTENT_TYPE)).isEqualTo(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8");
+
+		String formParameters = recordedRequest.getBody().readUtf8();
+		assertThat(formParameters).contains("grant_type=password");
+		assertThat(formParameters).contains("username=user1");
+		assertThat(formParameters).contains("password=password");
+		assertThat(formParameters).contains("scope=read+write");
+
+		assertThat(accessTokenResponse.getAccessToken().getTokenValue()).isEqualTo("access-token-1234");
+		assertThat(accessTokenResponse.getAccessToken().getTokenType()).isEqualTo(OAuth2AccessToken.TokenType.BEARER);
+		assertThat(accessTokenResponse.getAccessToken().getExpiresAt()).isBetween(expiresAtBefore, expiresAtAfter);
+		assertThat(accessTokenResponse.getAccessToken().getScopes()).containsExactly(clientRegistration.getScopes().toArray(new String[0]));
+		assertThat(accessTokenResponse.getRefreshToken()).isNull();
+	}
+
+	@Test
+	public void getTokenResponseWhenClientAuthenticationPostThenFormParametersAreSent() throws Exception {
+		String accessTokenSuccessResponse = "{\n" +
+				"	\"access_token\": \"access-token-1234\",\n" +
+				"   \"token_type\": \"bearer\",\n" +
+				"   \"expires_in\": \"3600\"\n" +
+				"}\n";
+		this.server.enqueue(jsonResponse(accessTokenSuccessResponse));
+
+		ClientRegistration clientRegistration = this.clientRegistrationBuilder
+				.clientAuthenticationMethod(ClientAuthenticationMethod.POST)
+				.build();
+		OAuth2PasswordGrantRequest passwordGrantRequest = new OAuth2PasswordGrantRequest(
+				clientRegistration, this.username, this.password);
+
+		this.tokenResponseClient.getTokenResponse(passwordGrantRequest).block();
+
+		RecordedRequest recordedRequest = this.server.takeRequest();
+		assertThat(recordedRequest.getHeader(HttpHeaders.AUTHORIZATION)).isNull();
+
+		String formParameters = recordedRequest.getBody().readUtf8();
+		assertThat(formParameters).contains("client_id=client-id");
+		assertThat(formParameters).contains("client_secret=client-secret");
+	}
+
+	@Test
+	public void getTokenResponseWhenSuccessResponseAndNotBearerTokenTypeThenThrowOAuth2AuthorizationException() {
+		String accessTokenSuccessResponse = "{\n" +
+				"	\"access_token\": \"access-token-1234\",\n" +
+				"   \"token_type\": \"not-bearer\",\n" +
+				"   \"expires_in\": \"3600\"\n" +
+				"}\n";
+		this.server.enqueue(jsonResponse(accessTokenSuccessResponse));
+
+		OAuth2PasswordGrantRequest passwordGrantRequest = new OAuth2PasswordGrantRequest(
+				this.clientRegistrationBuilder.build(), this.username, this.password);
+
+		assertThatThrownBy(() -> this.tokenResponseClient.getTokenResponse(passwordGrantRequest).block())
+				.isInstanceOf(OAuth2AuthorizationException.class)
+				.hasMessageContaining("[invalid_token_response] An error occurred parsing the Access Token response")
+				.hasMessageContaining("Token type must be \"Bearer\"");
+	}
+
+	@Test
+	public void getTokenResponseWhenSuccessResponseIncludesScopeThenAccessTokenHasResponseScope() throws Exception {
+		String accessTokenSuccessResponse = "{\n" +
+				"	\"access_token\": \"access-token-1234\",\n" +
+				"   \"token_type\": \"bearer\",\n" +
+				"   \"expires_in\": \"3600\",\n" +
+				"   \"scope\": \"read\"\n" +
+				"}\n";
+		this.server.enqueue(jsonResponse(accessTokenSuccessResponse));
+
+		OAuth2PasswordGrantRequest passwordGrantRequest = new OAuth2PasswordGrantRequest(
+				this.clientRegistrationBuilder.build(), this.username, this.password);
+
+		OAuth2AccessTokenResponse accessTokenResponse = this.tokenResponseClient.getTokenResponse(passwordGrantRequest).block();
+
+		RecordedRequest recordedRequest = this.server.takeRequest();
+		String formParameters = recordedRequest.getBody().readUtf8();
+		assertThat(formParameters).contains("scope=read");
+
+		assertThat(accessTokenResponse.getAccessToken().getScopes()).containsExactly("read");
+	}
+
+	@Test
+	public void getTokenResponseWhenErrorResponseThenThrowOAuth2AuthorizationException() {
+		String accessTokenErrorResponse = "{\n" +
+				"   \"error\": \"unauthorized_client\"\n" +
+				"}\n";
+		this.server.enqueue(jsonResponse(accessTokenErrorResponse).setResponseCode(400));
+
+		OAuth2PasswordGrantRequest passwordGrantRequest = new OAuth2PasswordGrantRequest(
+				this.clientRegistrationBuilder.build(), this.username, this.password);
+
+		assertThatThrownBy(() -> this.tokenResponseClient.getTokenResponse(passwordGrantRequest).block())
+				.isInstanceOf(OAuth2AuthorizationException.class)
+				.hasMessageContaining("[invalid_token_response] An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response")
+				.hasMessageContaining("HTTP Status Code 400");
+	}
+
+	@Test
+	public void getTokenResponseWhenServerErrorResponseThenThrowOAuth2AuthorizationException() {
+		this.server.enqueue(new MockResponse().setResponseCode(500));
+
+		OAuth2PasswordGrantRequest passwordGrantRequest = new OAuth2PasswordGrantRequest(
+				this.clientRegistrationBuilder.build(), this.username, this.password);
+
+		assertThatThrownBy(() -> this.tokenResponseClient.getTokenResponse(passwordGrantRequest).block())
+				.isInstanceOf(OAuth2AuthorizationException.class)
+				.hasMessageContaining("[invalid_token_response] An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response")
+				.hasMessageContaining("HTTP Status Code 500");
+	}
+
+	private MockResponse jsonResponse(String json) {
+		return new MockResponse()
+				.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
+				.setBody(json);
+	}
+}

+ 84 - 0
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java

@@ -590,6 +590,90 @@ public class ClientRegistrationTests {
 		).isInstanceOf(IllegalArgumentException.class);
 	}
 
+	@Test
+	public void buildWhenPasswordGrantAllAttributesProvidedThenAllAttributesAreSet() {
+		ClientRegistration registration = ClientRegistration.withRegistrationId(REGISTRATION_ID)
+				.clientId(CLIENT_ID)
+				.clientSecret(CLIENT_SECRET)
+				.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
+				.authorizationGrantType(AuthorizationGrantType.PASSWORD)
+				.scope(SCOPES.toArray(new String[0]))
+				.tokenUri(TOKEN_URI)
+				.clientName(CLIENT_NAME)
+				.build();
+
+		assertThat(registration.getRegistrationId()).isEqualTo(REGISTRATION_ID);
+		assertThat(registration.getClientId()).isEqualTo(CLIENT_ID);
+		assertThat(registration.getClientSecret()).isEqualTo(CLIENT_SECRET);
+		assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC);
+		assertThat(registration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.PASSWORD);
+		assertThat(registration.getScopes()).isEqualTo(SCOPES);
+		assertThat(registration.getProviderDetails().getTokenUri()).isEqualTo(TOKEN_URI);
+		assertThat(registration.getClientName()).isEqualTo(CLIENT_NAME);
+	}
+
+	@Test
+	public void buildWhenPasswordGrantRegistrationIdIsNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() ->
+				ClientRegistration.withRegistrationId(null)
+						.clientId(CLIENT_ID)
+						.clientSecret(CLIENT_SECRET)
+						.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
+						.authorizationGrantType(AuthorizationGrantType.PASSWORD)
+						.tokenUri(TOKEN_URI)
+						.build()
+		).isInstanceOf(IllegalArgumentException.class);
+	}
+
+	@Test
+	public void buildWhenPasswordGrantClientIdIsNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() ->
+				ClientRegistration.withRegistrationId(REGISTRATION_ID)
+						.clientId(null)
+						.clientSecret(CLIENT_SECRET)
+						.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
+						.authorizationGrantType(AuthorizationGrantType.PASSWORD)
+						.tokenUri(TOKEN_URI)
+						.build()
+		).isInstanceOf(IllegalArgumentException.class);
+	}
+
+	@Test
+	public void buildWhenPasswordGrantClientSecretIsNullThenDefaultToEmpty() {
+		ClientRegistration clientRegistration = ClientRegistration.withRegistrationId(REGISTRATION_ID)
+				.clientId(CLIENT_ID)
+				.clientSecret(null)
+				.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
+				.authorizationGrantType(AuthorizationGrantType.PASSWORD)
+				.tokenUri(TOKEN_URI)
+				.build();
+		assertThat(clientRegistration.getClientSecret()).isEqualTo("");
+	}
+
+	@Test
+	public void buildWhenPasswordGrantClientAuthenticationMethodNotProvidedThenDefaultToBasic() {
+		ClientRegistration clientRegistration = ClientRegistration.withRegistrationId(REGISTRATION_ID)
+				.clientId(CLIENT_ID)
+				.clientSecret(CLIENT_SECRET)
+				.authorizationGrantType(AuthorizationGrantType.PASSWORD)
+				.tokenUri(TOKEN_URI)
+				.build();
+		assertThat(clientRegistration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC);
+	}
+
+	@Test
+	public void buildWhenPasswordGrantTokenUriIsNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() ->
+				ClientRegistration.withRegistrationId(REGISTRATION_ID)
+						.clientId(CLIENT_ID)
+						.clientSecret(CLIENT_SECRET)
+						.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
+						.authorizationGrantType(AuthorizationGrantType.PASSWORD)
+						.tokenUri(null)
+						.build()
+		).isInstanceOf(IllegalArgumentException.class);
+	}
+
 	@Test
 	public void buildWhenCustomGrantAllAttributesProvidedThenAllAttributesAreSet() {
 		AuthorizationGrantType customGrantType = new AuthorizationGrantType("CUSTOM");

+ 11 - 0
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/TestClientRegistrations.java

@@ -61,4 +61,15 @@ public class TestClientRegistrations {
 				.clientId("client-id")
 				.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS);
 	}
+
+	public static ClientRegistration.Builder password() {
+		return ClientRegistration.withRegistrationId("password")
+				.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
+				.authorizationGrantType(AuthorizationGrantType.PASSWORD)
+				.scope("read", "write")
+				.tokenUri("https://example.com/login/oauth/access_token")
+				.clientName("Client Name")
+				.clientId("client-id")
+				.clientSecret("client-secret");
+	}
 }

+ 29 - 11
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizedClientManagerTests.java

@@ -199,6 +199,33 @@ public class DefaultOAuth2AuthorizedClientManagerTests {
 				eq(reauthorizedClient), eq(this.principal), eq(this.request), eq(this.response));
 	}
 
+	@Test
+	public void authorizeWhenRequestParameterUsernamePasswordThenMappedToContext() {
+		when(this.clientRegistrationRepository.findByRegistrationId(
+				eq(this.clientRegistration.getRegistrationId()))).thenReturn(this.clientRegistration);
+
+		when(this.authorizedClientProvider.authorize(any(OAuth2AuthorizationContext.class))).thenReturn(this.authorizedClient);
+
+		// Override the mock with the default
+		this.authorizedClientManager.setContextAttributesMapper(
+				new DefaultOAuth2AuthorizedClientManager.DefaultContextAttributesMapper());
+
+		this.request.addParameter(OAuth2ParameterNames.USERNAME, "username");
+		this.request.addParameter(OAuth2ParameterNames.PASSWORD, "password");
+
+		OAuth2AuthorizeRequest authorizeRequest = new OAuth2AuthorizeRequest(
+				this.clientRegistration.getRegistrationId(), this.principal, this.request, this.response);
+		this.authorizedClientManager.authorize(authorizeRequest);
+
+		verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
+
+		OAuth2AuthorizationContext authorizationContext = this.authorizationContextCaptor.getValue();
+		String username = authorizationContext.getAttribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME);
+		assertThat(username).isEqualTo("username");
+		String password = authorizationContext.getAttribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME);
+		assertThat(password).isEqualTo("password");
+	}
+
 	@SuppressWarnings("unchecked")
 	@Test
 	public void reauthorizeWhenUnsupportedProviderThenNotReauthorized() {
@@ -245,9 +272,8 @@ public class DefaultOAuth2AuthorizedClientManagerTests {
 				eq(reauthorizedClient), eq(this.principal), eq(this.request), eq(this.response));
 	}
 
-	@SuppressWarnings("unchecked")
 	@Test
-	public void reauthorizeWhenRequestScopeParameterThenMappedToContext() {
+	public void reauthorizeWhenRequestParameterScopeThenMappedToContext() {
 		OAuth2AuthorizedClient reauthorizedClient = new OAuth2AuthorizedClient(
 				this.clientRegistration, this.principal.getName(),
 				TestOAuth2AccessTokens.noScopes(), TestOAuth2RefreshTokens.refreshToken());
@@ -262,20 +288,12 @@ public class DefaultOAuth2AuthorizedClientManagerTests {
 
 		OAuth2AuthorizeRequest reauthorizeRequest = new OAuth2AuthorizeRequest(
 				this.authorizedClient, this.principal, this.request, this.response);
-		OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(reauthorizeRequest);
+		this.authorizedClientManager.authorize(reauthorizeRequest);
 
 		verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
 
 		OAuth2AuthorizationContext authorizationContext = this.authorizationContextCaptor.getValue();
-		assertThat(authorizationContext.getClientRegistration()).isEqualTo(this.clientRegistration);
-		assertThat(authorizationContext.getAuthorizedClient()).isSameAs(this.authorizedClient);
-		assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
-		assertThat(authorizationContext.getAttributes()).containsKey(OAuth2AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME);
 		String[] requestScopeAttribute = authorizationContext.getAttribute(OAuth2AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME);
 		assertThat(requestScopeAttribute).contains("read", "write");
-
-		assertThat(authorizedClient).isSameAs(reauthorizedClient);
-		verify(this.authorizedClientRepository).saveAuthorizedClient(
-				eq(reauthorizedClient), eq(this.principal), eq(this.request), eq(this.response));
 	}
 }

+ 50 - 1
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolverTests.java

@@ -30,20 +30,24 @@ import org.springframework.security.oauth2.client.ClientCredentialsOAuth2Authori
 import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
 import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider;
 import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder;
+import org.springframework.security.oauth2.client.PasswordOAuth2AuthorizedClientProvider;
 import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
 import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
 import org.springframework.security.oauth2.client.endpoint.DefaultClientCredentialsTokenResponseClient;
 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.registration.ClientRegistration;
 import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
 import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
+import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
 import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
 import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
 import org.springframework.security.oauth2.core.AuthorizationGrantType;
 import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
 import org.springframework.security.oauth2.core.OAuth2AccessToken;
 import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
 import org.springframework.util.ReflectionUtils;
 import org.springframework.web.context.request.ServletWebRequest;
 
@@ -65,6 +69,7 @@ public class OAuth2AuthorizedClientArgumentResolverTests {
 	private String principalName = "principal-1";
 	private ClientRegistration registration1;
 	private ClientRegistration registration2;
+	private ClientRegistration registration3;
 	private ClientRegistrationRepository clientRegistrationRepository;
 	private OAuth2AuthorizedClient authorizedClient1;
 	private OAuth2AuthorizedClient authorizedClient2;
@@ -101,7 +106,9 @@ public class OAuth2AuthorizedClientArgumentResolverTests {
 				.scope("read", "write")
 				.tokenUri("https://provider.com/oauth2/token")
 				.build();
-		this.clientRegistrationRepository = new InMemoryClientRegistrationRepository(this.registration1, this.registration2);
+		this.registration3 = TestClientRegistrations.password().registrationId("client3").build();
+		this.clientRegistrationRepository = new InMemoryClientRegistrationRepository(
+				this.registration1, this.registration2, this.registration3);
 		this.authorizedClientRepository = mock(OAuth2AuthorizedClientRepository.class);
 		OAuth2AuthorizedClientProvider authorizedClientProvider =
 				OAuth2AuthorizedClientProviderBuilder.builder()
@@ -267,6 +274,45 @@ public class OAuth2AuthorizedClientArgumentResolverTests {
 				eq(authorizedClient), eq(this.authentication), any(HttpServletRequest.class), any(HttpServletResponse.class));
 	}
 
+	@SuppressWarnings("unchecked")
+	@Test
+	public void resolveArgumentWhenAuthorizedClientNotFoundForPasswordClientThenResolvesFromTokenResponseClient() throws Exception {
+		OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> passwordTokenResponseClient =
+				mock(OAuth2AccessTokenResponseClient.class);
+		PasswordOAuth2AuthorizedClientProvider passwordAuthorizedClientProvider =
+				new PasswordOAuth2AuthorizedClientProvider();
+		passwordAuthorizedClientProvider.setAccessTokenResponseClient(passwordTokenResponseClient);
+		DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
+				this.clientRegistrationRepository, this.authorizedClientRepository);
+		authorizedClientManager.setAuthorizedClientProvider(passwordAuthorizedClientProvider);
+		this.argumentResolver = new OAuth2AuthorizedClientArgumentResolver(authorizedClientManager);
+
+		OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse
+				.withToken("access-token-1234")
+				.tokenType(OAuth2AccessToken.TokenType.BEARER)
+				.expiresIn(3600)
+				.build();
+		when(passwordTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse);
+
+		when(this.authorizedClientRepository.loadAuthorizedClient(anyString(), any(), any(HttpServletRequest.class)))
+				.thenReturn(null);
+		MethodParameter methodParameter = this.getMethodParameter("passwordClient", OAuth2AuthorizedClient.class);
+
+		this.request.setParameter(OAuth2ParameterNames.USERNAME, "username");
+		this.request.setParameter(OAuth2ParameterNames.PASSWORD, "password");
+
+		OAuth2AuthorizedClient authorizedClient = (OAuth2AuthorizedClient) this.argumentResolver.resolveArgument(
+				methodParameter, null, new ServletWebRequest(this.request, this.response), null);
+
+		assertThat(authorizedClient).isNotNull();
+		assertThat(authorizedClient.getClientRegistration()).isSameAs(this.registration3);
+		assertThat(authorizedClient.getPrincipalName()).isEqualTo(this.principalName);
+		assertThat(authorizedClient.getAccessToken()).isSameAs(accessTokenResponse.getAccessToken());
+
+		verify(this.authorizedClientRepository).saveAuthorizedClient(
+				eq(authorizedClient), eq(this.authentication), any(HttpServletRequest.class), any(HttpServletResponse.class));
+	}
+
 	private MethodParameter getMethodParameter(String methodName, Class<?>... paramTypes) {
 		Method method = ReflectionUtils.findMethod(TestController.class, methodName, paramTypes);
 		return new MethodParameter(method, 0);
@@ -293,5 +339,8 @@ public class OAuth2AuthorizedClientArgumentResolverTests {
 
 		void clientCredentialsClient(@RegisteredOAuth2AuthorizedClient("client2") OAuth2AuthorizedClient authorizedClient) {
 		}
+
+		void passwordClient(@RegisteredOAuth2AuthorizedClient("client3") OAuth2AuthorizedClient authorizedClient) {
+		}
 	}
 }

+ 71 - 2
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunctionTests.java

@@ -27,6 +27,7 @@ import org.springframework.core.codec.ByteBufferEncoder;
 import org.springframework.core.codec.CharSequenceEncoder;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
 import org.springframework.http.codec.EncoderHttpMessageWriter;
 import org.springframework.http.codec.FormHttpMessageWriter;
 import org.springframework.http.codec.HttpMessageWriter;
@@ -41,11 +42,13 @@ import org.springframework.mock.web.server.MockServerWebExchange;
 import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.core.authority.AuthorityUtils;
 import org.springframework.security.core.context.ReactiveSecurityContextHolder;
+import org.springframework.security.oauth2.client.OAuth2AuthorizationContext;
 import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
 import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProvider;
 import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProviderBuilder;
 import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
 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.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
 import org.springframework.security.oauth2.client.endpoint.WebClientReactiveClientCredentialsTokenResponseClient;
@@ -57,8 +60,10 @@ import org.springframework.security.oauth2.client.web.server.ServerOAuth2Authori
 import org.springframework.security.oauth2.core.OAuth2AccessToken;
 import org.springframework.security.oauth2.core.OAuth2RefreshToken;
 import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
 import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
 import org.springframework.security.oauth2.core.user.OAuth2User;
+import org.springframework.util.StringUtils;
 import org.springframework.web.reactive.function.BodyInserter;
 import org.springframework.web.reactive.function.client.ClientRequest;
 import org.springframework.web.server.ServerWebExchange;
@@ -102,6 +107,9 @@ public class ServerOAuth2AuthorizedClientExchangeFilterFunctionTests {
 	@Mock
 	private ReactiveOAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshTokenTokenResponseClient;
 
+	@Mock
+	private ReactiveOAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> passwordTokenResponseClient;
+
 	private ServerWebExchange serverWebExchange = MockServerWebExchange.builder(MockServerHttpRequest.get("/")).build();
 
 	@Captor
@@ -119,6 +127,8 @@ public class ServerOAuth2AuthorizedClientExchangeFilterFunctionTests {
 			Instant.now(),
 			Instant.now().plus(Duration.ofDays(1)));
 
+	private DefaultServerOAuth2AuthorizedClientManager authorizedClientManager;
+
 	@Before
 	public void setup() {
 		ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
@@ -126,10 +136,11 @@ public class ServerOAuth2AuthorizedClientExchangeFilterFunctionTests {
 						.authorizationCode()
 						.refreshToken(configurer -> configurer.accessTokenResponseClient(this.refreshTokenTokenResponseClient))
 						.clientCredentials(configurer -> configurer.accessTokenResponseClient(this.clientCredentialsTokenResponseClient))
+						.password(configurer -> configurer.accessTokenResponseClient(this.passwordTokenResponseClient))
 						.build();
-		DefaultServerOAuth2AuthorizedClientManager authorizedClientManager = new DefaultServerOAuth2AuthorizedClientManager(
+		this.authorizedClientManager = new DefaultServerOAuth2AuthorizedClientManager(
 				this.clientRegistrationRepository, this.authorizedClientRepository);
-		authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
+		this.authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
 		this.function = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
 	}
 
@@ -403,6 +414,64 @@ public class ServerOAuth2AuthorizedClientExchangeFilterFunctionTests {
 		assertThat(getBody(request0)).isEmpty();
 	}
 
+	@Test
+	public void filterWhenPasswordClientNotAuthorizedThenGetNewToken() {
+		TestingAuthenticationToken authentication = new TestingAuthenticationToken("test", "this");
+		ClientRegistration registration = TestClientRegistrations.password().build();
+
+		OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken("new-token")
+				.tokenType(OAuth2AccessToken.TokenType.BEARER)
+				.expiresIn(360)
+				.build();
+		when(this.passwordTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse));
+
+		when(this.clientRegistrationRepository.findByRegistrationId(eq(registration.getRegistrationId()))).thenReturn(Mono.just(registration));
+		when(this.authorizedClientRepository.loadAuthorizedClient(eq(registration.getRegistrationId()), eq(authentication), any())).thenReturn(Mono.empty());
+
+		// Set custom contextAttributesMapper capable of mapping the form parameters
+		this.authorizedClientManager.setContextAttributesMapper(authorizeRequest ->
+				Mono.just(authorizeRequest.getServerWebExchange())
+						.flatMap(ServerWebExchange::getFormData)
+						.map(formData -> {
+							Map<String, Object> contextAttributes = new HashMap<>();
+							String username = formData.getFirst(OAuth2ParameterNames.USERNAME);
+							String password = formData.getFirst(OAuth2ParameterNames.PASSWORD);
+							if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
+								contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username);
+								contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password);
+							}
+							return contextAttributes;
+						})
+		);
+
+		this.serverWebExchange = MockServerWebExchange.builder(
+				MockServerHttpRequest
+						.post("/")
+						.contentType(MediaType.APPLICATION_FORM_URLENCODED)
+						.body("username=username&password=password"))
+				.build();
+
+		ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com"))
+				.attributes(clientRegistrationId(registration.getRegistrationId()))
+				.build();
+
+		this.function.filter(request, this.exchange)
+				.subscriberContext(ReactiveSecurityContextHolder.withAuthentication(authentication))
+				.subscriberContext(serverWebExchange())
+				.block();
+
+		verify(this.passwordTokenResponseClient).getTokenResponse(any());
+		verify(this.authorizedClientRepository).saveAuthorizedClient(any(), eq(authentication), any());
+
+		List<ClientRequest> requests = this.exchange.getRequests();
+		assertThat(requests).hasSize(1);
+		ClientRequest request1 = requests.get(0);
+		assertThat(request1.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer new-token");
+		assertThat(request1.url().toASCIIString()).isEqualTo("https://example.com");
+		assertThat(request1.method()).isEqualTo(HttpMethod.GET);
+		assertThat(getBody(request1)).isEmpty();
+	}
+
 	@Test
 	public void filterWhenClientRegistrationIdThenAuthorizedClientResolved() {
 		OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", this.accessToken.getIssuedAt());

+ 43 - 0
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunctionTests.java

@@ -55,6 +55,7 @@ import org.springframework.security.oauth2.client.endpoint.DefaultClientCredenti
 import org.springframework.security.oauth2.client.endpoint.DefaultRefreshTokenTokenResponseClient;
 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.security.oauth2.client.registration.ClientRegistration;
 import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
@@ -64,6 +65,7 @@ import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepo
 import org.springframework.security.oauth2.core.OAuth2AccessToken;
 import org.springframework.security.oauth2.core.OAuth2RefreshToken;
 import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
 import org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses;
 import org.springframework.security.oauth2.core.user.OAuth2User;
 import org.springframework.web.client.RestOperations;
@@ -92,6 +94,7 @@ import static org.assertj.core.api.Assertions.assertThatCode;
 import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
 import static org.mockito.Mockito.*;
 import static org.springframework.http.HttpMethod.GET;
+import static org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction.clientRegistrationId;
 import static org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.*;
 
 /**
@@ -109,6 +112,8 @@ public class ServletOAuth2AuthorizedClientExchangeFilterFunctionTests {
 	@Mock
 	private OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshTokenTokenResponseClient;
 	@Mock
+	private OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> passwordTokenResponseClient;
+	@Mock
 	private WebClient.RequestHeadersSpec<?> spec;
 	@Captor
 	private ArgumentCaptor<Consumer<Map<String, Object>>> attrs;
@@ -141,6 +146,7 @@ public class ServletOAuth2AuthorizedClientExchangeFilterFunctionTests {
 						.authorizationCode()
 						.refreshToken(configurer -> configurer.accessTokenResponseClient(this.refreshTokenTokenResponseClient))
 						.clientCredentials(configurer -> configurer.accessTokenResponseClient(this.clientCredentialsTokenResponseClient))
+						.password(configurer -> configurer.accessTokenResponseClient(this.passwordTokenResponseClient))
 						.build();
 		DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
 				this.clientRegistrationRepository, this.authorizedClientRepository);
@@ -442,6 +448,43 @@ public class ServletOAuth2AuthorizedClientExchangeFilterFunctionTests {
 		assertThat(getBody(request1)).isEmpty();
 	}
 
+	@Test
+	public void filterWhenPasswordClientNotAuthorizedThenGetNewToken() {
+		OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken("new-token")
+				.tokenType(OAuth2AccessToken.TokenType.BEARER)
+				.expiresIn(360)
+				.build();
+		when(this.passwordTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse);
+
+		ClientRegistration registration = TestClientRegistrations.password().build();
+		when(this.clientRegistrationRepository.findByRegistrationId(eq(registration.getRegistrationId()))).thenReturn(registration);
+
+		MockHttpServletRequest servletRequest = new MockHttpServletRequest();
+		servletRequest.setParameter(OAuth2ParameterNames.USERNAME, "username");
+		servletRequest.setParameter(OAuth2ParameterNames.PASSWORD, "password");
+		MockHttpServletResponse servletResponse = new MockHttpServletResponse();
+
+		ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com"))
+				.attributes(clientRegistrationId(registration.getRegistrationId()))
+				.attributes(authentication(this.authentication))
+				.attributes(httpServletRequest(servletRequest))
+				.attributes(httpServletResponse(servletResponse))
+				.build();
+
+		this.function.filter(request, this.exchange).block();
+
+		verify(this.passwordTokenResponseClient).getTokenResponse(any());
+		verify(this.authorizedClientRepository).saveAuthorizedClient(any(), eq(authentication), any(), any());
+
+		List<ClientRequest> requests = this.exchange.getRequests();
+		assertThat(requests).hasSize(1);
+		ClientRequest request1 = requests.get(0);
+		assertThat(request1.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer new-token");
+		assertThat(request1.url().toASCIIString()).isEqualTo("https://example.com");
+		assertThat(request1.method()).isEqualTo(HttpMethod.GET);
+		assertThat(getBody(request1)).isEmpty();
+	}
+
 	@Test
 	public void filterWhenRefreshRequiredAndEmptyReactiveSecurityContextThenSaved() {
 		OAuth2AccessTokenResponse response = OAuth2AccessTokenResponse.withToken("token-1")

+ 50 - 12
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizedClientManagerTests.java

@@ -18,6 +18,7 @@ package org.springframework.security.oauth2.client.web.server;
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
+import org.springframework.http.MediaType;
 import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
 import org.springframework.mock.web.server.MockServerWebExchange;
 import org.springframework.security.authentication.TestingAuthenticationToken;
@@ -31,10 +32,13 @@ import org.springframework.security.oauth2.client.registration.TestClientRegistr
 import org.springframework.security.oauth2.core.TestOAuth2AccessTokens;
 import org.springframework.security.oauth2.core.TestOAuth2RefreshTokens;
 import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.util.StringUtils;
 import org.springframework.web.server.ServerWebExchange;
 import reactor.core.publisher.Mono;
 
 import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.function.Function;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -72,7 +76,7 @@ public class DefaultServerOAuth2AuthorizedClientManagerTests {
 		this.authorizedClientProvider = mock(ReactiveOAuth2AuthorizedClientProvider.class);
 		when(this.authorizedClientProvider.authorize(any(OAuth2AuthorizationContext.class))).thenReturn(Mono.empty());
 		this.contextAttributesMapper = mock(Function.class);
-		when(this.contextAttributesMapper.apply(any())).thenReturn(Collections.emptyMap());
+		when(this.contextAttributesMapper.apply(any())).thenReturn(Mono.just(Collections.emptyMap()));
 		this.authorizedClientManager = new DefaultServerOAuth2AuthorizedClientManager(
 				this.clientRegistrationRepository, this.authorizedClientRepository);
 		this.authorizedClientManager.setAuthorizedClientProvider(this.authorizedClientProvider);
@@ -209,6 +213,49 @@ public class DefaultServerOAuth2AuthorizedClientManagerTests {
 				eq(reauthorizedClient), eq(this.principal), eq(this.serverWebExchange));
 	}
 
+	@Test
+	public void authorizeWhenRequestFormParameterUsernamePasswordThenMappedToContext() {
+		when(this.clientRegistrationRepository.findByRegistrationId(
+				eq(this.clientRegistration.getRegistrationId()))).thenReturn(Mono.just(this.clientRegistration));
+
+		when(this.authorizedClientProvider.authorize(any(OAuth2AuthorizationContext.class))).thenReturn(Mono.just(this.authorizedClient));
+
+		// Set custom contextAttributesMapper capable of mapping the form parameters
+		this.authorizedClientManager.setContextAttributesMapper(authorizeRequest ->
+			Mono.just(authorizeRequest.getServerWebExchange())
+					.flatMap(ServerWebExchange::getFormData)
+					.map(formData -> {
+						Map<String, Object> contextAttributes = new HashMap<>();
+						String username = formData.getFirst(OAuth2ParameterNames.USERNAME);
+						String password = formData.getFirst(OAuth2ParameterNames.PASSWORD);
+						if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
+							contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username);
+							contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password);
+						}
+						return contextAttributes;
+					})
+		);
+
+		this.serverWebExchange = MockServerWebExchange.builder(
+				MockServerHttpRequest
+						.post("/")
+						.contentType(MediaType.APPLICATION_FORM_URLENCODED)
+						.body("username=username&password=password"))
+				.build();
+
+		ServerOAuth2AuthorizeRequest authorizeRequest = new ServerOAuth2AuthorizeRequest(
+				this.clientRegistration.getRegistrationId(), this.principal, this.serverWebExchange);
+		this.authorizedClientManager.authorize(authorizeRequest).block();
+
+		verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
+
+		OAuth2AuthorizationContext authorizationContext = this.authorizationContextCaptor.getValue();
+		String username = authorizationContext.getAttribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME);
+		assertThat(username).isEqualTo("username");
+		String password = authorizationContext.getAttribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME);
+		assertThat(password).isEqualTo("password");
+	}
+
 	@SuppressWarnings("unchecked")
 	@Test
 	public void reauthorizeWhenUnsupportedProviderThenNotReauthorized() {
@@ -255,9 +302,8 @@ public class DefaultServerOAuth2AuthorizedClientManagerTests {
 				eq(reauthorizedClient), eq(this.principal), eq(this.serverWebExchange));
 	}
 
-	@SuppressWarnings("unchecked")
 	@Test
-	public void reauthorizeWhenRequestScopeParameterThenMappedToContext() {
+	public void reauthorizeWhenRequestParameterScopeThenMappedToContext() {
 		OAuth2AuthorizedClient reauthorizedClient = new OAuth2AuthorizedClient(
 				this.clientRegistration, this.principal.getName(),
 				TestOAuth2AccessTokens.noScopes(), TestOAuth2RefreshTokens.refreshToken());
@@ -276,20 +322,12 @@ public class DefaultServerOAuth2AuthorizedClientManagerTests {
 
 		ServerOAuth2AuthorizeRequest reauthorizeRequest = new ServerOAuth2AuthorizeRequest(
 				this.authorizedClient, this.principal, this.serverWebExchange);
-		OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(reauthorizeRequest).block();
+		this.authorizedClientManager.authorize(reauthorizeRequest).block();
 
 		verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
 
 		OAuth2AuthorizationContext authorizationContext = this.authorizationContextCaptor.getValue();
-		assertThat(authorizationContext.getClientRegistration()).isEqualTo(this.clientRegistration);
-		assertThat(authorizationContext.getAuthorizedClient()).isSameAs(this.authorizedClient);
-		assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
-		assertThat(authorizationContext.getAttributes()).containsKey(OAuth2AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME);
 		String[] requestScopeAttribute = authorizationContext.getAttribute(OAuth2AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME);
 		assertThat(requestScopeAttribute).contains("read", "write");
-
-		assertThat(authorizedClient).isSameAs(reauthorizedClient);
-		verify(this.authorizedClientRepository).saveAuthorizedClient(
-				eq(reauthorizedClient), eq(this.principal), eq(this.serverWebExchange));
 	}
 }

+ 2 - 1
oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthorizationGrantType.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2018 the original author or authors.
+ * 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.
@@ -39,6 +39,7 @@ public final class AuthorizationGrantType implements Serializable {
 	public static final AuthorizationGrantType IMPLICIT = new AuthorizationGrantType("implicit");
 	public static final AuthorizationGrantType REFRESH_TOKEN = new AuthorizationGrantType("refresh_token");
 	public static final AuthorizationGrantType CLIENT_CREDENTIALS = new AuthorizationGrantType("client_credentials");
+	public static final AuthorizationGrantType PASSWORD = new AuthorizationGrantType("password");
 	private final String value;
 
 	/**

+ 11 - 1
oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2ParameterNames.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2018 the original author or authors.
+ * 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.
@@ -85,6 +85,16 @@ public interface OAuth2ParameterNames {
 	 */
 	String REFRESH_TOKEN = "refresh_token";
 
+	/**
+	 * {@code username} - used in Access Token Request.
+	 */
+	String USERNAME = "username";
+
+	/**
+	 * {@code password} - used in Access Token Request.
+	 */
+	String PASSWORD = "password";
+
 	/**
 	 * {@code error} - used in Authorization Response and Access Token Response.
 	 */

+ 6 - 1
oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/AuthorizationGrantTypeTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2018 the original author or authors.
+ * 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.
@@ -45,4 +45,9 @@ public class AuthorizationGrantTypeTests {
 	public void getValueWhenRefreshTokenGrantTypeThenReturnRefreshToken() {
 		assertThat(AuthorizationGrantType.REFRESH_TOKEN.getValue()).isEqualTo("refresh_token");
 	}
+
+	@Test
+	public void getValueWhenPasswordGrantTypeThenReturnPassword() {
+		assertThat(AuthorizationGrantType.PASSWORD.getValue()).isEqualTo("password");
+	}
 }

+ 1 - 0
samples/boot/oauth2webclient-webflux/src/main/java/sample/config/WebClientConfig.java

@@ -54,6 +54,7 @@ public class WebClientConfig {
 						.authorizationCode()
 						.refreshToken()
 						.clientCredentials()
+						.password()
 						.build();
 		DefaultServerOAuth2AuthorizedClientManager authorizedClientManager =
 				new DefaultServerOAuth2AuthorizedClientManager(

+ 1 - 0
samples/boot/oauth2webclient/src/main/java/sample/config/WebClientConfig.java

@@ -52,6 +52,7 @@ public class WebClientConfig {
 						.authorizationCode()
 						.refreshToken()
 						.clientCredentials()
+						.password()
 						.build();
 		DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
 				clientRegistrationRepository, authorizedClientRepository);