38 changed files with 2393 additions and 72 deletions
@ -0,0 +1,142 @@
@@ -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 @@
@@ -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; |
||||
} |
||||
} |
||||
@ -0,0 +1,124 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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; |
||||
} |
||||
} |
||||
@ -0,0 +1,190 @@
@@ -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 @@
@@ -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(); |
||||
} |
||||
} |
||||
@ -0,0 +1,220 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue