/*******************************************************************************
* 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.commons.codec.binary.Base64;
import org.cloudfoundry.identity.uaa.ServerRunning;
import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails;
import org.cloudfoundry.identity.uaa.constants.OriginKeys;
import org.cloudfoundry.identity.uaa.account.PasswordChangeRequest;
import org.cloudfoundry.identity.uaa.scim.ScimUser;
import org.cloudfoundry.identity.uaa.test.TestAccountSetup;
import org.cloudfoundry.identity.uaa.test.UaaTestAccounts;
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.oauth2.client.http.OAuth2ErrorHandler;
import org.springframework.security.oauth2.client.test.BeforeOAuth2Context;
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.implicit.ImplicitResourceDetails;
import org.springframework.security.oauth2.common.util.RandomValueStringGenerator;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.Map;
import static org.cloudfoundry.identity.uaa.constants.OriginKeys.LOGIN_SERVER;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse;
/**
* Integration test to verify that the Login Server authentication channel is
* open and working.
*
* @author Dave Syer
*/
public class LoginServerSecurityIntegrationTests {
private final String JOE = "joe" + new RandomValueStringGenerator().generate().toLowerCase();
private final String LOGIN_SERVER_JOE = "ls_joe" + new RandomValueStringGenerator().generate().toLowerCase();
private final String userEndpoint = "/Users";
private ScimUser joe;
@Rule
public ServerRunning serverRunning = ServerRunning.isRunning();
private UaaTestAccounts testAccounts = UaaTestAccounts.standard(serverRunning);
@Rule
public TestAccountSetup testAccountSetup = TestAccountSetup.standard(serverRunning, testAccounts);
@Rule
public OAuth2ContextSetup context = OAuth2ContextSetup.withTestAccounts(serverRunning, testAccounts);
private MultiValueMap<String, String> params = new LinkedMultiValueMap<String, String>();
private HttpHeaders headers = new HttpHeaders();
private ScimUser userForLoginServer;
@Before
public void init() {
params.set("source", "login");
params.set("redirect_uri", "http://localhost:8080/app/");
params.set("response_type", "token");
if (joe!=null) {
params.set("username", joe.getUserName());
}
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
((RestTemplate)serverRunning.getRestTemplate()).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 {
}
});
}
@BeforeOAuth2Context
@OAuth2ContextConfiguration(OAuth2ContextConfiguration.ClientCredentials.class)
public void setUpUserAccounts() {
// If running against vcap we don't want to run these tests because they
// create new user accounts
Assume.assumeTrue(!testAccounts.isProfileActive("vcap"));
RestOperations client = serverRunning.getRestTemplate();
ScimUser user = new ScimUser();
user.setPassword("password");
user.setUserName(JOE);
user.setName(new ScimUser.Name("Joe", "User"));
user.addEmail("joe@blah.com");
user.setVerified(true);
userForLoginServer = new ScimUser();
userForLoginServer.setPassword("password");
userForLoginServer.setUserName(LOGIN_SERVER_JOE);
userForLoginServer.setName(new ScimUser.Name("Joe_login_server", "User"));
userForLoginServer.addEmail("joe_ls@blah.com");
userForLoginServer.setVerified(true);
userForLoginServer.setOrigin(LOGIN_SERVER);
ResponseEntity<ScimUser> newuser = client.postForEntity(serverRunning.getUrl(userEndpoint), user,
ScimUser.class);
userForLoginServer = client.postForEntity(serverRunning.getUrl(userEndpoint), userForLoginServer,
ScimUser.class).getBody();
joe = newuser.getBody();
assertEquals(JOE, joe.getUserName());
PasswordChangeRequest change = new PasswordChangeRequest();
change.setPassword("Passwo3d");
HttpHeaders headers = new HttpHeaders();
ResponseEntity<Void> result = client
.exchange(serverRunning.getUrl(userEndpoint) + "/{id}/password",
HttpMethod.PUT, new HttpEntity<PasswordChangeRequest>(change, headers),
Void.class, joe.getId());
assertEquals(HttpStatus.OK, result.getStatusCode());
// The implicit grant for cf requires extra parameters in the
// authorization request
context.setParameters(Collections.singletonMap("credentials",
testAccounts.getJsonCredentials(joe.getUserName(), "Passwo3d")));
}
@Test
@OAuth2ContextConfiguration(LoginClient.class)
public void testAuthenticateReturnsUserID() throws Exception {
params.set("username", JOE);
params.set("password", "Passwo3d");
ResponseEntity<Map> response = serverRunning.postForMap("/authenticate", params, headers);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertEquals(JOE, response.getBody().get("username"));
assertEquals(OriginKeys.UAA, response.getBody().get(OriginKeys.ORIGIN));
assertTrue(StringUtils.hasText((String)response.getBody().get("user_id")));
}
@Test
@OAuth2ContextConfiguration(LoginClient.class)
public void testAuthenticateMarissaReturnsUserID() throws Exception {
params.set("username", testAccounts.getUserName());
params.set("password", testAccounts.getPassword());
ResponseEntity<Map> response = serverRunning.postForMap("/authenticate", params, headers);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertEquals("marissa", response.getBody().get("username"));
assertEquals(OriginKeys.UAA, response.getBody().get(OriginKeys.ORIGIN));
assertTrue(StringUtils.hasText((String)response.getBody().get("user_id")));
}
@Test
@OAuth2ContextConfiguration(LoginClient.class)
public void testAuthenticateMarissaFails() throws Exception {
params.set("username", testAccounts.getUserName());
params.set("password", "");
ResponseEntity<Map> response = serverRunning.postForMap("/authenticate", params, headers);
assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode());
}
@Test
public void testAuthenticateDoesNotReturnsUserID() throws Exception {
params.set("username", testAccounts.getUserName());
params.set("password", testAccounts.getPassword());
ResponseEntity<Map> response = serverRunning.postForMap("/authenticate", params, headers);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertEquals("marissa", response.getBody().get("username"));
assertNull(response.getBody().get(OriginKeys.ORIGIN));
assertNull(response.getBody().get("user_id"));
}
@Test
@OAuth2ContextConfiguration(LoginClient.class)
public void testLoginServerCanAuthenticateUserForCf() throws Exception {
ImplicitResourceDetails resource = testAccounts.getDefaultImplicitResource();
params.set("client_id", resource.getClientId());
params.set("username", userForLoginServer.getUserName());
params.set(OriginKeys.ORIGIN, userForLoginServer.getOrigin());
params.set(UaaAuthenticationDetails.ADD_NEW, "false");
String redirect = resource.getPreEstablishedRedirectUri();
if (redirect != null) {
params.set("redirect_uri", redirect);
}
@SuppressWarnings("rawtypes")
ResponseEntity<Map> response = serverRunning.postForMap(serverRunning.getAuthorizationUri(), params, headers);
assertEquals(HttpStatus.FOUND, response.getStatusCode());
String results = response.getHeaders().getLocation().toString();
assertNotNull("There should be scopes: " + results, results.contains("#access_token"));
}
@Test
@OAuth2ContextConfiguration(LoginClient.class)
public void testLoginServerCanAuthenticateUserForAuthorizationCode() throws Exception {
params.set("client_id", testAccounts.getDefaultAuthorizationCodeResource().getClientId());
params.set("response_type", "code");
params.set("username", userForLoginServer.getUserName());
params.set(OriginKeys.ORIGIN, userForLoginServer.getOrigin());
params.set(UaaAuthenticationDetails.ADD_NEW, "false");
@SuppressWarnings("rawtypes")
ResponseEntity<Map> response = serverRunning.postForMap(serverRunning.getAuthorizationUri(), params, headers);
assertEquals(HttpStatus.OK, response.getStatusCode());
@SuppressWarnings("unchecked")
Map<String, Object> results = response.getBody();
// The approval page messaging response
assertNotNull("There should be scopes: " + results, results.get("scopes"));
}
@Test
@OAuth2ContextConfiguration(LoginClient.class)
public void testLoginServerCanAuthenticateUserWithIDForAuthorizationCode() throws Exception {
params.set("client_id", testAccounts.getDefaultAuthorizationCodeResource().getClientId());
params.set("response_type", "code");
params.set("user_id", userForLoginServer.getId());
params.set(UaaAuthenticationDetails.ADD_NEW, "false");
@SuppressWarnings("rawtypes")
ResponseEntity<Map> response = serverRunning.postForMap(serverRunning.getAuthorizationUri(), params, headers);
assertEquals(HttpStatus.OK, response.getStatusCode());
@SuppressWarnings("unchecked")
Map<String, Object> results = response.getBody();
// The approval page messaging response
assertNotNull("There should be scopes: " + results, results.get("scopes"));
}
@Test
@OAuth2ContextConfiguration(LoginClient.class)
public void testMissingUserInfoIsError() throws Exception {
params.set("client_id", testAccounts.getDefaultImplicitResource().getClientId());
params.remove("username");
@SuppressWarnings("rawtypes")
ResponseEntity<Map> response = serverRunning.postForMap(serverRunning.getAuthorizationUri(), params, headers);
// TODO: should be 302
assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode());
@SuppressWarnings("unchecked")
Map<String, String> results = response.getBody();
assertNotNull("There should be an error: " + results, results.containsKey("error"));
}
@Test
@OAuth2ContextConfiguration(LoginClient.class)
public void testMissingUsernameIsError() throws Exception {
((RestTemplate) serverRunning.getRestTemplate())
.setRequestFactory(new HttpComponentsClientHttpRequestFactory());
params.set("client_id", testAccounts.getDefaultImplicitResource().getClientId());
params.remove("username");
// Some of the user info is there but not enough to determine a username
params.set("given_name", "Mabel");
@SuppressWarnings("rawtypes")
ResponseEntity<Map> response = serverRunning.postForMap(serverRunning.getAuthorizationUri(), params, headers);
// TODO: should be 302
assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode());
@SuppressWarnings("unchecked")
Map<String, String> results = response.getBody();
assertNotNull("There should be an error: " + results, results.containsKey("error"));
}
@Test
@OAuth2ContextConfiguration(LoginClient.class)
public void testWrongUsernameIsErrorAddNewEnabled() throws Exception {
((RestTemplate) serverRunning.getRestTemplate())
.setRequestFactory(new HttpComponentsClientHttpRequestFactory());
ImplicitResourceDetails resource = testAccounts.getDefaultImplicitResource();
params.set("client_id", resource.getClientId());
params.set("username", "bogus1");
params.set(UaaAuthenticationDetails.ADD_NEW, "true");
String redirect = resource.getPreEstablishedRedirectUri();
if (redirect != null) {
params.set("redirect_uri", redirect);
}
@SuppressWarnings("rawtypes")
ResponseEntity<Map> response = serverRunning.postForMap(serverRunning.getAuthorizationUri(), params, headers);
// add_new:true user accounts are automatically provisioned.
assertEquals(HttpStatus.FOUND, response.getStatusCode());
String results = response.getHeaders().getLocation().getFragment();
assertTrue("There should be an access token: " + results, results.contains("access_token"));
}
@Test
@OAuth2ContextConfiguration(LoginClient.class)
public void testWrongUsernameIsErrorAddNewDisabled() throws Exception {
((RestTemplate) serverRunning.getRestTemplate())
.setRequestFactory(new HttpComponentsClientHttpRequestFactory());
ImplicitResourceDetails resource = testAccounts.getDefaultImplicitResource();
params.set("client_id", resource.getClientId());
params.set("username", "bogus2");
params.set(UaaAuthenticationDetails.ADD_NEW, "false");
String redirect = resource.getPreEstablishedRedirectUri();
if (redirect != null) {
params.set("redirect_uri", redirect);
}
@SuppressWarnings("rawtypes")
ResponseEntity<Map> response = serverRunning.postForMap(serverRunning.getAuthorizationUri(), params, headers);
assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode());
@SuppressWarnings("unchecked")
Map<String, String> results = response.getBody();
assertNotNull("There should be an error: " + results, results.containsKey("error"));
}
@Test
@OAuth2ContextConfiguration(LoginClient.class)
public void testAddNewUserWithWrongEmailFormat() throws Exception {
((RestTemplate) serverRunning.getRestTemplate())
.setRequestFactory(new HttpComponentsClientHttpRequestFactory());
params.set("client_id", testAccounts.getDefaultImplicitResource().getClientId());
params.set("source","login");
params.set("username", "newuser");
params.remove("given_name");
params.remove("family_name");
params.set("email", "noAtSign");
params.set(UaaAuthenticationDetails.ADD_NEW, "true");
@SuppressWarnings("rawtypes")
ResponseEntity<Map> response = serverRunning.postForMap(serverRunning.getAuthorizationUri(), params, headers);
assertNotNull(response);
assertNotEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode());
assertEquals(HttpStatus.FOUND, response.getStatusCode());
@SuppressWarnings("unchecked")
Map<String, String> results = response.getBody();
if (results != null) {
assertFalse("There should not be an error: " + results, results.containsKey("error"));
}
}
@Test
@OAuth2ContextConfiguration(LoginClient.class)
public void testLoginServerCfPasswordToken() throws Exception {
ImplicitResourceDetails resource = testAccounts.getDefaultImplicitResource();
HttpHeaders headers = new HttpHeaders();
headers.add("Accept",MediaType.APPLICATION_JSON_VALUE);
params.set("client_id", resource.getClientId());
params.set("client_secret","");
params.set("source","login");
params.set("username", userForLoginServer.getUserName());
params.set(OriginKeys.ORIGIN, userForLoginServer.getOrigin());
params.set(UaaAuthenticationDetails.ADD_NEW, "false");
params.set("grant_type", "password");
String redirect = resource.getPreEstablishedRedirectUri();
if (redirect != null) {
params.set("redirect_uri", redirect);
}
@SuppressWarnings("rawtypes")
ResponseEntity<Map> response = serverRunning.postForMap(serverRunning.getAccessTokenUri(), params, headers);
assertEquals(HttpStatus.OK, response.getStatusCode());
Map results = response.getBody();
assertTrue("There should be a token: " + results, results.containsKey("access_token"));
assertTrue("There should be a refresh: " + results, results.containsKey("refresh_token"));
}
@Test
@OAuth2ContextConfiguration(LoginClient.class)
public void testLoginServerWithoutBearerToken() throws Exception {
ImplicitResourceDetails resource = testAccounts.getDefaultImplicitResource();
HttpHeaders headers = new HttpHeaders();
headers.add("Accept",MediaType.APPLICATION_JSON_VALUE);
headers.add("Authorization", getAuthorizationEncodedValue(resource.getClientId(), ""));
params.set("client_id", resource.getClientId());
params.set("client_secret","");
params.set("source","login");
params.set(UaaAuthenticationDetails.ADD_NEW, "false");
params.set("grant_type", "password");
String redirect = resource.getPreEstablishedRedirectUri();
if (redirect != null) {
params.set("redirect_uri", redirect);
}
@SuppressWarnings("rawtypes")
ResponseEntity<Map> response = serverRunning.postForMap(serverRunning.getAccessTokenUri(), params, headers);
assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode());
}
@Test
@OAuth2ContextConfiguration(LoginClient.class)
public void testLoginServerCfInvalidClientPasswordToken() throws Exception {
ImplicitResourceDetails resource = testAccounts.getDefaultImplicitResource();
HttpHeaders headers = new HttpHeaders();
headers.add("Accept",MediaType.APPLICATION_JSON_VALUE);
params.set("client_id", resource.getClientId());
params.set("client_secret","bogus");
params.set("source","login");
params.set(UaaAuthenticationDetails.ADD_NEW, "false");
params.set("grant_type", "password");
String redirect = resource.getPreEstablishedRedirectUri();
if (redirect != null) {
params.set("redirect_uri", redirect);
}
@SuppressWarnings("rawtypes")
ResponseEntity<Map> response = serverRunning.postForMap(serverRunning.getAccessTokenUri(), params, headers);
HttpStatus statusCode = response.getStatusCode();
assertTrue("Status code should be 401 or 403.", statusCode==HttpStatus.FORBIDDEN || statusCode==HttpStatus.UNAUTHORIZED);
}
@Test
@OAuth2ContextConfiguration(AppClient.class)
public void testLoginServerCfInvalidClientToken() throws Exception {
ImplicitResourceDetails resource = testAccounts.getDefaultImplicitResource();
HttpHeaders headers = new HttpHeaders();
headers.add("Accept",MediaType.APPLICATION_JSON_VALUE);
params.set("client_id", resource.getClientId());
params.set("client_secret","bogus");
params.set("source","login");
params.set(UaaAuthenticationDetails.ADD_NEW, "false");
params.set("grant_type", "password");
String redirect = resource.getPreEstablishedRedirectUri();
if (redirect != null) {
params.set("redirect_uri", redirect);
}
@SuppressWarnings("rawtypes")
ResponseEntity<Map> response = serverRunning.postForMap(serverRunning.getAccessTokenUri(), params, headers);
HttpStatus statusCode = response.getStatusCode();
assertTrue("Status code should be 401 or 403.", statusCode==HttpStatus.FORBIDDEN || statusCode==HttpStatus.UNAUTHORIZED);
}
private String getAuthorizationEncodedValue(String username, String password) {
String auth = username + ":" + password;
byte[] encodedAuth = Base64.encodeBase64(auth.getBytes(Charset.forName("US-ASCII")));
String authHeader = "Basic " + new String( encodedAuth );
return authHeader;
}
private static class LoginClient extends ClientCredentialsResourceDetails {
@SuppressWarnings("unused")
public LoginClient(Object target) {
LoginServerSecurityIntegrationTests test = (LoginServerSecurityIntegrationTests) target;
ClientCredentialsResourceDetails resource = test.testAccounts.getClientCredentialsResource(
new String[] {"oauth.login"}, "login", "loginsecret");
setClientId(resource.getClientId());
setClientSecret(resource.getClientSecret());
setId(getClientId());
setAccessTokenUri(test.serverRunning.getAccessTokenUri());
}
}
private static class AppClient extends ClientCredentialsResourceDetails {
@SuppressWarnings("unused")
public AppClient(Object target) {
LoginServerSecurityIntegrationTests test = (LoginServerSecurityIntegrationTests) target;
ClientCredentialsResourceDetails resource = test.testAccounts.getClientCredentialsResource("app", "appclientsecret");
setClientId(resource.getClientId());
setClientSecret(resource.getClientSecret());
setId(getClientId());
setAccessTokenUri(test.serverRunning.getAccessTokenUri());
}
}
}