CVE-2016-3084
Description
The UAA reset password flow in Cloud Foundry release v236 and earlier versions, UAA release v3.3.0 and earlier versions, all versions of Login-server, UAA release v10 and earlier versions and Pivotal Elastic Runtime versions prior to 1.7.2 is vulnerable to a brute force attack due to multiple active codes at a given time. This vulnerability is applicable only when using the UAA internal user store for authentication. Deployments enabled for integration via SAML or LDAP are not affected.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.cloudfoundry.identity:cloudfoundry-identity-serverMaven | < 3.3.0.1 | 3.3.0.1 |
Affected products
6- cpe:2.3:a:cloudfoundry:cloud_foundry_uaa_bosh:*:*:*:*:*:*:*:*Range: <=10
- cpe:2.3:a:pivotal_software:cloud_foundry_elastic_runtime:*:*:*:*:*:*:*:*Range: <=1.7.1
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:*:*:*:*:*:*:*:*Range: <=3.3.0
- cpe:2.3:a:pivotal_software:login-server:-:*:*:*:*:*:*:*
- Pivotal/Cloud Foundryv5Range: release v236 and earlier versions
Patches
74a119d314744Merge branch 'master' into develop
17 files changed · +89 −44
server/src/main/java/org/cloudfoundry/identity/uaa/account/UaaResetPasswordService.java+13 −11 modified@@ -80,20 +80,22 @@ private ResetPasswordResponse changePasswordCodeAuthenticated(String code, Strin throw new InvalidCodeException("invalid_code", "Sorry, your reset password link is no longer valid. Please request a new one", 422); } String userId; - String userName = null; - Date passwordLastModified = null; - String clientId = null; - String redirectUri = null; + String userName; + Date passwordLastModified; + String clientId; + String redirectUri; + PasswordChange change; try { - PasswordChange change = JsonUtils.readValue(expiringCode.getData(), PasswordChange.class); - userId = change.getUserId(); - userName = change.getUsername(); - passwordLastModified = change.getPasswordModifiedTime(); - clientId = change.getClientId(); - redirectUri = change.getRedirectUri(); + change = JsonUtils.readValue(expiringCode.getData(), PasswordChange.class); } catch (JsonUtils.JsonUtilException x) { - userId = expiringCode.getData(); + throw new InvalidCodeException("invalid_code", "Sorry, your reset password link is no longer valid. Please request a new one", 422); } + userId = change.getUserId(); + userName = change.getUsername(); + passwordLastModified = change.getPasswordModifiedTime(); + clientId = change.getClientId(); + redirectUri = change.getRedirectUri(); + ScimUser user = scimUserProvisioning.retrieve(userId); try { if (isUserModified(user, expiringCode.getExpiresAt(), userName, passwordLastModified)) {
server/src/main/java/org/cloudfoundry/identity/uaa/codestore/JdbcExpiringCodeStore.java+1 −1 modified@@ -43,7 +43,7 @@ public class JdbcExpiringCodeStore implements ExpiringCodeStore { private Log logger = LogFactory.getLog(getClass()); - private RandomValueStringGenerator generator = new RandomValueStringGenerator(6); + private RandomValueStringGenerator generator = new RandomValueStringGenerator(10); private JdbcTemplate jdbcTemplate;
server/src/main/java/org/cloudfoundry/identity/uaa/invitations/EmailInvitationsService.java+1 −1 modified@@ -54,7 +54,7 @@ public AcceptedInvitation acceptInvitation(String code, String password) { user = scimUserProvisioning.verifyUser(userId, user.getVersion()); - if (OriginKeys.UAA.equals(user.getOrigin())) { + if (OriginKeys.UAA.equals(user.getOrigin()) && StringUtils.hasText(password)) { PasswordChangeRequest request = new PasswordChangeRequest(); request.setPassword(password); scimUserProvisioning.changePassword(userId, null, password);
server/src/main/java/org/cloudfoundry/identity/uaa/scim/endpoints/PasswordChange.java+0 −2 modified@@ -1,11 +1,9 @@ package org.cloudfoundry.identity.uaa.scim.endpoints; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.Date; -@JsonIgnoreProperties(ignoreUnknown = true) public class PasswordChange { public PasswordChange() {}
server/src/main/resources/login-ui.xml+1 −1 modified@@ -151,7 +151,7 @@ entry-point-ref="loginEntryPoint" use-expressions="false" xmlns="http://www.springframework.org/schema/security"> - <intercept-url pattern="/**" access="IS_AUTHENTICATED_ANONYMOUSLY" /> + <intercept-url pattern="/**" access="IS_AUTHENTICATED_FULLY" /> <csrf disabled="false"/> <access-denied-handler ref="loginEntryPoint"/> </http>
server/src/test/java/org/cloudfoundry/identity/uaa/codestore/CodeStoreEndpointsTests.java+1 −1 modified@@ -51,7 +51,7 @@ public void testGenerateCode() throws Exception { assertNotNull(result); assertNotNull(result.getCode()); - assertTrue(result.getCode().trim().length() > 0); + assertTrue(result.getCode().trim().length() == 10); assertEquals(expiresAt, result.getExpiresAt());
server/src/test/java/org/cloudfoundry/identity/uaa/invitations/InvitationsControllerTest.java+1 −1 modified@@ -306,7 +306,7 @@ public void acceptInvitePage_for_verifiedUser() throws Exception { when(expiringCodeStore.retrieveCode("the_secret_code")).thenReturn(new ExpiringCode("code", new Timestamp(System.currentTimeMillis()), JsonUtils.writeValueAsString(codeData), null)); when(expiringCodeStore.generateCode(anyString(), anyObject(), eq(null))).thenReturn(new ExpiringCode("code", new Timestamp(System.currentTimeMillis()), JsonUtils.writeValueAsString(codeData), null)); - when(invitationsService.acceptInvitation(anyString(), anyString())).thenReturn(new InvitationsService.AcceptedInvitation("blah.test.com", new ScimUser())); + when(invitationsService.acceptInvitation(anyString(), eq(""))).thenReturn(new InvitationsService.AcceptedInvitation("blah.test.com", new ScimUser())); IdentityProvider provider = new IdentityProvider(); provider.setType(OriginKeys.UAA); when(providerProvisioning.retrieveByOrigin(anyString(), anyString())).thenReturn(provider);
server/src/test/java/org/cloudfoundry/identity/uaa/login/EmailInvitationsServiceTests.java+18 −2 modified@@ -14,7 +14,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -32,7 +31,6 @@ 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 org.thymeleaf.spring4.SpringTemplateEngine; import java.sql.Timestamp; import java.util.HashMap; @@ -48,6 +46,7 @@ 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; @@ -108,6 +107,23 @@ public void acceptInvitationNoClientId() throws Exception { assertEquals("/home", redirectLocation); } + @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), null)); + + 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");
server/src/test/java/org/cloudfoundry/identity/uaa/login/UaaResetPasswordServiceTests.java+18 −2 modified@@ -127,7 +127,7 @@ public void forgotPassword_PublishesResetPasswordRequestEvent() throws Exception ArgumentCaptor<ResetPasswordRequestEvent> captor = ArgumentCaptor.forClass(ResetPasswordRequestEvent.class); verify(publisher).publishEvent(captor.capture()); ResetPasswordRequestEvent event = captor.getValue(); - assertThat((String) event.getSource(), equalTo("user@example.com")); + assertThat(event.getSource(), equalTo("user@example.com")); assertThat(event.getCode(), equalTo("code")); assertThat(event.getAuthentication(), sameInstance(authentication)); } @@ -191,7 +191,7 @@ public void resetPassword_InvalidPasswordException_NewPasswordSameAsOld() { 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", null); + 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")) @@ -208,6 +208,22 @@ public void resetPassword_InvalidPasswordException_NewPasswordSameAsOld() { } } + @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 { + emailResetPasswordService.resetPassword("good_code", "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() { setupResetPassword("invalid_client", "redirect.example.com");
server/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/PasswordResetEndpointTest.java+8 −3 modified@@ -237,7 +237,8 @@ public void testCreatingAPasswordResetWithAUsernameContainingSpecialCharacters() @Test public void testChangingAPasswordWithAValidCode() throws Exception { when(expiringCodeStore.retrieveCode("secret_code")) - .thenReturn(new ExpiringCode("secret_code", new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), "eyedee", null)); + .thenReturn(new ExpiringCode("secret_code", new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), + "{\"user_id\":\"eyedee\",\"username\":\"user@example.com\",\"passwordModifiedTime\":null,\"client_id\":\"\",\"redirect_uri\":\"\"}", null)); ScimUser scimUser = new ScimUser("eyedee", "user@example.com", "User", "Man"); scimUser.setMeta(new ScimMeta(new Date(System.currentTimeMillis() - (1000 * 60 * 60 * 24)), new Date(System.currentTimeMillis() - (1000 * 60 * 60 * 24)), 0)); @@ -281,7 +282,9 @@ public void changing_password_with_invalid_code() throws Exception { @Test public void testChangingAPasswordForUnverifiedUser() throws Exception { when(expiringCodeStore.retrieveCode("secret_code")) - .thenReturn(new ExpiringCode("secret_code", new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), "eyedee", null)); + .thenReturn(new ExpiringCode("secret_code", new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), + "{\"user_id\":\"eyedee\",\"username\":\"user@example.com\",\"passwordModifiedTime\":null,\"client_id\":\"\",\"redirect_uri\":\"\"}", + null)); ScimUser scimUser = new ScimUser("eyedee", "user@example.com", "User", "Man"); scimUser.setMeta(new ScimMeta(new Date(System.currentTimeMillis() - (1000 * 60 * 60 * 24)), new Date(System.currentTimeMillis() - (1000 * 60 * 60 * 24)), 0)); @@ -338,7 +341,9 @@ public void changePassword_Returns422UnprocessableEntity_NewPasswordSameAsOld() Mockito.reset(passwordValidator); when(expiringCodeStore.retrieveCode("emailed_code")) - .thenReturn(new ExpiringCode("emailed_code", new Timestamp(System.currentTimeMillis()+ UaaResetPasswordService.PASSWORD_RESET_LIFETIME), "eyedee", null)); + .thenReturn(new ExpiringCode("emailed_code", new Timestamp(System.currentTimeMillis()+ UaaResetPasswordService.PASSWORD_RESET_LIFETIME), + "{\"user_id\":\"eyedee\",\"username\":\"user@example.com\",\"passwordModifiedTime\":null,\"client_id\":\"\",\"redirect_uri\":\"\"}", + null)); ScimUser scimUser = new ScimUser("eyedee", "user@example.com", "User", "Man"); scimUser.setMeta(new ScimMeta(new Date(System.currentTimeMillis()-(1000*60*60*24)), new Date(System.currentTimeMillis()-(1000*60*60*24)), 0));
uaa/src/main/webapp/WEB-INF/spring/codestore-endpoints.xml+1 −1 modified@@ -26,7 +26,7 @@ <http name="codeStoreSecurity" pattern="/Codes/**" create-session="stateless" authentication-manager-ref="emptyAuthenticationManager" entry-point-ref="oauthAuthenticationEntryPoint" xmlns="http://www.springframework.org/schema/security" use-expressions="true"> - <intercept-url pattern="/**" access="#oauth2.hasAnyScope('scim.create','scim.write','password.write')"/> + <intercept-url pattern="/**" access="#oauth2.hasAnyScope('oauth.login')"/> <custom-filter ref="resourceAgnosticAuthenticationFilter" position="PRE_AUTH_FILTER" /> <anonymous enabled="false" /> <expression-handler ref="oauthWebExpressionHandler" />
uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/AutologinIT.java+1 −1 modified@@ -249,7 +249,7 @@ public void testFormEncodedAutologinRequest() throws Exception { Map.class); String autologinCode = (String) autologinResponseEntity.getBody().get("code"); - assertEquals(6, autologinCode.length()); + assertEquals(10, autologinCode.length()); } @Test
uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/ChangePasswordIT.java+2 −4 modified@@ -90,6 +90,7 @@ public void setUp() throws Exception { @Test public void testChangePassword() throws Exception { + webDriver.get(baseUrl + "/change_password"); signIn(userEmail, PASSWORD); changePassword(PASSWORD, NEW_PASSWORD, "new"); @@ -108,7 +109,7 @@ public void displaysErrorWhenPasswordContravenesPolicy() { //the only policy we can contravene by default is the length String newPassword = new RandomValueStringGenerator(260).generate(); - + webDriver.get(baseUrl + "/change_password"); signIn(userEmail, PASSWORD); changePassword(PASSWORD, newPassword, newPassword); @@ -134,11 +135,8 @@ private void signOut() { } private void signIn(String userName, String password) { - webDriver.get(baseUrl + "/logout.do"); - webDriver.get(baseUrl + "/login"); webDriver.findElement(By.name("username")).sendKeys(userName); webDriver.findElement(By.name("password")).sendKeys(password); webDriver.findElement(By.xpath("//input[@value='Sign in']")).click(); - assertThat(webDriver.findElement(By.cssSelector("h1")).getText(), containsString("Where to?")); } }
uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/InvitationsIT.java+10 −7 modified@@ -91,7 +91,7 @@ public class InvitationsIT { @Before public void setup() throws Exception { scimToken = testClient.getOAuthAccessToken("admin", "adminsecret", "client_credentials", "scim.read,scim.write"); - loginToken = testClient.getOAuthAccessToken("login", "loginsecret", "client_credentials", "password.write,scim.write"); + loginToken = testClient.getOAuthAccessToken("login", "loginsecret", "client_credentials", "oauth.login"); screenShootRule.setWebDriver(webDriver); } @@ -199,9 +199,9 @@ private String createInvitation(String username, String userEmail, String redire return createInvitation(baseUrl, uaaUrl, username, userEmail, origin, redirectUri, loginToken, scimToken); } - public static String createInvitation(String baseUrl, String uaaUrl, String username, String userEmail, String origin, String redirectUri, String scimWriteToken, String scimReadToken) { + public static String createInvitation(String baseUrl, String uaaUrl, String username, String userEmail, String origin, String redirectUri, String loginToken, String scimToken) { HttpHeaders headers = new HttpHeaders(); - headers.add("Authorization", "Bearer " + scimWriteToken); + headers.add("Authorization", "Bearer " + scimToken); RestTemplate uaaTemplate = new RestTemplate(); ScimUser scimUser = new ScimUser(); scimUser.setUserName(username); @@ -211,8 +211,8 @@ public static String createInvitation(String baseUrl, String uaaUrl, String user String userId = null; try { - userId = IntegrationTestUtils.getUserIdByField(scimReadToken, baseUrl, origin, "email", userEmail); - scimUser = IntegrationTestUtils.getUser(scimReadToken, baseUrl, userId); + userId = IntegrationTestUtils.getUserIdByField(scimToken, baseUrl, origin, "email", userEmail); + scimUser = IntegrationTestUtils.getUser(scimToken, baseUrl, userId); } catch (RuntimeException x) { } if (userId == null) { @@ -224,12 +224,15 @@ public static String createInvitation(String baseUrl, String uaaUrl, String user userId = response.getBody().getId(); } else { scimUser.setVerified(false); - IntegrationTestUtils.updateUser(scimWriteToken, uaaUrl, scimUser); + IntegrationTestUtils.updateUser(scimToken, uaaUrl, scimUser); } + HttpHeaders invitationHeaders = new HttpHeaders(); + invitationHeaders.add("Authorization", "Bearer " + loginToken); + Timestamp expiry = new Timestamp(System.currentTimeMillis() + TimeUnit.MILLISECONDS.convert(System.currentTimeMillis() + 24 * 3600, TimeUnit.MILLISECONDS)); ExpiringCode expiringCode = new ExpiringCode(null, expiry, "{\"origin\":\"" + origin + "\", \"client_id\":\"app\", \"redirect_uri\":\"" + redirectUri + "\", \"user_id\":\"" + userId + "\", \"email\":\"" + userEmail + "\"}", null); - HttpEntity<ExpiringCode> expiringCodeRequest = new HttpEntity<>(expiringCode, headers); + HttpEntity<ExpiringCode> expiringCodeRequest = new HttpEntity<>(expiringCode, invitationHeaders); ResponseEntity<ExpiringCode> expiringCodeResponse = uaaTemplate.exchange(uaaUrl + "/Codes", HttpMethod.POST, expiringCodeRequest, ExpiringCode.class); expiringCode = expiringCodeResponse.getBody(); return expiringCode.getCode();
uaa/src/test/java/org/cloudfoundry/identity/uaa/login/ResetPasswordControllerMockMvcTests.java+5 −4 modified@@ -275,7 +275,8 @@ public void testResettingAPasswordUsingTimestampForUserModification() throws Exc List<ScimUser> users = getWebApplicationContext().getBean(ScimUserProvisioning.class).query("username eq \"marissa\""); assertNotNull(users); assertEquals(1, users.size()); - ExpiringCode code = codeStore.generateCode(users.get(0).getId(), new Timestamp(System.currentTimeMillis()+ UaaResetPasswordService.PASSWORD_RESET_LIFETIME), null); + PasswordChange passwordChange = new PasswordChange(users.get(0).getId(), users.get(0).getUserName(), null, null, null); + ExpiringCode code = codeStore.generateCode(JsonUtils.writeValueAsString(passwordChange), new Timestamp(System.currentTimeMillis()+ UaaResetPasswordService.PASSWORD_RESET_LIFETIME), null); MockHttpServletRequestBuilder post = createChangePasswordRequest(users.get(0), code, true, "newpassw0rD", "newpassw0rD"); @@ -302,11 +303,11 @@ public void resetPassword_ReturnsUnprocessableEntity_NewPasswordSameAsOld() thro assertNotNull(users); assertEquals(1, users.size()); ScimUser user = users.get(0); - - ExpiringCode code = codeStore.generateCode(user.getId(), new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), null); + PasswordChange passwordChange = new PasswordChange(user.getId(), user.getUserName(), null, null, null); + ExpiringCode code = codeStore.generateCode(JsonUtils.writeValueAsString(passwordChange), new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), null); getMockMvc().perform(createChangePasswordRequest(user, code, true, "d3faultPasswd", "d3faultPasswd")); - code = codeStore.generateCode(user.getId(), new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), null); + code = codeStore.generateCode(JsonUtils.writeValueAsString(passwordChange), new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), null); getMockMvc().perform(createChangePasswordRequest(user, code, true, "d3faultPasswd", "d3faultPasswd")) .andExpect(status().isUnprocessableEntity()) .andExpect(view().name("forgot_password"))
uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/codestore/ExpiringCodeStoreMockMvcTests.java+1 −1 modified@@ -43,7 +43,7 @@ public class ExpiringCodeStoreMockMvcTests extends InjectedMockContextTest { @Before public void setUp() throws Exception { testClient = new TestClient(getMockMvc()); - loginToken = testClient.getClientCredentialsOAuthAccessToken("login", "loginsecret", null); + loginToken = testClient.getClientCredentialsOAuthAccessToken("login", "loginsecret", "oauth.login"); getWebApplicationContext().getBean(JdbcTemplate.class).update("DELETE FROM expiring_code_store "); }
uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/DisableUserManagementSecurityFilterMockMvcTest.java+7 −1 modified@@ -351,8 +351,14 @@ public void changeEmailControllerVerifyEmailNotAllowed() throws Exception { @Test public void changePasswordControllerChangePasswordPageNotAllowed() throws Exception { + MockMvcUtils.setDisableInternalUserManagement(false, getWebApplicationContext()); + + ResultActions result = createUser(); + ScimUser createdUser = JsonUtils.readValue(result.andReturn().getResponse().getContentAsString(), ScimUser.class); MockMvcUtils.setDisableInternalUserManagement(true, getWebApplicationContext()); - getMockMvc().perform(get("/change_password")) + + getMockMvc().perform(get("/change_password") + .session(getUserSession(createdUser.getUserName(), PASSWD))) .andExpect(status().isForbidden()) .andExpect(content() .string(JsonObjectMatcherUtils.matchesJsonObject(
5c2377487befMerge branch 'releases/3.3.0.1'
21 files changed · +168 −60
gradle.properties+1 −1 modified@@ -1 +1 @@ -version=3.3.0 +version=3.3.0.1
server/src/main/java/org/cloudfoundry/identity/uaa/account/UaaResetPasswordService.java+17 −12 modified@@ -49,6 +49,7 @@ public class UaaResetPasswordService implements ResetPasswordService, ApplicationEventPublisherAware { public static final int PASSWORD_RESET_LIFETIME = 30 * 60 * 1000; + public static final String FORGOT_PASSWORD_INTENT_PREFIX = "forgot_password_for_id:"; private final ScimUserProvisioning scimUserProvisioning; private final ExpiringCodeStore expiringCodeStore; @@ -79,20 +80,22 @@ private ResetPasswordResponse changePasswordCodeAuthenticated(String code, Strin throw new InvalidCodeException("invalid_code", "Sorry, your reset password link is no longer valid. Please request a new one", 422); } String userId; - String userName = null; - Date passwordLastModified = null; - String clientId = null; - String redirectUri = null; + String userName; + Date passwordLastModified; + String clientId; + String redirectUri; + PasswordChange change; try { - PasswordChange change = JsonUtils.readValue(expiringCode.getData(), PasswordChange.class); - userId = change.getUserId(); - userName = change.getUsername(); - passwordLastModified = change.getPasswordModifiedTime(); - clientId = change.getClientId(); - redirectUri = change.getRedirectUri(); + change = JsonUtils.readValue(expiringCode.getData(), PasswordChange.class); } catch (JsonUtils.JsonUtilException x) { - userId = expiringCode.getData(); + throw new InvalidCodeException("invalid_code", "Sorry, your reset password link is no longer valid. Please request a new one", 422); } + userId = change.getUserId(); + userName = change.getUsername(); + passwordLastModified = change.getPasswordModifiedTime(); + clientId = change.getClientId(); + redirectUri = change.getRedirectUri(); + ScimUser user = scimUserProvisioning.retrieve(userId); try { if (isUserModified(user, expiringCode.getExpiresAt(), userName, passwordLastModified)) { @@ -141,7 +144,9 @@ public ForgotPasswordInfo forgotPassword(String email, String clientId, String r ScimUser scimUser = results.get(0); PasswordChange change = new PasswordChange(scimUser.getId(), scimUser.getUserName(), scimUser.getPasswordLastModified(), clientId, redirectUri); - ExpiringCode code = expiringCodeStore.generateCode(JsonUtils.writeValueAsString(change), new Timestamp(System.currentTimeMillis() + PASSWORD_RESET_LIFETIME), null); + String intent = FORGOT_PASSWORD_INTENT_PREFIX+scimUser.getId(); + expiringCodeStore.expireByIntent(intent); + ExpiringCode code = expiringCodeStore.generateCode(JsonUtils.writeValueAsString(change), new Timestamp(System.currentTimeMillis() + PASSWORD_RESET_LIFETIME), intent); publish(new ResetPasswordRequestEvent(email, code.getCode(), SecurityContextHolder.getContext().getAuthentication())); return new ForgotPasswordInfo(scimUser.getId(), code); }
server/src/main/java/org/cloudfoundry/identity/uaa/codestore/JdbcExpiringCodeStore.java+1 −1 modified@@ -43,7 +43,7 @@ public class JdbcExpiringCodeStore implements ExpiringCodeStore { private Log logger = LogFactory.getLog(getClass()); - private RandomValueStringGenerator generator = new RandomValueStringGenerator(6); + private RandomValueStringGenerator generator = new RandomValueStringGenerator(10); private JdbcTemplate jdbcTemplate;
server/src/main/java/org/cloudfoundry/identity/uaa/invitations/EmailInvitationsService.java+1 −1 modified@@ -54,7 +54,7 @@ public AcceptedInvitation acceptInvitation(String code, String password) { user = scimUserProvisioning.verifyUser(userId, user.getVersion()); - if (OriginKeys.UAA.equals(user.getOrigin())) { + if (OriginKeys.UAA.equals(user.getOrigin()) && StringUtils.hasText(password)) { PasswordChangeRequest request = new PasswordChangeRequest(); request.setPassword(password); scimUserProvisioning.changePassword(userId, null, password);
server/src/main/java/org/cloudfoundry/identity/uaa/scim/endpoints/PasswordChange.java+0 −2 modified@@ -1,11 +1,9 @@ package org.cloudfoundry.identity.uaa.scim.endpoints; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.Date; -@JsonIgnoreProperties(ignoreUnknown = true) public class PasswordChange { public PasswordChange() {}
server/src/main/resources/login-ui.xml+1 −1 modified@@ -151,7 +151,7 @@ entry-point-ref="loginEntryPoint" use-expressions="false" xmlns="http://www.springframework.org/schema/security"> - <intercept-url pattern="/**" access="IS_AUTHENTICATED_ANONYMOUSLY" /> + <intercept-url pattern="/**" access="IS_AUTHENTICATED_FULLY" /> <csrf disabled="false"/> <access-denied-handler ref="loginEntryPoint"/> </http>
server/src/test/java/org/cloudfoundry/identity/uaa/codestore/CodeStoreEndpointsTests.java+1 −1 modified@@ -51,7 +51,7 @@ public void testGenerateCode() throws Exception { assertNotNull(result); assertNotNull(result.getCode()); - assertTrue(result.getCode().trim().length() > 0); + assertTrue(result.getCode().trim().length() == 10); assertEquals(expiresAt, result.getExpiresAt());
server/src/test/java/org/cloudfoundry/identity/uaa/invitations/InvitationsControllerTest.java+1 −1 modified@@ -306,7 +306,7 @@ public void acceptInvitePage_for_verifiedUser() throws Exception { when(expiringCodeStore.retrieveCode("the_secret_code")).thenReturn(new ExpiringCode("code", new Timestamp(System.currentTimeMillis()), JsonUtils.writeValueAsString(codeData), null)); when(expiringCodeStore.generateCode(anyString(), anyObject(), eq(null))).thenReturn(new ExpiringCode("code", new Timestamp(System.currentTimeMillis()), JsonUtils.writeValueAsString(codeData), null)); - when(invitationsService.acceptInvitation(anyString(), anyString())).thenReturn(new InvitationsService.AcceptedInvitation("blah.test.com", new ScimUser())); + when(invitationsService.acceptInvitation(anyString(), eq(""))).thenReturn(new InvitationsService.AcceptedInvitation("blah.test.com", new ScimUser())); IdentityProvider provider = new IdentityProvider(); provider.setType(OriginKeys.UAA); when(providerProvisioning.retrieveByOrigin(anyString(), anyString())).thenReturn(provider);
server/src/test/java/org/cloudfoundry/identity/uaa/login/EmailInvitationsServiceTests.java+18 −2 modified@@ -14,7 +14,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -32,7 +31,6 @@ 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 org.thymeleaf.spring4.SpringTemplateEngine; import java.sql.Timestamp; import java.util.HashMap; @@ -48,6 +46,7 @@ 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; @@ -108,6 +107,23 @@ public void acceptInvitationNoClientId() throws Exception { assertEquals("/home", redirectLocation); } + @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), null)); + + 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");
server/src/test/java/org/cloudfoundry/identity/uaa/login/UaaResetPasswordServiceTests.java+30 −8 modified@@ -12,15 +12,15 @@ *******************************************************************************/ 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.authentication.InvalidCodeException; 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; @@ -93,18 +93,24 @@ public void forgotPassword_ResetCodeIsReturnedSuccessfully() throws Exception { 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), eq(null))).thenReturn(new ExpiringCode("code", expiresAt, "user-id-001", null)); + any(Timestamp.class), anyString())).thenReturn(new ExpiringCode("code", expiresAt, "user-id-001", null)); ForgotPasswordInfo forgotPasswordInfo = emailResetPasswordService.forgotPassword("user@example.com", "example", "redirect.example.com"); - assertThat(forgotPasswordInfo.getUserId(), equalTo("user-id-001")); + 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); @@ -115,13 +121,13 @@ public void forgotPassword_PublishesResetPasswordRequestEvent() throws Exception 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), eq(null))).thenReturn(new ExpiringCode("code", expiresAt, "user-id-001", null)); + when(codeStore.generateCode(anyString(), any(Timestamp.class), anyString())).thenReturn(new ExpiringCode("code", expiresAt, "user-id-001", null)); emailResetPasswordService.forgotPassword("user@example.com", "", ""); ArgumentCaptor<ResetPasswordRequestEvent> captor = ArgumentCaptor.forClass(ResetPasswordRequestEvent.class); verify(publisher).publishEvent(captor.capture()); ResetPasswordRequestEvent event = captor.getValue(); - assertThat((String) event.getSource(), equalTo("user@example.com")); + assertThat(event.getSource(), equalTo("user@example.com")); assertThat(event.getCode(), equalTo("code")); assertThat(event.getAuthentication(), sameInstance(authentication)); } @@ -185,7 +191,7 @@ public void resetPassword_InvalidPasswordException_NewPasswordSameAsOld() { 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", null); + 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")) @@ -202,6 +208,22 @@ public void resetPassword_InvalidPasswordException_NewPasswordSameAsOld() { } } + @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 { + emailResetPasswordService.resetPassword("good_code", "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() { setupResetPassword("invalid_client", "redirect.example.com");
server/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/PasswordResetEndpointTest.java+14 −9 modified@@ -81,10 +81,10 @@ public void setUp() throws Exception { PasswordChange change = new PasswordChange("id001", "user@example.com", yesterday, null, null); - when(expiringCodeStore.generateCode(eq("id001"), any(Timestamp.class), eq(null))) + when(expiringCodeStore.generateCode(eq("id001"), any(Timestamp.class), anyString())) .thenReturn(new ExpiringCode("secret_code", new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), "id001", null)); - when(expiringCodeStore.generateCode(eq(JsonUtils.writeValueAsString(change)), any(Timestamp.class), eq(null))) + when(expiringCodeStore.generateCode(eq(JsonUtils.writeValueAsString(change)), any(Timestamp.class), anyString())) .thenReturn(new ExpiringCode("secret_code", new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), JsonUtils.writeValueAsString(change), null)); } @@ -100,7 +100,7 @@ public void password_reset_with_client_id_and_redirect_uri() throws Exception { .thenReturn(Arrays.asList(user)); PasswordChange change = new PasswordChange("id001", email, yesterday, clientId, redirectUri); - when(expiringCodeStore.generateCode(anyString(), any(Timestamp.class), eq(null))) + when(expiringCodeStore.generateCode(anyString(), any(Timestamp.class), anyString())) .thenReturn(new ExpiringCode("secret_code", new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), JsonUtils.writeValueAsString(change), null)); MockHttpServletRequestBuilder post = post("/password_resets") @@ -113,7 +113,7 @@ public void password_reset_with_client_id_and_redirect_uri() throws Exception { mockMvc.perform(post) .andExpect(status().isCreated()); - verify(expiringCodeStore).generateCode(eq(JsonUtils.writeValueAsString(change)), any(Timestamp.class), eq(null)); + verify(expiringCodeStore).generateCode(eq(JsonUtils.writeValueAsString(change)), any(Timestamp.class), anyString()); } @Test @@ -137,7 +137,7 @@ public void password_reset_without_client_id_and_without_redirect_uri() throws E mockMvc.perform(post) .andExpect(status().isCreated()); - verify(expiringCodeStore).generateCode(eq(JsonUtils.writeValueAsString(change)), any(Timestamp.class), eq(null)); + verify(expiringCodeStore).generateCode(eq(JsonUtils.writeValueAsString(change)), any(Timestamp.class), anyString()); } @Test @@ -206,7 +206,7 @@ public void testCreatingAPasswordResetWithAUsernameContainingSpecialCharacters() .thenReturn(Arrays.asList(user)); PasswordChange change = new PasswordChange("id001", "user\"'@example.com", yesterday, null, null); - when(expiringCodeStore.generateCode(eq(JsonUtils.writeValueAsString(change)), any(Timestamp.class), eq(null))) + when(expiringCodeStore.generateCode(eq(JsonUtils.writeValueAsString(change)), any(Timestamp.class), anyString())) .thenReturn(new ExpiringCode("secret_code", new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), JsonUtils.writeValueAsString(change), null)); MockHttpServletRequestBuilder post = post("/password_resets") @@ -237,7 +237,8 @@ public void testCreatingAPasswordResetWithAUsernameContainingSpecialCharacters() @Test public void testChangingAPasswordWithAValidCode() throws Exception { when(expiringCodeStore.retrieveCode("secret_code")) - .thenReturn(new ExpiringCode("secret_code", new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), "eyedee", null)); + .thenReturn(new ExpiringCode("secret_code", new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), + "{\"user_id\":\"eyedee\",\"username\":\"user@example.com\",\"passwordModifiedTime\":null,\"client_id\":\"\",\"redirect_uri\":\"\"}", null)); ScimUser scimUser = new ScimUser("eyedee", "user@example.com", "User", "Man"); scimUser.setMeta(new ScimMeta(new Date(System.currentTimeMillis() - (1000 * 60 * 60 * 24)), new Date(System.currentTimeMillis() - (1000 * 60 * 60 * 24)), 0)); @@ -281,7 +282,9 @@ public void changing_password_with_invalid_code() throws Exception { @Test public void testChangingAPasswordForUnverifiedUser() throws Exception { when(expiringCodeStore.retrieveCode("secret_code")) - .thenReturn(new ExpiringCode("secret_code", new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), "eyedee", null)); + .thenReturn(new ExpiringCode("secret_code", new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), + "{\"user_id\":\"eyedee\",\"username\":\"user@example.com\",\"passwordModifiedTime\":null,\"client_id\":\"\",\"redirect_uri\":\"\"}", + null)); ScimUser scimUser = new ScimUser("eyedee", "user@example.com", "User", "Man"); scimUser.setMeta(new ScimMeta(new Date(System.currentTimeMillis() - (1000 * 60 * 60 * 24)), new Date(System.currentTimeMillis() - (1000 * 60 * 60 * 24)), 0)); @@ -338,7 +341,9 @@ public void changePassword_Returns422UnprocessableEntity_NewPasswordSameAsOld() Mockito.reset(passwordValidator); when(expiringCodeStore.retrieveCode("emailed_code")) - .thenReturn(new ExpiringCode("emailed_code", new Timestamp(System.currentTimeMillis()+ UaaResetPasswordService.PASSWORD_RESET_LIFETIME), "eyedee", null)); + .thenReturn(new ExpiringCode("emailed_code", new Timestamp(System.currentTimeMillis()+ UaaResetPasswordService.PASSWORD_RESET_LIFETIME), + "{\"user_id\":\"eyedee\",\"username\":\"user@example.com\",\"passwordModifiedTime\":null,\"client_id\":\"\",\"redirect_uri\":\"\"}", + null)); ScimUser scimUser = new ScimUser("eyedee", "user@example.com", "User", "Man"); scimUser.setMeta(new ScimMeta(new Date(System.currentTimeMillis()-(1000*60*60*24)), new Date(System.currentTimeMillis()-(1000*60*60*24)), 0));
uaa/src/main/webapp/WEB-INF/spring/codestore-endpoints.xml+1 −1 modified@@ -26,7 +26,7 @@ <http name="codeStoreSecurity" pattern="/Codes/**" create-session="stateless" authentication-manager-ref="emptyAuthenticationManager" entry-point-ref="oauthAuthenticationEntryPoint" xmlns="http://www.springframework.org/schema/security" use-expressions="true"> - <intercept-url pattern="/**" access="#oauth2.hasAnyScope('scim.create','scim.write','password.write')"/> + <intercept-url pattern="/**" access="#oauth2.hasAnyScope('oauth.login')"/> <custom-filter ref="resourceAgnosticAuthenticationFilter" position="PRE_AUTH_FILTER" /> <anonymous enabled="false" /> <expression-handler ref="oauthWebExpressionHandler" />
uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/AutologinIT.java+1 −1 modified@@ -249,7 +249,7 @@ public void testFormEncodedAutologinRequest() throws Exception { Map.class); String autologinCode = (String) autologinResponseEntity.getBody().get("code"); - assertEquals(6, autologinCode.length()); + assertEquals(10, autologinCode.length()); } @Test
uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/ChangePasswordIT.java+2 −4 modified@@ -90,6 +90,7 @@ public void setUp() throws Exception { @Test public void testChangePassword() throws Exception { + webDriver.get(baseUrl + "/change_password"); signIn(userEmail, PASSWORD); changePassword(PASSWORD, NEW_PASSWORD, "new"); @@ -108,7 +109,7 @@ public void displaysErrorWhenPasswordContravenesPolicy() { //the only policy we can contravene by default is the length String newPassword = new RandomValueStringGenerator(260).generate(); - + webDriver.get(baseUrl + "/change_password"); signIn(userEmail, PASSWORD); changePassword(PASSWORD, newPassword, newPassword); @@ -134,11 +135,8 @@ private void signOut() { } private void signIn(String userName, String password) { - webDriver.get(baseUrl + "/logout.do"); - webDriver.get(baseUrl + "/login"); webDriver.findElement(By.name("username")).sendKeys(userName); webDriver.findElement(By.name("password")).sendKeys(password); webDriver.findElement(By.xpath("//input[@value='Sign in']")).click(); - assertThat(webDriver.findElement(By.cssSelector("h1")).getText(), containsString("Where to?")); } }
uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/InvitationsIT.java+10 −7 modified@@ -91,7 +91,7 @@ public class InvitationsIT { @Before public void setup() throws Exception { scimToken = testClient.getOAuthAccessToken("admin", "adminsecret", "client_credentials", "scim.read,scim.write"); - loginToken = testClient.getOAuthAccessToken("login", "loginsecret", "client_credentials", "password.write,scim.write"); + loginToken = testClient.getOAuthAccessToken("login", "loginsecret", "client_credentials", "oauth.login"); screenShootRule.setWebDriver(webDriver); } @@ -199,9 +199,9 @@ private String createInvitation(String username, String userEmail, String redire return createInvitation(baseUrl, uaaUrl, username, userEmail, origin, redirectUri, loginToken, scimToken); } - public static String createInvitation(String baseUrl, String uaaUrl, String username, String userEmail, String origin, String redirectUri, String scimWriteToken, String scimReadToken) { + public static String createInvitation(String baseUrl, String uaaUrl, String username, String userEmail, String origin, String redirectUri, String loginToken, String scimToken) { HttpHeaders headers = new HttpHeaders(); - headers.add("Authorization", "Bearer " + scimWriteToken); + headers.add("Authorization", "Bearer " + scimToken); RestTemplate uaaTemplate = new RestTemplate(); ScimUser scimUser = new ScimUser(); scimUser.setUserName(username); @@ -211,8 +211,8 @@ public static String createInvitation(String baseUrl, String uaaUrl, String user String userId = null; try { - userId = IntegrationTestUtils.getUserIdByField(scimReadToken, baseUrl, origin, "email", userEmail); - scimUser = IntegrationTestUtils.getUser(scimReadToken, baseUrl, userId); + userId = IntegrationTestUtils.getUserIdByField(scimToken, baseUrl, origin, "email", userEmail); + scimUser = IntegrationTestUtils.getUser(scimToken, baseUrl, userId); } catch (RuntimeException x) { } if (userId == null) { @@ -224,12 +224,15 @@ public static String createInvitation(String baseUrl, String uaaUrl, String user userId = response.getBody().getId(); } else { scimUser.setVerified(false); - IntegrationTestUtils.updateUser(scimWriteToken, uaaUrl, scimUser); + IntegrationTestUtils.updateUser(scimToken, uaaUrl, scimUser); } + HttpHeaders invitationHeaders = new HttpHeaders(); + invitationHeaders.add("Authorization", "Bearer " + loginToken); + Timestamp expiry = new Timestamp(System.currentTimeMillis() + TimeUnit.MILLISECONDS.convert(System.currentTimeMillis() + 24 * 3600, TimeUnit.MILLISECONDS)); ExpiringCode expiringCode = new ExpiringCode(null, expiry, "{\"origin\":\"" + origin + "\", \"client_id\":\"app\", \"redirect_uri\":\"" + redirectUri + "\", \"user_id\":\"" + userId + "\", \"email\":\"" + userEmail + "\"}", null); - HttpEntity<ExpiringCode> expiringCodeRequest = new HttpEntity<>(expiringCode, headers); + HttpEntity<ExpiringCode> expiringCodeRequest = new HttpEntity<>(expiringCode, invitationHeaders); ResponseEntity<ExpiringCode> expiringCodeResponse = uaaTemplate.exchange(uaaUrl + "/Codes", HttpMethod.POST, expiringCodeRequest, ExpiringCode.class); expiringCode = expiringCodeResponse.getBody(); return expiringCode.getCode();
uaa/src/test/java/org/cloudfoundry/identity/uaa/login/AccountsControllerMockMvcTests.java+7 −1 modified@@ -3,6 +3,7 @@ import com.dumbster.smtp.SimpleSmtpServer; import com.dumbster.smtp.SmtpMessage; import org.apache.commons.lang3.RandomStringUtils; +import org.cloudfoundry.identity.uaa.account.EmailAccountCreationService; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; import org.cloudfoundry.identity.uaa.codestore.JdbcExpiringCodeStore; import org.cloudfoundry.identity.uaa.constants.OriginKeys; @@ -13,7 +14,6 @@ import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.PredictableGenerator; import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.scim.jdbc.JdbcScimUserProvisioning; -import org.cloudfoundry.identity.uaa.account.EmailAccountCreationService; import org.cloudfoundry.identity.uaa.test.TestClient; import org.cloudfoundry.identity.uaa.test.UaaTestAccounts; import org.cloudfoundry.identity.uaa.util.JsonUtils; @@ -99,6 +99,12 @@ public void restoreMailSender() { getWebApplicationContext().getBean("emailService", EmailService.class).setMailSender(originalSender); } + @After + public void resetGenerator() { + getWebApplicationContext().getBean(JdbcExpiringCodeStore.class).setGenerator(new RandomValueStringGenerator(24)); + } + + @AfterClass public static void stopMailServer() throws Exception { if (mailServer!=null) {
uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcTests.java+5 −0 modified@@ -142,6 +142,11 @@ public void setUpContext() throws Exception { identityZoneConfiguration = getWebApplicationContext().getBean(IdentityZoneProvisioning.class).retrieve(getUaa().getId()).getConfig(); } + @After + public void resetGenerator() { + getWebApplicationContext().getBean(JdbcExpiringCodeStore.class).setGenerator(new RandomValueStringGenerator(24)); + } + @After public void tearDown() throws Exception { //restore all properties
uaa/src/test/java/org/cloudfoundry/identity/uaa/login/ResetPasswordControllerMockMvcTests.java+42 −5 modified@@ -12,6 +12,7 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.login; +import org.cloudfoundry.identity.uaa.account.UaaResetPasswordService; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; import org.cloudfoundry.identity.uaa.codestore.ExpiringCode; import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeStore; @@ -20,14 +21,15 @@ import org.cloudfoundry.identity.uaa.mock.InjectedMockContextTest; import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils; import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.PredictableGenerator; -import org.cloudfoundry.identity.uaa.account.UaaResetPasswordService; import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning; import org.cloudfoundry.identity.uaa.scim.endpoints.PasswordChange; import org.cloudfoundry.identity.uaa.util.JsonUtils; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.springframework.http.MediaType; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpSession; import org.springframework.security.core.Authentication; @@ -73,6 +75,12 @@ public void initResetPasswordTest() throws Exception { codeStore = getWebApplicationContext().getBean(ExpiringCodeStore.class); } + @After + public void resetGenerator() { + getWebApplicationContext().getBean(JdbcExpiringCodeStore.class).setGenerator(new RandomValueStringGenerator(24)); + } + + @Test public void testResettingAPasswordUsingUsernameToEnsureNoModification() throws Exception { @@ -156,6 +164,34 @@ public void testResettingAPasswordChangesCodeInForm() throws Exception { .andExpect(redirectedUrl("home")); } + @Test + public void create_new_code_for_repeated_request() throws Exception { + String username = new RandomValueStringGenerator().generate() + "@test.org"; + ScimUser user = new ScimUser(null, username, "givenname","familyname"); + user.setPrimaryEmail(username); + user.setPassword("secret"); + String token = MockMvcUtils.utils().getClientCredentialsOAuthAccessToken(getMockMvc(), "admin", "adminsecret", null, null); + user = MockMvcUtils.utils().createUser(getMockMvc(), token, user); + + + PredictableGenerator generator = new PredictableGenerator(); + JdbcExpiringCodeStore store = getWebApplicationContext().getBean(JdbcExpiringCodeStore.class); + store.setGenerator(generator); + JdbcTemplate template = getWebApplicationContext().getBean(JdbcTemplate.class); + String intent = UaaResetPasswordService.FORGOT_PASSWORD_INTENT_PREFIX+user.getId(); + + getMockMvc().perform(post("/forgot_password.do") + .param("email", user.getUserName())) + .andExpect(redirectedUrl("email_sent?code=reset_password")); + + getMockMvc().perform(post("/forgot_password.do") + .param("email", user.getUserName())) + .andExpect(redirectedUrl("email_sent?code=reset_password")); + + assertEquals(1, (int)template.queryForObject("select count(*) from expiring_code_store where intent=?", new Object[] {intent}, Integer.class)); + + } + @Test public void redirectToSavedRequest_ifPresent() throws Exception { String username = new RandomValueStringGenerator().generate() + "@test.org"; @@ -239,7 +275,8 @@ public void testResettingAPasswordUsingTimestampForUserModification() throws Exc List<ScimUser> users = getWebApplicationContext().getBean(ScimUserProvisioning.class).query("username eq \"marissa\""); assertNotNull(users); assertEquals(1, users.size()); - ExpiringCode code = codeStore.generateCode(users.get(0).getId(), new Timestamp(System.currentTimeMillis()+ UaaResetPasswordService.PASSWORD_RESET_LIFETIME), null); + PasswordChange passwordChange = new PasswordChange(users.get(0).getId(), users.get(0).getUserName(), null, null, null); + ExpiringCode code = codeStore.generateCode(JsonUtils.writeValueAsString(passwordChange), new Timestamp(System.currentTimeMillis()+ UaaResetPasswordService.PASSWORD_RESET_LIFETIME), null); MockHttpServletRequestBuilder post = createChangePasswordRequest(users.get(0), code, true, "newpassw0rD", "newpassw0rD"); @@ -266,11 +303,11 @@ public void resetPassword_ReturnsUnprocessableEntity_NewPasswordSameAsOld() thro assertNotNull(users); assertEquals(1, users.size()); ScimUser user = users.get(0); - - ExpiringCode code = codeStore.generateCode(user.getId(), new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), null); + PasswordChange passwordChange = new PasswordChange(user.getId(), user.getUserName(), null, null, null); + ExpiringCode code = codeStore.generateCode(JsonUtils.writeValueAsString(passwordChange), new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), null); getMockMvc().perform(createChangePasswordRequest(user, code, true, "d3faultPasswd", "d3faultPasswd")); - code = codeStore.generateCode(user.getId(), new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), null); + code = codeStore.generateCode(JsonUtils.writeValueAsString(passwordChange), new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), null); getMockMvc().perform(createChangePasswordRequest(user, code, true, "d3faultPasswd", "d3faultPasswd")) .andExpect(status().isUnprocessableEntity()) .andExpect(view().name("forgot_password"))
uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/codestore/ExpiringCodeStoreMockMvcTests.java+1 −1 modified@@ -43,7 +43,7 @@ public class ExpiringCodeStoreMockMvcTests extends InjectedMockContextTest { @Before public void setUp() throws Exception { testClient = new TestClient(getMockMvc()); - loginToken = testClient.getClientCredentialsOAuthAccessToken("login", "loginsecret", null); + loginToken = testClient.getClientCredentialsOAuthAccessToken("login", "loginsecret", "oauth.login"); getWebApplicationContext().getBean(JdbcTemplate.class).update("DELETE FROM expiring_code_store "); }
uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/DisableUserManagementSecurityFilterMockMvcTest.java+7 −1 modified@@ -351,8 +351,14 @@ public void changeEmailControllerVerifyEmailNotAllowed() throws Exception { @Test public void changePasswordControllerChangePasswordPageNotAllowed() throws Exception { + MockMvcUtils.setDisableInternalUserManagement(false, getWebApplicationContext()); + + ResultActions result = createUser(); + ScimUser createdUser = JsonUtils.readValue(result.andReturn().getResponse().getContentAsString(), ScimUser.class); MockMvcUtils.setDisableInternalUserManagement(true, getWebApplicationContext()); - getMockMvc().perform(get("/change_password")) + + getMockMvc().perform(get("/change_password") + .session(getUserSession(createdUser.getUserName(), PASSWD))) .andExpect(status().isForbidden()) .andExpect(content() .string(JsonObjectMatcherUtils.matchesJsonObject(
uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/PasswordResetEndpointMockMvcTests.java+7 −0 modified@@ -23,6 +23,7 @@ import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.test.TestClient; import org.cloudfoundry.identity.uaa.util.JsonUtils; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.springframework.security.oauth2.common.util.OAuth2Utils; @@ -53,6 +54,7 @@ public class PasswordResetEndpointMockMvcTests extends InjectedMockContextTest { private String loginToken; private ScimUser user; + private RandomValueStringGenerator originalGenerator; @Before public void setUp() throws Exception { @@ -65,6 +67,11 @@ public void setUp() throws Exception { user = MockMvcUtils.utils().createUser(getMockMvc(), adminToken, user); } + @After + public void resetGenerator() { + getWebApplicationContext().getBean(JdbcExpiringCodeStore.class).setGenerator(new RandomValueStringGenerator(24)); + } + @Test public void changePassword_isSuccessful() throws Exception {
460627ed419eImprove reset password experience
4 files changed · +52 −14
server/src/main/java/org/cloudfoundry/identity/uaa/account/UaaResetPasswordService.java+4 −1 modified@@ -49,6 +49,7 @@ public class UaaResetPasswordService implements ResetPasswordService, ApplicationEventPublisherAware { public static final int PASSWORD_RESET_LIFETIME = 30 * 60 * 1000; + public static final String FORGOT_PASSWORD_INTENT_PREFIX = "forgot_password_for_id:"; private final ScimUserProvisioning scimUserProvisioning; private final ExpiringCodeStore expiringCodeStore; @@ -143,7 +144,9 @@ public ForgotPasswordInfo forgotPassword(String email, String clientId, String r ScimUser scimUser = results.get(0); PasswordChange change = new PasswordChange(scimUser.getId(), scimUser.getUserName(), scimUser.getPasswordLastModified(), clientId, redirectUri); - ExpiringCode code = expiringCodeStore.generateCode(JsonUtils.writeValueAsString(change), new Timestamp(System.currentTimeMillis() + PASSWORD_RESET_LIFETIME), null); + String intent = FORGOT_PASSWORD_INTENT_PREFIX+scimUser.getId(); + expiringCodeStore.expireByIntent(intent); + ExpiringCode code = expiringCodeStore.generateCode(JsonUtils.writeValueAsString(change), new Timestamp(System.currentTimeMillis() + PASSWORD_RESET_LIFETIME), intent); publish(new ResetPasswordRequestEvent(email, code.getCode(), SecurityContextHolder.getContext().getAuthentication())); return new ForgotPasswordInfo(scimUser.getId(), code); }
server/src/test/java/org/cloudfoundry/identity/uaa/login/UaaResetPasswordServiceTests.java+12 −6 modified@@ -12,15 +12,15 @@ *******************************************************************************/ 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.authentication.InvalidCodeException; 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; @@ -93,18 +93,24 @@ public void forgotPassword_ResetCodeIsReturnedSuccessfully() throws Exception { 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), eq(null))).thenReturn(new ExpiringCode("code", expiresAt, "user-id-001", null)); + any(Timestamp.class), anyString())).thenReturn(new ExpiringCode("code", expiresAt, "user-id-001", null)); ForgotPasswordInfo forgotPasswordInfo = emailResetPasswordService.forgotPassword("user@example.com", "example", "redirect.example.com"); - assertThat(forgotPasswordInfo.getUserId(), equalTo("user-id-001")); + 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); @@ -115,7 +121,7 @@ public void forgotPassword_PublishesResetPasswordRequestEvent() throws Exception 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), eq(null))).thenReturn(new ExpiringCode("code", expiresAt, "user-id-001", null)); + when(codeStore.generateCode(anyString(), any(Timestamp.class), anyString())).thenReturn(new ExpiringCode("code", expiresAt, "user-id-001", null)); emailResetPasswordService.forgotPassword("user@example.com", "", ""); ArgumentCaptor<ResetPasswordRequestEvent> captor = ArgumentCaptor.forClass(ResetPasswordRequestEvent.class);
server/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/PasswordResetEndpointTest.java+6 −6 modified@@ -81,10 +81,10 @@ public void setUp() throws Exception { PasswordChange change = new PasswordChange("id001", "user@example.com", yesterday, null, null); - when(expiringCodeStore.generateCode(eq("id001"), any(Timestamp.class), eq(null))) + when(expiringCodeStore.generateCode(eq("id001"), any(Timestamp.class), anyString())) .thenReturn(new ExpiringCode("secret_code", new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), "id001", null)); - when(expiringCodeStore.generateCode(eq(JsonUtils.writeValueAsString(change)), any(Timestamp.class), eq(null))) + when(expiringCodeStore.generateCode(eq(JsonUtils.writeValueAsString(change)), any(Timestamp.class), anyString())) .thenReturn(new ExpiringCode("secret_code", new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), JsonUtils.writeValueAsString(change), null)); } @@ -100,7 +100,7 @@ public void password_reset_with_client_id_and_redirect_uri() throws Exception { .thenReturn(Arrays.asList(user)); PasswordChange change = new PasswordChange("id001", email, yesterday, clientId, redirectUri); - when(expiringCodeStore.generateCode(anyString(), any(Timestamp.class), eq(null))) + when(expiringCodeStore.generateCode(anyString(), any(Timestamp.class), anyString())) .thenReturn(new ExpiringCode("secret_code", new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), JsonUtils.writeValueAsString(change), null)); MockHttpServletRequestBuilder post = post("/password_resets") @@ -113,7 +113,7 @@ public void password_reset_with_client_id_and_redirect_uri() throws Exception { mockMvc.perform(post) .andExpect(status().isCreated()); - verify(expiringCodeStore).generateCode(eq(JsonUtils.writeValueAsString(change)), any(Timestamp.class), eq(null)); + verify(expiringCodeStore).generateCode(eq(JsonUtils.writeValueAsString(change)), any(Timestamp.class), anyString()); } @Test @@ -137,7 +137,7 @@ public void password_reset_without_client_id_and_without_redirect_uri() throws E mockMvc.perform(post) .andExpect(status().isCreated()); - verify(expiringCodeStore).generateCode(eq(JsonUtils.writeValueAsString(change)), any(Timestamp.class), eq(null)); + verify(expiringCodeStore).generateCode(eq(JsonUtils.writeValueAsString(change)), any(Timestamp.class), anyString()); } @Test @@ -206,7 +206,7 @@ public void testCreatingAPasswordResetWithAUsernameContainingSpecialCharacters() .thenReturn(Arrays.asList(user)); PasswordChange change = new PasswordChange("id001", "user\"'@example.com", yesterday, null, null); - when(expiringCodeStore.generateCode(eq(JsonUtils.writeValueAsString(change)), any(Timestamp.class), eq(null))) + when(expiringCodeStore.generateCode(eq(JsonUtils.writeValueAsString(change)), any(Timestamp.class), anyString())) .thenReturn(new ExpiringCode("secret_code", new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), JsonUtils.writeValueAsString(change), null)); MockHttpServletRequestBuilder post = post("/password_resets")
uaa/src/test/java/org/cloudfoundry/identity/uaa/login/ResetPasswordControllerMockMvcTests.java+30 −1 modified@@ -12,6 +12,7 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.login; +import org.cloudfoundry.identity.uaa.account.UaaResetPasswordService; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; import org.cloudfoundry.identity.uaa.codestore.ExpiringCode; import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeStore; @@ -20,14 +21,14 @@ import org.cloudfoundry.identity.uaa.mock.InjectedMockContextTest; import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils; import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.PredictableGenerator; -import org.cloudfoundry.identity.uaa.account.UaaResetPasswordService; import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning; import org.cloudfoundry.identity.uaa.scim.endpoints.PasswordChange; import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.junit.Before; import org.junit.Test; import org.springframework.http.MediaType; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpSession; import org.springframework.security.core.Authentication; @@ -156,6 +157,34 @@ public void testResettingAPasswordChangesCodeInForm() throws Exception { .andExpect(redirectedUrl("home")); } + @Test + public void create_new_code_for_repeated_request() throws Exception { + String username = new RandomValueStringGenerator().generate() + "@test.org"; + ScimUser user = new ScimUser(null, username, "givenname","familyname"); + user.setPrimaryEmail(username); + user.setPassword("secret"); + String token = MockMvcUtils.utils().getClientCredentialsOAuthAccessToken(getMockMvc(), "admin", "adminsecret", null, null); + user = MockMvcUtils.utils().createUser(getMockMvc(), token, user); + + + PredictableGenerator generator = new PredictableGenerator(); + JdbcExpiringCodeStore store = getWebApplicationContext().getBean(JdbcExpiringCodeStore.class); + store.setGenerator(generator); + JdbcTemplate template = getWebApplicationContext().getBean(JdbcTemplate.class); + String intent = UaaResetPasswordService.FORGOT_PASSWORD_INTENT_PREFIX+user.getId(); + + getMockMvc().perform(post("/forgot_password.do") + .param("email", user.getUserName())) + .andExpect(redirectedUrl("email_sent?code=reset_password")); + + getMockMvc().perform(post("/forgot_password.do") + .param("email", user.getUserName())) + .andExpect(redirectedUrl("email_sent?code=reset_password")); + + assertEquals(1, (int)template.queryForObject("select count(*) from expiring_code_store where intent=?", new Object[] {intent}, Integer.class)); + + } + @Test public void redirectToSavedRequest_ifPresent() throws Exception { String username = new RandomValueStringGenerator().generate() + "@test.org";
1d3ad7399d01Improve reset password experience
5 files changed · +55 −15
server/src/main/java/org/cloudfoundry/identity/uaa/account/UaaResetPasswordService.java+4 −1 modified@@ -49,6 +49,7 @@ public class UaaResetPasswordService implements ResetPasswordService, ApplicationEventPublisherAware { public static final int PASSWORD_RESET_LIFETIME = 30 * 60 * 1000; + public static final String FORGOT_PASSWORD_INTENT_PREFIX = "forgot_password_for_id:"; private final ScimUserProvisioning scimUserProvisioning; private final ExpiringCodeStore expiringCodeStore; @@ -141,7 +142,9 @@ public ForgotPasswordInfo forgotPassword(String email, String clientId, String r ScimUser scimUser = results.get(0); PasswordChange change = new PasswordChange(scimUser.getId(), scimUser.getUserName(), scimUser.getPasswordLastModified(), clientId, redirectUri); - ExpiringCode code = expiringCodeStore.generateCode(JsonUtils.writeValueAsString(change), new Timestamp(System.currentTimeMillis() + PASSWORD_RESET_LIFETIME), null); + String intent = FORGOT_PASSWORD_INTENT_PREFIX+scimUser.getId(); + expiringCodeStore.expireByIntent(intent); + ExpiringCode code = expiringCodeStore.generateCode(JsonUtils.writeValueAsString(change), new Timestamp(System.currentTimeMillis() + PASSWORD_RESET_LIFETIME), intent); publish(new ResetPasswordRequestEvent(email, code.getCode(), SecurityContextHolder.getContext().getAuthentication())); return new ForgotPasswordInfo(scimUser.getId(), code); }
server/src/test/java/org/cloudfoundry/identity/uaa/login/UaaResetPasswordServiceTests.java+12 −6 modified@@ -12,15 +12,15 @@ *******************************************************************************/ 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.authentication.InvalidCodeException; 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; @@ -93,18 +93,24 @@ public void forgotPassword_ResetCodeIsReturnedSuccessfully() throws Exception { 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), eq(null))).thenReturn(new ExpiringCode("code", expiresAt, "user-id-001", null)); + any(Timestamp.class), anyString())).thenReturn(new ExpiringCode("code", expiresAt, "user-id-001", null)); ForgotPasswordInfo forgotPasswordInfo = emailResetPasswordService.forgotPassword("user@example.com", "example", "redirect.example.com"); - assertThat(forgotPasswordInfo.getUserId(), equalTo("user-id-001")); + 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); @@ -115,7 +121,7 @@ public void forgotPassword_PublishesResetPasswordRequestEvent() throws Exception 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), eq(null))).thenReturn(new ExpiringCode("code", expiresAt, "user-id-001", null)); + when(codeStore.generateCode(anyString(), any(Timestamp.class), anyString())).thenReturn(new ExpiringCode("code", expiresAt, "user-id-001", null)); emailResetPasswordService.forgotPassword("user@example.com", "", ""); ArgumentCaptor<ResetPasswordRequestEvent> captor = ArgumentCaptor.forClass(ResetPasswordRequestEvent.class);
server/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/PasswordResetEndpointTest.java+6 −6 modified@@ -81,10 +81,10 @@ public void setUp() throws Exception { PasswordChange change = new PasswordChange("id001", "user@example.com", yesterday, null, null); - when(expiringCodeStore.generateCode(eq("id001"), any(Timestamp.class), eq(null))) + when(expiringCodeStore.generateCode(eq("id001"), any(Timestamp.class), anyString())) .thenReturn(new ExpiringCode("secret_code", new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), "id001", null)); - when(expiringCodeStore.generateCode(eq(JsonUtils.writeValueAsString(change)), any(Timestamp.class), eq(null))) + when(expiringCodeStore.generateCode(eq(JsonUtils.writeValueAsString(change)), any(Timestamp.class), anyString())) .thenReturn(new ExpiringCode("secret_code", new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), JsonUtils.writeValueAsString(change), null)); } @@ -100,7 +100,7 @@ public void password_reset_with_client_id_and_redirect_uri() throws Exception { .thenReturn(Arrays.asList(user)); PasswordChange change = new PasswordChange("id001", email, yesterday, clientId, redirectUri); - when(expiringCodeStore.generateCode(anyString(), any(Timestamp.class), eq(null))) + when(expiringCodeStore.generateCode(anyString(), any(Timestamp.class), anyString())) .thenReturn(new ExpiringCode("secret_code", new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), JsonUtils.writeValueAsString(change), null)); MockHttpServletRequestBuilder post = post("/password_resets") @@ -113,7 +113,7 @@ public void password_reset_with_client_id_and_redirect_uri() throws Exception { mockMvc.perform(post) .andExpect(status().isCreated()); - verify(expiringCodeStore).generateCode(eq(JsonUtils.writeValueAsString(change)), any(Timestamp.class), eq(null)); + verify(expiringCodeStore).generateCode(eq(JsonUtils.writeValueAsString(change)), any(Timestamp.class), anyString()); } @Test @@ -137,7 +137,7 @@ public void password_reset_without_client_id_and_without_redirect_uri() throws E mockMvc.perform(post) .andExpect(status().isCreated()); - verify(expiringCodeStore).generateCode(eq(JsonUtils.writeValueAsString(change)), any(Timestamp.class), eq(null)); + verify(expiringCodeStore).generateCode(eq(JsonUtils.writeValueAsString(change)), any(Timestamp.class), anyString()); } @Test @@ -206,7 +206,7 @@ public void testCreatingAPasswordResetWithAUsernameContainingSpecialCharacters() .thenReturn(Arrays.asList(user)); PasswordChange change = new PasswordChange("id001", "user\"'@example.com", yesterday, null, null); - when(expiringCodeStore.generateCode(eq(JsonUtils.writeValueAsString(change)), any(Timestamp.class), eq(null))) + when(expiringCodeStore.generateCode(eq(JsonUtils.writeValueAsString(change)), any(Timestamp.class), anyString())) .thenReturn(new ExpiringCode("secret_code", new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), JsonUtils.writeValueAsString(change), null)); MockHttpServletRequestBuilder post = post("/password_resets")
uaa/src/test/java/org/cloudfoundry/identity/uaa/login/ResetPasswordControllerMockMvcTests.java+30 −1 modified@@ -12,6 +12,7 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.login; +import org.cloudfoundry.identity.uaa.account.UaaResetPasswordService; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; import org.cloudfoundry.identity.uaa.codestore.ExpiringCode; import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeStore; @@ -20,14 +21,14 @@ import org.cloudfoundry.identity.uaa.mock.InjectedMockContextTest; import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils; import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.PredictableGenerator; -import org.cloudfoundry.identity.uaa.account.UaaResetPasswordService; import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning; import org.cloudfoundry.identity.uaa.scim.endpoints.PasswordChange; import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.junit.Before; import org.junit.Test; import org.springframework.http.MediaType; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpSession; import org.springframework.security.core.Authentication; @@ -156,6 +157,34 @@ public void testResettingAPasswordChangesCodeInForm() throws Exception { .andExpect(redirectedUrl("home")); } + @Test + public void create_new_code_for_repeated_request() throws Exception { + String username = new RandomValueStringGenerator().generate() + "@test.org"; + ScimUser user = new ScimUser(null, username, "givenname","familyname"); + user.setPrimaryEmail(username); + user.setPassword("secret"); + String token = MockMvcUtils.utils().getClientCredentialsOAuthAccessToken(getMockMvc(), "admin", "adminsecret", null, null); + user = MockMvcUtils.utils().createUser(getMockMvc(), token, user); + + + PredictableGenerator generator = new PredictableGenerator(); + JdbcExpiringCodeStore store = getWebApplicationContext().getBean(JdbcExpiringCodeStore.class); + store.setGenerator(generator); + JdbcTemplate template = getWebApplicationContext().getBean(JdbcTemplate.class); + String intent = UaaResetPasswordService.FORGOT_PASSWORD_INTENT_PREFIX+user.getId(); + + getMockMvc().perform(post("/forgot_password.do") + .param("email", user.getUserName())) + .andExpect(redirectedUrl("email_sent?code=reset_password")); + + getMockMvc().perform(post("/forgot_password.do") + .param("email", user.getUserName())) + .andExpect(redirectedUrl("email_sent?code=reset_password")); + + assertEquals(1, (int)template.queryForObject("select count(*) from expiring_code_store where intent=?", new Object[] {intent}, Integer.class)); + + } + @Test public void redirectToSavedRequest_ifPresent() throws Exception { String username = new RandomValueStringGenerator().generate() + "@test.org";
uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpointDocs.java+3 −1 modified@@ -57,6 +57,7 @@ import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; public class ScimUserEndpointDocs extends InjectedMockContextTest { @@ -458,7 +459,7 @@ public void test_Change_Password() throws Exception { public void getUserVerificationLink() throws Exception { String accessToken = testClient.getClientCredentialsOAuthAccessToken("admin", "adminsecret", "uaa.admin"); - String email = "joel@example.com"; + String email = "joel"+new RandomValueStringGenerator().generate()+"@example.com"; ScimUser joel = new ScimUser(null, email, "Joel", "D'sa"); joel.setVerified(false); joel.addEmail(email); @@ -477,6 +478,7 @@ public void getUserVerificationLink() throws Exception { RequestDocumentation.parameterWithName("userId").description("The ID of the user to verify") ); getMockMvc().perform(get) + .andDo(print()) .andExpect(status().isOk()) .andDo(document("{ClassName}/{methodName}", preprocessResponse(prettyPrint()), pathParameters, requestHeaders, requestParameters, responseFields))
b3834364ab57Do no update password when existing user accepts invite
3 files changed · +20 −4
server/src/main/java/org/cloudfoundry/identity/uaa/invitations/EmailInvitationsService.java+1 −1 modified@@ -54,7 +54,7 @@ public AcceptedInvitation acceptInvitation(String code, String password) { user = scimUserProvisioning.verifyUser(userId, user.getVersion()); - if (OriginKeys.UAA.equals(user.getOrigin())) { + if (OriginKeys.UAA.equals(user.getOrigin()) && StringUtils.hasText(password)) { PasswordChangeRequest request = new PasswordChangeRequest(); request.setPassword(password); scimUserProvisioning.changePassword(userId, null, password);
server/src/test/java/org/cloudfoundry/identity/uaa/invitations/InvitationsControllerTest.java+1 −1 modified@@ -306,7 +306,7 @@ public void acceptInvitePage_for_verifiedUser() throws Exception { when(expiringCodeStore.retrieveCode("the_secret_code")).thenReturn(new ExpiringCode("code", new Timestamp(System.currentTimeMillis()), JsonUtils.writeValueAsString(codeData), null)); when(expiringCodeStore.generateCode(anyString(), anyObject(), eq(null))).thenReturn(new ExpiringCode("code", new Timestamp(System.currentTimeMillis()), JsonUtils.writeValueAsString(codeData), null)); - when(invitationsService.acceptInvitation(anyString(), anyString())).thenReturn(new InvitationsService.AcceptedInvitation("blah.test.com", new ScimUser())); + when(invitationsService.acceptInvitation(anyString(), eq(""))).thenReturn(new InvitationsService.AcceptedInvitation("blah.test.com", new ScimUser())); IdentityProvider provider = new IdentityProvider(); provider.setType(OriginKeys.UAA); when(providerProvisioning.retrieveByOrigin(anyString(), anyString())).thenReturn(provider);
server/src/test/java/org/cloudfoundry/identity/uaa/login/EmailInvitationsServiceTests.java+18 −2 modified@@ -14,7 +14,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -32,7 +31,6 @@ 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 org.thymeleaf.spring4.SpringTemplateEngine; import java.sql.Timestamp; import java.util.HashMap; @@ -48,6 +46,7 @@ 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; @@ -108,6 +107,23 @@ public void acceptInvitationNoClientId() throws Exception { assertEquals("/home", redirectLocation); } + @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), null)); + + 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");
14350228989eChange password page required full authentication to access
3 files changed · +10 −6
server/src/main/resources/login-ui.xml+1 −1 modified@@ -151,7 +151,7 @@ entry-point-ref="loginEntryPoint" use-expressions="false" xmlns="http://www.springframework.org/schema/security"> - <intercept-url pattern="/**" access="IS_AUTHENTICATED_ANONYMOUSLY" /> + <intercept-url pattern="/**" access="IS_AUTHENTICATED_FULLY" /> <csrf disabled="false"/> <access-denied-handler ref="loginEntryPoint"/> </http>
uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/ChangePasswordIT.java+2 −4 modified@@ -90,6 +90,7 @@ public void setUp() throws Exception { @Test public void testChangePassword() throws Exception { + webDriver.get(baseUrl + "/change_password"); signIn(userEmail, PASSWORD); changePassword(PASSWORD, NEW_PASSWORD, "new"); @@ -108,7 +109,7 @@ public void displaysErrorWhenPasswordContravenesPolicy() { //the only policy we can contravene by default is the length String newPassword = new RandomValueStringGenerator(260).generate(); - + webDriver.get(baseUrl + "/change_password"); signIn(userEmail, PASSWORD); changePassword(PASSWORD, newPassword, newPassword); @@ -134,11 +135,8 @@ private void signOut() { } private void signIn(String userName, String password) { - webDriver.get(baseUrl + "/logout.do"); - webDriver.get(baseUrl + "/login"); webDriver.findElement(By.name("username")).sendKeys(userName); webDriver.findElement(By.name("password")).sendKeys(password); webDriver.findElement(By.xpath("//input[@value='Sign in']")).click(); - assertThat(webDriver.findElement(By.cssSelector("h1")).getText(), containsString("Where to?")); } }
uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/DisableUserManagementSecurityFilterMockMvcTest.java+7 −1 modified@@ -351,8 +351,14 @@ public void changeEmailControllerVerifyEmailNotAllowed() throws Exception { @Test public void changePasswordControllerChangePasswordPageNotAllowed() throws Exception { + MockMvcUtils.setDisableInternalUserManagement(false, getWebApplicationContext()); + + ResultActions result = createUser(); + ScimUser createdUser = JsonUtils.readValue(result.andReturn().getResponse().getContentAsString(), ScimUser.class); MockMvcUtils.setDisableInternalUserManagement(true, getWebApplicationContext()); - getMockMvc().perform(get("/change_password")) + + getMockMvc().perform(get("/change_password") + .session(getUserSession(createdUser.getUserName(), PASSWD))) .andExpect(status().isForbidden()) .andExpect(content() .string(JsonObjectMatcherUtils.matchesJsonObject(
66132926f1baOnly Reset Password code data should deserialize to PasswordChange
5 files changed · +44 −22
server/src/main/java/org/cloudfoundry/identity/uaa/account/UaaResetPasswordService.java+13 −11 modified@@ -79,20 +79,22 @@ private ResetPasswordResponse changePasswordCodeAuthenticated(String code, Strin throw new InvalidCodeException("invalid_code", "Sorry, your reset password link is no longer valid. Please request a new one", 422); } String userId; - String userName = null; - Date passwordLastModified = null; - String clientId = null; - String redirectUri = null; + String userName; + Date passwordLastModified; + String clientId; + String redirectUri; + PasswordChange change; try { - PasswordChange change = JsonUtils.readValue(expiringCode.getData(), PasswordChange.class); - userId = change.getUserId(); - userName = change.getUsername(); - passwordLastModified = change.getPasswordModifiedTime(); - clientId = change.getClientId(); - redirectUri = change.getRedirectUri(); + change = JsonUtils.readValue(expiringCode.getData(), PasswordChange.class); } catch (JsonUtils.JsonUtilException x) { - userId = expiringCode.getData(); + throw new InvalidCodeException("invalid_code", "Sorry, your reset password link is no longer valid. Please request a new one", 422); } + userId = change.getUserId(); + userName = change.getUsername(); + passwordLastModified = change.getPasswordModifiedTime(); + clientId = change.getClientId(); + redirectUri = change.getRedirectUri(); + ScimUser user = scimUserProvisioning.retrieve(userId); try { if (isUserModified(user, expiringCode.getExpiresAt(), userName, passwordLastModified)) {
server/src/main/java/org/cloudfoundry/identity/uaa/scim/endpoints/PasswordChange.java+0 −2 modified@@ -1,11 +1,9 @@ package org.cloudfoundry.identity.uaa.scim.endpoints; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.Date; -@JsonIgnoreProperties(ignoreUnknown = true) public class PasswordChange { public PasswordChange() {}
server/src/test/java/org/cloudfoundry/identity/uaa/login/UaaResetPasswordServiceTests.java+18 −2 modified@@ -121,7 +121,7 @@ public void forgotPassword_PublishesResetPasswordRequestEvent() throws Exception ArgumentCaptor<ResetPasswordRequestEvent> captor = ArgumentCaptor.forClass(ResetPasswordRequestEvent.class); verify(publisher).publishEvent(captor.capture()); ResetPasswordRequestEvent event = captor.getValue(); - assertThat((String) event.getSource(), equalTo("user@example.com")); + assertThat(event.getSource(), equalTo("user@example.com")); assertThat(event.getCode(), equalTo("code")); assertThat(event.getAuthentication(), sameInstance(authentication)); } @@ -185,7 +185,7 @@ public void resetPassword_InvalidPasswordException_NewPasswordSameAsOld() { 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", null); + 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")) @@ -202,6 +202,22 @@ public void resetPassword_InvalidPasswordException_NewPasswordSameAsOld() { } } + @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 { + emailResetPasswordService.resetPassword("good_code", "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() { setupResetPassword("invalid_client", "redirect.example.com");
server/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/PasswordResetEndpointTest.java+8 −3 modified@@ -237,7 +237,8 @@ public void testCreatingAPasswordResetWithAUsernameContainingSpecialCharacters() @Test public void testChangingAPasswordWithAValidCode() throws Exception { when(expiringCodeStore.retrieveCode("secret_code")) - .thenReturn(new ExpiringCode("secret_code", new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), "eyedee", null)); + .thenReturn(new ExpiringCode("secret_code", new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), + "{\"user_id\":\"eyedee\",\"username\":\"user@example.com\",\"passwordModifiedTime\":null,\"client_id\":\"\",\"redirect_uri\":\"\"}", null)); ScimUser scimUser = new ScimUser("eyedee", "user@example.com", "User", "Man"); scimUser.setMeta(new ScimMeta(new Date(System.currentTimeMillis() - (1000 * 60 * 60 * 24)), new Date(System.currentTimeMillis() - (1000 * 60 * 60 * 24)), 0)); @@ -281,7 +282,9 @@ public void changing_password_with_invalid_code() throws Exception { @Test public void testChangingAPasswordForUnverifiedUser() throws Exception { when(expiringCodeStore.retrieveCode("secret_code")) - .thenReturn(new ExpiringCode("secret_code", new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), "eyedee", null)); + .thenReturn(new ExpiringCode("secret_code", new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), + "{\"user_id\":\"eyedee\",\"username\":\"user@example.com\",\"passwordModifiedTime\":null,\"client_id\":\"\",\"redirect_uri\":\"\"}", + null)); ScimUser scimUser = new ScimUser("eyedee", "user@example.com", "User", "Man"); scimUser.setMeta(new ScimMeta(new Date(System.currentTimeMillis() - (1000 * 60 * 60 * 24)), new Date(System.currentTimeMillis() - (1000 * 60 * 60 * 24)), 0)); @@ -338,7 +341,9 @@ public void changePassword_Returns422UnprocessableEntity_NewPasswordSameAsOld() Mockito.reset(passwordValidator); when(expiringCodeStore.retrieveCode("emailed_code")) - .thenReturn(new ExpiringCode("emailed_code", new Timestamp(System.currentTimeMillis()+ UaaResetPasswordService.PASSWORD_RESET_LIFETIME), "eyedee", null)); + .thenReturn(new ExpiringCode("emailed_code", new Timestamp(System.currentTimeMillis()+ UaaResetPasswordService.PASSWORD_RESET_LIFETIME), + "{\"user_id\":\"eyedee\",\"username\":\"user@example.com\",\"passwordModifiedTime\":null,\"client_id\":\"\",\"redirect_uri\":\"\"}", + null)); ScimUser scimUser = new ScimUser("eyedee", "user@example.com", "User", "Man"); scimUser.setMeta(new ScimMeta(new Date(System.currentTimeMillis()-(1000*60*60*24)), new Date(System.currentTimeMillis()-(1000*60*60*24)), 0));
uaa/src/test/java/org/cloudfoundry/identity/uaa/login/ResetPasswordControllerMockMvcTests.java+5 −4 modified@@ -239,7 +239,8 @@ public void testResettingAPasswordUsingTimestampForUserModification() throws Exc List<ScimUser> users = getWebApplicationContext().getBean(ScimUserProvisioning.class).query("username eq \"marissa\""); assertNotNull(users); assertEquals(1, users.size()); - ExpiringCode code = codeStore.generateCode(users.get(0).getId(), new Timestamp(System.currentTimeMillis()+ UaaResetPasswordService.PASSWORD_RESET_LIFETIME), null); + PasswordChange passwordChange = new PasswordChange(users.get(0).getId(), users.get(0).getUserName(), null, null, null); + ExpiringCode code = codeStore.generateCode(JsonUtils.writeValueAsString(passwordChange), new Timestamp(System.currentTimeMillis()+ UaaResetPasswordService.PASSWORD_RESET_LIFETIME), null); MockHttpServletRequestBuilder post = createChangePasswordRequest(users.get(0), code, true, "newpassw0rD", "newpassw0rD"); @@ -266,11 +267,11 @@ public void resetPassword_ReturnsUnprocessableEntity_NewPasswordSameAsOld() thro assertNotNull(users); assertEquals(1, users.size()); ScimUser user = users.get(0); - - ExpiringCode code = codeStore.generateCode(user.getId(), new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), null); + PasswordChange passwordChange = new PasswordChange(user.getId(), user.getUserName(), null, null, null); + ExpiringCode code = codeStore.generateCode(JsonUtils.writeValueAsString(passwordChange), new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), null); getMockMvc().perform(createChangePasswordRequest(user, code, true, "d3faultPasswd", "d3faultPasswd")); - code = codeStore.generateCode(user.getId(), new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), null); + code = codeStore.generateCode(JsonUtils.writeValueAsString(passwordChange), new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), null); getMockMvc().perform(createChangePasswordRequest(user, code, true, "d3faultPasswd", "d3faultPasswd")) .andExpect(status().isUnprocessableEntity()) .andExpect(view().name("forgot_password"))
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
10- github.com/advisories/GHSA-fm5c-2rwc-887wghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2016-3084ghsaADVISORY
- pivotal.io/security/cve-2016-3084nvdVendor AdvisoryWEB
- github.com/cloudfoundry/uaa/commit/14350228989e2aee900b8d48a848293bb5152b6fghsaWEB
- github.com/cloudfoundry/uaa/commit/1d3ad7399d010f6a29dc3bf8139d792121301ab8ghsaWEB
- github.com/cloudfoundry/uaa/commit/460627ed419e4227b10ff121248b3ffc009011a9ghsaWEB
- github.com/cloudfoundry/uaa/commit/4a119d314744460ed56bcd740b2e913bf3f560c1ghsaWEB
- github.com/cloudfoundry/uaa/commit/5c2377487bef9d716d5c8e5717df1fc00bc7b000ghsaWEB
- github.com/cloudfoundry/uaa/commit/66132926f1bac0b878da5841be2f93fa5075d88fghsaWEB
- github.com/cloudfoundry/uaa/commit/b3834364ab573e9655348193780a56a602fe87b7ghsaWEB
News mentions
0No linked articles in our index yet.