/*******************************************************************************
* Cloud Foundry
* Copyright (c) [2009-2016] Pivotal Software, Inc. All Rights Reserved.
*
* This product is licensed to you under the Apache License, Version 2.0 (the "License").
* You may not use this product except in compliance with the License.
*
* This product includes a number of subcomponents with
* separate copyright notices and license terms. Your use of these
* subcomponents is subject to the terms and conditions of the
* subcomponent's license, as noted in the LICENSE file.
*******************************************************************************/
package org.cloudfoundry.identity.uaa.login;
import org.cloudfoundry.identity.uaa.account.UaaResetPasswordService;
import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal;
import org.cloudfoundry.identity.uaa.codestore.ExpiringCode;
import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeStore;
import org.cloudfoundry.identity.uaa.codestore.JdbcExpiringCodeStore;
import org.cloudfoundry.identity.uaa.constants.OriginKeys;
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.provider.IdentityProvider;
import org.cloudfoundry.identity.uaa.provider.JdbcIdentityProviderProvisioning;
import org.cloudfoundry.identity.uaa.provider.PasswordPolicy;
import org.cloudfoundry.identity.uaa.provider.UaaIdentityProviderDefinition;
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.cloudfoundry.identity.uaa.zone.IdentityZone;
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;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.oauth2.common.util.RandomValueStringGenerator;
import org.springframework.security.web.PortResolverImpl;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.savedrequest.DefaultSavedRequest;
import org.springframework.security.web.savedrequest.SavedRequest;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import javax.servlet.http.Cookie;
import java.sql.Timestamp;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.cloudfoundry.identity.uaa.account.UaaResetPasswordService.FORGOT_PASSWORD_INTENT_PREFIX;
import static org.cloudfoundry.identity.uaa.constants.OriginKeys.UAA;
import static org.cloudfoundry.identity.uaa.web.UaaSavedRequestAwareAuthenticationSuccessHandler.SAVED_REQUEST_SESSION_ATTRIBUTE;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
public class ResetPasswordControllerMockMvcTests extends InjectedMockContextTest {
private ExpiringCodeStore codeStore;
@Before
public void initResetPasswordTest() throws Exception {
codeStore = getWebApplicationContext().getBean(ExpiringCodeStore.class);
}
@After
public void resetGenerator() throws Exception {
getWebApplicationContext().getBean(JdbcExpiringCodeStore.class).setGenerator(new RandomValueStringGenerator(24));
}
@Test
public void testResettingAPasswordUsingUsernameToEnsureNoModification() throws Exception {
List<ScimUser> users = getWebApplicationContext().getBean(ScimUserProvisioning.class).query("username eq \"marissa\"");
assertNotNull(users);
assertEquals(1, users.size());
PasswordChange change = new PasswordChange(users.get(0).getId(), users.get(0).getUserName(), users.get(0).getPasswordLastModified(), "", "");
ExpiringCode code = codeStore.generateCode(JsonUtils.writeValueAsString(change), new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), null);
MvcResult mvcResult = getMockMvc().perform(createChangePasswordRequest(users.get(0), code, true))
.andExpect(status().isFound())
.andExpect(redirectedUrl("/"))
.andReturn();
SecurityContext securityContext = (SecurityContext) mvcResult.getRequest().getSession().getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
Authentication authentication = securityContext.getAuthentication();
assertThat(authentication.getPrincipal(), instanceOf(UaaPrincipal.class));
UaaPrincipal principal = (UaaPrincipal) authentication.getPrincipal();
assertThat(principal.getId(), equalTo(users.get(0).getId()));
assertThat(principal.getName(), equalTo(users.get(0).getUserName()));
assertThat(principal.getEmail(), equalTo(users.get(0).getPrimaryEmail()));
assertThat(principal.getOrigin(), equalTo(OriginKeys.UAA));
}
@Test
public void testResettingPasswordUpdatesLastLogonTime() throws Exception {
List<ScimUser> users = getWebApplicationContext().getBean(ScimUserProvisioning.class).query("username eq \"marissa\"");
assertNotNull(users);
assertEquals(1, users.size());
Long lastLogonBeforeReset = users.get(0).getLastLogonTime();
PasswordChange change = new PasswordChange(users.get(0).getId(), users.get(0).getUserName(), users.get(0).getPasswordLastModified(), "", "");
ExpiringCode code = codeStore.generateCode(JsonUtils.writeValueAsString(change), new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), null);
MvcResult mvcResult = getMockMvc().perform(createChangePasswordRequest(users.get(0), code, true))
.andExpect(status().isFound())
.andExpect(redirectedUrl("/"))
.andReturn();
ScimUser userMarissa = getWebApplicationContext().getBean(ScimUserProvisioning.class).retrieve(users.get(0).getId());
assertNotNull(userMarissa.getLastLogonTime());
if(lastLogonBeforeReset != null) {
assertTrue(userMarissa.getLastLogonTime() > lastLogonBeforeReset);
}
}
@Test
public void testResettingAPasswordFailsWhenUsernameChanged() throws Exception {
ScimUserProvisioning userProvisioning = getWebApplicationContext().getBean(ScimUserProvisioning.class);
List<ScimUser> users = userProvisioning.query("username eq \"marissa\"");
assertNotNull(users);
assertEquals(1, users.size());
ScimUser user = users.get(0);
PasswordChange change = new PasswordChange(user.getId(), user.getUserName(), user.getPasswordLastModified(), "", "");
ExpiringCode code = codeStore.generateCode(JsonUtils.writeValueAsString(change), new Timestamp(System.currentTimeMillis() + 50000), null);
String formerUsername = user.getUserName();
user.setUserName("newusername");
user = userProvisioning.update(user.getId(), user);
try {
getMockMvc().perform(createChangePasswordRequest(users.get(0), code, true))
.andExpect(status().isUnprocessableEntity());
} finally {
user.setUserName(formerUsername);
userProvisioning.update(user.getId(), user);
}
}
@Test
public void testResettingAPassword_whenCodeIsValid_rendersTheChangePasswordForm() 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);
PasswordChange change = new PasswordChange(user.getId(), user.getUserName(), user.getPasswordLastModified(), "", "");
ExpiringCode code = codeStore.generateCode(JsonUtils.writeValueAsString(change), new Timestamp(System.currentTimeMillis() + 50000), FORGOT_PASSWORD_INTENT_PREFIX + user.getId());
MockHttpServletRequestBuilder get = get("/reset_password?code={code}", code.getCode())
.accept(MediaType.TEXT_HTML);
String content = getMockMvc().perform(get)
.andExpect(status().isOk())
.andReturn().getResponse().getContentAsString();
String renderedCode = findInRenderedPage(content, "\\<input type=\\\"hidden\\\" name=\\\"code\\\" value=\\\"(.*?)\\\"\\/\\>");
String renderedEmail = findInRenderedPage(content, "\\<input type=\\\"hidden\\\" name=\\\"email\\\" value=\\\"(.*?)\\\"\\/\\>");
assertEquals(renderedEmail, user.getPrimaryEmail());
getMockMvc().perform(createChangePasswordRequest(user, renderedCode, true, "secret1", "secret1"))
.andExpect(status().isFound())
.andExpect(redirectedUrl("/"));
}
private String findInRenderedPage(String renderedContent, String regexPattern) {
Pattern expiringCodePattern = Pattern.compile(regexPattern);
Matcher matcher = expiringCodePattern.matcher(renderedContent);
assertTrue(matcher.find());
return matcher.group(1);
}
@Test
public void new_code_overwrite_old_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 = 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";
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);
MockHttpSession session = new MockHttpSession();
SavedRequest savedRequest = new DefaultSavedRequest(new MockHttpServletRequest(), new PortResolverImpl()) {
@Override
public String getRedirectUrl() {
return "http://test/redirect/oauth/authorize";
}
@Override
public String[] getParameterValues(String name) {
if ("client_id".equals(name)) {
return new String[] {"admin"};
}
return new String[0];
}
@Override public List<Cookie> getCookies() { return null; }
@Override public String getMethod() { return null; }
@Override public List<String> getHeaderValues(String name) { return null; }
@Override
public Collection<String> getHeaderNames() { return null; }
@Override public List<Locale> getLocales() { return null; }
@Override public Map<String, String[]> getParameterMap() { return null; }
};
session.setAttribute(SAVED_REQUEST_SESSION_ATTRIBUTE, savedRequest);
PredictableGenerator generator = new PredictableGenerator();
JdbcExpiringCodeStore store = getWebApplicationContext().getBean(JdbcExpiringCodeStore.class);
store.setGenerator(generator);
getMockMvc().perform(post("/forgot_password.do")
.session(session)
.param("email", user.getUserName()))
.andExpect(redirectedUrl("email_sent?code=reset_password"));
getMockMvc().perform(createChangePasswordRequest(user, "test" + generator.counter.get(), true, "secret1", "secret1")
.session(session))
.andExpect(status().isFound())
.andExpect(redirectedUrl("http://test/redirect/oauth/authorize"))
.andReturn();
}
@Test
public void testResettingAPasswordFailsWhenPasswordChanged() 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);
ScimUserProvisioning userProvisioning = getWebApplicationContext().getBean(ScimUserProvisioning.class);
Thread.sleep(1000 - (System.currentTimeMillis() % 1000) + 10); //because password last modified is second only
PasswordChange change = new PasswordChange(user.getId(), user.getUserName(), user.getPasswordLastModified(), "", "");
ExpiringCode code = codeStore.generateCode(JsonUtils.writeValueAsString(change), new Timestamp(System.currentTimeMillis() + 50000), null);
userProvisioning.changePassword(user.getId(), "secret", "secr3t");
getMockMvc().perform(createChangePasswordRequest(user, code, true))
.andExpect(status().isUnprocessableEntity());
}
@Test
public void testResettingAPasswordNoCsrfParameter() throws Exception {
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);
getMockMvc().perform(createChangePasswordRequest(users.get(0), code, false))
.andExpect(status().isFound())
.andExpect(redirectedUrl("http://localhost/invalid_request"));
}
@Test
public void testResettingAPasswordUsingTimestampForUserModification() throws Exception {
List<ScimUser> users = getWebApplicationContext().getBean(ScimUserProvisioning.class).query("username eq \"marissa\"");
assertNotNull(users);
assertEquals(1, users.size());
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");
MvcResult mvcResult = getMockMvc().perform(post)
.andExpect(status().isFound())
.andExpect(redirectedUrl("/"))
.andReturn();
SecurityContext securityContext = (SecurityContext) mvcResult.getRequest().getSession().getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
Authentication authentication = securityContext.getAuthentication();
assertThat(authentication.getPrincipal(), instanceOf(UaaPrincipal.class));
UaaPrincipal principal = (UaaPrincipal) authentication.getPrincipal();
assertThat(principal.getId(), equalTo(users.get(0).getId()));
assertThat(principal.getName(), equalTo(users.get(0).getUserName()));
assertThat(principal.getEmail(), equalTo(users.get(0).getPrimaryEmail()));
assertThat(principal.getOrigin(), equalTo(OriginKeys.UAA));
}
@Test
public void resetPassword_ReturnsUnprocessableEntity_NewPasswordSameAsOld() throws Exception {
ScimUserProvisioning userProvisioning = getWebApplicationContext().getBean(ScimUserProvisioning.class);
List<ScimUser> users = userProvisioning.query("username eq \"marissa\"");
assertNotNull(users);
assertEquals(1, users.size());
ScimUser user = users.get(0);
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(JsonUtils.writeValueAsString(passwordChange), new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), null);
getMockMvc().perform(createChangePasswordRequest(user, code, true, "d3faultPasswd", "d3faultPasswd"))
.andExpect(status().isUnprocessableEntity())
.andExpect(request().attribute("message", equalTo("Your new password cannot be the same as the old password.")))
.andExpect(forwardedUrl("/reset_password"));
}
@Test
public void resetPassword_ReturnsUnprocessableEntity_NewPasswordNotAccordingToPolicy() throws Exception {
IdentityProvider<UaaIdentityProviderDefinition> uaaProvider = getWebApplicationContext().getBean(JdbcIdentityProviderProvisioning.class).retrieveByOrigin(UAA, IdentityZone.getUaa().getId());
UaaIdentityProviderDefinition currentDefinition = uaaProvider.getConfig();
PasswordPolicy passwordPolicy = new PasswordPolicy();
passwordPolicy.setMinLength(3);
passwordPolicy.setMaxLength(20);
uaaProvider.setConfig(new UaaIdentityProviderDefinition(passwordPolicy, null));
getWebApplicationContext().getBean(JdbcIdentityProviderProvisioning.class).update(uaaProvider);
ScimUserProvisioning userProvisioning = getWebApplicationContext().getBean(ScimUserProvisioning.class);
List<ScimUser> users = userProvisioning.query("username eq \"marissa\"");
assertNotNull(users);
assertEquals(1, users.size());
ScimUser user = users.get(0);
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(JsonUtils.writeValueAsString(passwordChange), new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), null);
getMockMvc().perform(createChangePasswordRequest(user, code, true, "a", "a"))
.andExpect(status().isUnprocessableEntity())
.andExpect(request().attribute("message", equalTo("Password must be at least 3 characters in length.")))
.andExpect(forwardedUrl("/reset_password"));
uaaProvider = getWebApplicationContext().getBean(JdbcIdentityProviderProvisioning.class).retrieveByOrigin(UAA, IdentityZone.getUaa().getId());
uaaProvider.setConfig(currentDefinition);
getWebApplicationContext().getBean(JdbcIdentityProviderProvisioning.class).update(uaaProvider);
}
private MockHttpServletRequestBuilder createChangePasswordRequest(ScimUser user, ExpiringCode code, boolean useCSRF) throws Exception {
return createChangePasswordRequest(user, code, useCSRF, "newpassw0rDl", "newpassw0rDl");
}
private MockHttpServletRequestBuilder createChangePasswordRequest(ScimUser user, ExpiringCode code, boolean useCSRF, String password, String passwordConfirmation) throws Exception {
return createChangePasswordRequest(user,code.getCode(),useCSRF, password,passwordConfirmation);
}
private MockHttpServletRequestBuilder createChangePasswordRequest(ScimUser user, String code, boolean useCSRF, String password, String passwordConfirmation) throws Exception {
MockHttpServletRequestBuilder post = post("/reset_password.do");
if (useCSRF) {
post.with(csrf());
}
post.param("code", code)
.param("email", user.getPrimaryEmail())
.param("password", password)
.param("password_confirmation", passwordConfirmation);
return post;
}
}