/******************************************************************************* * 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.ConflictException; import org.cloudfoundry.identity.uaa.account.ForgotPasswordInfo; import org.cloudfoundry.identity.uaa.account.NotFoundException; import org.cloudfoundry.identity.uaa.account.ResetPasswordService.ResetPasswordResponse; import org.cloudfoundry.identity.uaa.account.UaaResetPasswordService; import org.cloudfoundry.identity.uaa.account.event.ResetPasswordRequestEvent; import org.cloudfoundry.identity.uaa.authentication.InvalidCodeException; import org.cloudfoundry.identity.uaa.codestore.ExpiringCode; import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeStore; import org.cloudfoundry.identity.uaa.scim.ScimMeta; 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.validate.PasswordValidator; import org.cloudfoundry.identity.uaa.test.MockAuthentication; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.mockito.ArgumentCaptor; import org.springframework.context.ApplicationEventPublisher; import org.springframework.core.io.support.ResourcePropertySource; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; 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 java.sql.Timestamp; import java.util.Arrays; import java.util.Collections; import java.util.Date; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.sameInstance; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; 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.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY; public class UaaResetPasswordServiceTests { private UaaResetPasswordService uaaResetPasswordService; private ExpiringCodeStore codeStore; private ScimUserProvisioning scimUserProvisioning; private PasswordValidator passwordValidator; private ClientDetailsService clientDetailsService; private ResourcePropertySource resourcePropertySource; @Before public void setUp() throws Exception { SecurityContextHolder.clearContext(); scimUserProvisioning = mock(ScimUserProvisioning.class); codeStore = mock(ExpiringCodeStore.class); passwordValidator = mock(PasswordValidator.class); clientDetailsService = mock(ClientDetailsService.class); resourcePropertySource = mock(ResourcePropertySource.class); uaaResetPasswordService = new UaaResetPasswordService(scimUserProvisioning, codeStore, passwordValidator, clientDetailsService, resourcePropertySource); } @After public void tearDown() { SecurityContextHolder.clearContext(); IdentityZoneHolder.clear(); } @Rule public ExpectedException expectedException = ExpectedException.none(); @Test public void forgotPassword_ResetCodeIsReturnedSuccessfully() throws Exception { ScimUser user = new ScimUser("user-id-001","user@example.com","firstName","lastName"); user.setPasswordLastModified(new Date(1234)); user.setPrimaryEmail("user@example.com"); when(scimUserProvisioning.query(contains("origin"))).thenReturn(Arrays.asList(user)); Timestamp expiresAt = new Timestamp(System.currentTimeMillis()); ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class); when(codeStore.generateCode(eq("{\"user_id\":\"user-id-001\",\"username\":\"user@example.com\",\"passwordModifiedTime\":1234,\"client_id\":\"example\",\"redirect_uri\":\"redirect.example.com\"}"), any(Timestamp.class), anyString())).thenReturn(new ExpiringCode("code", expiresAt, "user-id-001", null)); ForgotPasswordInfo forgotPasswordInfo = uaaResetPasswordService.forgotPassword("user@example.com", "example", "redirect.example.com"); verify(codeStore).expireByIntent(captor.capture()); assertEquals(UaaResetPasswordService.FORGOT_PASSWORD_INTENT_PREFIX+user.getId(), captor.getValue()); assertThat(forgotPasswordInfo.getUserId(), equalTo("user-id-001")); ExpiringCode resetPasswordCode = forgotPasswordInfo.getResetPasswordCode(); assertThat(resetPasswordCode.getCode(), equalTo("code")); assertThat(resetPasswordCode.getExpiresAt(), equalTo(expiresAt)); assertThat(resetPasswordCode.getData(), equalTo("user-id-001")); } @Test public void forgotPassword_PublishesResetPasswordRequestEvent() throws Exception { ApplicationEventPublisher publisher = mock(ApplicationEventPublisher.class); Authentication authentication = mock(Authentication.class); uaaResetPasswordService.setApplicationEventPublisher(publisher); SecurityContextHolder.getContext().setAuthentication(authentication); ScimUser user = new ScimUser("user-id-001", "user@example.com", "firstName", "lastName"); user.setPrimaryEmail("user@example.com"); when(scimUserProvisioning.query(contains("origin"))).thenReturn(Arrays.asList(user)); Timestamp expiresAt = new Timestamp(System.currentTimeMillis()); when(codeStore.generateCode(anyString(), any(Timestamp.class), anyString())).thenReturn(new ExpiringCode("code", expiresAt, "user-id-001", null)); uaaResetPasswordService.forgotPassword("user@example.com", "", ""); ArgumentCaptor<ResetPasswordRequestEvent> captor = ArgumentCaptor.forClass(ResetPasswordRequestEvent.class); verify(publisher).publishEvent(captor.capture()); ResetPasswordRequestEvent event = captor.getValue(); assertThat(event.getSource(), equalTo("user@example.com")); assertThat(event.getCode(), equalTo("code")); assertThat(event.getAuthentication(), sameInstance(authentication)); } @Test public void forgotPassword_ThrowsConflictException() throws Exception { ScimUser user = new ScimUser("user-id-001","user@example.com","firstName","lastName"); user.setPrimaryEmail("user@example.com"); when(scimUserProvisioning.query(contains("origin"))).thenReturn(Arrays.asList(new ScimUser[]{})); when(scimUserProvisioning.query(eq("userName eq \"user@example.com\""))).thenReturn(Arrays.asList(new ScimUser[]{user})); when(codeStore.generateCode(anyString(), any(Timestamp.class), eq(null))).thenReturn(new ExpiringCode("code", new Timestamp(System.currentTimeMillis()), "user-id-001", null)); when(codeStore.retrieveCode(anyString())).thenReturn(new ExpiringCode("code", new Timestamp(System.currentTimeMillis()),"user-id-001", null)); try { uaaResetPasswordService.forgotPassword("user@example.com", "", ""); fail(); } catch (ConflictException e) { assertThat(e.getUserId(), equalTo("user-id-001")); } } @Test(expected = NotFoundException.class) public void forgotPassword_ThrowsNotFoundException_ScimUserNotFoundInUaa() throws Exception { uaaResetPasswordService.forgotPassword("user@example.com", "", ""); } @Test public void testResetPassword() throws Exception { ExpiringCode code = setupResetPassword("example", "redirect.example.com/login"); BaseClientDetails client = new BaseClientDetails(); client.setRegisteredRedirectUri(Collections.singleton("redirect.example.com/*")); when(clientDetailsService.loadClientByClientId("example")).thenReturn(client); ResetPasswordResponse response = uaaResetPasswordService.resetPassword(code, "new_secret"); Assert.assertEquals("usermans-id", response.getUser().getId()); Assert.assertEquals("userman", response.getUser().getUserName()); Assert.assertEquals("redirect.example.com/login", response.getRedirectUri()); } @Test(expected = InvalidPasswordException.class) public void resetPassword_validatesNewPassword() { doThrow(new InvalidPasswordException("foo")).when(passwordValidator).validate("new_secret"); ExpiringCode code1 = new ExpiringCode("secret_code", new Timestamp(System.currentTimeMillis() + 1000*60*10), "{}", null); uaaResetPasswordService.resetPassword(code1, "new_secret"); } @Test public void resetPassword_InvalidPasswordException_NewPasswordSameAsOld() { ScimUser user = new ScimUser("user-id", "username", "firstname", "lastname"); user.setMeta(new ScimMeta(new Date(), new Date(), 0)); user.setPrimaryEmail("foo@example.com"); ExpiringCode expiringCode = new ExpiringCode("good_code", new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), "{\"user_id\":\"user-id\",\"username\":\"username\",\"passwordModifiedTime\":null,\"client_id\":\"\",\"redirect_uri\":\"\"}", null); when(codeStore.retrieveCode("good_code")).thenReturn(expiringCode); when(scimUserProvisioning.retrieve("user-id")).thenReturn(user); when(scimUserProvisioning.checkPasswordMatches("user-id", "Passwo3dAsOld")) .thenThrow(new InvalidPasswordException("Your new password cannot be the same as the old password.", UNPROCESSABLE_ENTITY)); SecurityContext securityContext = mock(SecurityContext.class); when(securityContext.getAuthentication()).thenReturn(new MockAuthentication()); SecurityContextHolder.setContext(securityContext); try { uaaResetPasswordService.resetPassword(expiringCode, "Passwo3dAsOld"); fail(); } catch (InvalidPasswordException e) { assertEquals("Your new password cannot be the same as the old password.", e.getMessage()); assertEquals(UNPROCESSABLE_ENTITY, e.getStatus()); } } @Test public void resetPassword_InvalidCodeData() { ExpiringCode expiringCode = new ExpiringCode("good_code", new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), "user-id", null); when(codeStore.retrieveCode("good_code")).thenReturn(expiringCode); SecurityContext securityContext = mock(SecurityContext.class); when(securityContext.getAuthentication()).thenReturn(new MockAuthentication()); SecurityContextHolder.setContext(securityContext); try { uaaResetPasswordService.resetPassword(expiringCode, "password"); fail(); } catch (InvalidCodeException e) { assertEquals("Sorry, your reset password link is no longer valid. Please request a new one", e.getMessage()); } } @Test public void resetPassword_WithInvalidClientId() { ExpiringCode code = setupResetPassword("invalid_client", "redirect.example.com"); doThrow(new NoSuchClientException("no such client")).when(clientDetailsService).loadClientByClientId("invalid_client"); ResetPasswordResponse response = uaaResetPasswordService.resetPassword(code, "new_secret"); assertEquals("home", response.getRedirectUri()); } @Test public void resetPassword_WithNoClientId() { ExpiringCode code = setupResetPassword("", "redirect.example.com"); ResetPasswordResponse response = uaaResetPasswordService.resetPassword(code, "new_secret"); assertEquals("home", response.getRedirectUri()); } @Test public void resetPassword_WhereWildcardsDoNotMatch() { ExpiringCode code = setupResetPassword("example", "redirect.example.com"); BaseClientDetails client = new BaseClientDetails(); client.setRegisteredRedirectUri(Collections.singleton("doesnotmatch.example.com/*")); when(clientDetailsService.loadClientByClientId("example")).thenReturn(client); ResetPasswordResponse response = uaaResetPasswordService.resetPassword(code, "new_secret"); assertEquals("home", response.getRedirectUri()); } @Test public void resetPassword_WithNoRedirectUri() { ExpiringCode code = setupResetPassword("example", ""); BaseClientDetails client = new BaseClientDetails(); client.setRegisteredRedirectUri(Collections.singleton("redirect.example.com/*")); when(clientDetailsService.loadClientByClientId("example")).thenReturn(client); ResetPasswordResponse response = uaaResetPasswordService.resetPassword(code, "new_secret"); assertEquals("home", response.getRedirectUri()); } @Test public void resetPassword_ForcedChange() { String userId = "user-id"; ScimUser user = new ScimUser(userId, "username", "firstname", "lastname"); user.setMeta(new ScimMeta(new Date(), new Date(), 0)); user.setPrimaryEmail("foo@example.com"); when(scimUserProvisioning.retrieve(userId)).thenReturn(user); uaaResetPasswordService.resetUserPassword(userId, "password"); verify(scimUserProvisioning, times(1)).updatePasswordChangeRequired(userId, false); verify(scimUserProvisioning, times(1)).changePassword(userId, null, "password"); } @Test (expected = InvalidPasswordException.class) public void resetPassword_ForcedChange_NewPasswordSameAsOld() { String userId = "user-id"; ScimUser user = new ScimUser(userId, "username", "firstname", "lastname"); user.setMeta(new ScimMeta(new Date(), new Date(), 0)); user.setPrimaryEmail("foo@example.com"); when(scimUserProvisioning.retrieve(userId)).thenReturn(user); when(scimUserProvisioning.checkPasswordMatches("user-id", "password")) .thenThrow(new InvalidPasswordException("Your new password cannot be the same as the old password.", UNPROCESSABLE_ENTITY)); uaaResetPasswordService.resetUserPassword(userId, "password"); } @Test public void resetPassword_forcedChange_must_verify_password_policy() { String userId = "user-id"; ScimUser user = new ScimUser(userId, "username", "firstname", "lastname"); user.setMeta(new ScimMeta(new Date(), new Date(), 0)); user.setPrimaryEmail("foo@example.com"); when(scimUserProvisioning.retrieve(userId)).thenReturn(user); doThrow(new InvalidPasswordException("Password cannot contain whitespace characters.")).when(passwordValidator).validate("new password"); expectedException.expect(InvalidPasswordException.class); expectedException.expectMessage("Password cannot contain whitespace characters."); uaaResetPasswordService.resetUserPassword(userId, "new password"); } @Test public void updateLastLogonForUser() { String userId = "id1"; uaaResetPasswordService.updateLastLogonTime(userId); verify(scimUserProvisioning, times(1)).updateLastLogonTime(userId); } private ExpiringCode setupResetPassword(String clientId, String redirectUri) { ScimUser user = new ScimUser("usermans-id","userman","firstName","lastName"); user.setMeta(new ScimMeta(new Date(System.currentTimeMillis()-(1000*60*60*24)), new Date(System.currentTimeMillis()-(1000*60*60*24)), 0)); user.setPrimaryEmail("user@example.com"); when(scimUserProvisioning.retrieve(eq("usermans-id"))).thenReturn(user); ExpiringCode code = new ExpiringCode("code", new Timestamp(System.currentTimeMillis()), "{\"user_id\":\"usermans-id\",\"username\":\"userman\",\"passwordModifiedTime\":null,\"client_id\":\"" + clientId + "\",\"redirect_uri\":\"" + redirectUri + "\"}", null); when(codeStore.retrieveCode(eq("secret_code"))).thenReturn(code); SecurityContext securityContext = mock(SecurityContext.class); when(securityContext.getAuthentication()).thenReturn(new MockAuthentication()); SecurityContextHolder.setContext(securityContext); return code; } }