package org.cloudfoundry.identity.uaa.login;
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.invitations.EmailInvitationsService;
import org.cloudfoundry.identity.uaa.message.MessageService;
import org.cloudfoundry.identity.uaa.scim.ScimUser;
import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning;
import org.cloudfoundry.identity.uaa.util.JsonUtils;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.NoSuchClientException;
import org.springframework.security.oauth2.provider.client.BaseClientDetails;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.annotation.DirtiesContext.ClassMode;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.context.ConfigurableWebApplicationContext;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import java.sql.Timestamp;
import java.util.HashMap;
import java.util.Map;
import static org.cloudfoundry.identity.uaa.codestore.ExpiringCodeType.INVITATION;
import static org.cloudfoundry.identity.uaa.constants.OriginKeys.UAA;
import static org.cloudfoundry.identity.uaa.invitations.EmailInvitationsService.EMAIL;
import static org.cloudfoundry.identity.uaa.invitations.EmailInvitationsService.USER_ID;
import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyObject;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.security.oauth2.common.util.OAuth2Utils.CLIENT_ID;
import static org.springframework.security.oauth2.common.util.OAuth2Utils.REDIRECT_URI;
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(classes = EmailInvitationsServiceTests.ContextConfiguration.class)
@DirtiesContext(classMode=ClassMode.AFTER_EACH_TEST_METHOD)
public class EmailInvitationsServiceTests {
@Autowired
ConfigurableWebApplicationContext webApplicationContext;
@Autowired
ExpiringCodeStore expiringCodeStore;
@Autowired
EmailInvitationsService emailInvitationsService;
@Autowired
MessageService messageService;
@Autowired
ScimUserProvisioning scimUserProvisioning;
@Autowired
ClientDetailsService clientDetailsService;
@Rule
public ExpectedException expectedEx = ExpectedException.none();
@Before
public void setUp() throws Exception {
SecurityContextHolder.clearContext();
MockMvcBuilders.webAppContextSetup(webApplicationContext)
.build();
}
@After
public void tearDown() {
SecurityContextHolder.clearContext();
}
@Test
public void acceptInvitationNoClientId() throws Exception {
ScimUser user = new ScimUser("user-id-001", "user@example.com", "first", "last");
user.setOrigin(UAA);
when(scimUserProvisioning.retrieve(eq("user-id-001"))).thenReturn(user);
when(scimUserProvisioning.verifyUser(anyString(), anyInt())).thenReturn(user);
when(scimUserProvisioning.update(anyString(), anyObject())).thenReturn(user);
Map<String,String> userData = new HashMap<>();
userData.put(USER_ID, "user-id-001");
userData.put(EMAIL, "user@example.com");
when(expiringCodeStore.retrieveCode(anyString())).thenReturn(new ExpiringCode("code", new Timestamp(System.currentTimeMillis()), JsonUtils.writeValueAsString(userData), INVITATION.name()));
String redirectLocation = emailInvitationsService.acceptInvitation("code", "password").getRedirectUri();
verify(scimUserProvisioning).verifyUser(user.getId(), user.getVersion());
verify(scimUserProvisioning).changePassword(user.getId(), null, "password");
assertEquals("/home", redirectLocation);
}
@Test
public void nonMatchingCodeIntent() {
expectedEx.expect(HttpClientErrorException.class);
expectedEx.expectMessage("400 BAD_REQUEST");
Map<String,String> userData = new HashMap<>();
userData.put(USER_ID, "user-id-001");
userData.put(EMAIL, "user@example.com");
when(expiringCodeStore.retrieveCode(anyString())).thenReturn(new ExpiringCode("code", new Timestamp(System.currentTimeMillis()), JsonUtils.writeValueAsString(userData), "wrong-intent"));
emailInvitationsService.acceptInvitation("code", "password").getRedirectUri();
}
@Test
public void acceptInvitation_withoutPasswordUpdate() throws Exception {
ScimUser user = new ScimUser("user-id-001", "user@example.com", "first", "last");
user.setOrigin(UAA);
when(scimUserProvisioning.retrieve(eq("user-id-001"))).thenReturn(user);
when(scimUserProvisioning.verifyUser(anyString(), anyInt())).thenReturn(user);
Map<String,String> userData = new HashMap<>();
userData.put(USER_ID, "user-id-001");
userData.put(EMAIL, "user@example.com");
when(expiringCodeStore.retrieveCode(anyString())).thenReturn(new ExpiringCode("code", new Timestamp(System.currentTimeMillis()), JsonUtils.writeValueAsString(userData), INVITATION.name()));
emailInvitationsService.acceptInvitation("code", "").getRedirectUri();
verify(scimUserProvisioning).verifyUser(user.getId(), user.getVersion());
verify(scimUserProvisioning, never()).changePassword(anyString(), anyString(), anyString());
}
@Test
public void acceptInvitationWithClientNotFound() throws Exception {
ScimUser user = new ScimUser("user-id-001", "user@example.com", "first", "last");
user.setOrigin(OriginKeys.UAA);
when(scimUserProvisioning.verifyUser(anyString(), anyInt())).thenReturn(user);
when(scimUserProvisioning.update(anyString(), anyObject())).thenReturn(user);
when(scimUserProvisioning.retrieve(eq("user-id-001"))).thenReturn(user);
doThrow(new NoSuchClientException("Client not found")).when(clientDetailsService).loadClientByClientId("client-not-found");
Map<String,String> userData = new HashMap<>();
userData.put(USER_ID, "user-id-001");
userData.put(EMAIL, "user@example.com");
userData.put(CLIENT_ID, "client-not-found");
when(expiringCodeStore.retrieveCode(anyString())).thenReturn(new ExpiringCode("code", new Timestamp(System.currentTimeMillis()), JsonUtils.writeValueAsString(userData), INVITATION.name()));
String redirectLocation = emailInvitationsService.acceptInvitation("code", "password").getRedirectUri();
verify(scimUserProvisioning).verifyUser(user.getId(), user.getVersion());
verify(scimUserProvisioning).changePassword(user.getId(), null, "password");
assertEquals("/home", redirectLocation);
}
@Test
public void acceptInvitationWithValidRedirectUri() throws Exception {
ScimUser user = new ScimUser("user-id-001", "user@example.com", "first", "last");
user.setOrigin(UAA);
BaseClientDetails clientDetails = new BaseClientDetails("client-id", null, null, null, null, "http://example.com/*/");
when(scimUserProvisioning.retrieve(eq("user-id-001"))).thenReturn(user);
when(scimUserProvisioning.verifyUser(anyString(), anyInt())).thenReturn(user);
when(scimUserProvisioning.update(anyString(), anyObject())).thenReturn(user);
when(clientDetailsService.loadClientByClientId("acmeClientId")).thenReturn(clientDetails);
Map<String,String> userData = new HashMap<>();
userData.put(USER_ID, "user-id-001");
userData.put(EMAIL, "user@example.com");
userData.put(CLIENT_ID, "acmeClientId");
userData.put(REDIRECT_URI, "http://example.com/redirect/");
when(expiringCodeStore.retrieveCode(anyString())).thenReturn(new ExpiringCode("code", new Timestamp(System.currentTimeMillis()), JsonUtils.writeValueAsString(userData), INVITATION.name()));
String redirectLocation = emailInvitationsService.acceptInvitation("code", "password").getRedirectUri();
verify(scimUserProvisioning).verifyUser(user.getId(), user.getVersion());
verify(scimUserProvisioning).changePassword(user.getId(), null, "password");
assertEquals("http://example.com/redirect/", redirectLocation);
}
@Test
public void acceptInvitationWithInvalidRedirectUri() throws Exception {
ScimUser user = new ScimUser("user-id-001", "user@example.com", "first", "last");
user.setOrigin(UAA);
BaseClientDetails clientDetails = new BaseClientDetails("client-id", null, null, null, null, "http://example.com/redirect");
when(scimUserProvisioning.verifyUser(anyString(), anyInt())).thenReturn(user);
when(scimUserProvisioning.update(anyString(), anyObject())).thenReturn(user);
when(scimUserProvisioning.retrieve(eq("user-id-001"))).thenReturn(user);
when(clientDetailsService.loadClientByClientId("acmeClientId")).thenReturn(clientDetails);
Map<String,String> userData = new HashMap<>();
userData.put(USER_ID, "user-id-001");
userData.put(EMAIL, "user@example.com");
userData.put(REDIRECT_URI, "http://someother/redirect");
userData.put(CLIENT_ID, "acmeClientId");
when(expiringCodeStore.retrieveCode(anyString())).thenReturn(new ExpiringCode("code", new Timestamp(System.currentTimeMillis()), JsonUtils.writeValueAsString(userData), INVITATION.name()));
String redirectLocation = emailInvitationsService.acceptInvitation("code", "password").getRedirectUri();
verify(scimUserProvisioning).verifyUser(user.getId(), user.getVersion());
verify(scimUserProvisioning).changePassword(user.getId(), null, "password");
assertEquals("/home", redirectLocation);
}
// TODO: add cases for username no existing external user with username not email
@Test
public void accept_invitation_with_external_user_that_does_not_have_email_as_their_username() {
String userId = "user-id-001";
String email = "user@example.com";
String actualUsername = "actual_username";
ScimUser userBeforeAccept = new ScimUser(userId, email, "first", "last");
userBeforeAccept.setPrimaryEmail(email);
userBeforeAccept.setOrigin(OriginKeys.SAML);
when(scimUserProvisioning.verifyUser(eq(userId), anyInt())).thenReturn(userBeforeAccept);
when(scimUserProvisioning.retrieve(eq(userId))).thenReturn(userBeforeAccept);
BaseClientDetails clientDetails = new BaseClientDetails("client-id", null, null, null, null, "http://example.com/redirect");
when(clientDetailsService.loadClientByClientId("acmeClientId")).thenReturn(clientDetails);
Map<String,String> userData = new HashMap<>();
userData.put(USER_ID, userBeforeAccept.getId());
userData.put(EMAIL, userBeforeAccept.getPrimaryEmail());
userData.put(REDIRECT_URI, "http://someother/redirect");
userData.put(CLIENT_ID, "acmeClientId");
when(expiringCodeStore.retrieveCode(anyString())).thenReturn(new ExpiringCode("code", new Timestamp(System.currentTimeMillis()), JsonUtils.writeValueAsString(userData), INVITATION.name()));
ScimUser userAfterAccept = new ScimUser(userId, actualUsername, userBeforeAccept.getGivenName(), userBeforeAccept.getFamilyName());
userAfterAccept.setPrimaryEmail(email);
when(scimUserProvisioning.verifyUser(eq(userId), anyInt())).thenReturn(userAfterAccept);
ScimUser acceptedUser = emailInvitationsService.acceptInvitation("code", "password").getUser();
assertEquals(userAfterAccept.getUserName(), acceptedUser.getUserName());
assertEquals(userAfterAccept.getName(), acceptedUser.getName());
assertEquals(userAfterAccept.getPrimaryEmail(), acceptedUser.getPrimaryEmail());
verify(scimUserProvisioning).verifyUser(eq(userId), anyInt());
}
@Configuration
@EnableWebMvc
@Import(ThymeleafConfig.class)
static class ContextConfiguration extends WebMvcConfigurerAdapter {
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
@Bean
ExpiringCodeStore expiringCodeService() { return mock(ExpiringCodeStore.class); }
@Bean
MessageService messageService() {
return mock(MessageService.class);
}
@Bean
EmailInvitationsService emailInvitationsService() {
return new EmailInvitationsService();
}
@Bean
ClientDetailsService clientDetailsService() {
return mock(ClientDetailsService.class);
}
@Bean
ScimUserProvisioning scimUserProvisioning() {
return mock(ScimUserProvisioning.class);
}
}
}