Expand Up @@ -12,6 +12,7 @@
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeNotNull;
import static org.junit.Assume.assumeTrue;

import java.io.File;
Expand All @@ -34,6 +35,8 @@
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.io.IOUtils;
import org.cloudfoundry.client.lib.domain.ApplicationLog;
Expand All @@ -55,6 +58,7 @@
import org.cloudfoundry.client.lib.domain.CloudServicePlan;
import org.cloudfoundry.client.lib.domain.CloudSpace;
import org.cloudfoundry.client.lib.domain.CloudStack;
import org.cloudfoundry.client.lib.domain.CloudUser;
import org.cloudfoundry.client.lib.domain.CrashInfo;
import org.cloudfoundry.client.lib.domain.CrashesInfo;
import org.cloudfoundry.client.lib.domain.InstanceInfo;
Expand All @@ -63,7 +67,6 @@
import org.cloudfoundry.client.lib.domain.InstancesInfo;
import org.cloudfoundry.client.lib.domain.SecurityGroupRule;
import org.cloudfoundry.client.lib.domain.Staging;
import org.cloudfoundry.client.lib.domain.CloudUser;
import org.cloudfoundry.client.lib.oauth2.OauthClient;
import org.cloudfoundry.client.lib.rest.CloudControllerClient;
import org.cloudfoundry.client.lib.rest.CloudControllerClientFactory;
Expand Down Expand Up @@ -91,14 +94,15 @@
import org.junit.runner.Description;
import org.junit.runner.RunWith;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.*;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.ResponseExtractor;
Expand Down Expand Up @@ -157,6 +161,8 @@ public class CloudFoundryClientTest {

public static final int STARTUP_TIMEOUT = Integer.getInteger("ccng.startup.timeout", 60000);

private static final boolean SSO_ENABLED = "true".equalsIgnoreCase(System.getProperty("ccng.sso.enabled", "false").trim());

private static String defaultDomainName = null;

private static HttpProxyConfiguration httpProxyConfiguration;
Expand Down Expand Up @@ -2248,6 +2254,145 @@ private void deleteAnyOrphanedTestSecurityGroups(){
}
}

@Test
public void loginWithPasscode() throws Exception {
assumeTrue(SSO_ENABLED);

String passcode = getOtpPasscodeForTest();
assumeNotNull(passcode);
CloudCredentials credentials = new CloudCredentials(passcode);

URL cloudControllerUrl = new URL(CCNG_API_URL);
CloudFoundryClient clientForPasscodeLogin =
new CloudFoundryClient(credentials, cloudControllerUrl, httpProxyConfiguration);
// setting up client triggers login - so no need to login() again
assertTrue("orgs should be retrieved (login successful)", !clientForPasscodeLogin.getOrganizations().isEmpty());
}

@Test
public void loginWithPasscodeHandlesMultipleCallsToLogin() throws Exception {
assumeTrue(SSO_ENABLED);

String passcode = getOtpPasscodeForTest();
assumeNotNull(passcode);
CloudCredentials credentials = new CloudCredentials(passcode);

URL cloudControllerUrl = new URL(CCNG_API_URL);
CloudFoundryClient clientForPasscodeLogin =
new CloudFoundryClient(credentials, cloudControllerUrl, httpProxyConfiguration);
clientForPasscodeLogin.login();
clientForPasscodeLogin.login();
clientForPasscodeLogin.login();
assertTrue("orgs should be retrieved (login successful)", !clientForPasscodeLogin.getOrganizations().isEmpty());
}

@Test
public void refreshTokenOnExpirationWithPasscodeLogin() throws Exception {
assumeTrue(SSO_ENABLED);

String passcode = getOtpPasscodeForTest();
assumeNotNull(passcode);
CloudCredentials credentials = new CloudCredentials(passcode);

URL cloudControllerUrl = new URL(CCNG_API_URL);

CloudControllerClientFactory factory =
new CloudControllerClientFactory(httpProxyConfiguration, CCNG_API_SSL);
CloudControllerClient client = factory.newCloudController(cloudControllerUrl, credentials, CCNG_USER_ORG, CCNG_USER_SPACE);

client.login();

validateClientAccess(client);

OauthClient oauthClient = factory.getOauthClient();
OAuth2AccessToken token = oauthClient.getToken();
if (token instanceof DefaultOAuth2AccessToken) {
// set the token expiration to "now", forcing the access token to be refreshed
((DefaultOAuth2AccessToken) token).setExpiration(new Date());
validateClientAccess(client);
} else {
fail("Error forcing expiration of access token");
}
}

