/******************************************************************************* * 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.scim.endpoints; import com.fasterxml.jackson.core.type.TypeReference; import org.cloudfoundry.identity.uaa.codestore.ExpiringCode; import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeStore; import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeType; import org.cloudfoundry.identity.uaa.codestore.JdbcExpiringCodeStore; import org.cloudfoundry.identity.uaa.constants.OriginKeys; import org.cloudfoundry.identity.uaa.login.SavedAccountOption; import org.cloudfoundry.identity.uaa.mock.InjectedMockContextTest; import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils; import org.cloudfoundry.identity.uaa.scim.ScimUser; 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.security.oauth2.common.util.OAuth2Utils; import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultMatcher; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.Arrays; import java.util.Collections; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.utils; import static org.cloudfoundry.identity.uaa.zone.IdentityZoneSwitchingFilter.HEADER; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.springframework.http.MediaType.APPLICATION_JSON; 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.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; public class PasswordResetEndpointMockMvcTests extends InjectedMockContextTest { private String loginToken; private ScimUser user; private String adminToken; private RandomValueStringGenerator generator = new RandomValueStringGenerator(); @Before public void setUp() throws Exception { loginToken = testClient.getClientCredentialsOAuthAccessToken("login", "loginsecret", "oauth.login"); adminToken = testClient.getClientCredentialsOAuthAccessToken("admin", "adminsecret", null); user = new ScimUser(null, new RandomValueStringGenerator().generate()+"@test.org", "PasswordResetUserFirst", "PasswordResetUserLast"); user.setPrimaryEmail(user.getUserName()); user.setPassword("secr3T"); user = MockMvcUtils.utils().createUser(getMockMvc(), adminToken, user); } @After public void resetGenerator() throws Exception { getWebApplicationContext().getBean(JdbcExpiringCodeStore.class).setGenerator(new RandomValueStringGenerator(24)); } @Test public void changePassword_isSuccessful() throws Exception { MockMvcUtils.PredictableGenerator generator = new MockMvcUtils.PredictableGenerator(); JdbcExpiringCodeStore store = getWebApplicationContext().getBean(JdbcExpiringCodeStore.class); store.setGenerator(generator); String code = getExpiringCode(null, null); MockHttpServletRequestBuilder post = post("/password_change") .header("Authorization", "Bearer " + loginToken) .contentType(APPLICATION_JSON) .content("{\"code\":\"" + code + "\",\"new_password\":\"new_secr3T\"}") .accept(APPLICATION_JSON); getMockMvc().perform(post) .andExpect(status().isOk()) .andExpect(jsonPath("$.user_id").exists()) .andExpect(jsonPath("$.username").value(user.getUserName())) .andExpect(jsonPath("$.code").value("test" + generator.counter.get())); ExpiringCode expiringCode = store.retrieveCode("test" + generator.counter.get()); assertThat(expiringCode.getIntent(), is(ExpiringCodeType.AUTOLOGIN.name())); Map<String,String> data = JsonUtils.readValue(expiringCode.getData(), new TypeReference<Map<String,String>>() {}); assertThat(data.get("user_id"), is(user.getId())); assertThat(data.get("username"), is(user.getUserName())); assertThat(data.get(OAuth2Utils.CLIENT_ID), is("login")); assertThat(data.get(OriginKeys.ORIGIN), is(OriginKeys.UAA)); } @Test public void changePassword_isSuccessful_withOverridenClientId() throws Exception { MockMvcUtils.PredictableGenerator generator = new MockMvcUtils.PredictableGenerator(); JdbcExpiringCodeStore store = getWebApplicationContext().getBean(JdbcExpiringCodeStore.class); store.setGenerator(generator); String code = getExpiringCode("another-client", null); MockHttpServletRequestBuilder post = post("/password_change") .header("Authorization", "Bearer " + loginToken) .contentType(APPLICATION_JSON) .content("{\"code\":\"" + code + "\",\"new_password\":\"new_secr3T\"}") .accept(APPLICATION_JSON); getMockMvc().perform(post) .andExpect(status().isOk()) .andExpect(jsonPath("$.user_id").exists()) .andExpect(jsonPath("$.username").value(user.getUserName())) .andExpect(jsonPath("$.code").value("test" + generator.counter.get())); ExpiringCode expiringCode = store.retrieveCode("test" + generator.counter.get()); Map<String,String> data = JsonUtils.readValue(expiringCode.getData(), new TypeReference<Map<String,String>>() {}); assertThat(data.get(OAuth2Utils.CLIENT_ID), is("another-client")); } @Test public void changePassword_with_clientid_and_redirecturi() throws Exception { String code = getExpiringCode("app", "redirect.example.com"); String email = user.getUserName(); MockHttpServletRequestBuilder get = get("/reset_password") .param("code", code) .param("email", email); MvcResult result = getMockMvc().perform(get) .andExpect(status().isOk()) .andExpect(content().string(containsString(String.format("<input type=\"hidden\" name=\"email\" value=\"%s\"/>", email)))) .andReturn(); String resultingCodeString = getCodeFromPage(result); ExpiringCodeStore expiringCodeStore = (ExpiringCodeStore) getWebApplicationContext().getBean("codeStore"); ExpiringCode resultingCode = expiringCodeStore.retrieveCode(resultingCodeString); Map<String, String> resultingCodeData = JsonUtils.readValue(resultingCode.getData(), new TypeReference<Map<String, String>>() { }); assertEquals("app", resultingCodeData.get("client_id")); assertEquals(email, resultingCodeData.get("username")); assertEquals(user.getId(), resultingCodeData.get("user_id")); assertEquals("redirect.example.com", resultingCodeData.get("redirect_uri")); } @Test public void changePassword_do_with_clientid_and_redirecturi() throws Exception { String code = getExpiringCode("app", "http://localhost:8080/app/"); String email = user.getUserName(); MockHttpServletRequestBuilder get = get("/reset_password") .param("code", code) .param("email", email); MvcResult result = getMockMvc().perform(get) .andExpect(status().isOk()) .andExpect(content().string(containsString(String.format("<input type=\"hidden\" name=\"email\" value=\"%s\"/>", email)))) .andReturn(); String resultingCodeString = getCodeFromPage(result); MockHttpServletRequestBuilder post = post("/reset_password.do") .param("code", resultingCodeString) .param("email", email) .param("password", "newpass") .param("password_confirmation", "newpass") .with(csrf()); getMockMvc().perform(post) .andExpect(status().is3xxRedirection()) .andExpect(redirectedUrl("http://localhost:8080/app/")) .andExpect(savedAccountCookie(user)); } @SuppressWarnings("deprecation") private ResultMatcher savedAccountCookie(ScimUser user) { return result -> { SavedAccountOption savedAccountOption = new SavedAccountOption(); savedAccountOption.setEmail(user.getPrimaryEmail()); savedAccountOption.setUsername(user.getUserName()); savedAccountOption.setOrigin(user.getOrigin()); savedAccountOption.setUserId(user.getId()); String cookieName = "Saved-Account-" + user.getId(); cookie().value(cookieName, URLEncoder.encode(JsonUtils.writeValueAsString(savedAccountOption))).match(result); cookie().maxAge(cookieName, 365*24*60*60); }; } @Test public void changePassword_withInvalidPassword_returnsErrorJson() throws Exception { String toolongpassword = new RandomValueStringGenerator(260).generate(); String code = getExpiringCode(null, null); getMockMvc().perform(post("/password_change") .header("Authorization", "Bearer " + loginToken) .contentType(APPLICATION_JSON) .content("{\"code\":\"" + code + "\",\"new_password\":\""+toolongpassword+"\"}")) .andExpect(status().isUnprocessableEntity()) .andExpect(jsonPath("$.error").value("invalid_password")) .andExpect(jsonPath("$.message").value("Password must be no more than 255 characters in length.")); } @Test public void changePassword_ReturnsUnprocessableEntity_NewPasswordSameAsOld() throws Exception { // make sure password is the same as old resetPassword("d3faultPassword"); String code = getExpiringCode(null, null); MockHttpServletRequestBuilder post = post("/password_change") .header("Authorization", "Bearer " + loginToken) .contentType(APPLICATION_JSON) .content("{\"code\":\"" + code + "\",\"new_password\":\"d3faultPassword\"}") .accept(APPLICATION_JSON); getMockMvc().perform(post) .andExpect(status().isUnprocessableEntity()) .andExpect(jsonPath("$.error").value("invalid_password")) .andExpect(jsonPath("$.message").value("Your new password cannot be the same as the old password.")); } @Test public void uaaAdmin_canChangePassword() throws Exception { MvcResult mvcResult = getMockMvc().perform(post("/password_resets") .header("Authorization", "Bearer " + adminToken) .contentType(APPLICATION_JSON) .content(user.getUserName()) .accept(APPLICATION_JSON)) .andExpect(status().isCreated()).andReturn(); String responseString = mvcResult.getResponse().getContentAsString(); String code = JsonUtils.readValue(responseString, new TypeReference<Map<String, String>>() { }).get("code"); getMockMvc().perform(post("/password_change") .header("Authorization", "Bearer " + adminToken) .contentType(APPLICATION_JSON) .content("{\"code\":\"" + code + "\",\"new_password\":\"new-password\"}") .accept(APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.user_id").exists()) .andExpect(jsonPath("$.username").value(user.getUserName())); } @Test public void zoneAdminCanResetsAndChangePassword() throws Exception { String subdomain = generator.generate(); MockMvcUtils.IdentityZoneCreationResult result = utils().createOtherIdentityZoneAndReturnResult(subdomain, getMockMvc(), getWebApplicationContext(), null); IdentityZone identityZone = result.getIdentityZone(); String zoneAdminScope = "zones." + identityZone.getId() + ".admin"; ScimUser scimUser = MockMvcUtils.createAdminForZone(getMockMvc(), adminToken, zoneAdminScope); String zonifiedAdminClientId = generator.generate().toLowerCase(); String zonifiedAdminClientSecret = generator.generate().toLowerCase(); utils().createClient(this.getMockMvc(), adminToken, zonifiedAdminClientId , zonifiedAdminClientSecret, Collections.singleton("oauth"), Collections.singletonList(zoneAdminScope), Arrays.asList(new String[]{"client_credentials", "password"}), "uaa.none"); String zoneAdminAccessToken = testClient.getUserOAuthAccessToken(zonifiedAdminClientId, zonifiedAdminClientSecret, scimUser.getUserName(), "secr3T", zoneAdminScope); ScimUser userInZone = new ScimUser(null, new RandomValueStringGenerator().generate()+"@test.org", "PasswordResetUserFirst", "PasswordResetUserLast"); userInZone.setPrimaryEmail(userInZone.getUserName()); userInZone.setPassword("secr3T"); userInZone = MockMvcUtils.utils().createUserInZone(getMockMvc(), adminToken, userInZone, "",identityZone.getId()); getMockMvc().perform( post("/password_resets") .header("Authorization", "Bearer " + zoneAdminAccessToken) .header(HEADER, identityZone.getId()) .contentType(APPLICATION_JSON) .content(userInZone.getPrimaryEmail()) .accept(APPLICATION_JSON)) .andExpect(status().isCreated()) .andExpect(jsonPath("$.user_id").exists()) .andExpect(jsonPath("$.code").isNotEmpty()); } private String getExpiringCode(String clientId, String redirectUri) throws Exception { MockHttpServletRequestBuilder post = post("/password_resets") .header("Authorization", "Bearer " + loginToken) .contentType(APPLICATION_JSON) .param("client_id", clientId) .param("redirect_uri", redirectUri) .content(user.getUserName()) .accept(APPLICATION_JSON); MvcResult result = getMockMvc().perform(post) .andExpect(status().isCreated()) .andReturn(); String responseString = result.getResponse().getContentAsString(); Map<String,String> response = JsonUtils.readValue(responseString, new TypeReference<Map<String, String>>() { }); return response.get("code"); } private void resetPassword(String defaultPassword) throws Exception { String code = getExpiringCode(null, null); MockHttpServletRequestBuilder post = post("/password_change") .header("Authorization", "Bearer " + loginToken) .contentType(APPLICATION_JSON) .content("{\"code\":\"" + code + "\",\"new_password\":\"" + defaultPassword + "\"}") .accept(APPLICATION_JSON); getMockMvc().perform(post) .andExpect(status().isOk()) .andExpect(jsonPath("$.user_id").exists()) .andExpect(jsonPath("$.username").value(user.getUserName())); } private String getCodeFromPage(MvcResult result) throws UnsupportedEncodingException { Pattern codePattern = Pattern.compile("<input type=\"hidden\" name=\"code\" value=\"([A-Za-z0-9]+)\"/>"); Matcher codeMatcher = codePattern.matcher(result.getResponse().getContentAsString()); assertTrue(codeMatcher.find()); String pageCode = codeMatcher.group(1).toString(); return pageCode; } }