/******************************************************************************* * 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.login; import org.cloudfoundry.identity.uaa.account.EmailChangeEmailService; import org.cloudfoundry.identity.uaa.codestore.ExpiringCode; import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeStore; import org.cloudfoundry.identity.uaa.error.UaaException; import org.cloudfoundry.identity.uaa.message.EmailService; 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.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.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; 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.ClientDetailsService; import org.springframework.security.oauth2.provider.NoSuchClientException; import org.springframework.security.oauth2.provider.client.BaseClientDetails; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 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.Collections; import java.util.HashMap; import java.util.Map; import static org.cloudfoundry.identity.uaa.account.EmailChangeEmailService.CHANGE_EMAIL_REDIRECT_URL; import static org.cloudfoundry.identity.uaa.codestore.ExpiringCodeType.EMAIL; import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.contains; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {ThymeleafAdditional.class,ThymeleafConfig.class}) public class EmailChangeEmailServiceTest { private EmailChangeEmailService emailChangeEmailService; private ScimUserProvisioning scimUserProvisioning; private ExpiringCodeStore codeStore; private MessageService messageService; private MockHttpServletRequest request; private ClientDetailsService clientDetailsService; private String companyName; @Autowired @Qualifier("mailTemplateEngine") SpringTemplateEngine templateEngine; @After public void tearDown() throws Exception { SecurityContextHolder.clearContext(); IdentityZoneHolder.clear(); } @Before public void setUp() throws Exception { SecurityContextHolder.clearContext(); scimUserProvisioning = mock(ScimUserProvisioning.class); codeStore = mock(ExpiringCodeStore.class); clientDetailsService = mock(ClientDetailsService.class); messageService = mock(EmailService.class); emailChangeEmailService = new EmailChangeEmailService(templateEngine, messageService, scimUserProvisioning, codeStore, clientDetailsService); request = new MockHttpServletRequest(); request.setProtocol("http"); request.setContextPath("/login"); RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); } @Test public void beginEmailChange() throws Exception { setUpForBeginEmailChange(); Mockito.verify(messageService).sendMessage( eq("new@example.com"), eq(MessageType.CHANGE_EMAIL), eq("Account Email change verification"), contains("<a href=\"http://localhost/login/verify_email?code=the_secret_code\">Verify your email</a>") ); } @Test(expected = UaaException.class) public void beginEmailChangeWithUsernameConflict() throws Exception { ScimUser user = new ScimUser("user-001", "user@example.com", "test-name", "test-name"); user.setPrimaryEmail("user@example.com"); when(scimUserProvisioning.retrieve(anyString())).thenReturn(user); when(scimUserProvisioning.query(anyString())).thenReturn(Collections.singletonList(new ScimUser())); emailChangeEmailService.beginEmailChange("user-001", "user@example.com", "new@example.com", null, null); } @Test public void testBeginEmailChangeWithCompanyNameConfigured() throws Exception { IdentityZoneConfiguration defaultConfig = IdentityZoneHolder.get().getConfig(); BrandingInformation branding = new BrandingInformation(); branding.setCompanyName("Best Company"); IdentityZoneConfiguration config = new IdentityZoneConfiguration(); config.setBranding(branding); IdentityZoneHolder.get().setConfig(config); try { emailChangeEmailService = new EmailChangeEmailService(templateEngine, messageService, scimUserProvisioning, codeStore, clientDetailsService); setUpForBeginEmailChange(); ArgumentCaptor<String> emailBodyArgument = ArgumentCaptor.forClass(String.class); Mockito.verify(messageService).sendMessage( eq("new@example.com"), eq(MessageType.CHANGE_EMAIL), eq("Best Company Email change verification"), emailBodyArgument.capture() ); String emailBody = emailBodyArgument.getValue(); assertThat(emailBody, containsString("<a href=\"http://localhost/login/verify_email?code=the_secret_code\">Verify your email</a>")); assertThat(emailBody, containsString("a Best Company account")); } finally { IdentityZoneHolder.get().setConfig(defaultConfig); } } @Test public void testBeginEmailChangeInOtherZone() throws Exception { String zoneName = "The Twiglet Zone 2"; testBeginEmailChangeInOtherZone(zoneName); } @Test public void testBeginEmailChangeInOtherZone_UTF_8_ZoneName() throws Exception { String zoneName = "\u7433\u8D3A"; testBeginEmailChangeInOtherZone(zoneName); } public void testBeginEmailChangeInOtherZone(String zoneName) throws Exception { IdentityZone zone = MultitenancyFixture.identityZone("test-zone-id", "test"); zone.setName(zoneName); IdentityZoneHolder.set(zone); MockHttpServletRequest request = new MockHttpServletRequest(); request.setScheme("http"); request.setServerName("test.localhost"); request.setContextPath("/login"); ServletRequestAttributes attrs = new ServletRequestAttributes(request); RequestContextHolder.setRequestAttributes(attrs); setUpForBeginEmailChange(); ArgumentCaptor<String> emailBodyArgument = ArgumentCaptor.forClass(String.class); Mockito.verify(messageService).sendMessage( eq("new@example.com"), eq(MessageType.CHANGE_EMAIL), eq(zoneName+" Email change verification"), emailBodyArgument.capture() ); String emailBody = emailBodyArgument.getValue(); assertThat(emailBody, containsString(String.format("A request has been made to change the email for %s from %s to %s", zoneName, "user@example.com", "new@example.com"))); assertThat(emailBody, containsString("<a href=\"http://test.localhost/login/verify_email?code=the_secret_code\">Verify your email</a>")); assertThat(emailBody, containsString("Thank you,<br />\n "+zoneName)); } @Test public void testCompleteVerification() throws Exception { Map<String, String> response = setUpCompleteActivation("user-name", "app", "http://app.com/redirect"); Assert.assertEquals("user-001", response.get("userId")); Assert.assertEquals("user-name", response.get("username")); Assert.assertEquals("new@example.com", response.get("email")); Assert.assertEquals("http://app.com/redirect", response.get("redirect_url")); } @Test public void testCompleteVerificationWhereUsernameEqualsEmail() throws Exception { Map<String, String> response = setUpCompleteActivation("user@example.com", "app", "http://app.com/redirect"); Assert.assertEquals("user-001", response.get("userId")); Assert.assertEquals("new@example.com", response.get("username")); Assert.assertEquals("new@example.com", response.get("email")); Assert.assertEquals("http://app.com/redirect", response.get("redirect_url")); } @Test(expected = UaaException.class) public void testCompleteVerificationWithInvalidCode() throws Exception { when(codeStore.retrieveCode("invalid_code")).thenReturn(null); emailChangeEmailService.completeVerification("invalid_code"); } @Test(expected = UaaException.class) public void testCompleteVerificationWithInvalidIntent() throws Exception { when(codeStore.retrieveCode("invalid_code")).thenReturn(new ExpiringCode("invalid_code", new Timestamp(System.currentTimeMillis()), null, "invalid-intent")); emailChangeEmailService.completeVerification("invalid_code"); } @Test public void testCompleteActivationWithInvalidClientId() { Map<String, String> codeData = new HashMap<>(); codeData.put("user_id", "user-001"); codeData.put("client_id", "invalid-client"); codeData.put("email", "new@example.com"); when(codeStore.retrieveCode("the_secret_code")).thenReturn(new ExpiringCode("the_secret_code", new Timestamp(System.currentTimeMillis()), JsonUtils.writeValueAsString(codeData), null)); ScimUser user = new ScimUser("user-001", "user@example.com", "", ""); user.setPrimaryEmail("user@example.com"); when(scimUserProvisioning.retrieve("user-001")).thenReturn(user); doThrow(new NoSuchClientException("no such client")).when(clientDetailsService).loadClientByClientId("invalid-client"); Map<String, String> response = null; try { response = emailChangeEmailService.completeVerification("the_secret_code"); } catch (NoSuchClientException e) { assertNull(response.get("redirect_url")); } } @Test public void testCompleteActivationWithNoClientId() { Map<String, String> response = setUpCompleteActivation("user@example.com", null, null); Assert.assertEquals(null, response.get("redirect_url")); } @Test public void testCompleteActivationWhereWildcardsDoNotMatch() { Map<String, String> response = setUpCompleteActivation("user@example.com", "app", "http://blah.app.com/redirect"); Assert.assertEquals("http://fallback.url/redirect", response.get("redirect_url")); } @Test public void testCompleteActivationWithNoRedirectUri() { Map<String, String> response = setUpCompleteActivation("user@example.com", "app", null); Assert.assertEquals("http://fallback.url/redirect", response.get("redirect_url")); } private Map<String, String> setUpCompleteActivation(String username, String clientId, String redirectUri) { Map<String, String> codeData = new HashMap<>(); codeData.put("user_id", "user-001"); codeData.put("client_id", clientId); codeData.put("redirect_uri", redirectUri); codeData.put("email", "new@example.com"); BaseClientDetails clientDetails = new BaseClientDetails("client-id", null, null, "authorization_grant", null, "http://app.com/*"); clientDetails.addAdditionalInformation(CHANGE_EMAIL_REDIRECT_URL, "http://fallback.url/redirect"); when(codeStore.retrieveCode("the_secret_code")).thenReturn(new ExpiringCode("the_secret_code", new Timestamp(System.currentTimeMillis()), JsonUtils.writeValueAsString(codeData), null)); ScimUser user = new ScimUser("user-001", username, "", ""); user.setPrimaryEmail("user@example.com"); when(scimUserProvisioning.retrieve("user-001")).thenReturn(user); when(clientDetailsService.loadClientByClientId(clientId)).thenReturn(clientDetails); Map<String, String> response = emailChangeEmailService.completeVerification("the_secret_code"); ScimUser updatedUser = new ScimUser("user-001", "new@example.com", "", ""); user.setPrimaryEmail("new@example.com"); verify(scimUserProvisioning).update("user-001", updatedUser); return response; } private void setUpForBeginEmailChange() { ScimUser user = new ScimUser("user-001", "user-name", "test-name", "test-name"); user.setPrimaryEmail("user@example.com"); Map<String, String> codeData = new HashMap<>(); codeData.put("user_id", "user-001"); codeData.put("client_id", "app"); codeData.put("redirect_uri", "http://app.com"); codeData.put("email", "new@example.com"); when(scimUserProvisioning.retrieve("user-001")).thenReturn(user); when(scimUserProvisioning.query(anyString())).thenReturn(Collections.singletonList(new ScimUser())); String data = JsonUtils.writeValueAsString(codeData); when(codeStore.generateCode(eq(data), any(Timestamp.class), eq(EMAIL.name()))).thenReturn(new ExpiringCode("the_secret_code", new Timestamp(System.currentTimeMillis()), data, EMAIL.name())); emailChangeEmailService.beginEmailChange("user-001", "user@example.com", "new@example.com", "app", "http://app.com"); verify(codeStore).generateCode(eq(JsonUtils.writeValueAsString(codeData)), any(Timestamp.class), eq(EMAIL.name())); } }