@Test
public void obtainPasscodeForLoggedInUser() throws Exception {
assumeTrue(SSO_ENABLED);

String passcodePattern = "[A-Za-z0-9]{6}";

String passcode1 = getOtpPasscodeForTest();
assumeNotNull(passcode1);
assertTrue("Passcode is retrieved and looks valid", passcode1.matches(passcodePattern));

String passcode2 = getOtpPasscodeForTest();
assertTrue("Can obtain multiple passcodes", !passcode2.equals(passcode1));
assertTrue("Can obtain multiple passcodes", passcode2.matches(passcodePattern));
}

private String getOtpPasscodeForTest() throws Exception {
try {
CloudInfo info = connectedClient.getCloudInfo();
List<String> cookies = doFormAuthOnLoginServer(info.getAuthorizationEndpoint());
return retrieveOtpPasscodeFromLoginServerSession(info.getAuthorizationEndpoint(), cookies);
}
catch(Exception e) {
System.err.println("WARNING: Skipping some tests since obtaining passcodes failed: " + e.getMessage());
e.printStackTrace();
return null;
}
}

private List<String> doFormAuthOnLoginServer(String loginServerUrl) {
String loginEndpointUrl = loginServerUrl + "/login.do";

RestUtil restUtil = new RestUtil();
RestTemplate restTemplate = restUtil.createRestTemplate(httpProxyConfiguration, CCNG_API_SSL);
MultiValueMap<String, String> loginRequestBody = new LinkedMultiValueMap<>();
loginRequestBody.add("username", CCNG_USER_EMAIL);
loginRequestBody.add("password", CCNG_USER_PASS);
HttpHeaders loginRequestHeaders = new HttpHeaders();
loginRequestHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
HttpEntity<MultiValueMap<String, String>> loginRequest = new HttpEntity<>(loginRequestBody, loginRequestHeaders);
HttpEntity<String> response = restTemplate.exchange(loginEndpointUrl, HttpMethod.POST, loginRequest, String.class);

HttpHeaders headers = response.getHeaders();
List<String> cookies = headers.get("Set-Cookie");
if(cookies == null || cookies.isEmpty()) {
throw new RuntimeException("No cookies set when logging in on login server for passcodes: " + response);
}
return cookies;
}

private String retrieveOtpPasscodeFromLoginServerSession(String loginServerUrl, List<String> sessionCookies) {
String passcodeUrl = loginServerUrl + "/passcode";

RestUtil restUtil = new RestUtil();
RestTemplate restTemplate = restUtil.createRestTemplate(httpProxyConfiguration, CCNG_API_SSL);

HttpHeaders requestHeaders = new HttpHeaders();
for(String cookie : sessionCookies) {
requestHeaders.add("Cookie", cookie);
}
requestHeaders.add("Accept", "*/*");
HttpEntity requestEntity = new HttpEntity(null, requestHeaders);
ResponseEntity passcodeResponse = restTemplate.exchange(
passcodeUrl,
HttpMethod.GET,
requestEntity,
String.class);
return parseOtpPasscodeFromHtml((String) passcodeResponse.getBody());
}

private String parseOtpPasscodeFromHtml(String html) {
String passcodeRegex = "<h1>Temporary Authentication Code</h1>.*<h2>([A-Za-z0-9]{6})</h2>";
Pattern passcodePattern = Pattern.compile(passcodeRegex, Pattern.DOTALL);
Matcher passcodeMatcher = passcodePattern.matcher(html);
assertTrue(passcodeMatcher.find());

return passcodeMatcher.group(1);
}

//
// Shared test methods
//
Expand Down