Skip to content

Commit

Permalink
Merge pull request #136 from SAP/TokenPasswordGrant
Browse files Browse the repository at this point in the history
Token password grant
  • Loading branch information
nenaraab authored Sep 12, 2019
2 parents 611bd61 + e5e1e7e commit 760e3ba
Show file tree
Hide file tree
Showing 4 changed files with 266 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ public interface OAuth2TokenService {
* supplying a subdomain (tenant).
* @param optionalParameters
* optional request parameters, can be null.
*
* @return the OAuth2AccessToken.
* @throws OAuth2ServiceException
* in case of an error during the http request.
Expand All @@ -50,7 +49,6 @@ OAuth2TokenResponse retrieveAccessTokenViaClientCredentialsGrant(URI tokenEndpoi
* supplying a subdomain (tenant).
* @param optionalParameters
* optional request parameters, can be null.
*
* @return the OAuth2AccessToken.
* @throws OAuth2ServiceException
* in case of an error during the http request.
Expand Down Expand Up @@ -82,4 +80,28 @@ OAuth2TokenResponse retrieveAccessTokenViaUserTokenGrant(URI tokenEndpointUri,
*/
OAuth2TokenResponse retrieveAccessTokenViaRefreshToken(URI tokenEndpointUri, ClientCredentials clientCredentials,
String refreshToken, @Nullable String subdomain) throws OAuth2ServiceException;

/**
* @param tokenEndpointUri
* the token endpoint URI.
* @param clientCredentials
* the client id and secret of the OAuth client, the recipient of the
* token.
* @param username
* the username for the user trying to get a token
* @param password
* the password for the user trying to get a token
* @param subdomain
* optionally indicates what Identity Zone this request goes to by
* supplying a subdomain (tenant).
* @param optionalParameters
* optional request parameters, can be null.
* @return the OAuth2AccessToken
* @throws OAuth2ServiceException
* in case of an error during the http request.
*/
OAuth2TokenResponse retrieveAccessTokenViaPasswordGrant(URI tokenEndpointUri, ClientCredentials clientCredentials,
String username, String password, @Nullable String subdomain,
@Nullable Map<String, String> optionalParameters) throws OAuth2ServiceException;

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,15 @@ private OAuth2TokenServiceConstants() {
public static final String REFRESH_TOKEN = "refresh_token";
public static final String CLIENT_ID = "client_id";
public static final String CLIENT_SECRET = "client_secret";
public static final String USERNAME = "username";
public static final String PASSWORD = "password";

public static final String GRANT_TYPE = "grant_type";
public static final String GRANT_TYPE_USER_TOKEN = "user_token";
public static final String GRANT_TYPE_REFRESH_TOKEN = "refresh_token";
public static final String GRANT_TYPE_CLIENT_CREDENTIALS = "client_credentials";
public static final String GRANT_TYPE_PASSWORD = "password";

public static final String TOKEN_TYPE_OPAQUE = "opaque";

public static final String PARAMETER_CLIENT_ID = "client_id";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,5 @@
package com.sap.cloud.security.xsuaa.client;

import static com.sap.cloud.security.xsuaa.client.OAuth2TokenServiceConstants.ACCESS_TOKEN;
import static com.sap.cloud.security.xsuaa.client.OAuth2TokenServiceConstants.CLIENT_ID;
import static com.sap.cloud.security.xsuaa.client.OAuth2TokenServiceConstants.CLIENT_SECRET;
import static com.sap.cloud.security.xsuaa.client.OAuth2TokenServiceConstants.EXPIRES_IN;
import static com.sap.cloud.security.xsuaa.client.OAuth2TokenServiceConstants.GRANT_TYPE;
import static com.sap.cloud.security.xsuaa.client.OAuth2TokenServiceConstants.GRANT_TYPE_CLIENT_CREDENTIALS;
import static com.sap.cloud.security.xsuaa.client.OAuth2TokenServiceConstants.GRANT_TYPE_REFRESH_TOKEN;
import static com.sap.cloud.security.xsuaa.client.OAuth2TokenServiceConstants.GRANT_TYPE_USER_TOKEN;
import static com.sap.cloud.security.xsuaa.client.OAuth2TokenServiceConstants.PARAMETER_CLIENT_ID;
import static com.sap.cloud.security.xsuaa.client.OAuth2TokenServiceConstants.REFRESH_TOKEN;

import java.net.URI;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpEntity;
Expand All @@ -33,6 +17,13 @@
import org.springframework.web.util.UriBuilder;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import static com.sap.cloud.security.xsuaa.client.OAuth2TokenServiceConstants.*;

public class XsuaaOAuth2TokenService implements OAuth2TokenService {

private final RestOperations restOperations;
Expand All @@ -54,8 +45,7 @@ public OAuth2TokenResponse retrieveAccessTokenViaClientCredentialsGrant(@NonNull
// build parameters
Map<String, String> parameters = new HashMap<>();
parameters.put(GRANT_TYPE, GRANT_TYPE_CLIENT_CREDENTIALS);
parameters.put(CLIENT_ID, clientCredentials.getId());
parameters.put(CLIENT_SECRET, clientCredentials.getSecret());
addClientCredentialsToParameters(clientCredentials, parameters);
if (optionalParameters != null) {
optionalParameters.forEach(parameters::putIfAbsent);
}
Expand Down Expand Up @@ -101,15 +91,39 @@ public OAuth2TokenResponse retrieveAccessTokenViaRefreshToken(@NonNull URI token
Map<String, String> parameters = new HashMap<>();
parameters.put(GRANT_TYPE, GRANT_TYPE_REFRESH_TOKEN);
parameters.put(REFRESH_TOKEN, refreshToken);
parameters.put(CLIENT_ID, clientCredentials.getId());
parameters.put(CLIENT_SECRET, clientCredentials.getSecret());
addClientCredentialsToParameters(clientCredentials, parameters);

// build header
HttpHeaders headers = createHeadersWithoutAuthorization();

return requestAccessToken(replaceSubdomain(tokenEndpointUri, subdomain), headers, copyIntoForm(parameters));
}

@Override
public OAuth2TokenResponse retrieveAccessTokenViaPasswordGrant(@NonNull URI tokenEndpoint,
@NonNull ClientCredentials clientCredentials, @NonNull String username, @NonNull String password,
@Nullable String subdomain, @Nullable Map<String, String> optionalParameters)
throws OAuth2ServiceException {
Assert.notNull(tokenEndpoint, "tokenEndpoint is required");
Assert.notNull(clientCredentials, "clientCredentials are required");
Assert.notNull(username, "username is required");
Assert.notNull(password, "password is required");

Map<String, String> parameters = new HashMap<>();
parameters.put(GRANT_TYPE, GRANT_TYPE_PASSWORD);
parameters.put(USERNAME, username);
parameters.put(PASSWORD, password);
addClientCredentialsToParameters(clientCredentials, parameters);

if (optionalParameters != null) {
optionalParameters.forEach(parameters::putIfAbsent);
}

HttpHeaders headers = createHeadersWithoutAuthorization();

return requestAccessToken(replaceSubdomain(tokenEndpoint, subdomain), headers, copyIntoForm(parameters));
}

/**
* Utility method that replaces the subdomain of the URI with the given
* subdomain.
Expand Down Expand Up @@ -170,7 +184,7 @@ private OAuth2TokenResponse requestAccessToken(URI tokenEndpointUri, HttpHeaders

/**
* Creates a copy of the given map or an new empty map of type MultiValueMap.
*
*
* @return a new @link{MultiValueMap} that contains all entries of the optional
* map.
*/
Expand Down Expand Up @@ -208,6 +222,12 @@ private static HttpHeaders createHeadersWithAuthorization(String token) {
return headers;
}

private void addClientCredentialsToParameters(ClientCredentials clientCredentials,
Map<String, String> parameters) {
parameters.put(CLIENT_ID, clientCredentials.getId());
parameters.put(CLIENT_SECRET, clientCredentials.getSecret());
}

/** common utilities **/

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package com.sap.cloud.security.xsuaa.client;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.http.*;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestOperations;

import java.net.URI;
import java.util.HashMap;
import java.util.Map;

import static com.sap.cloud.security.xsuaa.client.OAuth2TokenServiceConstants.*;
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.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.when;

@RunWith(MockitoJUnitRunner.class)
public class XsuaaOAuth2TokenServicePasswordTest {

private OAuth2TokenService cut;

private String clientSecret = "test321";
private String clientId = "theClientId";
private String password = "test123";
private String username = "bob";
private String subdomain = "subdomain";
private ClientCredentials clientCredentials = new ClientCredentials(clientId, clientSecret);
private URI tokenEndpoint = URI.create("https://subdomain.myauth.server.com/oauth/token");
private Map<String, String> optionalParameters;
private Map<String, String> response;

@Mock
private RestOperations mockRestOperations;

@Before
public void setup() {
response = new HashMap();
response.putIfAbsent(ACCESS_TOKEN, "f529.dd6e30.d454677322aaabb0");
response.putIfAbsent(EXPIRES_IN, "43199");
when(mockRestOperations.postForEntity(any(), any(), any()))
.thenReturn(ResponseEntity.status(200).body(response));
optionalParameters = new HashMap<>();
cut = new XsuaaOAuth2TokenService(mockRestOperations);
}

@Test(expected = OAuth2ServiceException.class)
public void retrieveToken_httpStatusUnauthorized_throwsException() throws OAuth2ServiceException {
throwExceptionOnPost(HttpStatus.UNAUTHORIZED);

cut.retrieveAccessTokenViaPasswordGrant(tokenEndpoint, clientCredentials,
username, password, null, null);
}

@Test(expected = OAuth2ServiceException.class)
public void retrieveToken_httpStatusNotOk_throwsException() throws OAuth2ServiceException {
throwExceptionOnPost(HttpStatus.BAD_REQUEST);

cut.retrieveAccessTokenViaPasswordGrant(tokenEndpoint, clientCredentials,
username, password, null, null);
}

@Test
public void retrieveToken_requiredParametersMissing_throwsException() {
assertThatThrownBy(() -> cut.retrieveAccessTokenViaPasswordGrant(null, clientCredentials,
username, password, subdomain, optionalParameters)).isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> cut.retrieveAccessTokenViaPasswordGrant(tokenEndpoint, null,
username, password, subdomain, optionalParameters)).isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> cut.retrieveAccessTokenViaPasswordGrant(tokenEndpoint, clientCredentials,
null, password, subdomain, optionalParameters)).isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> cut.retrieveAccessTokenViaPasswordGrant(tokenEndpoint, clientCredentials,
username, null, subdomain, optionalParameters)).isInstanceOf(IllegalArgumentException.class);
}

@Test
public void retrieveToken_callsTokenEndpoint() throws OAuth2ServiceException {
cut.retrieveAccessTokenViaPasswordGrant(tokenEndpoint, clientCredentials,
username, password, null, null);

Mockito.verify(mockRestOperations, times(1))
.postForEntity(eq(tokenEndpoint), any(), any());
}

@Test
public void retrieveToken_setsCorrectGrantType() throws OAuth2ServiceException {
cut.retrieveAccessTokenViaPasswordGrant(tokenEndpoint, clientCredentials,
username, password, null, null);

ArgumentCaptor<HttpEntity<MultiValueMap<String, String>>> requestEntityCaptor = captureRequestEntity();

String actualGrantType = valueOfParameter(GRANT_TYPE, requestEntityCaptor);
assertThat(actualGrantType).isEqualTo(GRANT_TYPE_PASSWORD);
}

@Test
public void retrieveToken_setsUsername() throws OAuth2ServiceException {
cut.retrieveAccessTokenViaPasswordGrant(tokenEndpoint, clientCredentials,
username, password, null, null);

ArgumentCaptor<HttpEntity<MultiValueMap<String, String>>> requestEntityCaptor = captureRequestEntity();

assertThat(valueOfParameter(USERNAME, requestEntityCaptor)).isEqualTo(username);
}

@Test
public void retrieveToken_setsPassword() throws OAuth2ServiceException {
cut.retrieveAccessTokenViaPasswordGrant(tokenEndpoint, clientCredentials,
username, password, null, null);

ArgumentCaptor<HttpEntity<MultiValueMap<String, String>>> requestEntityCaptor = captureRequestEntity();

assertThat(valueOfParameter(PASSWORD, requestEntityCaptor)).isEqualTo(password);
}

@Test
public void retrieveToken_setsClientCredentials() throws OAuth2ServiceException {
cut.retrieveAccessTokenViaPasswordGrant(tokenEndpoint, clientCredentials,
username, password, null, null);

ArgumentCaptor<HttpEntity<MultiValueMap<String, String>>> requestEntityCaptor = captureRequestEntity();

assertThat(valueOfParameter(CLIENT_ID, requestEntityCaptor)).isEqualTo(clientCredentials.getId());
assertThat(valueOfParameter(CLIENT_SECRET, requestEntityCaptor)).isEqualTo(clientCredentials.getSecret());
}

@Test
public void retrieveToken_setsOptionalParameters() throws OAuth2ServiceException {
String tokenFormatParameterKey = "token_format";
String tokenFormat = "opaque";
String loginHintParameterKey = "login_hint";
String loginHint = "origin";

optionalParameters.put(tokenFormatParameterKey, tokenFormat);
optionalParameters.put(loginHintParameterKey, loginHint);

cut.retrieveAccessTokenViaPasswordGrant(tokenEndpoint, clientCredentials,
username, password, null, optionalParameters);

ArgumentCaptor<HttpEntity<MultiValueMap<String, String>>> requestEntityCaptor = captureRequestEntity();
assertThat(valueOfParameter(tokenFormatParameterKey, requestEntityCaptor)).isEqualTo(tokenFormat);
assertThat(valueOfParameter(loginHintParameterKey, requestEntityCaptor)).isEqualTo(loginHint);
}

@Test
public void retrieveToken_setsCorrectHeaders() throws OAuth2ServiceException {
cut.retrieveAccessTokenViaPasswordGrant(tokenEndpoint, clientCredentials,
username, password, null, optionalParameters);

ArgumentCaptor<HttpEntity<MultiValueMap<String, String>>> requestEntityCaptor = captureRequestEntity();
HttpHeaders headers = requestEntityCaptor.getValue().getHeaders();

assertThat(headers.getAccept()).containsExactly(MediaType.APPLICATION_JSON);
assertThat(headers.getContentType()).isEqualTo(MediaType.APPLICATION_FORM_URLENCODED);
}

@Test
public void retrieveToken() throws OAuth2ServiceException {
OAuth2TokenResponse actualResponse = cut.retrieveAccessTokenViaPasswordGrant(tokenEndpoint, clientCredentials,
username, password, null, null);

assertThat(actualResponse.getAccessToken()).isEqualTo(response.get(ACCESS_TOKEN));

assertThat(actualResponse.getExpiredAtDate()).isNotNull();
}

private ArgumentCaptor<HttpEntity<MultiValueMap<String, String>>> captureRequestEntity() {
ArgumentCaptor<HttpEntity<MultiValueMap<String, String>>> requestEntityCaptor = ArgumentCaptor
.forClass(HttpEntity.class);
Mockito.verify(mockRestOperations, times(1))
.postForEntity(
eq(tokenEndpoint),
requestEntityCaptor.capture(),
eq(Map.class));
return requestEntityCaptor;
}

private String valueOfParameter(
String parameterKey, ArgumentCaptor<HttpEntity<MultiValueMap<String, String>>> requestEntityCaptor) {
MultiValueMap<String, String> body = requestEntityCaptor.getValue().getBody();
return body.getFirst(parameterKey);
}

private void throwExceptionOnPost(HttpStatus unauthorized) {
when(mockRestOperations.postForEntity(any(), any(), any()))
.thenThrow(new HttpClientErrorException(unauthorized));
}

}

0 comments on commit 760e3ba

Please sign in to comment.