From dcd997ea435e73ca2f3c646b968658dbf643ea2f Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Sun, 16 Jun 2019 19:30:42 -0400 Subject: [PATCH] Add support for Resource Owner Password Credentials grant Fixes gh-6003 --- .../OAuth2ClientConfiguration.java | 5 +- .../ReactiveOAuth2ClientImportSelector.java | 1 + .../client/OAuth2AuthorizationContext.java | 17 +- ...OAuth2AuthorizedClientProviderBuilder.java | 100 +++++++- ...asswordOAuth2AuthorizedClientProvider.java | 142 +++++++++++ ...eactiveOAuth2AuthorizedClientProvider.java | 140 +++++++++++ ...OAuth2AuthorizedClientProviderBuilder.java | 94 +++++++- .../DefaultPasswordTokenResponseClient.java | 124 ++++++++++ .../endpoint/OAuth2PasswordGrantRequest.java | 81 +++++++ ...h2PasswordGrantRequestEntityConverter.java | 89 +++++++ ...ntReactivePasswordTokenResponseClient.java | 134 +++++++++++ .../registration/ClientRegistration.java | 24 +- .../DefaultOAuth2AuthorizedClientManager.java | 11 +- ...Auth2AuthorizedClientArgumentResolver.java | 2 + ...uthorizedClientExchangeFilterFunction.java | 2 + ...uthorizedClientExchangeFilterFunction.java | 2 + ...Auth2AuthorizedClientArgumentResolver.java | 1 + ...ltServerOAuth2AuthorizedClientManager.java | 52 +++-- ...2AuthorizedClientProviderBuilderTests.java | 36 +++ ...rdOAuth2AuthorizedClientProviderTests.java | 190 +++++++++++++++ ...veOAuth2AuthorizedClientProviderTests.java | 191 +++++++++++++++ ...2AuthorizedClientProviderBuilderTests.java | 50 ++++ ...faultPasswordTokenResponseClientTests.java | 220 ++++++++++++++++++ ...swordGrantRequestEntityConverterTests.java | 76 ++++++ .../OAuth2PasswordGrantRequestTests.java | 81 +++++++ ...ctivePasswordTokenResponseClientTests.java | 212 +++++++++++++++++ .../registration/ClientRegistrationTests.java | 84 +++++++ .../registration/TestClientRegistrations.java | 11 + ...ultOAuth2AuthorizedClientManagerTests.java | 40 +++- ...AuthorizedClientArgumentResolverTests.java | 51 +++- ...izedClientExchangeFilterFunctionTests.java | 73 +++++- ...izedClientExchangeFilterFunctionTests.java | 43 ++++ ...verOAuth2AuthorizedClientManagerTests.java | 62 ++++- .../oauth2/core/AuthorizationGrantType.java | 3 +- .../core/endpoint/OAuth2ParameterNames.java | 12 +- .../core/AuthorizationGrantTypeTests.java | 7 +- .../java/sample/config/WebClientConfig.java | 1 + .../java/sample/config/WebClientConfig.java | 1 + 38 files changed, 2393 insertions(+), 72 deletions(-) create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/PasswordOAuth2AuthorizedClientProvider.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/PasswordReactiveOAuth2AuthorizedClientProvider.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultPasswordTokenResponseClient.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/OAuth2PasswordGrantRequest.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/OAuth2PasswordGrantRequestEntityConverter.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactivePasswordTokenResponseClient.java create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/PasswordOAuth2AuthorizedClientProviderTests.java create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/PasswordReactiveOAuth2AuthorizedClientProviderTests.java create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultPasswordTokenResponseClientTests.java create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/OAuth2PasswordGrantRequestEntityConverterTests.java create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/OAuth2PasswordGrantRequestTests.java create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactivePasswordTokenResponseClientTests.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java index 72b83cb9c1..b29212d79e 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java +++ b/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); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2ClientImportSelector.java b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2ClientImportSelector.java index 73107c4ce1..85d5aa4ee6 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2ClientImportSelector.java +++ b/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()); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizationContext.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizationContext.java index d7aa1aec17..ffaa445bb2 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizationContext.java +++ b/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; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilder.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilder.java index 7c5345ad5f..7d02a4efcc 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilder.java +++ b/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 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 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 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). diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/PasswordOAuth2AuthorizedClientProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/PasswordOAuth2AuthorizedClientProvider.java new file mode 100644 index 0000000000..c4c80fd8d9 --- /dev/null +++ b/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 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. + * + *

+ * The following {@link OAuth2AuthorizationContext#getAttributes() context attributes} are supported: + *

    + *
  1. {@link OAuth2AuthorizationContext#USERNAME_ATTRIBUTE_NAME} (required) - a {@code String} value for the resource owner's username
  2. + *
  3. {@link OAuth2AuthorizationContext#PASSWORD_ATTRIBUTE_NAME} (required) - a {@code String} value for the resource owner's password
  4. + *
+ * + * @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 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; + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/PasswordReactiveOAuth2AuthorizedClientProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/PasswordReactiveOAuth2AuthorizedClientProvider.java new file mode 100644 index 0000000000..12383d2631 --- /dev/null +++ b/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 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. + * + *

+ * The following {@link OAuth2AuthorizationContext#getAttributes() context attributes} are supported: + *

    + *
  1. {@link OAuth2AuthorizationContext#USERNAME_ATTRIBUTE_NAME} (required) - a {@code String} value for the resource owner's username
  2. + *
  3. {@link OAuth2AuthorizationContext#PASSWORD_ATTRIBUTE_NAME} (required) - a {@code String} value for the resource owner's password
  4. + *
+ * + * @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 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 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; + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ReactiveOAuth2AuthorizedClientProviderBuilder.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ReactiveOAuth2AuthorizedClientProviderBuilder.java index a5fe54211f..482b5962ec 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ReactiveOAuth2AuthorizedClientProviderBuilder.java +++ b/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 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 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 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). diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultPasswordTokenResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultPasswordTokenResponseClient.java new file mode 100644 index 0000000000..e2f7180d2e --- /dev/null +++ b/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 Section 4.3.2 Access Token Request (Resource Owner Password Credentials Grant) + * @see Section 4.3.3 Access Token Response (Resource Owner Password Credentials Grant) + */ +public final class DefaultPasswordTokenResponseClient implements OAuth2AccessTokenResponseClient { + private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response"; + + private Converter> 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 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> 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. + * + *

+ * NOTE: At a minimum, the supplied {@code restOperations} must be configured with the following: + *

    + *
  1. {@link HttpMessageConverter}'s - {@link FormHttpMessageConverter} and {@link OAuth2AccessTokenResponseHttpMessageConverter}
  2. + *
  3. {@link ResponseErrorHandler} - {@link OAuth2ErrorResponseErrorHandler}
  4. + *
+ * + * @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; + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/OAuth2PasswordGrantRequest.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/OAuth2PasswordGrantRequest.java new file mode 100644 index 0000000000..0898fc32a0 --- /dev/null +++ b/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 Section 1.3.3 Resource Owner Password Credentials + */ +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; + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/OAuth2PasswordGrantRequestEntityConverter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/OAuth2PasswordGrantRequestEntityConverter.java new file mode 100644 index 0000000000..f2ba2b40a0 --- /dev/null +++ b/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> { + + /** + * 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 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 buildFormParameters(OAuth2PasswordGrantRequest passwordGrantRequest) { + ClientRegistration clientRegistration = passwordGrantRequest.getClientRegistration(); + + MultiValueMap 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; + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactivePasswordTokenResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactivePasswordTokenResponseClient.java new file mode 100644 index 0000000000..41fe121694 --- /dev/null +++ b/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 Section 4.3.2 Access Token Request (Resource Owner Password Credentials Grant) + * @see Section 4.3.3 Access Token Response (Resource Owner Password Credentials Grant) + */ +public final class WebClientReactivePasswordTokenResponseClient implements ReactiveOAuth2AccessTokenResponseClient { + private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response"; + private WebClient webClient = WebClient.builder().build(); + + @Override + public Mono 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 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 tokenRequestBody(OAuth2PasswordGrantRequest passwordGrantRequest) { + ClientRegistration clientRegistration = passwordGrantRequest.getClientRegistration(); + BodyInserters.FormInserter 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; + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java index 4313aa7497..2bfdce2f5f 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java +++ b/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; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizedClientManager.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizedClientManager.java index d364418613..40920c909d 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizedClientManager.java +++ b/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 apply(OAuth2AuthorizeRequest authorizeRequest) { - Map contextAttributes = Collections.emptyMap(); + Map 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; } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolver.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolver.java index b88f2ecad6..e371a346b3 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolver.java +++ b/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); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunction.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunction.java index 9719fe98b5..452c7c4ee3 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunction.java +++ b/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); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java index d32b560b31..b6026ef5b1 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java +++ b/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); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2AuthorizedClientArgumentResolver.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2AuthorizedClientArgumentResolver.java index 0784ec4000..3ab60df2ba 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2AuthorizedClientArgumentResolver.java +++ b/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); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizedClientManager.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizedClientManager.java index 54d61c7a11..0609bb2b57 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizedClientManager.java +++ b/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> contextAttributesMapper = new DefaultContextAttributesMapper(); + private Function>> 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 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 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> contextAttributesMapper) { + public void setContextAttributesMapper(Function>> 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> { + public static class DefaultContextAttributesMapper implements Function>> { @Override - public Map apply(ServerOAuth2AuthorizeRequest authorizeRequest) { - Map contextAttributes = Collections.emptyMap(); + public Mono> apply(ServerOAuth2AuthorizeRequest authorizeRequest) { + Map 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); } } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilderTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilderTests.java index 80ec21bb4c..d2ea51f1b9 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilderTests.java +++ b/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 diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/PasswordOAuth2AuthorizedClientProviderTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/PasswordOAuth2AuthorizedClientProviderTests.java new file mode 100644 index 0000000000..0fc32d7dbc --- /dev/null +++ b/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 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(); + } +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/PasswordReactiveOAuth2AuthorizedClientProviderTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/PasswordReactiveOAuth2AuthorizedClientProviderTests.java new file mode 100644 index 0000000000..2e91460f30 --- /dev/null +++ b/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 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(); + } +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/ReactiveOAuth2AuthorizedClientProviderBuilderTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/ReactiveOAuth2AuthorizedClientProviderBuilderTests.java index fd77cc4f1d..b22d959c3e 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/ReactiveOAuth2AuthorizedClientProviderBuilderTests.java +++ b/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 diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultPasswordTokenResponseClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultPasswordTokenResponseClientTests.java new file mode 100644 index 0000000000..a1679455bb --- /dev/null +++ b/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); + } +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/OAuth2PasswordGrantRequestEntityConverterTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/OAuth2PasswordGrantRequestEntityConverterTests.java new file mode 100644 index 0000000000..0fd6eb4a2c --- /dev/null +++ b/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 formParameters = (MultiValueMap) 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"); + } + +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/OAuth2PasswordGrantRequestTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/OAuth2PasswordGrantRequestTests.java new file mode 100644 index 0000000000..7c821f9897 --- /dev/null +++ b/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); + } +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactivePasswordTokenResponseClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactivePasswordTokenResponseClientTests.java new file mode 100644 index 0000000000..00730543c7 --- /dev/null +++ b/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); + } +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java index 0b10d0946e..e770376b9c 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java +++ b/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"); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/TestClientRegistrations.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/TestClientRegistrations.java index 6ed3587ba3..7cf750e9df 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/TestClientRegistrations.java +++ b/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"); + } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizedClientManagerTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizedClientManagerTests.java index 3942df494c..4a2fbae9d2 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizedClientManagerTests.java +++ b/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)); } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolverTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolverTests.java index 5aa115e077..4072c31369 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolverTests.java +++ b/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 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) { + } } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunctionTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunctionTests.java index 917509bac4..d166320add 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunctionTests.java +++ b/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 refreshTokenTokenResponseClient; + @Mock + private ReactiveOAuth2AccessTokenResponseClient 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 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 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()); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunctionTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunctionTests.java index f9278d5d64..9303c08e5f 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunctionTests.java +++ b/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 refreshTokenTokenResponseClient; @Mock + private OAuth2AccessTokenResponseClient passwordTokenResponseClient; + @Mock private WebClient.RequestHeadersSpec spec; @Captor private ArgumentCaptor>> 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 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") diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizedClientManagerTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizedClientManagerTests.java index 9729120cd5..c4b2a2de64 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizedClientManagerTests.java +++ b/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 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)); } } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthorizationGrantType.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthorizationGrantType.java index 266c11cccb..8eaf24e496 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthorizationGrantType.java +++ b/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; /** diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2ParameterNames.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2ParameterNames.java index b09814fbbb..3bb5b2e910 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2ParameterNames.java +++ b/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. */ diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/AuthorizationGrantTypeTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/AuthorizationGrantTypeTests.java index be5d1ba133..8205e162ca 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/AuthorizationGrantTypeTests.java +++ b/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"); + } } diff --git a/samples/boot/oauth2webclient-webflux/src/main/java/sample/config/WebClientConfig.java b/samples/boot/oauth2webclient-webflux/src/main/java/sample/config/WebClientConfig.java index 7fdba365b7..45172c9ae9 100644 --- a/samples/boot/oauth2webclient-webflux/src/main/java/sample/config/WebClientConfig.java +++ b/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( diff --git a/samples/boot/oauth2webclient/src/main/java/sample/config/WebClientConfig.java b/samples/boot/oauth2webclient/src/main/java/sample/config/WebClientConfig.java index 636bc53fd6..da29d36349 100644 --- a/samples/boot/oauth2webclient/src/main/java/sample/config/WebClientConfig.java +++ b/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);