/*******************************************************************************
* Cloud Foundry
* Copyright (c) [2009-2016] Pivotal Software, Inc. All Rights Reserved.
*
* This product is licensed to you under the Apache License, Version 2.0 (the "License").
* You may not use this product except in compliance with the License.
*
* This product includes a number of subcomponents with
* separate copyright notices and license terms. Your use of these
* subcomponents is subject to the terms and conditions of the
* subcomponent's license, as noted in the LICENSE file.
*******************************************************************************/
package org.cloudfoundry.identity.uaa.integration;
import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.cookie.BasicClientCookie;
import org.cloudfoundry.identity.uaa.ServerRunning;
import org.cloudfoundry.identity.uaa.scim.ScimUser;
import org.cloudfoundry.identity.uaa.test.TestAccountSetup;
import org.cloudfoundry.identity.uaa.test.UaaTestAccounts;
import org.cloudfoundry.identity.uaa.security.web.CookieBasedCsrfTokenRepository;
import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.security.jwt.Jwt;
import org.cloudfoundry.identity.uaa.oauth.jwt.JwtHelper;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.http.OAuth2ErrorHandler;
import org.springframework.security.oauth2.client.test.OAuth2ContextConfiguration;
import org.springframework.security.oauth2.client.test.OAuth2ContextSetup;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;
import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails;
import org.springframework.security.oauth2.common.AuthenticationScheme;
import org.springframework.security.oauth2.common.util.RandomValueStringGenerator;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriUtils;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import static org.cloudfoundry.identity.uaa.integration.util.IntegrationTestUtils.extractCookieCsrf;
import static org.cloudfoundry.identity.uaa.integration.util.IntegrationTestUtils.getHeaders;
import static org.cloudfoundry.identity.uaa.security.web.CookieBasedCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.springframework.security.oauth2.common.util.OAuth2Utils.USER_OAUTH_APPROVAL;
/**
* @author Luke Taylor
* @author Dave Syer
*/
@OAuth2ContextConfiguration(OAuth2ContextConfiguration.ClientCredentials.class)
public class OpenIdTokenAuthorizationWithApprovalIntegrationTests {
private final String userEndpoint = "/Users";
@Rule
public ServerRunning serverRunning = ServerRunning.isRunning();
private UaaTestAccounts testAccounts = UaaTestAccounts.standard(serverRunning);
@Rule
public OAuth2ContextSetup context = OAuth2ContextSetup.withTestAccounts(serverRunning, testAccounts);
@Rule
public TestAccountSetup testAccountSetup = TestAccountSetup.standard(serverRunning, testAccounts);
private RestTemplate client;
private RestTemplate loginClient;
private ScimUser user;
@Before
public void createRestTemplate() throws Exception {
ClientCredentialsResourceDetails clientCredentials =
getClientCredentialsResource(new String[] {"oauth.login"}, "login", "loginsecret");
loginClient = new OAuth2RestTemplate(clientCredentials);
loginClient.setRequestFactory(new StatelessRequestFactory());
loginClient.setErrorHandler(new OAuth2ErrorHandler(clientCredentials) {
// Pass errors through in response entity for status code analysis
@Override
public boolean hasError(ClientHttpResponse response) throws IOException {
return false;
}
@Override
public void handleError(ClientHttpResponse response) throws IOException {
}
});
Assume.assumeTrue(!testAccounts.isProfileActive("vcap"));
client = (RestTemplate)serverRunning.getRestTemplate();
client.setErrorHandler(new OAuth2ErrorHandler(context.getResource()) {
// Pass errors through in response entity for status code analysis
@Override
public boolean hasError(ClientHttpResponse response) throws IOException {
return false;
}
@Override
public void handleError(ClientHttpResponse response) throws IOException {
}
});
user = createUser(new RandomValueStringGenerator().generate(), "openiduser", "openidlast", "test@openid,com",true).getBody();
}
@Test
public void testOpenIdTokenUsingLoginClientOauthTokenEndpoint() throws Exception {
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
LinkedMultiValueMap<String, String> postBody = new LinkedMultiValueMap<>();
postBody.add("client_id", "app");
postBody.add("client_secret", "appclientsecret");
postBody.add("redirect_uri", "https://uaa.cloudfoundry.com/redirect/cf");
postBody.add("response_type", "token id_token");
postBody.add("grant_type", "password");
postBody.add("source", "login");
postBody.add("user_id", user.getId());
postBody.add("add_new", "false");
ResponseEntity<Map> responseEntity = loginClient.exchange(serverRunning.getBaseUrl() + "/oauth/token",
HttpMethod.POST,
new HttpEntity<>(postBody, headers),
Map.class);
Assert.assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
Map<String, Object> params = responseEntity.getBody();
Assert.assertTrue(params.get("jti") != null);
Assert.assertEquals("bearer", params.get("token_type"));
Assert.assertThat((Integer)params.get("expires_in"), Matchers.greaterThan(40000));
String[] scopes = UriUtils.decode((String)params.get("scope"), "UTF-8").split(" ");
Assert.assertThat(Arrays.asList(scopes), containsInAnyOrder(
"scim.userids",
"password.write",
"cloud_controller.write",
"openid",
"cloud_controller.read"
));
}
@Test
public void testOpenIdHybridFlowIdTokenAndCode() throws Exception {
//non approved
doOpenIdHybridFlowIdTokenAndReturnCode(new HashSet<>(Arrays.asList("token","code")), ".+access_token=.+code=.+");
//approved
doOpenIdHybridFlowIdTokenAndReturnCode(new HashSet<>(Arrays.asList("token","code")), ".+access_token=.+code=.+");
//approved using login client
doOpenIdHybridFlowForLoginClient(new HashSet<>(Arrays.asList("token","code")), ".+access_token=.+code=.+");
}
@Test
public void testOpenIdHybridFlowIdTokenAndTokenAndCode() throws Exception {
//non approved
doOpenIdHybridFlowIdTokenAndReturnCode(new HashSet<>(Arrays.asList("token","id_token", "code")), ".+access_token=.+id_token=.+code=.+");
//approved
doOpenIdHybridFlowIdTokenAndReturnCode(new HashSet<>(Arrays.asList("token","id_token", "code")), ".+access_token=.+id_token=.+code=.+");
//approved using login client
doOpenIdHybridFlowForLoginClient(new HashSet<>(Arrays.asList("token","id_token", "code")), ".+access_token=.+id_token=.+code=.+");
}
@Test
public void testOpenIdHybridFlowIdTokenAndToken() throws Exception {
//non approved
doOpenIdHybridFlowIdTokenAndReturnCode(new HashSet<>(Arrays.asList("id_token","code")), ".+id_token=.+code=.+");
//approved
doOpenIdHybridFlowIdTokenAndReturnCode(new HashSet<>(Arrays.asList("id_token","code")), ".+id_token=.+code=.+");
//approved using login client
doOpenIdHybridFlowForLoginClient(new HashSet<>(Arrays.asList("id_token","code")), ".+id_token=.+code=.+");
}
private String doOpenIdHybridFlowIdTokenAndReturnCode(Set<String> responseTypes, String responseTypeMatcher) throws Exception {
BasicCookieStore cookies = new BasicCookieStore();
AuthorizationCodeResourceDetails resource = testAccounts.getDefaultAuthorizationCodeResource();
StringBuilder responseType = new StringBuilder();
Iterator<String> rTypes = responseTypes.iterator();
while (rTypes.hasNext()) {
String type = rTypes.next();
responseType.append(type);
if (rTypes.hasNext()) {
responseType.append(" ");
}
}
String state = new RandomValueStringGenerator().generate();
String clientId = resource.getClientId();
String redirectUri = resource.getPreEstablishedRedirectUri();
String clientSecret = resource.getClientSecret();
String uri = serverRunning.getUrl("/oauth/authorize?response_type={response_type}&"+
"state={state}&client_id={client_id}&redirect_uri={redirect_uri}");
RestTemplate restTemplate = serverRunning.createRestTemplate();
ResponseEntity<Void> result = restTemplate.exchange(uri,
HttpMethod.GET,
new HttpEntity<Void>(null, getHeaders(cookies)),
Void.class,
responseType,
state,
clientId,
redirectUri);
assertEquals(HttpStatus.FOUND, result.getStatusCode());
String location = UriUtils.decode(result.getHeaders().getLocation().toString(), "UTF-8");
if (result.getHeaders().containsKey("Set-Cookie")) {
for (String cookie : result.getHeaders().get("Set-Cookie")) {
int nameLength = cookie.indexOf('=');
cookies.addCookie(new BasicClientCookie(cookie.substring(0, nameLength), cookie.substring(nameLength+1)));
}
}
ResponseEntity<String> response = serverRunning.getForString(location, getHeaders(cookies));
if (response.getHeaders().containsKey("Set-Cookie")) {
for (String cookie : response.getHeaders().get("Set-Cookie")) {
int nameLength = cookie.indexOf('=');
cookies.addCookie(new BasicClientCookie(cookie.substring(0, nameLength), cookie.substring(nameLength+1)));
}
}
// should be directed to the login screen...
assertTrue(response.getBody().contains("/login.do"));
assertTrue(response.getBody().contains("username"));
assertTrue(response.getBody().contains("password"));
MultiValueMap<String, String> formData = new LinkedMultiValueMap<String, String>();
formData.add("username", user.getUserName());
formData.add("password", "s3Cret");
formData.add(CookieBasedCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME, extractCookieCsrf(response.getBody()));
// Should be redirected to the original URL, but now authenticated
result = serverRunning.postForResponse("/login.do", getHeaders(cookies), formData);
assertEquals(HttpStatus.FOUND, result.getStatusCode());
cookies.clear();
if (result.getHeaders().containsKey("Set-Cookie")) {
for (String cookie : result.getHeaders().get("Set-Cookie")) {
int nameLength = cookie.indexOf('=');
cookies.addCookie(new BasicClientCookie(cookie.substring(0, nameLength), cookie.substring(nameLength+1)));
}
}
location = UriUtils.decode(result.getHeaders().getLocation().toString(), "UTF-8");
//response = serverRunning.getForString(location, headers);
response = restTemplate.exchange(location,
HttpMethod.GET,
new HttpEntity<>(null, getHeaders(cookies)),
String.class);
if (response.getHeaders().containsKey("Set-Cookie")) {
for (String cookie : response.getHeaders().get("Set-Cookie")) {
int nameLength = cookie.indexOf('=');
cookies.addCookie(new BasicClientCookie(cookie.substring(0, nameLength), cookie.substring(nameLength+1)));
}
}
if (response.getStatusCode() == HttpStatus.OK) {
// The grant access page should be returned
assertTrue(response.getBody().contains("Application Authorization</h1>"));
formData.clear();
formData.add(USER_OAUTH_APPROVAL, "true");
formData.add(DEFAULT_CSRF_COOKIE_NAME, extractCookieCsrf(response.getBody()));
result = serverRunning.postForResponse("/oauth/authorize", getHeaders(cookies), formData);
assertEquals(HttpStatus.FOUND, result.getStatusCode());
location = UriUtils.decode(result.getHeaders().getLocation().toString(), "UTF-8");
}
else {
// Token cached so no need for second approval
assertEquals(HttpStatus.FOUND, response.getStatusCode());
location = UriUtils.decode(response.getHeaders().getLocation().toString(), "UTF-8");
}
assertTrue("Wrong location: " + location,
location.matches(resource.getPreEstablishedRedirectUri() + responseTypeMatcher.toString()));
String code = location.split("code=")[1].split("&")[0];
exchangeCodeForToken(clientId, redirectUri, clientSecret, code, formData);
return code;
}
private void doOpenIdHybridFlowForLoginClient(Set<String> responseTypes, String responseTypeMatcher) throws Exception {
HttpHeaders headers = new HttpHeaders();
// TODO: should be able to handle just TEXT_HTML
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON, MediaType.ALL));
AuthorizationCodeResourceDetails resource = testAccounts.getDefaultAuthorizationCodeResource();
StringBuilder responseType = new StringBuilder();
Iterator<String> rTypes = responseTypes.iterator();
while (rTypes.hasNext()) {
String type = rTypes.next();
responseType.append(type);
if (rTypes.hasNext()) {
responseType.append(" ");
}
}
String state = new RandomValueStringGenerator().generate();
String clientId = resource.getClientId();
String redirectUri = resource.getPreEstablishedRedirectUri();
String clientSecret = resource.getClientSecret();
String uri = serverRunning.getUrl("/oauth/authorize?response_type={response_type}&"+
"state={state}&client_id={client_id}&client_secret={clientSecret}&redirect_uri={redirect_uri}&source=login&user_id={userId}&add_new=false");
HttpEntity<Void> request = new HttpEntity<>(null, headers);
ResponseEntity<Map> result = loginClient.exchange(
serverRunning.getUrl(uri),
HttpMethod.POST,
request,
Map.class,
responseType,
state,
clientId,
clientSecret,
redirectUri,
user.getId());
assertEquals(HttpStatus.FOUND, result.getStatusCode());
String location = UriUtils.decode(result.getHeaders().getLocation().toString(), "UTF-8");
assertTrue("Wrong location: " + location,
location.matches(resource.getPreEstablishedRedirectUri() + responseTypeMatcher.toString()));
}
private void exchangeCodeForToken(String clientId, String redirectUri, String clientSecret, String value, MultiValueMap<String, String> formData) {
formData.clear();
formData.add("client_id", clientId);
formData.add("redirect_uri", redirectUri);
formData.add("grant_type", "authorization_code");
formData.add("code", value);
HttpHeaders tokenHeaders = new HttpHeaders();
tokenHeaders.set("Authorization",
testAccounts.getAuthorizationHeader(clientId, clientSecret));
@SuppressWarnings("rawtypes")
ResponseEntity<Map> tokenResponse = serverRunning.postForMap("/oauth/token", formData, tokenHeaders);
assertEquals(HttpStatus.OK, tokenResponse.getStatusCode());
@SuppressWarnings("unchecked")
Map<String, String> body = tokenResponse.getBody();
Jwt token = JwtHelper.decode(body.get("access_token"));
assertTrue("Wrong claims: " + token.getClaims(), token.getClaims().contains("\"aud\""));
assertTrue("Wrong claims: " + token.getClaims(), token.getClaims().contains("\"user_id\""));
}
private ResponseEntity<ScimUser> createUser(String username, String firstName, String lastName,
String email, boolean verified) {
ScimUser user = new ScimUser();
user.setUserName(username);
user.setName(new ScimUser.Name(firstName, lastName));
user.addEmail(email);
user.setVerified(verified);
user.setActive(true);
user.setPassword("s3Cret");
return client.postForEntity(serverRunning.getUrl(userEndpoint), user, ScimUser.class);
}
private ClientCredentialsResourceDetails getClientCredentialsResource(String[] scope, String clientId,
String clientSecret) {
ClientCredentialsResourceDetails resource = new ClientCredentialsResourceDetails();
resource.setClientId(clientId);
resource.setClientSecret(clientSecret);
resource.setId(clientId);
if (scope != null) {
resource.setScope(Arrays.asList(scope));
}
resource.setClientAuthenticationScheme(AuthenticationScheme.header);
resource.setAccessTokenUri(serverRunning.getBaseUrl()+"/oauth/token");
return resource;
}
private static class StatelessRequestFactory extends HttpComponentsClientHttpRequestFactory {
@Override
public HttpClient getHttpClient() {
return HttpClientBuilder.create()
.useSystemProperties()
.disableRedirectHandling()
.disableCookieManagement()
.build();
}
}
}