38 changed files with 2393 additions and 72 deletions
@ -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; |
||||||
|
} |
||||||
|
} |
||||||
@ -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; |
||||||
|
} |
||||||
|
} |
||||||
@ -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(); |
||||||
|
} |
||||||
|
} |
||||||
@ -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); |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue