Add support for Resource Owner Password Credentials grant
Fixes gh-6003
This commit is contained in:
parent
de672e3ae9
commit
dcd997ea43
|
@ -74,8 +74,8 @@ final class OAuth2ClientConfiguration {
|
||||||
OAuth2AuthorizedClientProviderBuilder authorizedClientProviderBuilder =
|
OAuth2AuthorizedClientProviderBuilder authorizedClientProviderBuilder =
|
||||||
OAuth2AuthorizedClientProviderBuilder.builder()
|
OAuth2AuthorizedClientProviderBuilder.builder()
|
||||||
.authorizationCode()
|
.authorizationCode()
|
||||||
.refreshToken();
|
.refreshToken()
|
||||||
|
.password();
|
||||||
if (this.accessTokenResponseClient != null) {
|
if (this.accessTokenResponseClient != null) {
|
||||||
authorizedClientProviderBuilder.clientCredentials(configurer ->
|
authorizedClientProviderBuilder.clientCredentials(configurer ->
|
||||||
configurer.accessTokenResponseClient(this.accessTokenResponseClient));
|
configurer.accessTokenResponseClient(this.accessTokenResponseClient));
|
||||||
|
@ -83,7 +83,6 @@ final class OAuth2ClientConfiguration {
|
||||||
authorizedClientProviderBuilder.clientCredentials();
|
authorizedClientProviderBuilder.clientCredentials();
|
||||||
}
|
}
|
||||||
OAuth2AuthorizedClientProvider authorizedClientProvider = authorizedClientProviderBuilder.build();
|
OAuth2AuthorizedClientProvider authorizedClientProvider = authorizedClientProviderBuilder.build();
|
||||||
|
|
||||||
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
|
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
|
||||||
this.clientRegistrationRepository, this.authorizedClientRepository);
|
this.clientRegistrationRepository, this.authorizedClientRepository);
|
||||||
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
|
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
|
||||||
|
|
|
@ -71,6 +71,7 @@ final class ReactiveOAuth2ClientImportSelector implements ImportSelector {
|
||||||
.authorizationCode()
|
.authorizationCode()
|
||||||
.refreshToken()
|
.refreshToken()
|
||||||
.clientCredentials()
|
.clientCredentials()
|
||||||
|
.password()
|
||||||
.build();
|
.build();
|
||||||
DefaultServerOAuth2AuthorizedClientManager authorizedClientManager = new DefaultServerOAuth2AuthorizedClientManager(
|
DefaultServerOAuth2AuthorizedClientManager authorizedClientManager = new DefaultServerOAuth2AuthorizedClientManager(
|
||||||
this.clientRegistrationRepository, getAuthorizedClientRepository());
|
this.clientRegistrationRepository, getAuthorizedClientRepository());
|
||||||
|
|
|
@ -36,14 +36,21 @@ import java.util.Map;
|
||||||
*/
|
*/
|
||||||
public final class OAuth2AuthorizationContext {
|
public final class OAuth2AuthorizationContext {
|
||||||
/**
|
/**
|
||||||
* The name of the {@link #getAttribute(String) attribute}
|
* The name of the {@link #getAttribute(String) attribute} in the context associated to the value for the "request scope(s)".
|
||||||
* in the {@link OAuth2AuthorizationContext context}
|
* The value of the attribute is a {@code String[]} of scope(s) to be requested by the {@link #getClientRegistration() client}.
|
||||||
* 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}.
|
|
||||||
*/
|
*/
|
||||||
public static final String REQUEST_SCOPE_ATTRIBUTE_NAME = OAuth2AuthorizationContext.class.getName().concat(".REQUEST_SCOPE");
|
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 ClientRegistration clientRegistration;
|
||||||
private OAuth2AuthorizedClient authorizedClient;
|
private OAuth2AuthorizedClient authorizedClient;
|
||||||
private Authentication principal;
|
private Authentication principal;
|
||||||
|
|
|
@ -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.OAuth2AccessTokenResponseClient;
|
||||||
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
|
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.OAuth2RefreshTokenGrantRequest;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
import java.time.Clock;
|
import java.time.Clock;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A builder that builds a {@link DelegatingOAuth2AuthorizedClientProvider} composed of
|
* A builder that builds a {@link DelegatingOAuth2AuthorizedClientProvider} composed of
|
||||||
* one or more {@link OAuth2AuthorizedClientProvider}(s) that implement specific authorization grants.
|
* one or more {@link OAuth2AuthorizedClientProvider}(s) that implement specific authorization grants.
|
||||||
* The supported authorization grants are {@link #authorizationCode() authorization_code},
|
* 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
|
* In addition to the standard authorization grants, an implementation of an extension grant
|
||||||
* may be supplied via {@link #provider(OAuth2AuthorizedClientProvider)}.
|
* may be supplied via {@link #provider(OAuth2AuthorizedClientProvider)}.
|
||||||
*
|
*
|
||||||
|
@ -43,6 +45,7 @@ import java.util.function.Consumer;
|
||||||
* @see AuthorizationCodeOAuth2AuthorizedClientProvider
|
* @see AuthorizationCodeOAuth2AuthorizedClientProvider
|
||||||
* @see RefreshTokenOAuth2AuthorizedClientProvider
|
* @see RefreshTokenOAuth2AuthorizedClientProvider
|
||||||
* @see ClientCredentialsOAuth2AuthorizedClientProvider
|
* @see ClientCredentialsOAuth2AuthorizedClientProvider
|
||||||
|
* @see PasswordOAuth2AuthorizedClientProvider
|
||||||
* @see DelegatingOAuth2AuthorizedClientProvider
|
* @see DelegatingOAuth2AuthorizedClientProvider
|
||||||
*/
|
*/
|
||||||
public final class OAuth2AuthorizedClientProviderBuilder {
|
public final class OAuth2AuthorizedClientProviderBuilder {
|
||||||
|
@ -279,6 +282,95 @@ public final class OAuth2AuthorizedClientProviderBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures support for the {@code password} grant.
|
||||||
|
*
|
||||||
|
* @return the {@link OAuth2AuthorizedClientProviderBuilder}
|
||||||
|
*/
|
||||||
|
public OAuth2AuthorizedClientProviderBuilder password() {
|
||||||
|
this.builders.computeIfAbsent(PasswordOAuth2AuthorizedClientProvider.class, k -> new PasswordGrantBuilder());
|
||||||
|
return OAuth2AuthorizedClientProviderBuilder.this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures support for the {@code password} grant.
|
||||||
|
*
|
||||||
|
* @param builderConsumer a {@code Consumer} of {@link PasswordGrantBuilder} used for further configuration
|
||||||
|
* @return the {@link OAuth2AuthorizedClientProviderBuilder}
|
||||||
|
*/
|
||||||
|
public OAuth2AuthorizedClientProviderBuilder password(Consumer<PasswordGrantBuilder> builderConsumer) {
|
||||||
|
PasswordGrantBuilder builder = (PasswordGrantBuilder) this.builders.computeIfAbsent(
|
||||||
|
PasswordOAuth2AuthorizedClientProvider.class, k -> new PasswordGrantBuilder());
|
||||||
|
builderConsumer.accept(builder);
|
||||||
|
return OAuth2AuthorizedClientProviderBuilder.this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A builder for the {@code password} grant.
|
||||||
|
*/
|
||||||
|
public class PasswordGrantBuilder implements Builder {
|
||||||
|
private OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient;
|
||||||
|
private Duration clockSkew;
|
||||||
|
private Clock clock;
|
||||||
|
|
||||||
|
private PasswordGrantBuilder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the client used when requesting an access token credential at the Token Endpoint.
|
||||||
|
*
|
||||||
|
* @param accessTokenResponseClient the client used when requesting an access token credential at the Token Endpoint
|
||||||
|
* @return the {@link PasswordGrantBuilder}
|
||||||
|
*/
|
||||||
|
public PasswordGrantBuilder accessTokenResponseClient(OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient) {
|
||||||
|
this.accessTokenResponseClient = accessTokenResponseClient;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the maximum acceptable clock skew, which is used when checking the access token expiry.
|
||||||
|
* An access token is considered expired if it's before {@code Instant.now(this.clock) - clockSkew}.
|
||||||
|
*
|
||||||
|
* @param clockSkew the maximum acceptable clock skew
|
||||||
|
* @return the {@link PasswordGrantBuilder}
|
||||||
|
*/
|
||||||
|
public PasswordGrantBuilder clockSkew(Duration clockSkew) {
|
||||||
|
this.clockSkew = clockSkew;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the access token expiry.
|
||||||
|
*
|
||||||
|
* @param clock the clock
|
||||||
|
* @return the {@link PasswordGrantBuilder}
|
||||||
|
*/
|
||||||
|
public PasswordGrantBuilder clock(Clock clock) {
|
||||||
|
this.clock = clock;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds an instance of {@link PasswordOAuth2AuthorizedClientProvider}.
|
||||||
|
*
|
||||||
|
* @return the {@link PasswordOAuth2AuthorizedClientProvider}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public OAuth2AuthorizedClientProvider build() {
|
||||||
|
PasswordOAuth2AuthorizedClientProvider authorizedClientProvider = new PasswordOAuth2AuthorizedClientProvider();
|
||||||
|
if (this.accessTokenResponseClient != null) {
|
||||||
|
authorizedClientProvider.setAccessTokenResponseClient(this.accessTokenResponseClient);
|
||||||
|
}
|
||||||
|
if (this.clockSkew != null) {
|
||||||
|
authorizedClientProvider.setClockSkew(this.clockSkew);
|
||||||
|
}
|
||||||
|
if (this.clock != null) {
|
||||||
|
authorizedClientProvider.setClock(this.clock);
|
||||||
|
}
|
||||||
|
return authorizedClientProvider;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds an instance of {@link DelegatingOAuth2AuthorizedClientProvider}
|
* Builds an instance of {@link DelegatingOAuth2AuthorizedClientProvider}
|
||||||
* composed of one or more {@link OAuth2AuthorizedClientProvider}(s).
|
* composed of one or more {@link OAuth2AuthorizedClientProvider}(s).
|
||||||
|
|
|
@ -0,0 +1,142 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2019 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.client;
|
||||||
|
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.security.oauth2.client.endpoint.DefaultPasswordTokenResponseClient;
|
||||||
|
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
|
||||||
|
import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
|
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
|
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of an {@link OAuth2AuthorizedClientProvider}
|
||||||
|
* for the {@link AuthorizationGrantType#PASSWORD password} grant.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.2
|
||||||
|
* @see OAuth2AuthorizedClientProvider
|
||||||
|
* @see DefaultPasswordTokenResponseClient
|
||||||
|
*/
|
||||||
|
public final class PasswordOAuth2AuthorizedClientProvider implements OAuth2AuthorizedClientProvider {
|
||||||
|
private OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient =
|
||||||
|
new DefaultPasswordTokenResponseClient();
|
||||||
|
private Duration clockSkew = Duration.ofSeconds(60);
|
||||||
|
private Clock clock = Clock.systemUTC();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to authorize (or re-authorize) the {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided {@code context}.
|
||||||
|
* Returns {@code null} if authorization (or re-authorization) is not supported,
|
||||||
|
* e.g. the client's {@link ClientRegistration#getAuthorizationGrantType() authorization grant type}
|
||||||
|
* is not {@link AuthorizationGrantType#PASSWORD password} OR
|
||||||
|
* the {@link OAuth2AuthorizationContext#USERNAME_ATTRIBUTE_NAME username} and/or
|
||||||
|
* {@link OAuth2AuthorizationContext#PASSWORD_ATTRIBUTE_NAME password} attributes
|
||||||
|
* are not available in the provided {@code context} OR
|
||||||
|
* the {@link OAuth2AuthorizedClient#getAccessToken() access token} is not expired.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The following {@link OAuth2AuthorizationContext#getAttributes() context attributes} are supported:
|
||||||
|
* <ol>
|
||||||
|
* <li>{@link OAuth2AuthorizationContext#USERNAME_ATTRIBUTE_NAME} (required) - a {@code String} value for the resource owner's username</li>
|
||||||
|
* <li>{@link OAuth2AuthorizationContext#PASSWORD_ATTRIBUTE_NAME} (required) - a {@code String} value for the resource owner's password</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* @param context the context that holds authorization-specific state for the client
|
||||||
|
* @return the {@link OAuth2AuthorizedClient} or {@code null} if authorization (or re-authorization) is not supported
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) {
|
||||||
|
Assert.notNull(context, "context cannot be null");
|
||||||
|
|
||||||
|
ClientRegistration clientRegistration = context.getClientRegistration();
|
||||||
|
OAuth2AuthorizedClient authorizedClient = context.getAuthorizedClient();
|
||||||
|
|
||||||
|
if (!AuthorizationGrantType.PASSWORD.equals(clientRegistration.getAuthorizationGrantType())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String username = context.getAttribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME);
|
||||||
|
String password = context.getAttribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME);
|
||||||
|
if (!StringUtils.hasText(username) || !StringUtils.hasText(password)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authorizedClient != null && !hasTokenExpired(authorizedClient.getAccessToken())) {
|
||||||
|
// If client is already authorized and access token is NOT expired than no need for re-authorization
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authorizedClient != null && hasTokenExpired(authorizedClient.getAccessToken()) && authorizedClient.getRefreshToken() != null) {
|
||||||
|
// If client is already authorized and access token is expired and a refresh token is available,
|
||||||
|
// than return and allow RefreshTokenOAuth2AuthorizedClientProvider to handle the refresh
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
OAuth2PasswordGrantRequest passwordGrantRequest =
|
||||||
|
new OAuth2PasswordGrantRequest(clientRegistration, username, password);
|
||||||
|
OAuth2AccessTokenResponse tokenResponse =
|
||||||
|
this.accessTokenResponseClient.getTokenResponse(passwordGrantRequest);
|
||||||
|
|
||||||
|
return new OAuth2AuthorizedClient(clientRegistration, context.getPrincipal().getName(),
|
||||||
|
tokenResponse.getAccessToken(), tokenResponse.getRefreshToken());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasTokenExpired(AbstractOAuth2Token token) {
|
||||||
|
return token.getExpiresAt().isBefore(Instant.now(this.clock).minus(this.clockSkew));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the client used when requesting an access token credential at the Token Endpoint for the {@code password} grant.
|
||||||
|
*
|
||||||
|
* @param accessTokenResponseClient the client used when requesting an access token credential at the Token Endpoint for the {@code password} grant
|
||||||
|
*/
|
||||||
|
public void setAccessTokenResponseClient(OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient) {
|
||||||
|
Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");
|
||||||
|
this.accessTokenResponseClient = accessTokenResponseClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the maximum acceptable clock skew, which is used when checking the
|
||||||
|
* {@link OAuth2AuthorizedClient#getAccessToken() access token} expiry. The default is 60 seconds.
|
||||||
|
* An access token is considered expired if it's before {@code Instant.now(this.clock) - clockSkew}.
|
||||||
|
*
|
||||||
|
* @param clockSkew the maximum acceptable clock skew
|
||||||
|
*/
|
||||||
|
public void setClockSkew(Duration clockSkew) {
|
||||||
|
Assert.notNull(clockSkew, "clockSkew cannot be null");
|
||||||
|
Assert.isTrue(clockSkew.getSeconds() >= 0, "clockSkew must be >= 0");
|
||||||
|
this.clockSkew = clockSkew;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the access token expiry.
|
||||||
|
*
|
||||||
|
* @param clock the clock
|
||||||
|
*/
|
||||||
|
public void setClock(Clock clock) {
|
||||||
|
Assert.notNull(clock, "clock cannot be null");
|
||||||
|
this.clock = clock;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,140 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2019 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.client;
|
||||||
|
|
||||||
|
import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
|
||||||
|
import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
|
||||||
|
import org.springframework.security.oauth2.client.endpoint.WebClientReactivePasswordTokenResponseClient;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
|
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
|
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of a {@link ReactiveOAuth2AuthorizedClientProvider}
|
||||||
|
* for the {@link AuthorizationGrantType#PASSWORD password} grant.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.2
|
||||||
|
* @see ReactiveOAuth2AuthorizedClientProvider
|
||||||
|
* @see WebClientReactivePasswordTokenResponseClient
|
||||||
|
*/
|
||||||
|
public final class PasswordReactiveOAuth2AuthorizedClientProvider implements ReactiveOAuth2AuthorizedClientProvider {
|
||||||
|
private ReactiveOAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient =
|
||||||
|
new WebClientReactivePasswordTokenResponseClient();
|
||||||
|
private Duration clockSkew = Duration.ofSeconds(60);
|
||||||
|
private Clock clock = Clock.systemUTC();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to authorize (or re-authorize) the {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided {@code context}.
|
||||||
|
* Returns an empty {@code Mono} if authorization (or re-authorization) is not supported,
|
||||||
|
* e.g. the client's {@link ClientRegistration#getAuthorizationGrantType() authorization grant type}
|
||||||
|
* is not {@link AuthorizationGrantType#PASSWORD password} OR
|
||||||
|
* the {@link OAuth2AuthorizationContext#USERNAME_ATTRIBUTE_NAME username} and/or
|
||||||
|
* {@link OAuth2AuthorizationContext#PASSWORD_ATTRIBUTE_NAME password} attributes
|
||||||
|
* are not available in the provided {@code context} OR
|
||||||
|
* the {@link OAuth2AuthorizedClient#getAccessToken() access token} is not expired.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The following {@link OAuth2AuthorizationContext#getAttributes() context attributes} are supported:
|
||||||
|
* <ol>
|
||||||
|
* <li>{@link OAuth2AuthorizationContext#USERNAME_ATTRIBUTE_NAME} (required) - a {@code String} value for the resource owner's username</li>
|
||||||
|
* <li>{@link OAuth2AuthorizationContext#PASSWORD_ATTRIBUTE_NAME} (required) - a {@code String} value for the resource owner's password</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* @param context the context that holds authorization-specific state for the client
|
||||||
|
* @return the {@link OAuth2AuthorizedClient} or an empty {@code Mono} if authorization (or re-authorization) is not supported
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Mono<OAuth2AuthorizedClient> authorize(OAuth2AuthorizationContext context) {
|
||||||
|
Assert.notNull(context, "context cannot be null");
|
||||||
|
|
||||||
|
ClientRegistration clientRegistration = context.getClientRegistration();
|
||||||
|
OAuth2AuthorizedClient authorizedClient = context.getAuthorizedClient();
|
||||||
|
|
||||||
|
if (!AuthorizationGrantType.PASSWORD.equals(clientRegistration.getAuthorizationGrantType())) {
|
||||||
|
return Mono.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
String username = context.getAttribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME);
|
||||||
|
String password = context.getAttribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME);
|
||||||
|
if (!StringUtils.hasText(username) || !StringUtils.hasText(password)) {
|
||||||
|
return Mono.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authorizedClient != null && !hasTokenExpired(authorizedClient.getAccessToken())) {
|
||||||
|
// If client is already authorized and access token is NOT expired than no need for re-authorization
|
||||||
|
return Mono.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authorizedClient != null && hasTokenExpired(authorizedClient.getAccessToken()) && authorizedClient.getRefreshToken() != null) {
|
||||||
|
// If client is already authorized and access token is expired and a refresh token is available,
|
||||||
|
// than return and allow RefreshTokenReactiveOAuth2AuthorizedClientProvider to handle the refresh
|
||||||
|
return Mono.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
OAuth2PasswordGrantRequest passwordGrantRequest =
|
||||||
|
new OAuth2PasswordGrantRequest(clientRegistration, username, password);
|
||||||
|
|
||||||
|
return Mono.just(passwordGrantRequest)
|
||||||
|
.flatMap(this.accessTokenResponseClient::getTokenResponse)
|
||||||
|
.map(tokenResponse -> new OAuth2AuthorizedClient(clientRegistration, context.getPrincipal().getName(),
|
||||||
|
tokenResponse.getAccessToken(), tokenResponse.getRefreshToken()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasTokenExpired(AbstractOAuth2Token token) {
|
||||||
|
return token.getExpiresAt().isBefore(Instant.now(this.clock).minus(this.clockSkew));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the client used when requesting an access token credential at the Token Endpoint for the {@code password} grant.
|
||||||
|
*
|
||||||
|
* @param accessTokenResponseClient the client used when requesting an access token credential at the Token Endpoint for the {@code password} grant
|
||||||
|
*/
|
||||||
|
public void setAccessTokenResponseClient(ReactiveOAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient) {
|
||||||
|
Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");
|
||||||
|
this.accessTokenResponseClient = accessTokenResponseClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the maximum acceptable clock skew, which is used when checking the
|
||||||
|
* {@link OAuth2AuthorizedClient#getAccessToken() access token} expiry. The default is 60 seconds.
|
||||||
|
* An access token is considered expired if it's before {@code Instant.now(this.clock) - clockSkew}.
|
||||||
|
*
|
||||||
|
* @param clockSkew the maximum acceptable clock skew
|
||||||
|
*/
|
||||||
|
public void setClockSkew(Duration clockSkew) {
|
||||||
|
Assert.notNull(clockSkew, "clockSkew cannot be null");
|
||||||
|
Assert.isTrue(clockSkew.getSeconds() >= 0, "clockSkew must be >= 0");
|
||||||
|
this.clockSkew = clockSkew;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the access token expiry.
|
||||||
|
*
|
||||||
|
* @param clock the clock
|
||||||
|
*/
|
||||||
|
public void setClock(Clock clock) {
|
||||||
|
Assert.notNull(clock, "clock cannot be null");
|
||||||
|
this.clock = clock;
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,6 +16,7 @@
|
||||||
package org.springframework.security.oauth2.client;
|
package org.springframework.security.oauth2.client;
|
||||||
|
|
||||||
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
|
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.OAuth2RefreshTokenGrantRequest;
|
||||||
import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
|
import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
@ -33,7 +34,8 @@ import java.util.stream.Collectors;
|
||||||
* A builder that builds a {@link DelegatingReactiveOAuth2AuthorizedClientProvider} composed of
|
* A builder that builds a {@link DelegatingReactiveOAuth2AuthorizedClientProvider} composed of
|
||||||
* one or more {@link ReactiveOAuth2AuthorizedClientProvider}(s) that implement specific authorization grants.
|
* one or more {@link ReactiveOAuth2AuthorizedClientProvider}(s) that implement specific authorization grants.
|
||||||
* The supported authorization grants are {@link #authorizationCode() authorization_code},
|
* 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
|
* In addition to the standard authorization grants, an implementation of an extension grant
|
||||||
* may be supplied via {@link #provider(ReactiveOAuth2AuthorizedClientProvider)}.
|
* may be supplied via {@link #provider(ReactiveOAuth2AuthorizedClientProvider)}.
|
||||||
*
|
*
|
||||||
|
@ -43,6 +45,7 @@ import java.util.stream.Collectors;
|
||||||
* @see AuthorizationCodeReactiveOAuth2AuthorizedClientProvider
|
* @see AuthorizationCodeReactiveOAuth2AuthorizedClientProvider
|
||||||
* @see RefreshTokenReactiveOAuth2AuthorizedClientProvider
|
* @see RefreshTokenReactiveOAuth2AuthorizedClientProvider
|
||||||
* @see ClientCredentialsReactiveOAuth2AuthorizedClientProvider
|
* @see ClientCredentialsReactiveOAuth2AuthorizedClientProvider
|
||||||
|
* @see PasswordReactiveOAuth2AuthorizedClientProvider
|
||||||
* @see DelegatingReactiveOAuth2AuthorizedClientProvider
|
* @see DelegatingReactiveOAuth2AuthorizedClientProvider
|
||||||
*/
|
*/
|
||||||
public final class ReactiveOAuth2AuthorizedClientProviderBuilder {
|
public final class ReactiveOAuth2AuthorizedClientProviderBuilder {
|
||||||
|
@ -279,6 +282,95 @@ public final class ReactiveOAuth2AuthorizedClientProviderBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures support for the {@code password} grant.
|
||||||
|
*
|
||||||
|
* @return the {@link ReactiveOAuth2AuthorizedClientProviderBuilder}
|
||||||
|
*/
|
||||||
|
public ReactiveOAuth2AuthorizedClientProviderBuilder password() {
|
||||||
|
this.builders.computeIfAbsent(PasswordReactiveOAuth2AuthorizedClientProvider.class, k -> new PasswordGrantBuilder());
|
||||||
|
return ReactiveOAuth2AuthorizedClientProviderBuilder.this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures support for the {@code password} grant.
|
||||||
|
*
|
||||||
|
* @param builderConsumer a {@code Consumer} of {@link PasswordGrantBuilder} used for further configuration
|
||||||
|
* @return the {@link ReactiveOAuth2AuthorizedClientProviderBuilder}
|
||||||
|
*/
|
||||||
|
public ReactiveOAuth2AuthorizedClientProviderBuilder password(Consumer<PasswordGrantBuilder> builderConsumer) {
|
||||||
|
PasswordGrantBuilder builder = (PasswordGrantBuilder) this.builders.computeIfAbsent(
|
||||||
|
PasswordReactiveOAuth2AuthorizedClientProvider.class, k -> new PasswordGrantBuilder());
|
||||||
|
builderConsumer.accept(builder);
|
||||||
|
return ReactiveOAuth2AuthorizedClientProviderBuilder.this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A builder for the {@code password} grant.
|
||||||
|
*/
|
||||||
|
public class PasswordGrantBuilder implements Builder {
|
||||||
|
private ReactiveOAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient;
|
||||||
|
private Duration clockSkew;
|
||||||
|
private Clock clock;
|
||||||
|
|
||||||
|
private PasswordGrantBuilder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the client used when requesting an access token credential at the Token Endpoint.
|
||||||
|
*
|
||||||
|
* @param accessTokenResponseClient the client used when requesting an access token credential at the Token Endpoint
|
||||||
|
* @return the {@link PasswordGrantBuilder}
|
||||||
|
*/
|
||||||
|
public PasswordGrantBuilder accessTokenResponseClient(ReactiveOAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient) {
|
||||||
|
this.accessTokenResponseClient = accessTokenResponseClient;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the maximum acceptable clock skew, which is used when checking the access token expiry.
|
||||||
|
* An access token is considered expired if it's before {@code Instant.now(this.clock) - clockSkew}.
|
||||||
|
*
|
||||||
|
* @param clockSkew the maximum acceptable clock skew
|
||||||
|
* @return the {@link PasswordGrantBuilder}
|
||||||
|
*/
|
||||||
|
public PasswordGrantBuilder clockSkew(Duration clockSkew) {
|
||||||
|
this.clockSkew = clockSkew;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the access token expiry.
|
||||||
|
*
|
||||||
|
* @param clock the clock
|
||||||
|
* @return the {@link PasswordGrantBuilder}
|
||||||
|
*/
|
||||||
|
public PasswordGrantBuilder clock(Clock clock) {
|
||||||
|
this.clock = clock;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds an instance of {@link PasswordReactiveOAuth2AuthorizedClientProvider}.
|
||||||
|
*
|
||||||
|
* @return the {@link PasswordReactiveOAuth2AuthorizedClientProvider}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public ReactiveOAuth2AuthorizedClientProvider build() {
|
||||||
|
PasswordReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = new PasswordReactiveOAuth2AuthorizedClientProvider();
|
||||||
|
if (this.accessTokenResponseClient != null) {
|
||||||
|
authorizedClientProvider.setAccessTokenResponseClient(this.accessTokenResponseClient);
|
||||||
|
}
|
||||||
|
if (this.clockSkew != null) {
|
||||||
|
authorizedClientProvider.setClockSkew(this.clockSkew);
|
||||||
|
}
|
||||||
|
if (this.clock != null) {
|
||||||
|
authorizedClientProvider.setClock(this.clock);
|
||||||
|
}
|
||||||
|
return authorizedClientProvider;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds an instance of {@link DelegatingReactiveOAuth2AuthorizedClientProvider}
|
* Builds an instance of {@link DelegatingReactiveOAuth2AuthorizedClientProvider}
|
||||||
* composed of one or more {@link ReactiveOAuth2AuthorizedClientProvider}(s).
|
* composed of one or more {@link ReactiveOAuth2AuthorizedClientProvider}(s).
|
||||||
|
|
|
@ -0,0 +1,124 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2019 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.client.endpoint;
|
||||||
|
|
||||||
|
import org.springframework.core.convert.converter.Converter;
|
||||||
|
import org.springframework.http.RequestEntity;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.http.converter.FormHttpMessageConverter;
|
||||||
|
import org.springframework.http.converter.HttpMessageConverter;
|
||||||
|
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
|
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||||
|
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
import org.springframework.web.client.ResponseErrorHandler;
|
||||||
|
import org.springframework.web.client.RestClientException;
|
||||||
|
import org.springframework.web.client.RestOperations;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default implementation of an {@link OAuth2AccessTokenResponseClient}
|
||||||
|
* for the {@link AuthorizationGrantType#PASSWORD password} grant.
|
||||||
|
* This implementation uses a {@link RestOperations} when requesting
|
||||||
|
* an access token credential at the Authorization Server's Token Endpoint.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.2
|
||||||
|
* @see OAuth2AccessTokenResponseClient
|
||||||
|
* @see OAuth2PasswordGrantRequest
|
||||||
|
* @see OAuth2AccessTokenResponse
|
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.3.2">Section 4.3.2 Access Token Request (Resource Owner Password Credentials Grant)</a>
|
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.3.3">Section 4.3.3 Access Token Response (Resource Owner Password Credentials Grant)</a>
|
||||||
|
*/
|
||||||
|
public final class DefaultPasswordTokenResponseClient implements OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> {
|
||||||
|
private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response";
|
||||||
|
|
||||||
|
private Converter<OAuth2PasswordGrantRequest, RequestEntity<?>> requestEntityConverter =
|
||||||
|
new OAuth2PasswordGrantRequestEntityConverter();
|
||||||
|
|
||||||
|
private RestOperations restOperations;
|
||||||
|
|
||||||
|
public DefaultPasswordTokenResponseClient() {
|
||||||
|
RestTemplate restTemplate = new RestTemplate(Arrays.asList(
|
||||||
|
new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter()));
|
||||||
|
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
|
||||||
|
this.restOperations = restTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OAuth2AccessTokenResponse getTokenResponse(OAuth2PasswordGrantRequest passwordGrantRequest) {
|
||||||
|
Assert.notNull(passwordGrantRequest, "passwordGrantRequest cannot be null");
|
||||||
|
|
||||||
|
RequestEntity<?> request = this.requestEntityConverter.convert(passwordGrantRequest);
|
||||||
|
|
||||||
|
ResponseEntity<OAuth2AccessTokenResponse> response;
|
||||||
|
try {
|
||||||
|
response = this.restOperations.exchange(request, OAuth2AccessTokenResponse.class);
|
||||||
|
} catch (RestClientException ex) {
|
||||||
|
OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE,
|
||||||
|
"An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: " + ex.getMessage(), null);
|
||||||
|
throw new OAuth2AuthorizationException(oauth2Error, ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
OAuth2AccessTokenResponse tokenResponse = response.getBody();
|
||||||
|
|
||||||
|
if (CollectionUtils.isEmpty(tokenResponse.getAccessToken().getScopes())) {
|
||||||
|
// As per spec, in Section 5.1 Successful Access Token Response
|
||||||
|
// https://tools.ietf.org/html/rfc6749#section-5.1
|
||||||
|
// If AccessTokenResponse.scope is empty, then default to the scope
|
||||||
|
// originally requested by the client in the Token Request
|
||||||
|
tokenResponse = OAuth2AccessTokenResponse.withResponse(tokenResponse)
|
||||||
|
.scopes(passwordGrantRequest.getClientRegistration().getScopes())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link Converter} used for converting the {@link OAuth2PasswordGrantRequest}
|
||||||
|
* to a {@link RequestEntity} representation of the OAuth 2.0 Access Token Request.
|
||||||
|
*
|
||||||
|
* @param requestEntityConverter the {@link Converter} used for converting to a {@link RequestEntity} representation of the Access Token Request
|
||||||
|
*/
|
||||||
|
public void setRequestEntityConverter(Converter<OAuth2PasswordGrantRequest, RequestEntity<?>> requestEntityConverter) {
|
||||||
|
Assert.notNull(requestEntityConverter, "requestEntityConverter cannot be null");
|
||||||
|
this.requestEntityConverter = requestEntityConverter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link RestOperations} used when requesting the OAuth 2.0 Access Token Response.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* <b>NOTE:</b> At a minimum, the supplied {@code restOperations} must be configured with the following:
|
||||||
|
* <ol>
|
||||||
|
* <li>{@link HttpMessageConverter}'s - {@link FormHttpMessageConverter} and {@link OAuth2AccessTokenResponseHttpMessageConverter}</li>
|
||||||
|
* <li>{@link ResponseErrorHandler} - {@link OAuth2ErrorResponseErrorHandler}</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* @param restOperations the {@link RestOperations} used when requesting the Access Token Response
|
||||||
|
*/
|
||||||
|
public void setRestOperations(RestOperations restOperations) {
|
||||||
|
Assert.notNull(restOperations, "restOperations cannot be null");
|
||||||
|
this.restOperations = restOperations;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2019 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.client.endpoint;
|
||||||
|
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An OAuth 2.0 Resource Owner Password Credentials Grant request
|
||||||
|
* that holds the resource owner's credentials.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.2
|
||||||
|
* @see AbstractOAuth2AuthorizationGrantRequest
|
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.3.3">Section 1.3.3 Resource Owner Password Credentials</a>
|
||||||
|
*/
|
||||||
|
public class OAuth2PasswordGrantRequest extends AbstractOAuth2AuthorizationGrantRequest {
|
||||||
|
private final ClientRegistration clientRegistration;
|
||||||
|
private final String username;
|
||||||
|
private final String password;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an {@code OAuth2PasswordGrantRequest} using the provided parameters.
|
||||||
|
*
|
||||||
|
* @param clientRegistration the client registration
|
||||||
|
* @param username the resource owner's username
|
||||||
|
* @param password the resource owner's password
|
||||||
|
*/
|
||||||
|
public OAuth2PasswordGrantRequest(ClientRegistration clientRegistration, String username, String password) {
|
||||||
|
super(AuthorizationGrantType.PASSWORD);
|
||||||
|
Assert.notNull(clientRegistration, "clientRegistration cannot be null");
|
||||||
|
Assert.isTrue(AuthorizationGrantType.PASSWORD.equals(clientRegistration.getAuthorizationGrantType()),
|
||||||
|
"clientRegistration.authorizationGrantType must be AuthorizationGrantType.PASSWORD");
|
||||||
|
Assert.hasText(username, "username cannot be empty");
|
||||||
|
Assert.hasText(password, "password cannot be empty");
|
||||||
|
this.clientRegistration = clientRegistration;
|
||||||
|
this.username = username;
|
||||||
|
this.password = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@link ClientRegistration client registration}.
|
||||||
|
*
|
||||||
|
* @return the {@link ClientRegistration}
|
||||||
|
*/
|
||||||
|
public ClientRegistration getClientRegistration() {
|
||||||
|
return this.clientRegistration;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the resource owner's username.
|
||||||
|
*
|
||||||
|
* @return the resource owner's username
|
||||||
|
*/
|
||||||
|
public String getUsername() {
|
||||||
|
return this.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the resource owner's password.
|
||||||
|
*
|
||||||
|
* @return the resource owner's password
|
||||||
|
*/
|
||||||
|
public String getPassword() {
|
||||||
|
return this.password;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2019 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.client.endpoint;
|
||||||
|
|
||||||
|
import org.springframework.core.convert.converter.Converter;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.RequestEntity;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
|
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
|
import org.springframework.util.MultiValueMap;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link Converter} that converts the provided {@link OAuth2PasswordGrantRequest}
|
||||||
|
* to a {@link RequestEntity} representation of an OAuth 2.0 Access Token Request
|
||||||
|
* for the Resource Owner Password Credentials Grant.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.2
|
||||||
|
* @see Converter
|
||||||
|
* @see OAuth2PasswordGrantRequest
|
||||||
|
* @see RequestEntity
|
||||||
|
*/
|
||||||
|
public class OAuth2PasswordGrantRequestEntityConverter implements Converter<OAuth2PasswordGrantRequest, RequestEntity<?>> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@link RequestEntity} used for the Access Token Request.
|
||||||
|
*
|
||||||
|
* @param passwordGrantRequest the password grant request
|
||||||
|
* @return the {@link RequestEntity} used for the Access Token Request
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public RequestEntity<?> convert(OAuth2PasswordGrantRequest passwordGrantRequest) {
|
||||||
|
ClientRegistration clientRegistration = passwordGrantRequest.getClientRegistration();
|
||||||
|
|
||||||
|
HttpHeaders headers = OAuth2AuthorizationGrantRequestEntityUtils.getTokenRequestHeaders(clientRegistration);
|
||||||
|
MultiValueMap<String, String> formParameters = buildFormParameters(passwordGrantRequest);
|
||||||
|
URI uri = UriComponentsBuilder.fromUriString(clientRegistration.getProviderDetails().getTokenUri())
|
||||||
|
.build()
|
||||||
|
.toUri();
|
||||||
|
|
||||||
|
return new RequestEntity<>(formParameters, headers, HttpMethod.POST, uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a {@link MultiValueMap} of the form parameters used for the Access Token Request body.
|
||||||
|
*
|
||||||
|
* @param passwordGrantRequest the password grant request
|
||||||
|
* @return a {@link MultiValueMap} of the form parameters used for the Access Token Request body
|
||||||
|
*/
|
||||||
|
private MultiValueMap<String, String> buildFormParameters(OAuth2PasswordGrantRequest passwordGrantRequest) {
|
||||||
|
ClientRegistration clientRegistration = passwordGrantRequest.getClientRegistration();
|
||||||
|
|
||||||
|
MultiValueMap<String, String> formParameters = new LinkedMultiValueMap<>();
|
||||||
|
formParameters.add(OAuth2ParameterNames.GRANT_TYPE, passwordGrantRequest.getGrantType().getValue());
|
||||||
|
formParameters.add(OAuth2ParameterNames.USERNAME, passwordGrantRequest.getUsername());
|
||||||
|
formParameters.add(OAuth2ParameterNames.PASSWORD, passwordGrantRequest.getPassword());
|
||||||
|
if (!CollectionUtils.isEmpty(clientRegistration.getScopes())) {
|
||||||
|
formParameters.add(OAuth2ParameterNames.SCOPE,
|
||||||
|
StringUtils.collectionToDelimitedString(clientRegistration.getScopes(), " "));
|
||||||
|
}
|
||||||
|
if (ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod())) {
|
||||||
|
formParameters.add(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());
|
||||||
|
formParameters.add(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret());
|
||||||
|
}
|
||||||
|
|
||||||
|
return formParameters;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,134 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2019 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.client.endpoint;
|
||||||
|
|
||||||
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
|
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
|
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.reactive.function.BodyInserters;
|
||||||
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import static org.springframework.security.oauth2.core.web.reactive.function.OAuth2BodyExtractors.oauth2AccessTokenResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of a {@link ReactiveOAuth2AccessTokenResponseClient}
|
||||||
|
* for the {@link AuthorizationGrantType#PASSWORD password} grant.
|
||||||
|
* This implementation uses {@link WebClient} when requesting
|
||||||
|
* an access token credential at the Authorization Server's Token Endpoint.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.2
|
||||||
|
* @see ReactiveOAuth2AccessTokenResponseClient
|
||||||
|
* @see OAuth2PasswordGrantRequest
|
||||||
|
* @see OAuth2AccessTokenResponse
|
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.3.2">Section 4.3.2 Access Token Request (Resource Owner Password Credentials Grant)</a>
|
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.3.3">Section 4.3.3 Access Token Response (Resource Owner Password Credentials Grant)</a>
|
||||||
|
*/
|
||||||
|
public final class WebClientReactivePasswordTokenResponseClient implements ReactiveOAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> {
|
||||||
|
private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response";
|
||||||
|
private WebClient webClient = WebClient.builder().build();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<OAuth2AccessTokenResponse> getTokenResponse(OAuth2PasswordGrantRequest passwordGrantRequest) {
|
||||||
|
Assert.notNull(passwordGrantRequest, "passwordGrantRequest cannot be null");
|
||||||
|
return Mono.defer(() -> {
|
||||||
|
ClientRegistration clientRegistration = passwordGrantRequest.getClientRegistration();
|
||||||
|
return this.webClient.post()
|
||||||
|
.uri(clientRegistration.getProviderDetails().getTokenUri())
|
||||||
|
.headers(tokenRequestHeaders(clientRegistration))
|
||||||
|
.body(tokenRequestBody(passwordGrantRequest))
|
||||||
|
.exchange()
|
||||||
|
.flatMap(response -> {
|
||||||
|
HttpStatus status = HttpStatus.resolve(response.rawStatusCode());
|
||||||
|
if (status == null || !status.is2xxSuccessful()) {
|
||||||
|
OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE,
|
||||||
|
"An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: " +
|
||||||
|
"HTTP Status Code " + response.rawStatusCode(), null);
|
||||||
|
return response
|
||||||
|
.bodyToMono(DataBuffer.class)
|
||||||
|
.map(DataBufferUtils::release)
|
||||||
|
.then(Mono.error(new OAuth2AuthorizationException(oauth2Error)));
|
||||||
|
}
|
||||||
|
return response.body(oauth2AccessTokenResponse());
|
||||||
|
})
|
||||||
|
.map(tokenResponse -> {
|
||||||
|
if (CollectionUtils.isEmpty(tokenResponse.getAccessToken().getScopes())) {
|
||||||
|
// As per spec, in Section 5.1 Successful Access Token Response
|
||||||
|
// https://tools.ietf.org/html/rfc6749#section-5.1
|
||||||
|
// If AccessTokenResponse.scope is empty, then default to the scope
|
||||||
|
// originally requested by the client in the Token Request
|
||||||
|
tokenResponse = OAuth2AccessTokenResponse.withResponse(tokenResponse)
|
||||||
|
.scopes(passwordGrantRequest.getClientRegistration().getScopes())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
return tokenResponse;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Consumer<HttpHeaders> tokenRequestHeaders(ClientRegistration clientRegistration) {
|
||||||
|
return headers -> {
|
||||||
|
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||||
|
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
|
||||||
|
if (ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
|
||||||
|
headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BodyInserters.FormInserter<String> tokenRequestBody(OAuth2PasswordGrantRequest passwordGrantRequest) {
|
||||||
|
ClientRegistration clientRegistration = passwordGrantRequest.getClientRegistration();
|
||||||
|
BodyInserters.FormInserter<String> body = BodyInserters.fromFormData(
|
||||||
|
OAuth2ParameterNames.GRANT_TYPE, passwordGrantRequest.getGrantType().getValue());
|
||||||
|
body.with(OAuth2ParameterNames.USERNAME, passwordGrantRequest.getUsername());
|
||||||
|
body.with(OAuth2ParameterNames.PASSWORD, passwordGrantRequest.getPassword());
|
||||||
|
if (!CollectionUtils.isEmpty(passwordGrantRequest.getClientRegistration().getScopes())) {
|
||||||
|
body.with(OAuth2ParameterNames.SCOPE,
|
||||||
|
StringUtils.collectionToDelimitedString(passwordGrantRequest.getClientRegistration().getScopes(), " "));
|
||||||
|
}
|
||||||
|
if (ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod())) {
|
||||||
|
body.with(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());
|
||||||
|
body.with(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret());
|
||||||
|
}
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link WebClient} used when requesting the OAuth 2.0 Access Token Response.
|
||||||
|
*
|
||||||
|
* @param webClient the {@link WebClient} used when requesting the Access Token Response
|
||||||
|
*/
|
||||||
|
public void setWebClient(WebClient webClient) {
|
||||||
|
Assert.notNull(webClient, "webClient cannot be null");
|
||||||
|
this.webClient = webClient;
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,6 +15,13 @@
|
||||||
*/
|
*/
|
||||||
package org.springframework.security.oauth2.client.registration;
|
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.io.Serializable;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
@ -24,13 +31,6 @@ import java.util.LinkedHashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
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.
|
* 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");
|
Assert.notNull(this.authorizationGrantType, "authorizationGrantType cannot be null");
|
||||||
if (AuthorizationGrantType.CLIENT_CREDENTIALS.equals(this.authorizationGrantType)) {
|
if (AuthorizationGrantType.CLIENT_CREDENTIALS.equals(this.authorizationGrantType)) {
|
||||||
this.validateClientCredentialsGrantType();
|
this.validateClientCredentialsGrantType();
|
||||||
|
} else if (AuthorizationGrantType.PASSWORD.equals(this.authorizationGrantType)) {
|
||||||
|
this.validatePasswordGrantType();
|
||||||
} else if (AuthorizationGrantType.IMPLICIT.equals(this.authorizationGrantType)) {
|
} else if (AuthorizationGrantType.IMPLICIT.equals(this.authorizationGrantType)) {
|
||||||
this.validateImplicitGrantType();
|
this.validateImplicitGrantType();
|
||||||
} else if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(this.authorizationGrantType)) {
|
} 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");
|
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() {
|
private void validateScopes() {
|
||||||
if (this.scopes == null) {
|
if (this.scopes == null) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -28,7 +28,6 @@ import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
@ -134,13 +133,19 @@ public final class DefaultOAuth2AuthorizedClientManager implements OAuth2Authori
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, Object> apply(OAuth2AuthorizeRequest authorizeRequest) {
|
public Map<String, Object> apply(OAuth2AuthorizeRequest authorizeRequest) {
|
||||||
Map<String, Object> contextAttributes = Collections.emptyMap();
|
Map<String, Object> contextAttributes = new HashMap<>();
|
||||||
String scope = authorizeRequest.getServletRequest().getParameter(OAuth2ParameterNames.SCOPE);
|
String scope = authorizeRequest.getServletRequest().getParameter(OAuth2ParameterNames.SCOPE);
|
||||||
if (StringUtils.hasText(scope)) {
|
if (StringUtils.hasText(scope)) {
|
||||||
contextAttributes = new HashMap<>();
|
|
||||||
contextAttributes.put(OAuth2AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME,
|
contextAttributes.put(OAuth2AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME,
|
||||||
StringUtils.delimitedListToStringArray(scope, " "));
|
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;
|
return contextAttributes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -105,6 +105,7 @@ public final class OAuth2AuthorizedClientArgumentResolver implements HandlerMeth
|
||||||
.authorizationCode()
|
.authorizationCode()
|
||||||
.refreshToken()
|
.refreshToken()
|
||||||
.clientCredentials()
|
.clientCredentials()
|
||||||
|
.password()
|
||||||
.build();
|
.build();
|
||||||
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
|
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
|
||||||
clientRegistrationRepository, authorizedClientRepository);
|
clientRegistrationRepository, authorizedClientRepository);
|
||||||
|
@ -193,6 +194,7 @@ public final class OAuth2AuthorizedClientArgumentResolver implements HandlerMeth
|
||||||
.authorizationCode()
|
.authorizationCode()
|
||||||
.refreshToken()
|
.refreshToken()
|
||||||
.clientCredentials(configurer -> configurer.accessTokenResponseClient(clientCredentialsTokenResponseClient))
|
.clientCredentials(configurer -> configurer.accessTokenResponseClient(clientCredentialsTokenResponseClient))
|
||||||
|
.password()
|
||||||
.build();
|
.build();
|
||||||
((DefaultOAuth2AuthorizedClientManager) this.authorizedClientManager).setAuthorizedClientProvider(authorizedClientProvider);
|
((DefaultOAuth2AuthorizedClientManager) this.authorizedClientManager).setAuthorizedClientProvider(authorizedClientProvider);
|
||||||
}
|
}
|
||||||
|
|
|
@ -122,6 +122,7 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
|
||||||
.authorizationCode()
|
.authorizationCode()
|
||||||
.refreshToken()
|
.refreshToken()
|
||||||
.clientCredentials()
|
.clientCredentials()
|
||||||
|
.password()
|
||||||
.build();
|
.build();
|
||||||
DefaultServerOAuth2AuthorizedClientManager authorizedClientManager = new DefaultServerOAuth2AuthorizedClientManager(
|
DefaultServerOAuth2AuthorizedClientManager authorizedClientManager = new DefaultServerOAuth2AuthorizedClientManager(
|
||||||
clientRegistrationRepository, authorizedClientRepository);
|
clientRegistrationRepository, authorizedClientRepository);
|
||||||
|
@ -263,6 +264,7 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
|
||||||
.authorizationCode()
|
.authorizationCode()
|
||||||
.refreshToken(configurer -> configurer.clockSkew(this.accessTokenExpiresSkew))
|
.refreshToken(configurer -> configurer.clockSkew(this.accessTokenExpiresSkew))
|
||||||
.clientCredentials(this::updateClientCredentialsProvider)
|
.clientCredentials(this::updateClientCredentialsProvider)
|
||||||
|
.password(configurer -> configurer.clockSkew(this.accessTokenExpiresSkew))
|
||||||
.build();
|
.build();
|
||||||
((DefaultServerOAuth2AuthorizedClientManager) this.authorizedClientManager).setAuthorizedClientProvider(authorizedClientProvider);
|
((DefaultServerOAuth2AuthorizedClientManager) this.authorizedClientManager).setAuthorizedClientProvider(authorizedClientProvider);
|
||||||
}
|
}
|
||||||
|
|
|
@ -166,6 +166,7 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction
|
||||||
.authorizationCode()
|
.authorizationCode()
|
||||||
.refreshToken()
|
.refreshToken()
|
||||||
.clientCredentials()
|
.clientCredentials()
|
||||||
|
.password()
|
||||||
.build();
|
.build();
|
||||||
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
|
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
|
||||||
clientRegistrationRepository, authorizedClientRepository);
|
clientRegistrationRepository, authorizedClientRepository);
|
||||||
|
@ -210,6 +211,7 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction
|
||||||
.authorizationCode()
|
.authorizationCode()
|
||||||
.refreshToken(configurer -> configurer.clockSkew(this.accessTokenExpiresSkew))
|
.refreshToken(configurer -> configurer.clockSkew(this.accessTokenExpiresSkew))
|
||||||
.clientCredentials(this::updateClientCredentialsProvider)
|
.clientCredentials(this::updateClientCredentialsProvider)
|
||||||
|
.password(configurer -> configurer.clockSkew(this.accessTokenExpiresSkew))
|
||||||
.build();
|
.build();
|
||||||
((DefaultOAuth2AuthorizedClientManager) this.authorizedClientManager).setAuthorizedClientProvider(authorizedClientProvider);
|
((DefaultOAuth2AuthorizedClientManager) this.authorizedClientManager).setAuthorizedClientProvider(authorizedClientProvider);
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,6 +98,7 @@ public final class OAuth2AuthorizedClientArgumentResolver implements HandlerMeth
|
||||||
.authorizationCode()
|
.authorizationCode()
|
||||||
.refreshToken()
|
.refreshToken()
|
||||||
.clientCredentials()
|
.clientCredentials()
|
||||||
|
.password()
|
||||||
.build();
|
.build();
|
||||||
DefaultServerOAuth2AuthorizedClientManager authorizedClientManager = new DefaultServerOAuth2AuthorizedClientManager(
|
DefaultServerOAuth2AuthorizedClientManager authorizedClientManager = new DefaultServerOAuth2AuthorizedClientManager(
|
||||||
clientRegistrationRepository, authorizedClientRepository);
|
clientRegistrationRepository, authorizedClientRepository);
|
||||||
|
|
|
@ -19,6 +19,7 @@ import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.oauth2.client.OAuth2AuthorizationContext;
|
import org.springframework.security.oauth2.client.OAuth2AuthorizationContext;
|
||||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
||||||
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProvider;
|
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.client.registration.ReactiveClientRegistrationRepository;
|
||||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
@ -26,7 +27,6 @@ import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
@ -43,7 +43,7 @@ public final class DefaultServerOAuth2AuthorizedClientManager implements ServerO
|
||||||
private final ReactiveClientRegistrationRepository clientRegistrationRepository;
|
private final ReactiveClientRegistrationRepository clientRegistrationRepository;
|
||||||
private final ServerOAuth2AuthorizedClientRepository authorizedClientRepository;
|
private final ServerOAuth2AuthorizedClientRepository authorizedClientRepository;
|
||||||
private ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = context -> Mono.empty();
|
private ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = context -> Mono.empty();
|
||||||
private Function<ServerOAuth2AuthorizeRequest, Map<String, Object>> contextAttributesMapper = new DefaultContextAttributesMapper();
|
private Function<ServerOAuth2AuthorizeRequest, Mono<Map<String, Object>>> contextAttributesMapper = new DefaultContextAttributesMapper();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a {@code DefaultServerOAuth2AuthorizedClientManager} using the provided parameters.
|
* Constructs a {@code DefaultServerOAuth2AuthorizedClientManager} using the provided parameters.
|
||||||
|
@ -72,28 +72,21 @@ public final class DefaultServerOAuth2AuthorizedClientManager implements ServerO
|
||||||
this.authorizedClientRepository.loadAuthorizedClient(clientRegistrationId, principal, serverWebExchange)))
|
this.authorizedClientRepository.loadAuthorizedClient(clientRegistrationId, principal, serverWebExchange)))
|
||||||
.flatMap(authorizedClient -> {
|
.flatMap(authorizedClient -> {
|
||||||
// Re-authorize
|
// Re-authorize
|
||||||
OAuth2AuthorizationContext reauthorizationContext =
|
return authorizationContext(authorizeRequest, authorizedClient)
|
||||||
OAuth2AuthorizationContext.withAuthorizedClient(authorizedClient)
|
|
||||||
.principal(principal)
|
|
||||||
.attributes(this.contextAttributesMapper.apply(authorizeRequest))
|
|
||||||
.build();
|
|
||||||
return Mono.just(reauthorizationContext)
|
|
||||||
.flatMap(this.authorizedClientProvider::authorize)
|
.flatMap(this.authorizedClientProvider::authorize)
|
||||||
.doOnNext(reauthorizedClient ->
|
.doOnNext(reauthorizedClient ->
|
||||||
this.authorizedClientRepository.saveAuthorizedClient(
|
this.authorizedClientRepository.saveAuthorizedClient(
|
||||||
reauthorizedClient, principal, serverWebExchange))
|
reauthorizedClient, principal, serverWebExchange))
|
||||||
// Return the `authorizedClient` if `reauthorizedClient` is null, e.g. re-authorization is not supported
|
// Default to the existing authorizedClient if the client was not re-authorized
|
||||||
.defaultIfEmpty(authorizedClient);
|
.defaultIfEmpty(authorizeRequest.getAuthorizedClient() != null ?
|
||||||
|
authorizeRequest.getAuthorizedClient() : authorizedClient);
|
||||||
})
|
})
|
||||||
.switchIfEmpty(Mono.defer(() ->
|
.switchIfEmpty(Mono.defer(() ->
|
||||||
// Authorize
|
// Authorize
|
||||||
this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId)
|
this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId)
|
||||||
.switchIfEmpty(Mono.error(() -> new IllegalArgumentException(
|
.switchIfEmpty(Mono.error(() -> new IllegalArgumentException(
|
||||||
"Could not find ClientRegistration with id '" + clientRegistrationId + "'")))
|
"Could not find ClientRegistration with id '" + clientRegistrationId + "'")))
|
||||||
.map(clientRegistration -> OAuth2AuthorizationContext.withClientRegistration(clientRegistration)
|
.flatMap(clientRegistration -> authorizationContext(authorizeRequest, clientRegistration))
|
||||||
.principal(principal)
|
|
||||||
.attributes(this.contextAttributesMapper.apply(authorizeRequest))
|
|
||||||
.build())
|
|
||||||
.flatMap(this.authorizedClientProvider::authorize)
|
.flatMap(this.authorizedClientProvider::authorize)
|
||||||
.doOnNext(authorizedClient ->
|
.doOnNext(authorizedClient ->
|
||||||
this.authorizedClientRepository.saveAuthorizedClient(
|
this.authorizedClientRepository.saveAuthorizedClient(
|
||||||
|
@ -101,6 +94,26 @@ public final class DefaultServerOAuth2AuthorizedClientManager implements ServerO
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Mono<OAuth2AuthorizationContext> authorizationContext(ServerOAuth2AuthorizeRequest authorizeRequest,
|
||||||
|
OAuth2AuthorizedClient authorizedClient) {
|
||||||
|
return Mono.just(authorizeRequest)
|
||||||
|
.flatMap(this.contextAttributesMapper::apply)
|
||||||
|
.map(attrs -> OAuth2AuthorizationContext.withAuthorizedClient(authorizedClient)
|
||||||
|
.principal(authorizeRequest.getPrincipal())
|
||||||
|
.attributes(attrs)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<OAuth2AuthorizationContext> authorizationContext(ServerOAuth2AuthorizeRequest authorizeRequest,
|
||||||
|
ClientRegistration clientRegistration) {
|
||||||
|
return Mono.just(authorizeRequest)
|
||||||
|
.flatMap(this.contextAttributesMapper::apply)
|
||||||
|
.map(attrs -> OAuth2AuthorizationContext.withClientRegistration(clientRegistration)
|
||||||
|
.principal(authorizeRequest.getPrincipal())
|
||||||
|
.attributes(attrs)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the {@link ReactiveOAuth2AuthorizedClientProvider} used for authorizing (or re-authorizing) an OAuth 2.0 Client.
|
* 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
|
* @param contextAttributesMapper the {@code Function} used for supplying the {@code Map} of attributes
|
||||||
* to the {@link OAuth2AuthorizationContext#getAttributes() authorization context}
|
* to the {@link OAuth2AuthorizationContext#getAttributes() authorization context}
|
||||||
*/
|
*/
|
||||||
public void setContextAttributesMapper(Function<ServerOAuth2AuthorizeRequest, Map<String, Object>> contextAttributesMapper) {
|
public void setContextAttributesMapper(Function<ServerOAuth2AuthorizeRequest, Mono<Map<String, Object>>> contextAttributesMapper) {
|
||||||
Assert.notNull(contextAttributesMapper, "contextAttributesMapper cannot be null");
|
Assert.notNull(contextAttributesMapper, "contextAttributesMapper cannot be null");
|
||||||
this.contextAttributesMapper = contextAttributesMapper;
|
this.contextAttributesMapper = contextAttributesMapper;
|
||||||
}
|
}
|
||||||
|
@ -126,18 +139,17 @@ public final class DefaultServerOAuth2AuthorizedClientManager implements ServerO
|
||||||
/**
|
/**
|
||||||
* The default implementation of the {@link #setContextAttributesMapper(Function) contextAttributesMapper}.
|
* The default implementation of the {@link #setContextAttributesMapper(Function) contextAttributesMapper}.
|
||||||
*/
|
*/
|
||||||
public static class DefaultContextAttributesMapper implements Function<ServerOAuth2AuthorizeRequest, Map<String, Object>> {
|
public static class DefaultContextAttributesMapper implements Function<ServerOAuth2AuthorizeRequest, Mono<Map<String, Object>>> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, Object> apply(ServerOAuth2AuthorizeRequest authorizeRequest) {
|
public Mono<Map<String, Object>> apply(ServerOAuth2AuthorizeRequest authorizeRequest) {
|
||||||
Map<String, Object> contextAttributes = Collections.emptyMap();
|
Map<String, Object> contextAttributes = new HashMap<>();
|
||||||
String scope = authorizeRequest.getServerWebExchange().getRequest().getQueryParams().getFirst(OAuth2ParameterNames.SCOPE);
|
String scope = authorizeRequest.getServerWebExchange().getRequest().getQueryParams().getFirst(OAuth2ParameterNames.SCOPE);
|
||||||
if (StringUtils.hasText(scope)) {
|
if (StringUtils.hasText(scope)) {
|
||||||
contextAttributes = new HashMap<>();
|
|
||||||
contextAttributes.put(OAuth2AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME,
|
contextAttributes.put(OAuth2AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME,
|
||||||
StringUtils.delimitedListToStringArray(scope, " "));
|
StringUtils.delimitedListToStringArray(scope, " "));
|
||||||
}
|
}
|
||||||
return contextAttributes;
|
return Mono.just(contextAttributes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.oauth2.client.endpoint.DefaultClientCredentialsTokenResponseClient;
|
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.endpoint.DefaultRefreshTokenTokenResponseClient;
|
||||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
||||||
|
@ -48,6 +49,7 @@ public class OAuth2AuthorizedClientProviderBuilderTests {
|
||||||
private RestOperations accessTokenClient;
|
private RestOperations accessTokenClient;
|
||||||
private DefaultClientCredentialsTokenResponseClient clientCredentialsTokenResponseClient;
|
private DefaultClientCredentialsTokenResponseClient clientCredentialsTokenResponseClient;
|
||||||
private DefaultRefreshTokenTokenResponseClient refreshTokenTokenResponseClient;
|
private DefaultRefreshTokenTokenResponseClient refreshTokenTokenResponseClient;
|
||||||
|
private DefaultPasswordTokenResponseClient passwordTokenResponseClient;
|
||||||
private Authentication principal;
|
private Authentication principal;
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
|
@ -61,6 +63,8 @@ public class OAuth2AuthorizedClientProviderBuilderTests {
|
||||||
this.refreshTokenTokenResponseClient.setRestOperations(this.accessTokenClient);
|
this.refreshTokenTokenResponseClient.setRestOperations(this.accessTokenClient);
|
||||||
this.clientCredentialsTokenResponseClient = new DefaultClientCredentialsTokenResponseClient();
|
this.clientCredentialsTokenResponseClient = new DefaultClientCredentialsTokenResponseClient();
|
||||||
this.clientCredentialsTokenResponseClient.setRestOperations(this.accessTokenClient);
|
this.clientCredentialsTokenResponseClient.setRestOperations(this.accessTokenClient);
|
||||||
|
this.passwordTokenResponseClient = new DefaultPasswordTokenResponseClient();
|
||||||
|
this.passwordTokenResponseClient.setRestOperations(this.accessTokenClient);
|
||||||
this.principal = new TestingAuthenticationToken("principal", "password");
|
this.principal = new TestingAuthenticationToken("principal", "password");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,6 +129,25 @@ public class OAuth2AuthorizedClientProviderBuilderTests {
|
||||||
verify(this.accessTokenClient).exchange(any(RequestEntity.class), eq(OAuth2AccessTokenResponse.class));
|
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
|
@Test
|
||||||
public void buildWhenAllProvidersThenProvidersAuthorize() {
|
public void buildWhenAllProvidersThenProvidersAuthorize() {
|
||||||
OAuth2AuthorizedClientProvider authorizedClientProvider =
|
OAuth2AuthorizedClientProvider authorizedClientProvider =
|
||||||
|
@ -132,6 +155,7 @@ public class OAuth2AuthorizedClientProviderBuilderTests {
|
||||||
.authorizationCode()
|
.authorizationCode()
|
||||||
.refreshToken(configurer -> configurer.accessTokenResponseClient(this.refreshTokenTokenResponseClient))
|
.refreshToken(configurer -> configurer.accessTokenResponseClient(this.refreshTokenTokenResponseClient))
|
||||||
.clientCredentials(configurer -> configurer.accessTokenResponseClient(this.clientCredentialsTokenResponseClient))
|
.clientCredentials(configurer -> configurer.accessTokenResponseClient(this.clientCredentialsTokenResponseClient))
|
||||||
|
.password(configurer -> configurer.accessTokenResponseClient(this.passwordTokenResponseClient))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
|
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
|
||||||
|
@ -172,6 +196,18 @@ public class OAuth2AuthorizedClientProviderBuilderTests {
|
||||||
|
|
||||||
assertThat(authorizedClient).isNotNull();
|
assertThat(authorizedClient).isNotNull();
|
||||||
verify(this.accessTokenClient, times(2)).exchange(any(RequestEntity.class), eq(OAuth2AccessTokenResponse.class));
|
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
|
@Test
|
||||||
|
|
|
@ -0,0 +1,190 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2019 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.client;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
|
||||||
|
import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
|
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||||
|
import org.springframework.security.oauth2.core.TestOAuth2RefreshTokens;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link PasswordOAuth2AuthorizedClientProvider}.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
*/
|
||||||
|
public class PasswordOAuth2AuthorizedClientProviderTests {
|
||||||
|
private PasswordOAuth2AuthorizedClientProvider authorizedClientProvider;
|
||||||
|
private OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient;
|
||||||
|
private ClientRegistration clientRegistration;
|
||||||
|
private Authentication principal;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setup() {
|
||||||
|
this.authorizedClientProvider = new PasswordOAuth2AuthorizedClientProvider();
|
||||||
|
this.accessTokenResponseClient = mock(OAuth2AccessTokenResponseClient.class);
|
||||||
|
this.authorizedClientProvider.setAccessTokenResponseClient(this.accessTokenResponseClient);
|
||||||
|
this.clientRegistration = TestClientRegistrations.password().build();
|
||||||
|
this.principal = new TestingAuthenticationToken("principal", "password");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void setAccessTokenResponseClientWhenClientIsNullThenThrowIllegalArgumentException() {
|
||||||
|
assertThatThrownBy(() -> this.authorizedClientProvider.setAccessTokenResponseClient(null))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("accessTokenResponseClient cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void setClockSkewWhenNullThenThrowIllegalArgumentException() {
|
||||||
|
assertThatThrownBy(() -> this.authorizedClientProvider.setClockSkew(null))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("clockSkew cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void setClockSkewWhenNegativeSecondsThenThrowIllegalArgumentException() {
|
||||||
|
assertThatThrownBy(() -> this.authorizedClientProvider.setClockSkew(Duration.ofSeconds(-1)))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("clockSkew must be >= 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void setClockWhenNullThenThrowIllegalArgumentException() {
|
||||||
|
assertThatThrownBy(() -> this.authorizedClientProvider.setClock(null))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("clock cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authorizeWhenContextIsNullThenThrowIllegalArgumentException() {
|
||||||
|
assertThatThrownBy(() -> this.authorizedClientProvider.authorize(null))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("context cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authorizeWhenNotPasswordThenUnableToAuthorize() {
|
||||||
|
ClientRegistration clientRegistration = TestClientRegistrations.clientCredentials().build();
|
||||||
|
|
||||||
|
OAuth2AuthorizationContext authorizationContext =
|
||||||
|
OAuth2AuthorizationContext.withClientRegistration(clientRegistration)
|
||||||
|
.principal(this.principal)
|
||||||
|
.build();
|
||||||
|
assertThat(this.authorizedClientProvider.authorize(authorizationContext)).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authorizeWhenPasswordAndNotAuthorizedAndEmptyUsernameThenUnableToAuthorize() {
|
||||||
|
OAuth2AuthorizationContext authorizationContext =
|
||||||
|
OAuth2AuthorizationContext.withClientRegistration(this.clientRegistration)
|
||||||
|
.principal(this.principal)
|
||||||
|
.attribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, null)
|
||||||
|
.attribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, "password")
|
||||||
|
.build();
|
||||||
|
assertThat(this.authorizedClientProvider.authorize(authorizationContext)).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authorizeWhenPasswordAndNotAuthorizedAndEmptyPasswordThenUnableToAuthorize() {
|
||||||
|
OAuth2AuthorizationContext authorizationContext =
|
||||||
|
OAuth2AuthorizationContext.withClientRegistration(this.clientRegistration)
|
||||||
|
.principal(this.principal)
|
||||||
|
.attribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, "username")
|
||||||
|
.attribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, null)
|
||||||
|
.build();
|
||||||
|
assertThat(this.authorizedClientProvider.authorize(authorizationContext)).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authorizeWhenPasswordAndNotAuthorizedThenAuthorize() {
|
||||||
|
OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build();
|
||||||
|
when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse);
|
||||||
|
|
||||||
|
OAuth2AuthorizationContext authorizationContext =
|
||||||
|
OAuth2AuthorizationContext.withClientRegistration(this.clientRegistration)
|
||||||
|
.principal(this.principal)
|
||||||
|
.attribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, "username")
|
||||||
|
.attribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, "password")
|
||||||
|
.build();
|
||||||
|
OAuth2AuthorizedClient authorizedClient = this.authorizedClientProvider.authorize(authorizationContext);
|
||||||
|
|
||||||
|
assertThat(authorizedClient.getClientRegistration()).isSameAs(this.clientRegistration);
|
||||||
|
assertThat(authorizedClient.getPrincipalName()).isEqualTo(this.principal.getName());
|
||||||
|
assertThat(authorizedClient.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authorizeWhenPasswordAndAuthorizedWithoutRefreshTokenAndTokenExpiredThenReauthorize() {
|
||||||
|
Instant issuedAt = Instant.now().minus(Duration.ofDays(1));
|
||||||
|
Instant expiresAt = issuedAt.plus(Duration.ofMinutes(60));
|
||||||
|
OAuth2AccessToken accessToken = new OAuth2AccessToken(
|
||||||
|
OAuth2AccessToken.TokenType.BEARER, "access-token-expired", issuedAt, expiresAt);
|
||||||
|
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(
|
||||||
|
this.clientRegistration, this.principal.getName(), accessToken); // without refresh token
|
||||||
|
|
||||||
|
OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build();
|
||||||
|
when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse);
|
||||||
|
|
||||||
|
OAuth2AuthorizationContext authorizationContext =
|
||||||
|
OAuth2AuthorizationContext.withAuthorizedClient(authorizedClient)
|
||||||
|
.attribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, "username")
|
||||||
|
.attribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, "password")
|
||||||
|
.principal(this.principal)
|
||||||
|
.build();
|
||||||
|
authorizedClient = this.authorizedClientProvider.authorize(authorizationContext);
|
||||||
|
|
||||||
|
assertThat(authorizedClient.getClientRegistration()).isSameAs(this.clientRegistration);
|
||||||
|
assertThat(authorizedClient.getPrincipalName()).isEqualTo(this.principal.getName());
|
||||||
|
assertThat(authorizedClient.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authorizeWhenPasswordAndAuthorizedWithRefreshTokenAndTokenExpiredThenNotReauthorize() {
|
||||||
|
Instant issuedAt = Instant.now().minus(Duration.ofDays(1));
|
||||||
|
Instant expiresAt = issuedAt.plus(Duration.ofMinutes(60));
|
||||||
|
OAuth2AccessToken accessToken = new OAuth2AccessToken(
|
||||||
|
OAuth2AccessToken.TokenType.BEARER, "access-token-expired", issuedAt, expiresAt);
|
||||||
|
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(
|
||||||
|
this.clientRegistration, this.principal.getName(),
|
||||||
|
accessToken, TestOAuth2RefreshTokens.refreshToken()); // with refresh token
|
||||||
|
|
||||||
|
OAuth2AuthorizationContext authorizationContext =
|
||||||
|
OAuth2AuthorizationContext.withAuthorizedClient(authorizedClient)
|
||||||
|
.attribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, "username")
|
||||||
|
.attribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, "password")
|
||||||
|
.principal(this.principal)
|
||||||
|
.build();
|
||||||
|
assertThat(this.authorizedClientProvider.authorize(authorizationContext)).isNull();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,191 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2019 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.client;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
|
||||||
|
import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
|
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||||
|
import org.springframework.security.oauth2.core.TestOAuth2RefreshTokens;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link PasswordReactiveOAuth2AuthorizedClientProvider}.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
*/
|
||||||
|
public class PasswordReactiveOAuth2AuthorizedClientProviderTests {
|
||||||
|
private PasswordReactiveOAuth2AuthorizedClientProvider authorizedClientProvider;
|
||||||
|
private ReactiveOAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient;
|
||||||
|
private ClientRegistration clientRegistration;
|
||||||
|
private Authentication principal;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setup() {
|
||||||
|
this.authorizedClientProvider = new PasswordReactiveOAuth2AuthorizedClientProvider();
|
||||||
|
this.accessTokenResponseClient = mock(ReactiveOAuth2AccessTokenResponseClient.class);
|
||||||
|
this.authorizedClientProvider.setAccessTokenResponseClient(this.accessTokenResponseClient);
|
||||||
|
this.clientRegistration = TestClientRegistrations.password().build();
|
||||||
|
this.principal = new TestingAuthenticationToken("principal", "password");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void setAccessTokenResponseClientWhenClientIsNullThenThrowIllegalArgumentException() {
|
||||||
|
assertThatThrownBy(() -> this.authorizedClientProvider.setAccessTokenResponseClient(null))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("accessTokenResponseClient cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void setClockSkewWhenNullThenThrowIllegalArgumentException() {
|
||||||
|
assertThatThrownBy(() -> this.authorizedClientProvider.setClockSkew(null))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("clockSkew cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void setClockSkewWhenNegativeSecondsThenThrowIllegalArgumentException() {
|
||||||
|
assertThatThrownBy(() -> this.authorizedClientProvider.setClockSkew(Duration.ofSeconds(-1)))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("clockSkew must be >= 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void setClockWhenNullThenThrowIllegalArgumentException() {
|
||||||
|
assertThatThrownBy(() -> this.authorizedClientProvider.setClock(null))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("clock cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authorizeWhenContextIsNullThenThrowIllegalArgumentException() {
|
||||||
|
assertThatThrownBy(() -> this.authorizedClientProvider.authorize(null).block())
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("context cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authorizeWhenNotPasswordThenUnableToAuthorize() {
|
||||||
|
ClientRegistration clientRegistration = TestClientRegistrations.clientCredentials().build();
|
||||||
|
|
||||||
|
OAuth2AuthorizationContext authorizationContext =
|
||||||
|
OAuth2AuthorizationContext.withClientRegistration(clientRegistration)
|
||||||
|
.principal(this.principal)
|
||||||
|
.build();
|
||||||
|
assertThat(this.authorizedClientProvider.authorize(authorizationContext).block()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authorizeWhenPasswordAndNotAuthorizedAndEmptyUsernameThenUnableToAuthorize() {
|
||||||
|
OAuth2AuthorizationContext authorizationContext =
|
||||||
|
OAuth2AuthorizationContext.withClientRegistration(this.clientRegistration)
|
||||||
|
.principal(this.principal)
|
||||||
|
.attribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, null)
|
||||||
|
.attribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, "password")
|
||||||
|
.build();
|
||||||
|
assertThat(this.authorizedClientProvider.authorize(authorizationContext).block()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authorizeWhenPasswordAndNotAuthorizedAndEmptyPasswordThenUnableToAuthorize() {
|
||||||
|
OAuth2AuthorizationContext authorizationContext =
|
||||||
|
OAuth2AuthorizationContext.withClientRegistration(this.clientRegistration)
|
||||||
|
.principal(this.principal)
|
||||||
|
.attribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, "username")
|
||||||
|
.attribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, null)
|
||||||
|
.build();
|
||||||
|
assertThat(this.authorizedClientProvider.authorize(authorizationContext).block()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authorizeWhenPasswordAndNotAuthorizedThenAuthorize() {
|
||||||
|
OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build();
|
||||||
|
when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse));
|
||||||
|
|
||||||
|
OAuth2AuthorizationContext authorizationContext =
|
||||||
|
OAuth2AuthorizationContext.withClientRegistration(this.clientRegistration)
|
||||||
|
.principal(this.principal)
|
||||||
|
.attribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, "username")
|
||||||
|
.attribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, "password")
|
||||||
|
.build();
|
||||||
|
OAuth2AuthorizedClient authorizedClient = this.authorizedClientProvider.authorize(authorizationContext).block();
|
||||||
|
|
||||||
|
assertThat(authorizedClient.getClientRegistration()).isSameAs(this.clientRegistration);
|
||||||
|
assertThat(authorizedClient.getPrincipalName()).isEqualTo(this.principal.getName());
|
||||||
|
assertThat(authorizedClient.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authorizeWhenPasswordAndAuthorizedWithoutRefreshTokenAndTokenExpiredThenReauthorize() {
|
||||||
|
Instant issuedAt = Instant.now().minus(Duration.ofDays(1));
|
||||||
|
Instant expiresAt = issuedAt.plus(Duration.ofMinutes(60));
|
||||||
|
OAuth2AccessToken accessToken = new OAuth2AccessToken(
|
||||||
|
OAuth2AccessToken.TokenType.BEARER, "access-token-expired", issuedAt, expiresAt);
|
||||||
|
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(
|
||||||
|
this.clientRegistration, this.principal.getName(), accessToken); // without refresh token
|
||||||
|
|
||||||
|
OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build();
|
||||||
|
when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse));
|
||||||
|
|
||||||
|
OAuth2AuthorizationContext authorizationContext =
|
||||||
|
OAuth2AuthorizationContext.withAuthorizedClient(authorizedClient)
|
||||||
|
.attribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, "username")
|
||||||
|
.attribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, "password")
|
||||||
|
.principal(this.principal)
|
||||||
|
.build();
|
||||||
|
authorizedClient = this.authorizedClientProvider.authorize(authorizationContext).block();
|
||||||
|
|
||||||
|
assertThat(authorizedClient.getClientRegistration()).isSameAs(this.clientRegistration);
|
||||||
|
assertThat(authorizedClient.getPrincipalName()).isEqualTo(this.principal.getName());
|
||||||
|
assertThat(authorizedClient.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authorizeWhenPasswordAndAuthorizedWithRefreshTokenAndTokenExpiredThenNotReauthorize() {
|
||||||
|
Instant issuedAt = Instant.now().minus(Duration.ofDays(1));
|
||||||
|
Instant expiresAt = issuedAt.plus(Duration.ofMinutes(60));
|
||||||
|
OAuth2AccessToken accessToken = new OAuth2AccessToken(
|
||||||
|
OAuth2AccessToken.TokenType.BEARER, "access-token-expired", issuedAt, expiresAt);
|
||||||
|
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(
|
||||||
|
this.clientRegistration, this.principal.getName(),
|
||||||
|
accessToken, TestOAuth2RefreshTokens.refreshToken()); // with refresh token
|
||||||
|
|
||||||
|
OAuth2AuthorizationContext authorizationContext =
|
||||||
|
OAuth2AuthorizationContext.withAuthorizedClient(authorizedClient)
|
||||||
|
.attribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, "username")
|
||||||
|
.attribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, "password")
|
||||||
|
.principal(this.principal)
|
||||||
|
.build();
|
||||||
|
assertThat(this.authorizedClientProvider.authorize(authorizationContext).block()).isNull();
|
||||||
|
}
|
||||||
|
}
|
|
@ -149,6 +149,37 @@ public class ReactiveOAuth2AuthorizedClientProviderBuilderTests {
|
||||||
assertThat(formParameters).contains("grant_type=client_credentials");
|
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
|
@Test
|
||||||
public void buildWhenAllProvidersThenProvidersAuthorize() throws Exception {
|
public void buildWhenAllProvidersThenProvidersAuthorize() throws Exception {
|
||||||
String accessTokenSuccessResponse = "{\n" +
|
String accessTokenSuccessResponse = "{\n" +
|
||||||
|
@ -158,12 +189,14 @@ public class ReactiveOAuth2AuthorizedClientProviderBuilderTests {
|
||||||
"}\n";
|
"}\n";
|
||||||
this.server.enqueue(jsonResponse(accessTokenSuccessResponse));
|
this.server.enqueue(jsonResponse(accessTokenSuccessResponse));
|
||||||
this.server.enqueue(jsonResponse(accessTokenSuccessResponse));
|
this.server.enqueue(jsonResponse(accessTokenSuccessResponse));
|
||||||
|
this.server.enqueue(jsonResponse(accessTokenSuccessResponse));
|
||||||
|
|
||||||
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
|
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
|
||||||
ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
|
ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
|
||||||
.authorizationCode()
|
.authorizationCode()
|
||||||
.refreshToken()
|
.refreshToken()
|
||||||
.clientCredentials()
|
.clientCredentials()
|
||||||
|
.password()
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// authorization_code
|
// authorization_code
|
||||||
|
@ -211,6 +244,23 @@ public class ReactiveOAuth2AuthorizedClientProviderBuilderTests {
|
||||||
recordedRequest = this.server.takeRequest();
|
recordedRequest = this.server.takeRequest();
|
||||||
formParameters = recordedRequest.getBody().readUtf8();
|
formParameters = recordedRequest.getBody().readUtf8();
|
||||||
assertThat(formParameters).contains("grant_type=client_credentials");
|
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
|
@Test
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2019 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.client.endpoint;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.RequestEntity;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
|
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
|
import org.springframework.util.MultiValueMap;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link OAuth2PasswordGrantRequestEntityConverter}.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
*/
|
||||||
|
public class OAuth2PasswordGrantRequestEntityConverterTests {
|
||||||
|
private OAuth2PasswordGrantRequestEntityConverter converter = new OAuth2PasswordGrantRequestEntityConverter();
|
||||||
|
private OAuth2PasswordGrantRequest passwordGrantRequest;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setup() {
|
||||||
|
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration()
|
||||||
|
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
|
||||||
|
.scope("read", "write")
|
||||||
|
.build();
|
||||||
|
this.passwordGrantRequest = new OAuth2PasswordGrantRequest(clientRegistration, "user1", "password");
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Test
|
||||||
|
public void convertWhenGrantRequestValidThenConverts() {
|
||||||
|
RequestEntity<?> requestEntity = this.converter.convert(this.passwordGrantRequest);
|
||||||
|
|
||||||
|
ClientRegistration clientRegistration = this.passwordGrantRequest.getClientRegistration();
|
||||||
|
|
||||||
|
assertThat(requestEntity.getMethod()).isEqualTo(HttpMethod.POST);
|
||||||
|
assertThat(requestEntity.getUrl().toASCIIString()).isEqualTo(
|
||||||
|
clientRegistration.getProviderDetails().getTokenUri());
|
||||||
|
|
||||||
|
HttpHeaders headers = requestEntity.getHeaders();
|
||||||
|
assertThat(headers.getAccept()).contains(MediaType.APPLICATION_JSON_UTF8);
|
||||||
|
assertThat(headers.getContentType()).isEqualTo(
|
||||||
|
MediaType.valueOf(APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8"));
|
||||||
|
assertThat(headers.getFirst(HttpHeaders.AUTHORIZATION)).startsWith("Basic ");
|
||||||
|
|
||||||
|
MultiValueMap<String, String> formParameters = (MultiValueMap<String, String>) requestEntity.getBody();
|
||||||
|
assertThat(formParameters.getFirst(OAuth2ParameterNames.GRANT_TYPE)).isEqualTo(
|
||||||
|
AuthorizationGrantType.PASSWORD.getValue());
|
||||||
|
assertThat(formParameters.getFirst(OAuth2ParameterNames.USERNAME)).isEqualTo("user1");
|
||||||
|
assertThat(formParameters.getFirst(OAuth2ParameterNames.PASSWORD)).isEqualTo("password");
|
||||||
|
assertThat(formParameters.getFirst(OAuth2ParameterNames.SCOPE)).isEqualTo("read write");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -590,6 +590,90 @@ public class ClientRegistrationTests {
|
||||||
).isInstanceOf(IllegalArgumentException.class);
|
).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
|
@Test
|
||||||
public void buildWhenCustomGrantAllAttributesProvidedThenAllAttributesAreSet() {
|
public void buildWhenCustomGrantAllAttributesProvidedThenAllAttributesAreSet() {
|
||||||
AuthorizationGrantType customGrantType = new AuthorizationGrantType("CUSTOM");
|
AuthorizationGrantType customGrantType = new AuthorizationGrantType("CUSTOM");
|
||||||
|
|
|
@ -61,4 +61,15 @@ public class TestClientRegistrations {
|
||||||
.clientId("client-id")
|
.clientId("client-id")
|
||||||
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS);
|
.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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -199,6 +199,33 @@ public class DefaultOAuth2AuthorizedClientManagerTests {
|
||||||
eq(reauthorizedClient), eq(this.principal), eq(this.request), eq(this.response));
|
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")
|
@SuppressWarnings("unchecked")
|
||||||
@Test
|
@Test
|
||||||
public void reauthorizeWhenUnsupportedProviderThenNotReauthorized() {
|
public void reauthorizeWhenUnsupportedProviderThenNotReauthorized() {
|
||||||
|
@ -245,9 +272,8 @@ public class DefaultOAuth2AuthorizedClientManagerTests {
|
||||||
eq(reauthorizedClient), eq(this.principal), eq(this.request), eq(this.response));
|
eq(reauthorizedClient), eq(this.principal), eq(this.request), eq(this.response));
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
@Test
|
@Test
|
||||||
public void reauthorizeWhenRequestScopeParameterThenMappedToContext() {
|
public void reauthorizeWhenRequestParameterScopeThenMappedToContext() {
|
||||||
OAuth2AuthorizedClient reauthorizedClient = new OAuth2AuthorizedClient(
|
OAuth2AuthorizedClient reauthorizedClient = new OAuth2AuthorizedClient(
|
||||||
this.clientRegistration, this.principal.getName(),
|
this.clientRegistration, this.principal.getName(),
|
||||||
TestOAuth2AccessTokens.noScopes(), TestOAuth2RefreshTokens.refreshToken());
|
TestOAuth2AccessTokens.noScopes(), TestOAuth2RefreshTokens.refreshToken());
|
||||||
|
@ -262,20 +288,12 @@ public class DefaultOAuth2AuthorizedClientManagerTests {
|
||||||
|
|
||||||
OAuth2AuthorizeRequest reauthorizeRequest = new OAuth2AuthorizeRequest(
|
OAuth2AuthorizeRequest reauthorizeRequest = new OAuth2AuthorizeRequest(
|
||||||
this.authorizedClient, this.principal, this.request, this.response);
|
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());
|
verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
|
||||||
|
|
||||||
OAuth2AuthorizationContext authorizationContext = this.authorizationContextCaptor.getValue();
|
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);
|
String[] requestScopeAttribute = authorizationContext.getAttribute(OAuth2AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME);
|
||||||
assertThat(requestScopeAttribute).contains("read", "write");
|
assertThat(requestScopeAttribute).contains("read", "write");
|
||||||
|
|
||||||
assertThat(authorizedClient).isSameAs(reauthorizedClient);
|
|
||||||
verify(this.authorizedClientRepository).saveAuthorizedClient(
|
|
||||||
eq(reauthorizedClient), eq(this.principal), eq(this.request), eq(this.response));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,20 +30,24 @@ import org.springframework.security.oauth2.client.ClientCredentialsOAuth2Authori
|
||||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
||||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider;
|
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider;
|
||||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder;
|
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.annotation.RegisteredOAuth2AuthorizedClient;
|
||||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||||
import org.springframework.security.oauth2.client.endpoint.DefaultClientCredentialsTokenResponseClient;
|
import org.springframework.security.oauth2.client.endpoint.DefaultClientCredentialsTokenResponseClient;
|
||||||
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
|
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
|
||||||
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
|
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.ClientRegistration;
|
||||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||||
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
|
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.DefaultOAuth2AuthorizedClientManager;
|
||||||
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
|
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
|
||||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
import org.springframework.util.ReflectionUtils;
|
import org.springframework.util.ReflectionUtils;
|
||||||
import org.springframework.web.context.request.ServletWebRequest;
|
import org.springframework.web.context.request.ServletWebRequest;
|
||||||
|
|
||||||
|
@ -65,6 +69,7 @@ public class OAuth2AuthorizedClientArgumentResolverTests {
|
||||||
private String principalName = "principal-1";
|
private String principalName = "principal-1";
|
||||||
private ClientRegistration registration1;
|
private ClientRegistration registration1;
|
||||||
private ClientRegistration registration2;
|
private ClientRegistration registration2;
|
||||||
|
private ClientRegistration registration3;
|
||||||
private ClientRegistrationRepository clientRegistrationRepository;
|
private ClientRegistrationRepository clientRegistrationRepository;
|
||||||
private OAuth2AuthorizedClient authorizedClient1;
|
private OAuth2AuthorizedClient authorizedClient1;
|
||||||
private OAuth2AuthorizedClient authorizedClient2;
|
private OAuth2AuthorizedClient authorizedClient2;
|
||||||
|
@ -101,7 +106,9 @@ public class OAuth2AuthorizedClientArgumentResolverTests {
|
||||||
.scope("read", "write")
|
.scope("read", "write")
|
||||||
.tokenUri("https://provider.com/oauth2/token")
|
.tokenUri("https://provider.com/oauth2/token")
|
||||||
.build();
|
.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);
|
this.authorizedClientRepository = mock(OAuth2AuthorizedClientRepository.class);
|
||||||
OAuth2AuthorizedClientProvider authorizedClientProvider =
|
OAuth2AuthorizedClientProvider authorizedClientProvider =
|
||||||
OAuth2AuthorizedClientProviderBuilder.builder()
|
OAuth2AuthorizedClientProviderBuilder.builder()
|
||||||
|
@ -267,6 +274,45 @@ public class OAuth2AuthorizedClientArgumentResolverTests {
|
||||||
eq(authorizedClient), eq(this.authentication), any(HttpServletRequest.class), any(HttpServletResponse.class));
|
eq(authorizedClient), eq(this.authentication), any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Test
|
||||||
|
public void resolveArgumentWhenAuthorizedClientNotFoundForPasswordClientThenResolvesFromTokenResponseClient() throws Exception {
|
||||||
|
OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> passwordTokenResponseClient =
|
||||||
|
mock(OAuth2AccessTokenResponseClient.class);
|
||||||
|
PasswordOAuth2AuthorizedClientProvider passwordAuthorizedClientProvider =
|
||||||
|
new PasswordOAuth2AuthorizedClientProvider();
|
||||||
|
passwordAuthorizedClientProvider.setAccessTokenResponseClient(passwordTokenResponseClient);
|
||||||
|
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
|
||||||
|
this.clientRegistrationRepository, this.authorizedClientRepository);
|
||||||
|
authorizedClientManager.setAuthorizedClientProvider(passwordAuthorizedClientProvider);
|
||||||
|
this.argumentResolver = new OAuth2AuthorizedClientArgumentResolver(authorizedClientManager);
|
||||||
|
|
||||||
|
OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse
|
||||||
|
.withToken("access-token-1234")
|
||||||
|
.tokenType(OAuth2AccessToken.TokenType.BEARER)
|
||||||
|
.expiresIn(3600)
|
||||||
|
.build();
|
||||||
|
when(passwordTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse);
|
||||||
|
|
||||||
|
when(this.authorizedClientRepository.loadAuthorizedClient(anyString(), any(), any(HttpServletRequest.class)))
|
||||||
|
.thenReturn(null);
|
||||||
|
MethodParameter methodParameter = this.getMethodParameter("passwordClient", OAuth2AuthorizedClient.class);
|
||||||
|
|
||||||
|
this.request.setParameter(OAuth2ParameterNames.USERNAME, "username");
|
||||||
|
this.request.setParameter(OAuth2ParameterNames.PASSWORD, "password");
|
||||||
|
|
||||||
|
OAuth2AuthorizedClient authorizedClient = (OAuth2AuthorizedClient) this.argumentResolver.resolveArgument(
|
||||||
|
methodParameter, null, new ServletWebRequest(this.request, this.response), null);
|
||||||
|
|
||||||
|
assertThat(authorizedClient).isNotNull();
|
||||||
|
assertThat(authorizedClient.getClientRegistration()).isSameAs(this.registration3);
|
||||||
|
assertThat(authorizedClient.getPrincipalName()).isEqualTo(this.principalName);
|
||||||
|
assertThat(authorizedClient.getAccessToken()).isSameAs(accessTokenResponse.getAccessToken());
|
||||||
|
|
||||||
|
verify(this.authorizedClientRepository).saveAuthorizedClient(
|
||||||
|
eq(authorizedClient), eq(this.authentication), any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||||
|
}
|
||||||
|
|
||||||
private MethodParameter getMethodParameter(String methodName, Class<?>... paramTypes) {
|
private MethodParameter getMethodParameter(String methodName, Class<?>... paramTypes) {
|
||||||
Method method = ReflectionUtils.findMethod(TestController.class, methodName, paramTypes);
|
Method method = ReflectionUtils.findMethod(TestController.class, methodName, paramTypes);
|
||||||
return new MethodParameter(method, 0);
|
return new MethodParameter(method, 0);
|
||||||
|
@ -293,5 +339,8 @@ public class OAuth2AuthorizedClientArgumentResolverTests {
|
||||||
|
|
||||||
void clientCredentialsClient(@RegisteredOAuth2AuthorizedClient("client2") OAuth2AuthorizedClient authorizedClient) {
|
void clientCredentialsClient(@RegisteredOAuth2AuthorizedClient("client2") OAuth2AuthorizedClient authorizedClient) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void passwordClient(@RegisteredOAuth2AuthorizedClient("client3") OAuth2AuthorizedClient authorizedClient) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ import org.springframework.core.codec.ByteBufferEncoder;
|
||||||
import org.springframework.core.codec.CharSequenceEncoder;
|
import org.springframework.core.codec.CharSequenceEncoder;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.codec.EncoderHttpMessageWriter;
|
import org.springframework.http.codec.EncoderHttpMessageWriter;
|
||||||
import org.springframework.http.codec.FormHttpMessageWriter;
|
import org.springframework.http.codec.FormHttpMessageWriter;
|
||||||
import org.springframework.http.codec.HttpMessageWriter;
|
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.authentication.TestingAuthenticationToken;
|
||||||
import org.springframework.security.core.authority.AuthorityUtils;
|
import org.springframework.security.core.authority.AuthorityUtils;
|
||||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
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.OAuth2AuthorizedClient;
|
||||||
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProvider;
|
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProvider;
|
||||||
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProviderBuilder;
|
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProviderBuilder;
|
||||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||||
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
|
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.OAuth2RefreshTokenGrantRequest;
|
||||||
import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
|
import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
|
||||||
import org.springframework.security.oauth2.client.endpoint.WebClientReactiveClientCredentialsTokenResponseClient;
|
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.OAuth2AccessToken;
|
||||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
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.DefaultOAuth2User;
|
||||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
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.BodyInserter;
|
||||||
import org.springframework.web.reactive.function.client.ClientRequest;
|
import org.springframework.web.reactive.function.client.ClientRequest;
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
@ -102,6 +107,9 @@ public class ServerOAuth2AuthorizedClientExchangeFilterFunctionTests {
|
||||||
@Mock
|
@Mock
|
||||||
private ReactiveOAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshTokenTokenResponseClient;
|
private ReactiveOAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshTokenTokenResponseClient;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ReactiveOAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> passwordTokenResponseClient;
|
||||||
|
|
||||||
private ServerWebExchange serverWebExchange = MockServerWebExchange.builder(MockServerHttpRequest.get("/")).build();
|
private ServerWebExchange serverWebExchange = MockServerWebExchange.builder(MockServerHttpRequest.get("/")).build();
|
||||||
|
|
||||||
@Captor
|
@Captor
|
||||||
|
@ -119,6 +127,8 @@ public class ServerOAuth2AuthorizedClientExchangeFilterFunctionTests {
|
||||||
Instant.now(),
|
Instant.now(),
|
||||||
Instant.now().plus(Duration.ofDays(1)));
|
Instant.now().plus(Duration.ofDays(1)));
|
||||||
|
|
||||||
|
private DefaultServerOAuth2AuthorizedClientManager authorizedClientManager;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setup() {
|
public void setup() {
|
||||||
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
|
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
|
||||||
|
@ -126,10 +136,11 @@ public class ServerOAuth2AuthorizedClientExchangeFilterFunctionTests {
|
||||||
.authorizationCode()
|
.authorizationCode()
|
||||||
.refreshToken(configurer -> configurer.accessTokenResponseClient(this.refreshTokenTokenResponseClient))
|
.refreshToken(configurer -> configurer.accessTokenResponseClient(this.refreshTokenTokenResponseClient))
|
||||||
.clientCredentials(configurer -> configurer.accessTokenResponseClient(this.clientCredentialsTokenResponseClient))
|
.clientCredentials(configurer -> configurer.accessTokenResponseClient(this.clientCredentialsTokenResponseClient))
|
||||||
|
.password(configurer -> configurer.accessTokenResponseClient(this.passwordTokenResponseClient))
|
||||||
.build();
|
.build();
|
||||||
DefaultServerOAuth2AuthorizedClientManager authorizedClientManager = new DefaultServerOAuth2AuthorizedClientManager(
|
this.authorizedClientManager = new DefaultServerOAuth2AuthorizedClientManager(
|
||||||
this.clientRegistrationRepository, this.authorizedClientRepository);
|
this.clientRegistrationRepository, this.authorizedClientRepository);
|
||||||
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
|
this.authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
|
||||||
this.function = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
|
this.function = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -403,6 +414,64 @@ public class ServerOAuth2AuthorizedClientExchangeFilterFunctionTests {
|
||||||
assertThat(getBody(request0)).isEmpty();
|
assertThat(getBody(request0)).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void filterWhenPasswordClientNotAuthorizedThenGetNewToken() {
|
||||||
|
TestingAuthenticationToken authentication = new TestingAuthenticationToken("test", "this");
|
||||||
|
ClientRegistration registration = TestClientRegistrations.password().build();
|
||||||
|
|
||||||
|
OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken("new-token")
|
||||||
|
.tokenType(OAuth2AccessToken.TokenType.BEARER)
|
||||||
|
.expiresIn(360)
|
||||||
|
.build();
|
||||||
|
when(this.passwordTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse));
|
||||||
|
|
||||||
|
when(this.clientRegistrationRepository.findByRegistrationId(eq(registration.getRegistrationId()))).thenReturn(Mono.just(registration));
|
||||||
|
when(this.authorizedClientRepository.loadAuthorizedClient(eq(registration.getRegistrationId()), eq(authentication), any())).thenReturn(Mono.empty());
|
||||||
|
|
||||||
|
// Set custom contextAttributesMapper capable of mapping the form parameters
|
||||||
|
this.authorizedClientManager.setContextAttributesMapper(authorizeRequest ->
|
||||||
|
Mono.just(authorizeRequest.getServerWebExchange())
|
||||||
|
.flatMap(ServerWebExchange::getFormData)
|
||||||
|
.map(formData -> {
|
||||||
|
Map<String, Object> contextAttributes = new HashMap<>();
|
||||||
|
String username = formData.getFirst(OAuth2ParameterNames.USERNAME);
|
||||||
|
String password = formData.getFirst(OAuth2ParameterNames.PASSWORD);
|
||||||
|
if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
|
||||||
|
contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username);
|
||||||
|
contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password);
|
||||||
|
}
|
||||||
|
return contextAttributes;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this.serverWebExchange = MockServerWebExchange.builder(
|
||||||
|
MockServerHttpRequest
|
||||||
|
.post("/")
|
||||||
|
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||||
|
.body("username=username&password=password"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com"))
|
||||||
|
.attributes(clientRegistrationId(registration.getRegistrationId()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
this.function.filter(request, this.exchange)
|
||||||
|
.subscriberContext(ReactiveSecurityContextHolder.withAuthentication(authentication))
|
||||||
|
.subscriberContext(serverWebExchange())
|
||||||
|
.block();
|
||||||
|
|
||||||
|
verify(this.passwordTokenResponseClient).getTokenResponse(any());
|
||||||
|
verify(this.authorizedClientRepository).saveAuthorizedClient(any(), eq(authentication), any());
|
||||||
|
|
||||||
|
List<ClientRequest> requests = this.exchange.getRequests();
|
||||||
|
assertThat(requests).hasSize(1);
|
||||||
|
ClientRequest request1 = requests.get(0);
|
||||||
|
assertThat(request1.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer new-token");
|
||||||
|
assertThat(request1.url().toASCIIString()).isEqualTo("https://example.com");
|
||||||
|
assertThat(request1.method()).isEqualTo(HttpMethod.GET);
|
||||||
|
assertThat(getBody(request1)).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void filterWhenClientRegistrationIdThenAuthorizedClientResolved() {
|
public void filterWhenClientRegistrationIdThenAuthorizedClientResolved() {
|
||||||
OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", this.accessToken.getIssuedAt());
|
OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", this.accessToken.getIssuedAt());
|
||||||
|
|
|
@ -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.DefaultRefreshTokenTokenResponseClient;
|
||||||
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
|
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
|
||||||
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
|
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.OAuth2RefreshTokenGrantRequest;
|
||||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
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.OAuth2AccessToken;
|
||||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
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.endpoint.TestOAuth2AccessTokenResponses;
|
||||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
import org.springframework.web.client.RestOperations;
|
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.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
|
||||||
import static org.mockito.Mockito.*;
|
import static org.mockito.Mockito.*;
|
||||||
import static org.springframework.http.HttpMethod.GET;
|
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.*;
|
import static org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -109,6 +112,8 @@ public class ServletOAuth2AuthorizedClientExchangeFilterFunctionTests {
|
||||||
@Mock
|
@Mock
|
||||||
private OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshTokenTokenResponseClient;
|
private OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshTokenTokenResponseClient;
|
||||||
@Mock
|
@Mock
|
||||||
|
private OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> passwordTokenResponseClient;
|
||||||
|
@Mock
|
||||||
private WebClient.RequestHeadersSpec<?> spec;
|
private WebClient.RequestHeadersSpec<?> spec;
|
||||||
@Captor
|
@Captor
|
||||||
private ArgumentCaptor<Consumer<Map<String, Object>>> attrs;
|
private ArgumentCaptor<Consumer<Map<String, Object>>> attrs;
|
||||||
|
@ -141,6 +146,7 @@ public class ServletOAuth2AuthorizedClientExchangeFilterFunctionTests {
|
||||||
.authorizationCode()
|
.authorizationCode()
|
||||||
.refreshToken(configurer -> configurer.accessTokenResponseClient(this.refreshTokenTokenResponseClient))
|
.refreshToken(configurer -> configurer.accessTokenResponseClient(this.refreshTokenTokenResponseClient))
|
||||||
.clientCredentials(configurer -> configurer.accessTokenResponseClient(this.clientCredentialsTokenResponseClient))
|
.clientCredentials(configurer -> configurer.accessTokenResponseClient(this.clientCredentialsTokenResponseClient))
|
||||||
|
.password(configurer -> configurer.accessTokenResponseClient(this.passwordTokenResponseClient))
|
||||||
.build();
|
.build();
|
||||||
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
|
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
|
||||||
this.clientRegistrationRepository, this.authorizedClientRepository);
|
this.clientRegistrationRepository, this.authorizedClientRepository);
|
||||||
|
@ -442,6 +448,43 @@ public class ServletOAuth2AuthorizedClientExchangeFilterFunctionTests {
|
||||||
assertThat(getBody(request1)).isEmpty();
|
assertThat(getBody(request1)).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void filterWhenPasswordClientNotAuthorizedThenGetNewToken() {
|
||||||
|
OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken("new-token")
|
||||||
|
.tokenType(OAuth2AccessToken.TokenType.BEARER)
|
||||||
|
.expiresIn(360)
|
||||||
|
.build();
|
||||||
|
when(this.passwordTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse);
|
||||||
|
|
||||||
|
ClientRegistration registration = TestClientRegistrations.password().build();
|
||||||
|
when(this.clientRegistrationRepository.findByRegistrationId(eq(registration.getRegistrationId()))).thenReturn(registration);
|
||||||
|
|
||||||
|
MockHttpServletRequest servletRequest = new MockHttpServletRequest();
|
||||||
|
servletRequest.setParameter(OAuth2ParameterNames.USERNAME, "username");
|
||||||
|
servletRequest.setParameter(OAuth2ParameterNames.PASSWORD, "password");
|
||||||
|
MockHttpServletResponse servletResponse = new MockHttpServletResponse();
|
||||||
|
|
||||||
|
ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com"))
|
||||||
|
.attributes(clientRegistrationId(registration.getRegistrationId()))
|
||||||
|
.attributes(authentication(this.authentication))
|
||||||
|
.attributes(httpServletRequest(servletRequest))
|
||||||
|
.attributes(httpServletResponse(servletResponse))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
this.function.filter(request, this.exchange).block();
|
||||||
|
|
||||||
|
verify(this.passwordTokenResponseClient).getTokenResponse(any());
|
||||||
|
verify(this.authorizedClientRepository).saveAuthorizedClient(any(), eq(authentication), any(), any());
|
||||||
|
|
||||||
|
List<ClientRequest> requests = this.exchange.getRequests();
|
||||||
|
assertThat(requests).hasSize(1);
|
||||||
|
ClientRequest request1 = requests.get(0);
|
||||||
|
assertThat(request1.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer new-token");
|
||||||
|
assertThat(request1.url().toASCIIString()).isEqualTo("https://example.com");
|
||||||
|
assertThat(request1.method()).isEqualTo(HttpMethod.GET);
|
||||||
|
assertThat(getBody(request1)).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void filterWhenRefreshRequiredAndEmptyReactiveSecurityContextThenSaved() {
|
public void filterWhenRefreshRequiredAndEmptyReactiveSecurityContextThenSaved() {
|
||||||
OAuth2AccessTokenResponse response = OAuth2AccessTokenResponse.withToken("token-1")
|
OAuth2AccessTokenResponse response = OAuth2AccessTokenResponse.withToken("token-1")
|
||||||
|
|
|
@ -18,6 +18,7 @@ package org.springframework.security.oauth2.client.web.server;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
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.TestOAuth2AccessTokens;
|
||||||
import org.springframework.security.oauth2.core.TestOAuth2RefreshTokens;
|
import org.springframework.security.oauth2.core.TestOAuth2RefreshTokens;
|
||||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
@ -72,7 +76,7 @@ public class DefaultServerOAuth2AuthorizedClientManagerTests {
|
||||||
this.authorizedClientProvider = mock(ReactiveOAuth2AuthorizedClientProvider.class);
|
this.authorizedClientProvider = mock(ReactiveOAuth2AuthorizedClientProvider.class);
|
||||||
when(this.authorizedClientProvider.authorize(any(OAuth2AuthorizationContext.class))).thenReturn(Mono.empty());
|
when(this.authorizedClientProvider.authorize(any(OAuth2AuthorizationContext.class))).thenReturn(Mono.empty());
|
||||||
this.contextAttributesMapper = mock(Function.class);
|
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.authorizedClientManager = new DefaultServerOAuth2AuthorizedClientManager(
|
||||||
this.clientRegistrationRepository, this.authorizedClientRepository);
|
this.clientRegistrationRepository, this.authorizedClientRepository);
|
||||||
this.authorizedClientManager.setAuthorizedClientProvider(this.authorizedClientProvider);
|
this.authorizedClientManager.setAuthorizedClientProvider(this.authorizedClientProvider);
|
||||||
|
@ -209,6 +213,49 @@ public class DefaultServerOAuth2AuthorizedClientManagerTests {
|
||||||
eq(reauthorizedClient), eq(this.principal), eq(this.serverWebExchange));
|
eq(reauthorizedClient), eq(this.principal), eq(this.serverWebExchange));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authorizeWhenRequestFormParameterUsernamePasswordThenMappedToContext() {
|
||||||
|
when(this.clientRegistrationRepository.findByRegistrationId(
|
||||||
|
eq(this.clientRegistration.getRegistrationId()))).thenReturn(Mono.just(this.clientRegistration));
|
||||||
|
|
||||||
|
when(this.authorizedClientProvider.authorize(any(OAuth2AuthorizationContext.class))).thenReturn(Mono.just(this.authorizedClient));
|
||||||
|
|
||||||
|
// Set custom contextAttributesMapper capable of mapping the form parameters
|
||||||
|
this.authorizedClientManager.setContextAttributesMapper(authorizeRequest ->
|
||||||
|
Mono.just(authorizeRequest.getServerWebExchange())
|
||||||
|
.flatMap(ServerWebExchange::getFormData)
|
||||||
|
.map(formData -> {
|
||||||
|
Map<String, Object> contextAttributes = new HashMap<>();
|
||||||
|
String username = formData.getFirst(OAuth2ParameterNames.USERNAME);
|
||||||
|
String password = formData.getFirst(OAuth2ParameterNames.PASSWORD);
|
||||||
|
if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
|
||||||
|
contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username);
|
||||||
|
contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password);
|
||||||
|
}
|
||||||
|
return contextAttributes;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this.serverWebExchange = MockServerWebExchange.builder(
|
||||||
|
MockServerHttpRequest
|
||||||
|
.post("/")
|
||||||
|
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||||
|
.body("username=username&password=password"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
ServerOAuth2AuthorizeRequest authorizeRequest = new ServerOAuth2AuthorizeRequest(
|
||||||
|
this.clientRegistration.getRegistrationId(), this.principal, this.serverWebExchange);
|
||||||
|
this.authorizedClientManager.authorize(authorizeRequest).block();
|
||||||
|
|
||||||
|
verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
|
||||||
|
|
||||||
|
OAuth2AuthorizationContext authorizationContext = this.authorizationContextCaptor.getValue();
|
||||||
|
String username = authorizationContext.getAttribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME);
|
||||||
|
assertThat(username).isEqualTo("username");
|
||||||
|
String password = authorizationContext.getAttribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME);
|
||||||
|
assertThat(password).isEqualTo("password");
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
@Test
|
@Test
|
||||||
public void reauthorizeWhenUnsupportedProviderThenNotReauthorized() {
|
public void reauthorizeWhenUnsupportedProviderThenNotReauthorized() {
|
||||||
|
@ -255,9 +302,8 @@ public class DefaultServerOAuth2AuthorizedClientManagerTests {
|
||||||
eq(reauthorizedClient), eq(this.principal), eq(this.serverWebExchange));
|
eq(reauthorizedClient), eq(this.principal), eq(this.serverWebExchange));
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
@Test
|
@Test
|
||||||
public void reauthorizeWhenRequestScopeParameterThenMappedToContext() {
|
public void reauthorizeWhenRequestParameterScopeThenMappedToContext() {
|
||||||
OAuth2AuthorizedClient reauthorizedClient = new OAuth2AuthorizedClient(
|
OAuth2AuthorizedClient reauthorizedClient = new OAuth2AuthorizedClient(
|
||||||
this.clientRegistration, this.principal.getName(),
|
this.clientRegistration, this.principal.getName(),
|
||||||
TestOAuth2AccessTokens.noScopes(), TestOAuth2RefreshTokens.refreshToken());
|
TestOAuth2AccessTokens.noScopes(), TestOAuth2RefreshTokens.refreshToken());
|
||||||
|
@ -276,20 +322,12 @@ public class DefaultServerOAuth2AuthorizedClientManagerTests {
|
||||||
|
|
||||||
ServerOAuth2AuthorizeRequest reauthorizeRequest = new ServerOAuth2AuthorizeRequest(
|
ServerOAuth2AuthorizeRequest reauthorizeRequest = new ServerOAuth2AuthorizeRequest(
|
||||||
this.authorizedClient, this.principal, this.serverWebExchange);
|
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());
|
verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
|
||||||
|
|
||||||
OAuth2AuthorizationContext authorizationContext = this.authorizationContextCaptor.getValue();
|
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);
|
String[] requestScopeAttribute = authorizationContext.getAttribute(OAuth2AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME);
|
||||||
assertThat(requestScopeAttribute).contains("read", "write");
|
assertThat(requestScopeAttribute).contains("read", "write");
|
||||||
|
|
||||||
assertThat(authorizedClient).isSameAs(reauthorizedClient);
|
|
||||||
verify(this.authorizedClientRepository).saveAuthorizedClient(
|
|
||||||
eq(reauthorizedClient), eq(this.principal), eq(this.serverWebExchange));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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 IMPLICIT = new AuthorizationGrantType("implicit");
|
||||||
public static final AuthorizationGrantType REFRESH_TOKEN = new AuthorizationGrantType("refresh_token");
|
public static final AuthorizationGrantType REFRESH_TOKEN = new AuthorizationGrantType("refresh_token");
|
||||||
public static final AuthorizationGrantType CLIENT_CREDENTIALS = new AuthorizationGrantType("client_credentials");
|
public static final AuthorizationGrantType CLIENT_CREDENTIALS = new AuthorizationGrantType("client_credentials");
|
||||||
|
public static final AuthorizationGrantType PASSWORD = new AuthorizationGrantType("password");
|
||||||
private final String value;
|
private final String value;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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";
|
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.
|
* {@code error} - used in Authorization Response and Access Token Response.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -45,4 +45,9 @@ public class AuthorizationGrantTypeTests {
|
||||||
public void getValueWhenRefreshTokenGrantTypeThenReturnRefreshToken() {
|
public void getValueWhenRefreshTokenGrantTypeThenReturnRefreshToken() {
|
||||||
assertThat(AuthorizationGrantType.REFRESH_TOKEN.getValue()).isEqualTo("refresh_token");
|
assertThat(AuthorizationGrantType.REFRESH_TOKEN.getValue()).isEqualTo("refresh_token");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getValueWhenPasswordGrantTypeThenReturnPassword() {
|
||||||
|
assertThat(AuthorizationGrantType.PASSWORD.getValue()).isEqualTo("password");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,6 +54,7 @@ public class WebClientConfig {
|
||||||
.authorizationCode()
|
.authorizationCode()
|
||||||
.refreshToken()
|
.refreshToken()
|
||||||
.clientCredentials()
|
.clientCredentials()
|
||||||
|
.password()
|
||||||
.build();
|
.build();
|
||||||
DefaultServerOAuth2AuthorizedClientManager authorizedClientManager =
|
DefaultServerOAuth2AuthorizedClientManager authorizedClientManager =
|
||||||
new DefaultServerOAuth2AuthorizedClientManager(
|
new DefaultServerOAuth2AuthorizedClientManager(
|
||||||
|
|
|
@ -52,6 +52,7 @@ public class WebClientConfig {
|
||||||
.authorizationCode()
|
.authorizationCode()
|
||||||
.refreshToken()
|
.refreshToken()
|
||||||
.clientCredentials()
|
.clientCredentials()
|
||||||
|
.password()
|
||||||
.build();
|
.build();
|
||||||
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
|
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
|
||||||
clientRegistrationRepository, authorizedClientRepository);
|
clientRegistrationRepository, authorizedClientRepository);
|
||||||
|
|
Loading…
Reference in New Issue