package org.cloudfoundry.identity.uaa.login;
import org.cloudfoundry.identity.uaa.account.AccountCreationService;
import org.cloudfoundry.identity.uaa.account.EmailAccountCreationService;
import org.cloudfoundry.identity.uaa.codestore.ExpiringCode;
import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeStore;
import org.cloudfoundry.identity.uaa.constants.OriginKeys;
import org.cloudfoundry.identity.uaa.error.UaaException;
import org.cloudfoundry.identity.uaa.message.MessageService;
import org.cloudfoundry.identity.uaa.message.MessageType;
import org.cloudfoundry.identity.uaa.scim.ScimUser;
import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning;
import org.cloudfoundry.identity.uaa.scim.exception.InvalidPasswordException;
import org.cloudfoundry.identity.uaa.scim.exception.ScimResourceAlreadyExistsException;
import org.cloudfoundry.identity.uaa.scim.validate.PasswordValidator;
import org.cloudfoundry.identity.uaa.util.JsonUtils;
import org.cloudfoundry.identity.uaa.zone.BrandingInformation;
import org.cloudfoundry.identity.uaa.zone.IdentityZone;
import org.cloudfoundry.identity.uaa.zone.IdentityZoneConfiguration;
import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder;
import org.cloudfoundry.identity.uaa.zone.MultitenancyFixture;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.NoSuchClientException;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.thymeleaf.spring4.SpringTemplateEngine;
import java.sql.Timestamp;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import static org.cloudfoundry.identity.uaa.codestore.ExpiringCodeType.REGISTRATION;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {ThymeleafAdditional.class,ThymeleafConfig.class})
public class EmailAccountCreationServiceTests {
private EmailAccountCreationService emailAccountCreationService;
private MessageService messageService;
private ExpiringCodeStore codeStore;
private ScimUserProvisioning scimUserProvisioning;
private ClientDetailsService clientDetailsService;
private ScimUser user = null;
private ExpiringCode code = null;
private ClientDetails details = null;
private PasswordValidator passwordValidator;
@Autowired
@Qualifier("mailTemplateEngine")
SpringTemplateEngine templateEngine;
@Rule
public ExpectedException expectedEx = ExpectedException.none();
@Before
public void setUp() throws Exception {
SecurityContextHolder.clearContext();
messageService = mock(MessageService.class);
codeStore = mock(ExpiringCodeStore.class);
scimUserProvisioning = mock(ScimUserProvisioning.class);
clientDetailsService = mock(ClientDetailsService.class);
details = mock(ClientDetails.class);
passwordValidator = mock(PasswordValidator.class);
emailAccountCreationService = initEmailAccountCreationService();
MockHttpServletRequest request = new MockHttpServletRequest();
request.setScheme("http");
request.setServerName("uaa.example.com");
ServletRequestAttributes attrs = new ServletRequestAttributes(request);
RequestContextHolder.setRequestAttributes(attrs);
}
private EmailAccountCreationService initEmailAccountCreationService() {
return new EmailAccountCreationService(templateEngine, messageService, codeStore,
scimUserProvisioning, clientDetailsService, passwordValidator);
}
@After
public void tearDown() {
SecurityContextHolder.clearContext();
IdentityZoneHolder.clear();
}
@Test
public void testBeginActivation() throws Exception {
String redirectUri = "";
String data = setUpForSuccess(redirectUri);
when(scimUserProvisioning.createUser(any(ScimUser.class), anyString())).thenReturn(user);
when(codeStore.generateCode(eq(data), any(Timestamp.class), eq(REGISTRATION.name()))).thenReturn(code);
emailAccountCreationService.beginActivation("user@example.com", "password", "login", redirectUri);
String emailBody = captorEmailBody("Activate your account");
assertThat(emailBody, containsString("an account"));
assertThat(emailBody, containsString("<a href=\"http://uaa.example.com/verify_user?code=the_secret_code\">Activate your account</a>"));
}
@Test
public void testBeginActivationInOtherZone() throws Exception {
String redirectUri = "http://login.example.com/redirect/";
String data = setUpForSuccess(redirectUri);
IdentityZone zone = MultitenancyFixture.identityZone("test-zone-id", "test");
IdentityZoneHolder.set(zone);
MockHttpServletRequest request = new MockHttpServletRequest();
request.setScheme("http");
request.setServerName("test.uaa.example.com");
ServletRequestAttributes attrs = new ServletRequestAttributes(request);
RequestContextHolder.setRequestAttributes(attrs);
when(scimUserProvisioning.createUser(any(ScimUser.class), anyString())).thenReturn(user);
when(codeStore.generateCode(eq(data), any(Timestamp.class), eq(REGISTRATION.name()))).thenReturn(code);
emailAccountCreationService.beginActivation("user@example.com", "password", "login", redirectUri);
String emailBody = captorEmailBody("Activate your account");
assertThat(emailBody, containsString("A request has been made to activate an account for:"));
assertThat(emailBody, containsString("<a href=\"http://test.uaa.example.com/verify_user?code=the_secret_code\">Activate your account</a>"));
assertThat(emailBody, containsString("Thank you,<br />\n " + zone.getName()));
assertThat(emailBody, not(containsString("Cloud Foundry")));
}
@Test
public void testBeginActivationWithCompanyNameConfigured() throws Exception {
testBeginActivationWithCompanyNameConfigured("Best Company");
}
public void testBeginActivationWithCompanyNameConfigured(String companyName) throws Exception {
IdentityZoneConfiguration defaultConfig = IdentityZoneHolder.get().getConfig();
BrandingInformation branding = new BrandingInformation();
branding.setCompanyName(companyName);
IdentityZoneConfiguration config = new IdentityZoneConfiguration();
config.setBranding(branding);
IdentityZoneHolder.get().setConfig(config);
try {
emailAccountCreationService = initEmailAccountCreationService();
String data = setUpForSuccess(null);
when(scimUserProvisioning.createUser(any(ScimUser.class), anyString())).thenReturn(user);
when(codeStore.generateCode(eq(data), any(Timestamp.class), eq(REGISTRATION.name()))).thenReturn(code);
emailAccountCreationService.beginActivation("user@example.com", "password", "login", null);
String emailBody = captorEmailBody("Activate your " + companyName + " account");
assertThat(emailBody, containsString(companyName + " account"));
assertThat(emailBody, containsString("<a href=\"http://uaa.example.com/verify_user?code=the_secret_code\">Activate your account</a>"));
} finally {
IdentityZoneHolder.get().setConfig(defaultConfig);
}
}
@Test
public void testBeginActivationWithCompanyNameConfigured_With_UTF8() throws Exception {
String utf8String = "\u7433\u8D3A";
testBeginActivationWithCompanyNameConfigured(utf8String);
}
@Test(expected = UaaException.class)
public void testBeginActivationWithExistingUser() throws Exception {
setUpForSuccess(null);
user.setVerified(true);
when(scimUserProvisioning.query(anyString())).thenReturn(Arrays.asList(new ScimUser[]{user}));
when(scimUserProvisioning.createUser(any(ScimUser.class), anyString())).thenThrow(new ScimResourceAlreadyExistsException("duplicate"));
emailAccountCreationService.beginActivation("user@example.com", "password", "login", null);
}
@Test
public void testBeginActivationWithUnverifiedExistingUser() throws Exception {
String data = setUpForSuccess("existing-user-id", null);
user.setId("existing-user-id");
user.setVerified(false);
when(scimUserProvisioning.createUser(any(ScimUser.class), anyString())).thenThrow(new ScimResourceAlreadyExistsException("duplicate"));
when(scimUserProvisioning.query(anyString())).thenReturn(Arrays.asList(new ScimUser[]{user}));
when(codeStore.generateCode(eq(data), any(Timestamp.class), eq(REGISTRATION.name()))).thenReturn(code);
MockHttpServletRequest request = new MockHttpServletRequest();
request.setProtocol("http");
request.setContextPath("/login");
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
emailAccountCreationService.beginActivation("user@example.com", "password", "login", null);
verify(messageService).sendMessage(
eq("user@example.com"),
eq(MessageType.CREATE_ACCOUNT_CONFIRMATION),
anyString(),
anyString()
);
}
@Test
public void testCompleteActivation() throws Exception {
setUpForSuccess("");
when(scimUserProvisioning.createUser(any(ScimUser.class), anyString())).thenReturn(user);
when(codeStore.retrieveCode("the_secret_code")).thenReturn(code);
when(scimUserProvisioning.retrieve(anyString())).thenReturn(user);
when(scimUserProvisioning.verifyUser(anyString(), anyInt())).thenReturn(user);
ClientDetails client = mock(ClientDetails.class);
when(clientDetailsService.loadClientByClientId(anyString())).thenReturn(client);
when(client.getRegisteredRedirectUri()).thenReturn(Collections.emptySet());
Map<String, Object> map = new HashMap<>();
map.put(EmailAccountCreationService.SIGNUP_REDIRECT_URL, "http://fallback.url/redirect");
when(client.getAdditionalInformation()).thenReturn(map);
AccountCreationService.AccountCreationResponse accountCreation = emailAccountCreationService.completeActivation("the_secret_code");
assertEquals("user@example.com", accountCreation.getUsername());
assertEquals("newly-created-user-id", accountCreation.getUserId());
assertNotNull(accountCreation.getUserId());
}
@Test
public void completeActivation_usesAntPathMatching() throws Exception {
setUpForSuccess("http://redirect.uri/");
when(scimUserProvisioning.createUser(any(ScimUser.class), anyString())).thenReturn(user);
when(codeStore.retrieveCode("the_secret_code")).thenReturn(code);
when(scimUserProvisioning.retrieve(anyString())).thenReturn(user);
when(scimUserProvisioning.verifyUser(anyString(), anyInt())).thenReturn(user);
ClientDetails client = mock(ClientDetails.class);
when(clientDetailsService.loadClientByClientId(anyString())).thenReturn(client);
when(client.getRegisteredRedirectUri()).thenReturn(Collections.singleton("http://redirect.uri/*"));
AccountCreationService.AccountCreationResponse accountCreation = emailAccountCreationService.completeActivation("the_secret_code");
assertThat(accountCreation.getRedirectLocation(), equalTo("http://redirect.uri/"));
}
@Test
public void completeActivitionWithClientNotFound() throws Exception {
setUpForSuccess("");
when(codeStore.retrieveCode("the_secret_code")).thenReturn(code);
when(scimUserProvisioning.verifyUser(anyString(), anyInt())).thenReturn(user);
when(scimUserProvisioning.retrieve(anyString())).thenReturn(user);
doThrow(new NoSuchClientException("Client not found")).when(clientDetailsService).loadClientByClientId(anyString());
AccountCreationService.AccountCreationResponse accountCreation = emailAccountCreationService.completeActivation("the_secret_code");
assertEquals("home", accountCreation.getRedirectLocation());
}
@Test
public void completeActivationWithInvalidClientRedirect() throws Exception {
setUpForSuccess("http://redirect_not_found.example.com/");
when(scimUserProvisioning.createUser(any(ScimUser.class), anyString())).thenReturn(user);
when(codeStore.retrieveCode("the_secret_code")).thenReturn(code);
when(scimUserProvisioning.verifyUser(anyString(), anyInt())).thenReturn(user);
when(scimUserProvisioning.retrieve(anyString())).thenReturn(user);
when(clientDetailsService.loadClientByClientId(anyString())).thenReturn(details);
AccountCreationService.AccountCreationResponse accountCreation = emailAccountCreationService.completeActivation("the_secret_code");
assertEquals("user@example.com", accountCreation.getUsername());
assertEquals("newly-created-user-id", accountCreation.getUserId());
assertEquals("home", accountCreation.getRedirectLocation());
}
@Test
public void completeActivationWithValidClientRedirect() throws Exception {
setUpForSuccess("http://example.com/redirect");
when(scimUserProvisioning.createUser(any(ScimUser.class), anyString())).thenReturn(user);
when(codeStore.retrieveCode("the_secret_code")).thenReturn(code);
when(scimUserProvisioning.verifyUser(anyString(), anyInt())).thenReturn(user);
when(scimUserProvisioning.retrieve(anyString())).thenReturn(user);
when(clientDetailsService.loadClientByClientId(anyString())).thenReturn(details);
AccountCreationService.AccountCreationResponse accountCreation = emailAccountCreationService.completeActivation("the_secret_code");
assertEquals("http://example.com/redirect", accountCreation.getRedirectLocation());
}
@Test
public void testCompleteActivationWithExpiredCode() throws Exception {
when(codeStore.retrieveCode("expiring_code")).thenReturn(null);
try {
emailAccountCreationService.completeActivation("expiring_code");
fail();
} catch (HttpClientErrorException e) {
assertThat(e.getStatusCode(), equalTo(BAD_REQUEST));
}
}
@Test(expected = InvalidPasswordException.class)
public void beginActivation_throwsException_ifPasswordViolatesPolicy() throws Exception {
doThrow(new InvalidPasswordException("Oh hell no")).when(passwordValidator).validate(anyString());
emailAccountCreationService.beginActivation("user@example.com", "some password", null, null);
verify(passwordValidator).validate("some password");
}
@Test
public void nonMatchingCodeTypeDisallowsActivation() throws Exception {
expectedEx.expect(HttpClientErrorException.class);
expectedEx.expectMessage("400 BAD_REQUEST");
Timestamp ts = new Timestamp(System.currentTimeMillis() + (60 * 60 * 1000));
Map<String, Object> data = new HashMap<>();
data.put("user_id", "user-id");
data.put("email", "user@example.com");
data.put("client_id", "login");
code = new ExpiringCode("the_secret_code", ts, JsonUtils.writeValueAsString(data), "incorrect-intent-type");
when(codeStore.retrieveCode("the_secret_code")).thenReturn(code);
emailAccountCreationService.completeActivation("the_secret_code");
}
private String setUpForSuccess(String redirectUri) throws Exception {
return setUpForSuccess("newly-created-user-id", redirectUri);
}
private String setUpForSuccess(String userId, String redirectUri) throws Exception {
user = new ScimUser(
userId,
"user@example.com",
"givenName",
"familyName");
user.setPrimaryEmail("user@example.com");
user.setPassword("password");
user.setOrigin(OriginKeys.UAA);
user.setActive(true);
user.setVerified(false);
Timestamp ts = new Timestamp(System.currentTimeMillis() + (60 * 60 * 1000)); // 1 hour
Map<String, Object> data = new HashMap<>();
data.put("user_id", userId);
data.put("email", "user@example.com");
data.put("client_id", "login");
if (redirectUri != null) {
data.put("redirect_uri", redirectUri);
}
code = new ExpiringCode("the_secret_code", ts, JsonUtils.writeValueAsString(data), REGISTRATION.name());
when(details.getClientId()).thenReturn("login");
when(details.getRegisteredRedirectUri()).thenReturn(Collections.singleton("http://example.com/*"));
return JsonUtils.writeValueAsString(data);
}
private String captorEmailBody(String subject) {
ArgumentCaptor<String> emailBodyArgument = ArgumentCaptor.forClass(String.class);
verify(messageService).sendMessage(
eq("user@example.com"),
eq(MessageType.CREATE_ACCOUNT_CONFIRMATION),
eq(subject),
emailBodyArgument.capture()
);
return emailBodyArgument.getValue();
}
}