Critical severity9.8NVD Advisory· Published Jun 13, 2017· Updated May 13, 2026
CVE-2017-4992
CVE-2017-4992
Description
An issue was discovered in Cloud Foundry Foundation cf-release versions prior to v261; UAA release 2.x versions prior to v2.7.4.17, 3.6.x versions prior to v3.6.11, 3.9.x versions prior to v3.9.13, and other versions prior to v4.2.0; and UAA bosh release (uaa-release) 13.x versions prior to v13.15, 24.x versions prior to v24.10, 30.x versions prior to 30.3, and other versions prior to v37. There is privilege escalation (arbitrary password reset) with user invitations.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.cloudfoundry.identity:cloudfoundry-identity-serverMaven | >= 2.0.0, < 2.7.4.17 | 2.7.4.17 |
org.cloudfoundry.identity:cloudfoundry-identity-serverMaven | >= 3.0.0, < 3.6.11 | 3.6.11 |
org.cloudfoundry.identity:cloudfoundry-identity-serverMaven | >= 3.7.0, < 3.9.13 | 3.9.13 |
org.cloudfoundry.identity:cloudfoundry-identity-serverMaven | >= 3.10.0, < 4.2.0 | 4.2.0 |
Affected products
73cpe:2.3:a:cloudfoundry:cloud_foundry_uaa_bosh:*:*:*:*:*:*:*:*+ 27 more
- cpe:2.3:a:cloudfoundry:cloud_foundry_uaa_bosh:*:*:*:*:*:*:*:*range: <=27
- cpe:2.3:a:cloudfoundry:cloud_foundry_uaa_bosh:13.1:*:*:*:*:*:*:*
- cpe:2.3:a:cloudfoundry:cloud_foundry_uaa_bosh:13.10:*:*:*:*:*:*:*
- cpe:2.3:a:cloudfoundry:cloud_foundry_uaa_bosh:13.11:*:*:*:*:*:*:*
- cpe:2.3:a:cloudfoundry:cloud_foundry_uaa_bosh:13.12:*:*:*:*:*:*:*
- cpe:2.3:a:cloudfoundry:cloud_foundry_uaa_bosh:13.13:*:*:*:*:*:*:*
- cpe:2.3:a:cloudfoundry:cloud_foundry_uaa_bosh:13.14:*:*:*:*:*:*:*
- cpe:2.3:a:cloudfoundry:cloud_foundry_uaa_bosh:13.2:*:*:*:*:*:*:*
- cpe:2.3:a:cloudfoundry:cloud_foundry_uaa_bosh:13.3:*:*:*:*:*:*:*
- cpe:2.3:a:cloudfoundry:cloud_foundry_uaa_bosh:13.4:*:*:*:*:*:*:*
- cpe:2.3:a:cloudfoundry:cloud_foundry_uaa_bosh:13.5:*:*:*:*:*:*:*
- cpe:2.3:a:cloudfoundry:cloud_foundry_uaa_bosh:13.6:*:*:*:*:*:*:*
- cpe:2.3:a:cloudfoundry:cloud_foundry_uaa_bosh:13.7:*:*:*:*:*:*:*
- cpe:2.3:a:cloudfoundry:cloud_foundry_uaa_bosh:13.8:*:*:*:*:*:*:*
- cpe:2.3:a:cloudfoundry:cloud_foundry_uaa_bosh:13.9:*:*:*:*:*:*:*
- cpe:2.3:a:cloudfoundry:cloud_foundry_uaa_bosh:24:*:*:*:*:*:*:*
- cpe:2.3:a:cloudfoundry:cloud_foundry_uaa_bosh:24.1:*:*:*:*:*:*:*
- cpe:2.3:a:cloudfoundry:cloud_foundry_uaa_bosh:24.2:*:*:*:*:*:*:*
- cpe:2.3:a:cloudfoundry:cloud_foundry_uaa_bosh:24.3:*:*:*:*:*:*:*
- cpe:2.3:a:cloudfoundry:cloud_foundry_uaa_bosh:24.4:*:*:*:*:*:*:*
- cpe:2.3:a:cloudfoundry:cloud_foundry_uaa_bosh:24.5:*:*:*:*:*:*:*
- cpe:2.3:a:cloudfoundry:cloud_foundry_uaa_bosh:24.6:*:*:*:*:*:*:*
- cpe:2.3:a:cloudfoundry:cloud_foundry_uaa_bosh:24.7:*:*:*:*:*:*:*
- cpe:2.3:a:cloudfoundry:cloud_foundry_uaa_bosh:24.8:*:*:*:*:*:*:*
- cpe:2.3:a:cloudfoundry:cloud_foundry_uaa_bosh:24.9:*:*:*:*:*:*:*
- cpe:2.3:a:cloudfoundry:cloud_foundry_uaa_bosh:30:*:*:*:*:*:*:*
- cpe:2.3:a:cloudfoundry:cloud_foundry_uaa_bosh:30.1:*:*:*:*:*:*:*
- cpe:2.3:a:cloudfoundry:cloud_foundry_uaa_bosh:30.2:*:*:*:*:*:*:*
cpe:2.3:a:pivotal_software:cloud_foundry_uaa:*:*:*:*:*:*:*:*+ 43 more
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:*:*:*:*:*:*:*:*range: <=4.2.0
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:2.2.5.4:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:2.7.1:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:2.7.2:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:2.7.3:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:2.7.4:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:2.7.4.1:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:2.7.4.11:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:2.7.4.12:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:2.7.4.13:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:2.7.4.14:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:2.7.4.15:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:2.7.4.16:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:2.7.4.2:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:2.7.4.3:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:2.7.4.4:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:2.7.4.5:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:2.7.4.6:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:2.7.4.7:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:2.7.4.8:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:2.7.4.9:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:3.6.1:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:3.6.10:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:3.6.2:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:3.6.3:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:3.6.4:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:3.6.5:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:3.6.6:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:3.6.7:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:3.6.8:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:3.6.9:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:3.9.1:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:3.9.10:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:3.9.11:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:3.9.12:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:3.9.13:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:3.9.2:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:3.9.3:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:3.9.4:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:3.9.5:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:3.9.6:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:3.9.7:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:3.9.8:*:*:*:*:*:*:*
- cpe:2.3:a:pivotal_software:cloud_foundry_uaa:3.9.9:*:*:*:*:*:*:*
Patches
47 files changed · +256 −13
login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsController.java+24 −1 modified@@ -42,6 +42,7 @@ import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; @@ -215,17 +216,39 @@ public String acceptInvitation(@RequestParam("password") String password, UaaPrincipal principal = (UaaPrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + final ExpiringCode expiringCode = expiringCodeStore.retrieveCode(code); + + if (expiringCode == null || expiringCode.getData() == null) { + logger.debug("Failing invitation. Code not found."); + SecurityContextHolder.clearContext(); + return handleUnprocessableEntity(model, response, "error_message_code", "code_expired", "invitations/accept_invite"); + } + Map<String,String> data = JsonUtils.readValue(expiringCode.getData(), new TypeReference<Map<String,String>>() {}); + if (principal == null || data.get("user_id") == null || !data.get("user_id").equals(principal.getId())) { + logger.debug("Failing invitation. Code and user ID mismatch."); + SecurityContextHolder.clearContext(); + return handleUnprocessableEntity(model, response, "error_message_code", "code_expired", "invitations/accept_invite"); + } + + final String newCode = expiringCodeStore.generateCode(expiringCode.getData(), new Timestamp(System.currentTimeMillis() + (10 * 60 * 1000))).getCode(); if (!validation.valid()) { + model.addAttribute("code", newCode); model.addAttribute("email", principal.getEmail()); return handleUnprocessableEntity(model, response, "error_message_code", validation.getMessageCode(), "invitations/accept_invite"); } try { passwordValidator.validate(password); } catch (InvalidPasswordException e) { + model.addAttribute("code", newCode); model.addAttribute("email", principal.getEmail()); return handleUnprocessableEntity(model, response, "error_message", e.getMessagesAsOneString(), "invitations/accept_invite"); } - AcceptedInvitation invitation = invitationsService.acceptInvitation(code, password); + AcceptedInvitation invitation; + try { + invitation = invitationsService.acceptInvitation(newCode, password); + } catch (HttpClientErrorException e) { + return handleUnprocessableEntity(model, response, "error_message_code", "code_expired", "invitations/accept_invite"); + } principal = new UaaPrincipal( invitation.getUser().getId(), invitation.getUser().getUserName(),
login/src/main/resources/login-ui.xml+3 −2 modified@@ -106,7 +106,7 @@ <intercept-url pattern="/invitations/accept" access="isFullyAuthenticated() or isAnonymous()" method="GET"/> <intercept-url pattern="/invitations/accept.do" access="hasAuthority('uaa.invited')" method="POST"/> <intercept-url pattern="/invitations/accept_enterprise.do" access="hasAuthority('uaa.invited')" method="POST"/> - <intercept-url pattern="/**" access="isFullyAuthenticated()" /> + <intercept-url pattern="/**" access="denyAll" /> <csrf disabled="false"/> <custom-filter ref="acceptInvitationSecurityContextPersistenceFilter" before="FIRST"/> </http> @@ -116,7 +116,8 @@ use-expressions="true" pattern="/invite_users/**" xmlns="http://www.springframework.org/schema/security"> - <intercept-url pattern="/invite_users" access="#oauth2.hasAnyScope('scim.invite')" method="POST"/> + <intercept-url pattern="/**" access="#oauth2.hasAnyScope('scim.invite')" method="POST"/> + <intercept-url pattern="**" access="denyAll"/> <expression-handler ref="oauthWebExpressionHandler" /> <access-denied-handler ref="oauthAccessDeniedHandler" /> <custom-filter ref="resourceAgnosticAuthenticationFilter" position="PRE_AUTH_FILTER" />
login/src/test/java/org/cloudfoundry/identity/uaa/invitations/InvitationsControllerTest.java+126 −7 modified@@ -46,6 +46,7 @@ import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.context.ConfigurableWebApplicationContext; import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer; import org.springframework.web.servlet.config.annotation.EnableWebMvc; @@ -60,15 +61,17 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyObject; import static org.mockito.Matchers.anyString; 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.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; +import static org.springframework.http.HttpStatus.BAD_REQUEST; 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; @@ -324,15 +327,71 @@ public void testAcceptInvitePageWithExpiredCode() throws Exception { assertNull(SecurityContextHolder.getContext().getAuthentication()); } + @Test + public void missing_code() throws Exception { + MockHttpServletRequestBuilder post = startAcceptInviteFlow("a", "a"); + + when(expiringCodeStore.retrieveCode("thecode")).thenReturn(null); + + IdentityProvider identityProvider = new IdentityProvider(); + identityProvider.setType(Origin.UAA); + when(providerProvisioning.retrieveByOrigin("uaa", "uaa")).thenReturn(identityProvider); + mockMvc.perform(post) + .andExpect(status().isUnprocessableEntity()) + .andExpect(model().attribute("error_message_code", "code_expired")) + .andExpect(view().name("invitations/accept_invite")); + verify(expiringCodeStore).retrieveCode("thecode"); + verify(expiringCodeStore, never()).generateCode(anyString(),anyObject()); + verify(invitationsService, never()).acceptInvitation(anyString(), anyString()); + + } + + @Test + public void invalid_principal_id() throws Exception { + MockHttpServletRequestBuilder post = startAcceptInviteFlow("a", "a"); + + Map<String,String> codeData = getInvitationsCode(Origin.UAA); + codeData.put("user_id", "invalid id"); + String codeDataString = JsonUtils.writeValueAsString(codeData); + when(expiringCodeStore.retrieveCode("thecode")).thenReturn(new ExpiringCode("thecode", new Timestamp(1), codeDataString), null); + + IdentityProvider identityProvider = new IdentityProvider(); + identityProvider.setType(Origin.UAA); + when(providerProvisioning.retrieveByOrigin("uaa", "uaa")).thenReturn(identityProvider); + mockMvc.perform(post) + .andExpect(status().isUnprocessableEntity()) + .andExpect(model().attribute("error_message_code", "code_expired")) + .andExpect(view().name("invitations/accept_invite")); + verify(expiringCodeStore).retrieveCode("thecode"); + verify(expiringCodeStore, never()).generateCode(anyString(),anyObject()); + verify(invitationsService, never()).acceptInvitation(anyString(), anyString()); + + } + @Test public void testAcceptInviteWithContraveningPassword() throws Exception { doThrow(new InvalidPasswordException(newArrayList("Msg 2c", "Msg 1c"))).when(passwordValidator).validate("a"); MockHttpServletRequestBuilder post = startAcceptInviteFlow("a"); + Map<String,String> codeData = getInvitationsCode(Origin.UAA); + String codeDataString = JsonUtils.writeValueAsString(codeData); + when(expiringCodeStore.retrieveCode("thecode")).thenReturn(new ExpiringCode("thecode", new Timestamp(1), codeDataString), null); + when(expiringCodeStore.retrieveCode("thenewcode")).thenReturn(new ExpiringCode("thenewcode", new Timestamp(1), codeDataString), null); + when(expiringCodeStore.generateCode(eq(codeDataString),any())).thenReturn( + new ExpiringCode("thenewcode", new Timestamp(1), codeDataString), + new ExpiringCode("thenewcode2", new Timestamp(1), codeDataString) + ); + + IdentityProvider identityProvider = new IdentityProvider(); + identityProvider.setType(Origin.UAA); + when(providerProvisioning.retrieveByOrigin("uaa", "uaa")).thenReturn(identityProvider); mockMvc.perform(post) .andExpect(status().isUnprocessableEntity()) .andExpect(model().attribute("error_message", "Msg 1c Msg 2c")) + .andExpect(model().attribute("code", "thenewcode")) .andExpect(view().name("invitations/accept_invite")); + verify(expiringCodeStore).retrieveCode("thecode"); + verify(expiringCodeStore, times(1)).generateCode(anyString(),anyObject()); verify(invitationsService, never()).acceptInvitation(anyString(), anyString()); } @@ -342,6 +401,17 @@ public void testAcceptInvite() throws Exception { user.setPrimaryEmail(user.getUserName()); MockHttpServletRequestBuilder post = startAcceptInviteFlow("passw0rd"); + Map<String,String> codeData = getInvitationsCode(Origin.UAA); + String codeDataString = JsonUtils.writeValueAsString(codeData); + ExpiringCode thecode = new ExpiringCode("thecode", new Timestamp(1), codeDataString); + ExpiringCode thenewcode = new ExpiringCode("thenewcode", new Timestamp(1), codeDataString); + ExpiringCode thenewcode2 = new ExpiringCode("thenewcode2", new Timestamp(1), codeDataString); + when(expiringCodeStore.retrieveCode("thecode")).thenReturn(thecode, null); + when(expiringCodeStore.retrieveCode("thenewcode")).thenReturn(thenewcode, null); + when(expiringCodeStore.generateCode(eq(codeDataString),any())) + .thenReturn(thenewcode) + .thenReturn(thenewcode2); + when(invitationsService.acceptInvitation(anyString(), eq("passw0rd"))).thenReturn(new InvitationsService.AcceptedInvitation("/home", user)); mockMvc.perform(post) @@ -351,15 +421,18 @@ public void testAcceptInvite() throws Exception { verify(invitationsService).acceptInvitation(anyString(), eq("passw0rd")); } - public MockHttpServletRequestBuilder startAcceptInviteFlow(String password) { + private MockHttpServletRequestBuilder startAcceptInviteFlow(String password) { + return startAcceptInviteFlow(password, password); + } + private MockHttpServletRequestBuilder startAcceptInviteFlow(String password, String confirmPassword) { UaaPrincipal uaaPrincipal = new UaaPrincipal("user-id-001", "user@example.com", "user@example.com", Origin.UAA, null, IdentityZoneHolder.get().getId()); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(uaaPrincipal, null, UaaAuthority.USER_AUTHORITIES); SecurityContextHolder.getContext().setAuthentication(token); return post("/invitations/accept.do") .param("code","thecode") .param("password", password) - .param("password_confirmation", password); + .param("password_confirmation", confirmPassword); } @Test @@ -371,6 +444,10 @@ public void acceptInviteWithValidClientRedirect() throws Exception { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(uaaPrincipal, null, UaaAuthority.USER_AUTHORITIES); SecurityContextHolder.getContext().setAuthentication(token); + Map<String,String> codeData = getInvitationsCode(Origin.UAA); + String codeDataString = JsonUtils.writeValueAsString(codeData); + when(expiringCodeStore.retrieveCode("thecode")).thenReturn(new ExpiringCode("thecode", new Timestamp(1), codeDataString), null); + when(expiringCodeStore.generateCode(eq(codeDataString),any())).thenReturn(new ExpiringCode("thenewcode", new Timestamp(1), codeDataString)); when(invitationsService.acceptInvitation(anyString(), eq("password"))).thenReturn(new InvitationsService.AcceptedInvitation("valid.redirect.com", user)); MockHttpServletRequestBuilder post = post("/invitations/accept.do") @@ -394,6 +471,11 @@ public void acceptInviteWithInvalidClientRedirect() throws Exception { ScimUser user = new ScimUser(uaaPrincipal.getId(), uaaPrincipal.getName(),"fname", "lname"); user.setPrimaryEmail(user.getUserName()); + Map<String,String> codeData = getInvitationsCode(Origin.UAA); + String codeDataString = JsonUtils.writeValueAsString(codeData); + when(expiringCodeStore.retrieveCode("thecode")).thenReturn(new ExpiringCode("thecode", new Timestamp(1), codeDataString), null); + when(expiringCodeStore.generateCode(eq(codeDataString),any())).thenReturn(new ExpiringCode("thenewcode", new Timestamp(1), codeDataString)); + when(invitationsService.acceptInvitation(anyString(), eq("password"))).thenReturn(new InvitationsService.AcceptedInvitation("/home", user)); MockHttpServletRequestBuilder post = post("/invitations/accept.do") @@ -408,24 +490,61 @@ public void acceptInviteWithInvalidClientRedirect() throws Exception { .andExpect(redirectedUrl("/home")); } + @Test + public void invalidCodeOnAcceptPost() throws Exception { + UaaPrincipal uaaPrincipal = new UaaPrincipal("user-id-001", "user@example.com", "user@example.com", Origin.UAA, null,IdentityZoneHolder.get().getId()); + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(uaaPrincipal, null, UaaAuthority.USER_AUTHORITIES); + SecurityContextHolder.getContext().setAuthentication(token); + + Map<String,String> codeData = getInvitationsCode(Origin.UAA); + String codeDataString = JsonUtils.writeValueAsString(codeData); + when(expiringCodeStore.retrieveCode("thecode")).thenReturn(new ExpiringCode("thecode", new Timestamp(1), codeDataString), null); + when(expiringCodeStore.generateCode(eq(codeDataString),any())).thenReturn(new ExpiringCode("thenewcode", new Timestamp(1), codeDataString)); + + doThrow(new HttpClientErrorException(BAD_REQUEST)).when(invitationsService).acceptInvitation(anyString(), anyString()); + + MockHttpServletRequestBuilder post = post("/invitations/accept.do") + .param("code","thecode") + .param("password", "password") + .param("password_confirmation", "password"); + + mockMvc.perform(post) + .andExpect(status().isUnprocessableEntity()) + .andExpect(model().attribute("error_message_code", "code_expired")) + .andExpect(view().name("invitations/accept_invite")); + } + @Test public void testAcceptInviteWithoutMatchingPasswords() throws Exception { UaaPrincipal uaaPrincipal = new UaaPrincipal("user-id-001", "user@example.com", "user@example.com", Origin.UAA, null,IdentityZoneHolder.get().getId()); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(uaaPrincipal, null, UaaAuthority.USER_AUTHORITIES); SecurityContextHolder.getContext().setAuthentication(token); + Map<String,String> codeData = getInvitationsCode(Origin.UAA); + String codeDataString = JsonUtils.writeValueAsString(codeData); + when(expiringCodeStore.retrieveCode("thecode")).thenReturn(new ExpiringCode("thecode", new Timestamp(1), codeDataString), null); + when(expiringCodeStore.retrieveCode("thenewcode")).thenReturn(new ExpiringCode("thenewcode", new Timestamp(1), codeDataString), null); + when(expiringCodeStore.generateCode(eq(codeDataString),any())).thenReturn( + new ExpiringCode("thenewcode", new Timestamp(1), codeDataString), + new ExpiringCode("thenewcode2", new Timestamp(1), codeDataString) + ); + + + IdentityProvider identityProvider = new IdentityProvider(); + identityProvider.setType(Origin.UAA); + when(providerProvisioning.retrieveByOrigin("uaa", "uaa")).thenReturn(identityProvider); MockHttpServletRequestBuilder post = post("/invitations/accept.do") .param("code", "thecode") .param("password", "password") .param("password_confirmation", "does not match"); - mockMvc.perform(post) .andExpect(status().isUnprocessableEntity()) .andExpect(model().attribute("error_message_code", "form_error")) - .andExpect(model().attribute("email", "user@example.com")) + .andExpect(model().attribute("code", "thenewcode")) .andExpect(view().name("invitations/accept_invite")); - - verifyZeroInteractions(invitationsService); + verify(expiringCodeStore).retrieveCode("thecode"); + verify(expiringCodeStore, times(1)).generateCode(anyString(),anyObject()); + verify(invitationsService, never()).acceptInvitation(anyString(), anyString()); }
uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/InvitationsIT.java+22 −3 modified@@ -30,22 +30,25 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.oauth2.client.test.TestAccounts; import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.web.client.DefaultResponseErrorHandler; import org.springframework.web.client.RestTemplate; import java.security.SecureRandom; import java.sql.Timestamp; import java.util.concurrent.TimeUnit; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; +import static org.springframework.http.HttpMethod.POST; +import static org.springframework.http.MediaType.APPLICATION_JSON; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = DefaultIntegrationTestConfig.class) @@ -101,6 +104,22 @@ public void logout_and_clear_cookies() { webDriver.manage().deleteAllCookies(); } + @Test + public void invite_fails() { + RestTemplate uaaTemplate = new RestTemplate(); + uaaTemplate.setErrorHandler(new DefaultResponseErrorHandler() { + @Override + protected boolean hasError(HttpStatus statusCode) { + return statusCode.is5xxServerError(); + } + }); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(APPLICATION_JSON); + HttpEntity<String> request = new HttpEntity<>("{\"emails\":[\"marissa@test.org\"]}", headers); + ResponseEntity<Void> response = uaaTemplate.exchange(uaaUrl + "/invite_users/?client_id=admin&redirect_uri={uri}", POST, request, Void.class, "https://www.google.com"); + assertThat(response.getStatusCode(), is(HttpStatus.UNAUTHORIZED)); + } + @Test public void testInviteUserWithClientRedirect() throws Exception { String userEmail = "user-" + new RandomValueStringGenerator().generate() + "@example.com"; @@ -208,7 +227,7 @@ public static String createInvitation(String baseUrl, String uaaUrl, String user } if (userId == null) { HttpEntity<ScimUser> request = new HttpEntity<>(scimUser, headers); - ResponseEntity<ScimUser> response = uaaTemplate.exchange(uaaUrl + "/Users", HttpMethod.POST, request, ScimUser.class); + ResponseEntity<ScimUser> response = uaaTemplate.exchange(uaaUrl + "/Users", POST, request, ScimUser.class); if (response.getStatusCode().value()!= HttpStatus.CREATED.value()) { throw new IllegalStateException("Unable to create test user:"+scimUser); } @@ -221,7 +240,7 @@ public static String createInvitation(String baseUrl, String uaaUrl, String user 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 + "\"}"); HttpEntity<ExpiringCode> expiringCodeRequest = new HttpEntity<>(expiringCode, headers); - ResponseEntity<ExpiringCode> expiringCodeResponse = uaaTemplate.exchange(uaaUrl + "/Codes", HttpMethod.POST, expiringCodeRequest, ExpiringCode.class); + ResponseEntity<ExpiringCode> expiringCodeResponse = uaaTemplate.exchange(uaaUrl + "/Codes", POST, expiringCodeRequest, ExpiringCode.class); expiringCode = expiringCodeResponse.getBody(); return expiringCode.getCode(); }
uaa/src/test/java/org/cloudfoundry/identity/uaa/login/InvitationsServiceMockMvcTests.java+69 −0 modified@@ -51,15 +51,18 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; 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.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrlPattern; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; public class InvitationsServiceMockMvcTests extends InjectedMockContextTest { @@ -206,6 +209,72 @@ public void accept_invitation_for_verified_user_sends_redirect() throws Exceptio .andExpect(redirectedUrl(REDIRECT_URI)); } + @Test + public void accept_invitation_for_uaa_user_should_expire_invitelink() throws Exception { + String email = new RandomValueStringGenerator().generate().toLowerCase() + "@test.org"; + URL inviteLink = inviteUser(email, userInviteToken, null, clientId, Origin.UAA); + assertEquals(Origin.UAA, queryUserForField(email, Origin.ORIGIN, String.class)); + + String code = extractInvitationCode(inviteLink.toString()); + MockHttpServletRequestBuilder get = get("/invitations/accept") + .param("code", code) + .accept(MediaType.TEXT_HTML); + getMockMvc().perform(get) + .andExpect(status().isOk()); + + getMockMvc().perform(get) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + public void invalid_code() throws Exception { + String email = new RandomValueStringGenerator().generate().toLowerCase()+"@test.org"; + String invalid = new RandomValueStringGenerator().generate().toLowerCase()+"@test.org"; + URL inviteLink = inviteUser(email, userInviteToken, null, clientId, Origin.UAA); + URL invalidLink = inviteUser(invalid, userInviteToken, null, clientId, Origin.UAA); + + assertFalse("User should not be verified", queryUserForField(email, "verified", Boolean.class)); + assertEquals(Origin.UAA, queryUserForField(email, Origin.ORIGIN, String.class)); + + String code = extractInvitationCode(inviteLink.toString()); + String invalidCode = extractInvitationCode(invalidLink.toString()); + + MvcResult result = getMockMvc().perform(get("/invitations/accept") + .param("code", code) + .accept(MediaType.TEXT_HTML) + ) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("Email: " + email))) + .andReturn(); + + MockHttpSession session = (MockHttpSession) result.getRequest().getSession(false); + result = getMockMvc().perform( + post("/invitations/accept.do") + .session(session) + .param("password", "s3cret") + .param("password_confirmation", "s3cret") + .param("code",invalidCode) + .with(csrf()) + ) + .andExpect(status().isUnprocessableEntity()) + .andExpect(model().attribute("error_message_code", "code_expired")) + .andExpect(view().name("invitations/accept_invite")) + .andReturn(); + + assertFalse("User should be not yet be verified", queryUserForField(email, "verified", Boolean.class)); + assertNull(session.getAttribute("SPRING_SECURITY_CONTEXT")); + + session = (MockHttpSession) result.getRequest().getSession(false); + //not logged in anymore + getMockMvc().perform( + get("/profile") + .session(session) + .accept(MediaType.TEXT_HTML) + ) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("http://localhost/login")); + } + @Test public void accept_invitation_sets_your_password() throws Exception { String email = new RandomValueStringGenerator().generate().toLowerCase()+"@test.org";
uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcTests.java+10 −0 modified@@ -18,6 +18,8 @@ import org.cloudfoundry.identity.uaa.authentication.login.LoginInfoEndpoint; import org.cloudfoundry.identity.uaa.authentication.login.Prompt; import org.cloudfoundry.identity.uaa.client.ClientConstants; +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.config.LockoutPolicy; import org.cloudfoundry.identity.uaa.login.saml.IdentityProviderConfiguratorTests; @@ -68,6 +70,7 @@ import javax.servlet.http.Cookie; import java.lang.reflect.Field; +import java.sql.Timestamp; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -979,6 +982,13 @@ public void testCsrfForInvitationAcceptPost() throws Exception { inviteContext.setAuthentication(inviteToken); inviteSession.setAttribute("SPRING_SECURITY_CONTEXT", inviteContext); + Map<String, String> codeData = new HashMap(); + codeData.put("user_id", ((UaaPrincipal)marissaContext.getAuthentication().getPrincipal()).getId()); + codeData.put("email", ((UaaPrincipal)marissaContext.getAuthentication().getPrincipal()).getEmail()); + codeData.put("origin", Origin.UAA); + + ExpiringCode code = getWebApplicationContext().getBean(ExpiringCodeStore.class).generateCode(JsonUtils.writeValueAsString(codeData), new Timestamp(System.currentTimeMillis() + 1000 * 60)); + //logged in with valid CSRF MockHttpServletRequestBuilder post = post("/invitations/accept.do") .session(inviteSession)
uaa/src/test/java/org/cloudfoundry/identity/uaa/login/PasscodeMockMvcTests.java+2 −0 modified@@ -14,6 +14,7 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpSession; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -64,6 +65,7 @@ public void clearSecContext() { } @Before public void setUp() throws Exception { + getWebApplicationContext().getBean(JdbcTemplate.class).update("delete from expiring_code_store"); FilterChainProxy springSecurityFilterChain = (FilterChainProxy) getWebApplicationContext().getBean("org.springframework.security.filterChainProxy"); if (captureSecurityContextFilter==null) { captureSecurityContextFilter = new CaptureSecurityContextFilter();
6 files changed · +224 −13
server/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsController.java+18 −1 modified@@ -238,19 +238,36 @@ public String acceptInvitation(@RequestParam("password") String password, UaaPrincipal principal = (UaaPrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + final ExpiringCode expiringCode = expiringCodeStore.retrieveCode(code); + + if (expiringCode == null || expiringCode.getData() == null) { + logger.debug("Failing invitation. Code not found."); + SecurityContextHolder.clearContext(); + return handleUnprocessableEntity(model, response, "error_message_code", "code_expired", "invitations/accept_invite"); + } + Map<String,String> data = JsonUtils.readValue(expiringCode.getData(), new TypeReference<Map<String,String>>() {}); + if (principal == null || data.get("user_id") == null || !data.get("user_id").equals(principal.getId())) { + logger.debug("Failing invitation. Code and user ID mismatch."); + SecurityContextHolder.clearContext(); + return handleUnprocessableEntity(model, response, "error_message_code", "code_expired", "invitations/accept_invite"); + } + + final String newCode = expiringCodeStore.generateCode(expiringCode.getData(), new Timestamp(System.currentTimeMillis() + (10 * 60 * 1000)), expiringCode.getIntent()).getCode(); if (!validation.valid()) { + model.addAttribute("code", newCode); model.addAttribute("email", principal.getEmail()); return handleUnprocessableEntity(model, response, "error_message_code", validation.getMessageCode(), "invitations/accept_invite"); } try { passwordValidator.validate(password); } catch (InvalidPasswordException e) { + model.addAttribute("code", newCode); model.addAttribute("email", principal.getEmail()); return handleUnprocessableEntity(model, response, "error_message", e.getMessagesAsOneString(), "invitations/accept_invite"); } AcceptedInvitation invitation; try { - invitation = invitationsService.acceptInvitation(code, password); + invitation = invitationsService.acceptInvitation(newCode, password); } catch (HttpClientErrorException e) { return handleUnprocessableEntity(model, response, "error_message_code", "code_expired", "invitations/accept_invite"); }
server/src/main/resources/login-ui.xml+3 −2 modified@@ -106,7 +106,7 @@ <intercept-url pattern="/invitations/accept" access="isFullyAuthenticated() or isAnonymous()" method="GET"/> <intercept-url pattern="/invitations/accept.do" access="hasAuthority('uaa.invited')" method="POST"/> <intercept-url pattern="/invitations/accept_enterprise.do" access="hasAuthority('uaa.invited')" method="POST"/> - <intercept-url pattern="/**" access="isFullyAuthenticated()" /> + <intercept-url pattern="/**" access="denyAll" /> <csrf disabled="false"/> <custom-filter ref="acceptInvitationSecurityContextPersistenceFilter" before="FIRST"/> </http> @@ -116,7 +116,8 @@ use-expressions="true" pattern="/invite_users/**" xmlns="http://www.springframework.org/schema/security"> - <intercept-url pattern="/invite_users" access="#oauth2.hasAnyScope('scim.invite') or #oauth2.hasScopeInAuthZone('zones.{zone.id}.admin')" method="POST"/> + <intercept-url pattern="/**" access="#oauth2.hasAnyScope('scim.invite') or #oauth2.hasScopeInAuthZone('zones.{zone.id}.admin')" method="POST"/> + <intercept-url pattern="**" access="denyAll"/> <expression-handler ref="oauthWebExpressionHandler" /> <access-denied-handler ref="oauthAccessDeniedHandler" /> <custom-filter ref="resourceAgnosticAuthenticationFilter" position="PRE_AUTH_FILTER" />
server/src/test/java/org/cloudfoundry/identity/uaa/invitations/InvitationsControllerTest.java+104 −6 modified@@ -64,14 +64,15 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyObject; import static org.mockito.Matchers.anyString; 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.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import static org.springframework.http.HttpStatus.BAD_REQUEST; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -395,15 +396,71 @@ public void testAcceptInvitePageWithExpiredCode() throws Exception { assertNull(SecurityContextHolder.getContext().getAuthentication()); } + @Test + public void missing_code() throws Exception { + MockHttpServletRequestBuilder post = startAcceptInviteFlow("a", "a"); + + when(expiringCodeStore.retrieveCode("thecode")).thenReturn(null); + + IdentityProvider identityProvider = new IdentityProvider(); + identityProvider.setType(OriginKeys.UAA); + when(providerProvisioning.retrieveByOrigin("uaa", "uaa")).thenReturn(identityProvider); + mockMvc.perform(post) + .andExpect(status().isUnprocessableEntity()) + .andExpect(model().attribute("error_message_code", "code_expired")) + .andExpect(view().name("invitations/accept_invite")); + verify(expiringCodeStore).retrieveCode("thecode"); + verify(expiringCodeStore, never()).generateCode(anyString(),anyObject(),anyString()); + verify(invitationsService, never()).acceptInvitation(anyString(), anyString()); + + } + + @Test + public void invalid_principal_id() throws Exception { + MockHttpServletRequestBuilder post = startAcceptInviteFlow("a", "a"); + + Map<String,String> codeData = getInvitationsCode(OriginKeys.UAA); + codeData.put("user_id", "invalid id"); + String codeDataString = JsonUtils.writeValueAsString(codeData); + when(expiringCodeStore.retrieveCode("thecode")).thenReturn(new ExpiringCode("thecode", new Timestamp(1), codeDataString, INVITATION.name()), null); + + IdentityProvider identityProvider = new IdentityProvider(); + identityProvider.setType(OriginKeys.UAA); + when(providerProvisioning.retrieveByOrigin("uaa", "uaa")).thenReturn(identityProvider); + mockMvc.perform(post) + .andExpect(status().isUnprocessableEntity()) + .andExpect(model().attribute("error_message_code", "code_expired")) + .andExpect(view().name("invitations/accept_invite")); + verify(expiringCodeStore).retrieveCode("thecode"); + verify(expiringCodeStore, never()).generateCode(anyString(),anyObject(),anyString()); + verify(invitationsService, never()).acceptInvitation(anyString(), anyString()); + + } + @Test public void testAcceptInviteWithContraveningPassword() throws Exception { doThrow(new InvalidPasswordException(Arrays.asList("Msg 2c", "Msg 1c"))).when(passwordValidator).validate("a"); MockHttpServletRequestBuilder post = startAcceptInviteFlow("a"); + Map<String,String> codeData = getInvitationsCode(OriginKeys.UAA); + String codeDataString = JsonUtils.writeValueAsString(codeData); + when(expiringCodeStore.retrieveCode("thecode")).thenReturn(new ExpiringCode("thecode", new Timestamp(1), codeDataString, INVITATION.name()), null); + when(expiringCodeStore.retrieveCode("thenewcode")).thenReturn(new ExpiringCode("thenewcode", new Timestamp(1), codeDataString, INVITATION.name()), null); + when(expiringCodeStore.generateCode(eq(codeDataString),any(), eq(INVITATION.name()))).thenReturn( + new ExpiringCode("thenewcode", new Timestamp(1), codeDataString, INVITATION.name()), + new ExpiringCode("thenewcode2", new Timestamp(1), codeDataString, INVITATION.name()) + ); + + IdentityProvider identityProvider = new IdentityProvider(); + identityProvider.setType(OriginKeys.UAA); + when(providerProvisioning.retrieveByOrigin("uaa", "uaa")).thenReturn(identityProvider); mockMvc.perform(post) .andExpect(status().isUnprocessableEntity()) .andExpect(model().attribute("error_message", "Msg 1c Msg 2c")) + .andExpect(model().attribute("code", "thenewcode")) .andExpect(view().name("invitations/accept_invite")); + verify(expiringCodeStore).retrieveCode("thecode"); + verify(expiringCodeStore, times(1)).generateCode(anyString(),anyObject(),anyString()); verify(invitationsService, never()).acceptInvitation(anyString(), anyString()); } @@ -413,6 +470,17 @@ public void testAcceptInvite() throws Exception { user.setPrimaryEmail(user.getUserName()); MockHttpServletRequestBuilder post = startAcceptInviteFlow("passw0rd"); + Map<String,String> codeData = getInvitationsCode(OriginKeys.UAA); + String codeDataString = JsonUtils.writeValueAsString(codeData); + ExpiringCode thecode = new ExpiringCode("thecode", new Timestamp(1), codeDataString, INVITATION.name()); + ExpiringCode thenewcode = new ExpiringCode("thenewcode", new Timestamp(1), codeDataString, INVITATION.name()); + ExpiringCode thenewcode2 = new ExpiringCode("thenewcode2", new Timestamp(1), codeDataString, INVITATION.name()); + when(expiringCodeStore.retrieveCode("thecode")).thenReturn(thecode, null); + when(expiringCodeStore.retrieveCode("thenewcode")).thenReturn(thenewcode, null); + when(expiringCodeStore.generateCode(eq(codeDataString),any(), eq(INVITATION.name()))) + .thenReturn(thenewcode) + .thenReturn(thenewcode2); + when(invitationsService.acceptInvitation(anyString(), eq("passw0rd"))).thenReturn(new InvitationsService.AcceptedInvitation("/home", user)); mockMvc.perform(post) @@ -423,14 +491,17 @@ public void testAcceptInvite() throws Exception { } private MockHttpServletRequestBuilder startAcceptInviteFlow(String password) { + return startAcceptInviteFlow(password, password); + } + private MockHttpServletRequestBuilder startAcceptInviteFlow(String password, String confirmPassword) { UaaPrincipal uaaPrincipal = new UaaPrincipal("user-id-001", "user@example.com", "user@example.com", OriginKeys.UAA, null, IdentityZoneHolder.get().getId()); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(uaaPrincipal, null, UaaAuthority.USER_AUTHORITIES); SecurityContextHolder.getContext().setAuthentication(token); return post("/invitations/accept.do") .param("code","thecode") .param("password", password) - .param("password_confirmation", password); + .param("password_confirmation", confirmPassword); } @Test @@ -442,6 +513,10 @@ public void acceptInviteWithValidClientRedirect() throws Exception { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(uaaPrincipal, null, UaaAuthority.USER_AUTHORITIES); SecurityContextHolder.getContext().setAuthentication(token); + Map<String,String> codeData = getInvitationsCode(OriginKeys.UAA); + String codeDataString = JsonUtils.writeValueAsString(codeData); + when(expiringCodeStore.retrieveCode("thecode")).thenReturn(new ExpiringCode("thecode", new Timestamp(1), codeDataString, INVITATION.name()), null); + when(expiringCodeStore.generateCode(eq(codeDataString),any(), eq(INVITATION.name()))).thenReturn(new ExpiringCode("thenewcode", new Timestamp(1), codeDataString, INVITATION.name())); when(invitationsService.acceptInvitation(anyString(), eq("password"))).thenReturn(new InvitationsService.AcceptedInvitation("valid.redirect.com", user)); MockHttpServletRequestBuilder post = post("/invitations/accept.do") @@ -463,6 +538,11 @@ public void acceptInviteWithInvalidClientRedirect() throws Exception { ScimUser user = new ScimUser(uaaPrincipal.getId(), uaaPrincipal.getName(),"fname", "lname"); user.setPrimaryEmail(user.getUserName()); + Map<String,String> codeData = getInvitationsCode(OriginKeys.UAA); + String codeDataString = JsonUtils.writeValueAsString(codeData); + when(expiringCodeStore.retrieveCode("thecode")).thenReturn(new ExpiringCode("thecode", new Timestamp(1), codeDataString, INVITATION.name()), null); + when(expiringCodeStore.generateCode(eq(codeDataString),any(), eq(INVITATION.name()))).thenReturn(new ExpiringCode("thenewcode", new Timestamp(1), codeDataString, INVITATION.name())); + when(invitationsService.acceptInvitation(anyString(), eq("password"))).thenReturn(new InvitationsService.AcceptedInvitation("/home", user)); MockHttpServletRequestBuilder post = post("/invitations/accept.do") @@ -481,6 +561,11 @@ public void invalidCodeOnAcceptPost() throws Exception { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(uaaPrincipal, null, UaaAuthority.USER_AUTHORITIES); SecurityContextHolder.getContext().setAuthentication(token); + Map<String,String> codeData = getInvitationsCode(OriginKeys.UAA); + String codeDataString = JsonUtils.writeValueAsString(codeData); + when(expiringCodeStore.retrieveCode("thecode")).thenReturn(new ExpiringCode("thecode", new Timestamp(1), codeDataString, INVITATION.name()), null); + when(expiringCodeStore.generateCode(eq(codeDataString),any(), eq(INVITATION.name()))).thenReturn(new ExpiringCode("thenewcode", new Timestamp(1), codeDataString, INVITATION.name())); + doThrow(new HttpClientErrorException(BAD_REQUEST)).when(invitationsService).acceptInvitation(anyString(), anyString()); MockHttpServletRequestBuilder post = post("/invitations/accept.do") @@ -500,18 +585,31 @@ public void testAcceptInviteWithoutMatchingPasswords() throws Exception { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(uaaPrincipal, null, UaaAuthority.USER_AUTHORITIES); SecurityContextHolder.getContext().setAuthentication(token); + Map<String,String> codeData = getInvitationsCode(OriginKeys.UAA); + String codeDataString = JsonUtils.writeValueAsString(codeData); + when(expiringCodeStore.retrieveCode("thecode")).thenReturn(new ExpiringCode("thecode", new Timestamp(1), codeDataString, INVITATION.name()), null); + when(expiringCodeStore.retrieveCode("thenewcode")).thenReturn(new ExpiringCode("thenewcode", new Timestamp(1), codeDataString, INVITATION.name()), null); + when(expiringCodeStore.generateCode(eq(codeDataString),any(), eq(INVITATION.name()))).thenReturn( + new ExpiringCode("thenewcode", new Timestamp(1), codeDataString, INVITATION.name()), + new ExpiringCode("thenewcode2", new Timestamp(1), codeDataString, INVITATION.name()) + ); + + + IdentityProvider identityProvider = new IdentityProvider(); + identityProvider.setType(OriginKeys.UAA); + when(providerProvisioning.retrieveByOrigin("uaa", "uaa")).thenReturn(identityProvider); MockHttpServletRequestBuilder post = post("/invitations/accept.do") .param("code", "thecode") .param("password", "password") .param("password_confirmation", "does not match"); - mockMvc.perform(post) .andExpect(status().isUnprocessableEntity()) .andExpect(model().attribute("error_message_code", "form_error")) - .andExpect(model().attribute("email", "user@example.com")) + .andExpect(model().attribute("code", "thenewcode")) .andExpect(view().name("invitations/accept_invite")); - - verifyZeroInteractions(invitationsService); + verify(expiringCodeStore).retrieveCode("thecode"); + verify(expiringCodeStore, times(1)).generateCode(anyString(),anyObject(),anyString()); + verify(invitationsService, never()).acceptInvitation(anyString(), anyString()); }
uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/InvitationsIT.java+17 −0 modified@@ -43,6 +43,7 @@ import org.springframework.security.oauth2.provider.client.BaseClientDetails; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.web.client.DefaultResponseErrorHandler; import org.springframework.web.client.RestTemplate; import java.net.URL; @@ -117,6 +118,22 @@ public void logout_and_clear_cookies() { webDriver.manage().deleteAllCookies(); } + @Test + public void invite_fails() { + RestTemplate uaaTemplate = new RestTemplate(); + uaaTemplate.setErrorHandler(new DefaultResponseErrorHandler() { + @Override + protected boolean hasError(HttpStatus statusCode) { + return statusCode.is5xxServerError(); + } + }); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(APPLICATION_JSON); + HttpEntity<String> request = new HttpEntity<>("{\"emails\":[\"marissa@test.org\"]}", headers); + ResponseEntity<Void> response = uaaTemplate.exchange(uaaUrl + "/invite_users/?client_id=admin&redirect_uri={uri}", POST, request, Void.class, "https://www.google.com"); + assertThat(response.getStatusCode(), is(HttpStatus.UNAUTHORIZED)); + } + @Test public void testInviteUserWithClientRedirect() throws Exception { String userEmail = "user-" + new RandomValueStringGenerator().generate() + "@example.com";
uaa/src/test/java/org/cloudfoundry/identity/uaa/login/InvitationsServiceMockMvcTests.java+72 −4 modified@@ -17,16 +17,15 @@ import org.cloudfoundry.identity.uaa.codestore.InMemoryExpiringCodeStore; import org.cloudfoundry.identity.uaa.constants.OriginKeys; import org.cloudfoundry.identity.uaa.message.EmailService; -import org.cloudfoundry.identity.uaa.provider.AbstractIdentityProviderDefinition; -import org.cloudfoundry.identity.uaa.constants.OriginKeys; -import org.cloudfoundry.identity.uaa.provider.LdapIdentityProviderDefinition; -import org.cloudfoundry.identity.uaa.provider.SamlIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.message.util.FakeJavaMailSender; import org.cloudfoundry.identity.uaa.mock.InjectedMockContextTest; import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils; import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.IdentityZoneCreationResult; import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.ZoneScimInviteData; +import org.cloudfoundry.identity.uaa.provider.AbstractIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.provider.IdentityProvider; +import org.cloudfoundry.identity.uaa.provider.LdapIdentityProviderDefinition; +import org.cloudfoundry.identity.uaa.provider.SamlIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.junit.After; import org.junit.Before; @@ -58,15 +57,18 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; 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.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrlPattern; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; public class InvitationsServiceMockMvcTests extends InjectedMockContextTest { @@ -227,6 +229,72 @@ public void accept_invitation_for_verified_user_sends_redirect() throws Exceptio .andExpect(redirectedUrl(REDIRECT_URI)); } + @Test + public void accept_invitation_for_uaa_user_should_expire_invitelink() throws Exception { + String email = new RandomValueStringGenerator().generate().toLowerCase() + "@test.org"; + URL inviteLink = inviteUser(email, userInviteToken, null, clientId, OriginKeys.UAA); + assertEquals(OriginKeys.UAA, queryUserForField(email, OriginKeys.ORIGIN, String.class)); + + String code = extractInvitationCode(inviteLink.toString()); + MockHttpServletRequestBuilder get = get("/invitations/accept") + .param("code", code) + .accept(MediaType.TEXT_HTML); + getMockMvc().perform(get) + .andExpect(status().isOk()); + + getMockMvc().perform(get) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + public void invalid_code() throws Exception { + String email = new RandomValueStringGenerator().generate().toLowerCase()+"@test.org"; + String invalid = new RandomValueStringGenerator().generate().toLowerCase()+"@test.org"; + URL inviteLink = inviteUser(email, userInviteToken, null, clientId, OriginKeys.UAA); + URL invalidLink = inviteUser(invalid, userInviteToken, null, clientId, OriginKeys.UAA); + + assertFalse("User should not be verified", queryUserForField(email, "verified", Boolean.class)); + assertEquals(OriginKeys.UAA, queryUserForField(email, OriginKeys.ORIGIN, String.class)); + + String code = extractInvitationCode(inviteLink.toString()); + String invalidCode = extractInvitationCode(invalidLink.toString()); + + MvcResult result = getMockMvc().perform(get("/invitations/accept") + .param("code", code) + .accept(MediaType.TEXT_HTML) + ) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("Email: " + email))) + .andReturn(); + + MockHttpSession session = (MockHttpSession) result.getRequest().getSession(false); + result = getMockMvc().perform( + post("/invitations/accept.do") + .session(session) + .param("password", "s3cret") + .param("password_confirmation", "s3cret") + .param("code",invalidCode) + .with(csrf()) + ) + .andExpect(status().isUnprocessableEntity()) + .andExpect(model().attribute("error_message_code", "code_expired")) + .andExpect(view().name("invitations/accept_invite")) + .andReturn(); + + assertFalse("User should be not yet be verified", queryUserForField(email, "verified", Boolean.class)); + assertNull(session.getAttribute("SPRING_SECURITY_CONTEXT")); + + session = (MockHttpSession) result.getRequest().getSession(false); + //not logged in anymore + getMockMvc().perform( + get("/profile") + .session(session) + .accept(MediaType.TEXT_HTML) + ) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("http://localhost/login")); + } + @Test public void accept_invitation_sets_your_password() throws Exception { String email = new RandomValueStringGenerator().generate().toLowerCase()+"@test.org";
uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcTests.java+10 −0 modified@@ -13,6 +13,8 @@ package org.cloudfoundry.identity.uaa.login; 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; @@ -79,6 +81,7 @@ import javax.servlet.http.HttpSession; import java.lang.reflect.Field; import java.net.URL; +import java.sql.Timestamp; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -1453,6 +1456,13 @@ public void testCsrfForInvitationAcceptPost() throws Exception { inviteContext.setAuthentication(inviteToken); inviteSession.setAttribute("SPRING_SECURITY_CONTEXT", inviteContext); + Map<String, String> codeData = new HashMap(); + codeData.put("user_id", ((UaaPrincipal)marissaContext.getAuthentication().getPrincipal()).getId()); + codeData.put("email", ((UaaPrincipal)marissaContext.getAuthentication().getPrincipal()).getEmail()); + codeData.put("origin", OriginKeys.UAA); + + ExpiringCode code = getWebApplicationContext().getBean(ExpiringCodeStore.class).generateCode(JsonUtils.writeValueAsString(codeData), new Timestamp(System.currentTimeMillis() + 1000 * 60), null); + //logged in with valid CSRF MockHttpServletRequestBuilder post = post("/invitations/accept.do") .session(inviteSession)
6 files changed · +185 −15
server/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsController.java+18 −3 modified@@ -250,19 +250,34 @@ public String acceptInvitation(@RequestParam("password") String password, UaaPrincipal principal = (UaaPrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + final ExpiringCode expiringCode = expiringCodeStore.retrieveCode(code); + + if (expiringCode == null || expiringCode.getData() == null) { + logger.debug("Failing invitation. Code not found."); + SecurityContextHolder.clearContext(); + return handleUnprocessableEntity(model, response, "error_message_code", "code_expired", "invitations/accept_invite"); + } + Map<String,String> data = JsonUtils.readValue(expiringCode.getData(), new TypeReference<Map<String,String>>() {}); + if (principal == null || data.get("user_id") == null || !data.get("user_id").equals(principal.getId())) { + logger.debug("Failing invitation. Code and user ID mismatch."); + SecurityContextHolder.clearContext(); + return handleUnprocessableEntity(model, response, "error_message_code", "code_expired", "invitations/accept_invite"); + } + + final String newCode = expiringCodeStore.generateCode(expiringCode.getData(), new Timestamp(System.currentTimeMillis() + (10 * 60 * 1000)), expiringCode.getIntent()).getCode(); if (!validation.valid()) { - return processErrorReload(code, model, principal.getEmail(), response, "error_message_code", validation.getMessageCode()); + return processErrorReload(newCode, model, principal.getEmail(), response, "error_message_code", validation.getMessageCode()); // return handleUnprocessableEntity(model, response, "error_message_code", validation.getMessageCode(), "invitations/accept_invite"); } try { passwordValidator.validate(password); } catch (InvalidPasswordException e) { - return processErrorReload(code, model, principal.getEmail(), response, "error_message", e.getMessagesAsOneString()); + return processErrorReload(newCode, model, principal.getEmail(), response, "error_message", e.getMessagesAsOneString()); // return handleUnprocessableEntity(model, response, "error_message", e.getMessagesAsOneString(), "invitations/accept_invite"); } AcceptedInvitation invitation; try { - invitation = invitationsService.acceptInvitation(code, password); + invitation = invitationsService.acceptInvitation(newCode, password); } catch (HttpClientErrorException e) { return handleUnprocessableEntity(model, response, "error_message_code", "code_expired", "invitations/accept_invite"); }
server/src/main/resources/spring/login-ui.xml+3 −2 modified@@ -113,7 +113,7 @@ <intercept-url pattern="/invitations/accept" access="isFullyAuthenticated() or isAnonymous()" method="GET"/> <intercept-url pattern="/invitations/accept.do" access="hasAuthority('uaa.invited')" method="POST"/> <intercept-url pattern="/invitations/accept_enterprise.do" access="hasAuthority('uaa.invited')" method="POST"/> - <intercept-url pattern="/**" access="isFullyAuthenticated()"/> + <intercept-url pattern="/**" access="denyAll"/> <csrf disabled="false"/> <custom-filter ref="acceptInvitationSecurityContextPersistenceFilter" before="FIRST"/> </http> @@ -123,9 +123,10 @@ use-expressions="true" pattern="/invite_users/**" xmlns="http://www.springframework.org/schema/security"> - <intercept-url pattern="/invite_users" + <intercept-url pattern="/**" access="#oauth2.hasAnyScope('scim.invite') or #oauth2.hasScopeInAuthZone('zones.{zone.id}.admin')" method="POST"/> + <intercept-url pattern="**" access="denyAll"/> <expression-handler ref="oauthWebExpressionHandler"/> <access-denied-handler ref="oauthAccessDeniedHandler"/> <custom-filter ref="resourceAgnosticAuthenticationFilter" position="PRE_AUTH_FILTER"/>
server/src/test/java/org/cloudfoundry/identity/uaa/invitations/InvitationsControllerTest.java+89 −9 modified@@ -73,6 +73,7 @@ import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.springframework.http.HttpStatus.BAD_REQUEST; @@ -433,24 +434,71 @@ public void testAcceptInvitePageWithExpiredCode() throws Exception { assertNull(SecurityContextHolder.getContext().getAuthentication()); } + @Test + public void missing_code() throws Exception { + MockHttpServletRequestBuilder post = startAcceptInviteFlow("a", "a"); + + when(expiringCodeStore.retrieveCode("thecode")).thenReturn(null); + + IdentityProvider identityProvider = new IdentityProvider(); + identityProvider.setType(OriginKeys.UAA); + when(providerProvisioning.retrieveByOrigin("uaa", "uaa")).thenReturn(identityProvider); + mockMvc.perform(post) + .andExpect(status().isUnprocessableEntity()) + .andExpect(model().attribute("error_message_code", "code_expired")) + .andExpect(view().name("invitations/accept_invite")); + verify(expiringCodeStore).retrieveCode("thecode"); + verify(expiringCodeStore, never()).generateCode(anyString(),anyObject(),anyString()); + verify(invitationsService, never()).acceptInvitation(anyString(), anyString()); + + } + + @Test + public void invalid_principal_id() throws Exception { + MockHttpServletRequestBuilder post = startAcceptInviteFlow("a", "a"); + + Map<String,String> codeData = getInvitationsCode(OriginKeys.UAA); + codeData.put("user_id", "invalid id"); + String codeDataString = JsonUtils.writeValueAsString(codeData); + when(expiringCodeStore.retrieveCode("thecode")).thenReturn(new ExpiringCode("thecode", new Timestamp(1), codeDataString, INVITATION.name()), null); + + IdentityProvider identityProvider = new IdentityProvider(); + identityProvider.setType(OriginKeys.UAA); + when(providerProvisioning.retrieveByOrigin("uaa", "uaa")).thenReturn(identityProvider); + mockMvc.perform(post) + .andExpect(status().isUnprocessableEntity()) + .andExpect(model().attribute("error_message_code", "code_expired")) + .andExpect(view().name("invitations/accept_invite")); + verify(expiringCodeStore).retrieveCode("thecode"); + verify(expiringCodeStore, never()).generateCode(anyString(),anyObject(),anyString()); + verify(invitationsService, never()).acceptInvitation(anyString(), anyString()); + + } + @Test public void testAcceptInviteWithContraveningPassword() throws Exception { doThrow(new InvalidPasswordException(Arrays.asList("Msg 2c", "Msg 1c"))).when(passwordValidator).validate("a"); MockHttpServletRequestBuilder post = startAcceptInviteFlow("a", "a"); Map<String,String> codeData = getInvitationsCode(OriginKeys.UAA); - when(expiringCodeStore.retrieveCode("thecode")).thenReturn(new ExpiringCode("thecode", new Timestamp(1), "{\"origin\":\"uaa\"}", "intent"), null); + String codeDataString = JsonUtils.writeValueAsString(codeData); + when(expiringCodeStore.retrieveCode("thecode")).thenReturn(new ExpiringCode("thecode", new Timestamp(1), codeDataString, INVITATION.name()), null); + when(expiringCodeStore.retrieveCode("thenewcode")).thenReturn(new ExpiringCode("thenewcode", new Timestamp(1), codeDataString, INVITATION.name()), null); + when(expiringCodeStore.generateCode(eq(codeDataString),any(), eq(INVITATION.name()))).thenReturn( + new ExpiringCode("thenewcode", new Timestamp(1), codeDataString, INVITATION.name()), + new ExpiringCode("thenewcode2", new Timestamp(1), codeDataString, INVITATION.name()) + ); + IdentityProvider identityProvider = new IdentityProvider(); identityProvider.setType(OriginKeys.UAA); when(providerProvisioning.retrieveByOrigin("uaa", "uaa")).thenReturn(identityProvider); - when(expiringCodeStore.generateCode(anyString(), anyObject(), anyString())).thenReturn(createCode(codeData)); mockMvc.perform(post) .andExpect(status().isFound()) .andExpect(model().attribute("error_message", "Msg 1c Msg 2c")) - .andExpect(model().attribute("code", "code")) + .andExpect(model().attribute("code", "thenewcode2")) .andExpect(view().name("redirect:accept")); verify(expiringCodeStore).retrieveCode("thecode"); - verify(expiringCodeStore).generateCode(anyString(),anyObject(),anyString()); + verify(expiringCodeStore, times(2)).generateCode(anyString(),anyObject(),anyString()); verify(invitationsService, never()).acceptInvitation(anyString(), anyString()); } @@ -460,6 +508,17 @@ public void testAcceptInvite() throws Exception { user.setPrimaryEmail(user.getUserName()); MockHttpServletRequestBuilder post = startAcceptInviteFlow("passw0rd","passw0rd"); + Map<String,String> codeData = getInvitationsCode(OriginKeys.UAA); + String codeDataString = JsonUtils.writeValueAsString(codeData); + ExpiringCode thecode = new ExpiringCode("thecode", new Timestamp(1), codeDataString, INVITATION.name()); + ExpiringCode thenewcode = new ExpiringCode("thenewcode", new Timestamp(1), codeDataString, INVITATION.name()); + ExpiringCode thenewcode2 = new ExpiringCode("thenewcode2", new Timestamp(1), codeDataString, INVITATION.name()); + when(expiringCodeStore.retrieveCode("thecode")).thenReturn(thecode, null); + when(expiringCodeStore.retrieveCode("thenewcode")).thenReturn(thenewcode, null); + when(expiringCodeStore.generateCode(eq(codeDataString),any(), eq(INVITATION.name()))) + .thenReturn(thenewcode) + .thenReturn(thenewcode2); + when(invitationsService.acceptInvitation(anyString(), eq("passw0rd"))).thenReturn(new InvitationsService.AcceptedInvitation("/home", user)); mockMvc.perform(post) @@ -489,6 +548,10 @@ public void acceptInviteWithValidClientRedirect() throws Exception { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(uaaPrincipal, null, UaaAuthority.USER_AUTHORITIES); SecurityContextHolder.getContext().setAuthentication(token); + Map<String,String> codeData = getInvitationsCode(OriginKeys.UAA); + String codeDataString = JsonUtils.writeValueAsString(codeData); + when(expiringCodeStore.retrieveCode("thecode")).thenReturn(new ExpiringCode("thecode", new Timestamp(1), codeDataString, INVITATION.name()), null); + when(expiringCodeStore.generateCode(eq(codeDataString),any(), eq(INVITATION.name()))).thenReturn(new ExpiringCode("thenewcode", new Timestamp(1), codeDataString, INVITATION.name())); when(invitationsService.acceptInvitation(anyString(), eq("password"))).thenReturn(new InvitationsService.AcceptedInvitation("valid.redirect.com", user)); MockHttpServletRequestBuilder post = post("/invitations/accept.do") @@ -510,6 +573,11 @@ public void acceptInviteWithInvalidClientRedirect() throws Exception { ScimUser user = new ScimUser(uaaPrincipal.getId(), uaaPrincipal.getName(),"fname", "lname"); user.setPrimaryEmail(user.getUserName()); + Map<String,String> codeData = getInvitationsCode(OriginKeys.UAA); + String codeDataString = JsonUtils.writeValueAsString(codeData); + when(expiringCodeStore.retrieveCode("thecode")).thenReturn(new ExpiringCode("thecode", new Timestamp(1), codeDataString, INVITATION.name()), null); + when(expiringCodeStore.generateCode(eq(codeDataString),any(), eq(INVITATION.name()))).thenReturn(new ExpiringCode("thenewcode", new Timestamp(1), codeDataString, INVITATION.name())); + when(invitationsService.acceptInvitation(anyString(), eq("password"))).thenReturn(new InvitationsService.AcceptedInvitation("/home", user)); MockHttpServletRequestBuilder post = post("/invitations/accept.do") @@ -528,6 +596,11 @@ public void invalidCodeOnAcceptPost() throws Exception { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(uaaPrincipal, null, UaaAuthority.USER_AUTHORITIES); SecurityContextHolder.getContext().setAuthentication(token); + Map<String,String> codeData = getInvitationsCode(OriginKeys.UAA); + String codeDataString = JsonUtils.writeValueAsString(codeData); + when(expiringCodeStore.retrieveCode("thecode")).thenReturn(new ExpiringCode("thecode", new Timestamp(1), codeDataString, INVITATION.name()), null); + when(expiringCodeStore.generateCode(eq(codeDataString),any(), eq(INVITATION.name()))).thenReturn(new ExpiringCode("thenewcode", new Timestamp(1), codeDataString, INVITATION.name())); + doThrow(new HttpClientErrorException(BAD_REQUEST)).when(invitationsService).acceptInvitation(anyString(), anyString()); MockHttpServletRequestBuilder post = post("/invitations/accept.do") @@ -545,19 +618,26 @@ public void invalidCodeOnAcceptPost() throws Exception { public void testAcceptInviteWithoutMatchingPasswords() throws Exception { MockHttpServletRequestBuilder post = startAcceptInviteFlow("a","b"); - Map<String,String> codeData = getInvitationsCode("test-oidc"); - when(expiringCodeStore.retrieveCode("thecode")).thenReturn(new ExpiringCode("thecode", new Timestamp(1), "{\"origin\":\"uaa\"}", "intent"), null); + Map<String,String> codeData = getInvitationsCode(OriginKeys.UAA); + String codeDataString = JsonUtils.writeValueAsString(codeData); + when(expiringCodeStore.retrieveCode("thecode")).thenReturn(new ExpiringCode("thecode", new Timestamp(1), codeDataString, INVITATION.name()), null); + when(expiringCodeStore.retrieveCode("thenewcode")).thenReturn(new ExpiringCode("thenewcode", new Timestamp(1), codeDataString, INVITATION.name()), null); + when(expiringCodeStore.generateCode(eq(codeDataString),any(), eq(INVITATION.name()))).thenReturn( + new ExpiringCode("thenewcode", new Timestamp(1), codeDataString, INVITATION.name()), + new ExpiringCode("thenewcode2", new Timestamp(1), codeDataString, INVITATION.name()) + ); + + IdentityProvider identityProvider = new IdentityProvider(); identityProvider.setType(OriginKeys.UAA); when(providerProvisioning.retrieveByOrigin("uaa", "uaa")).thenReturn(identityProvider); - when(expiringCodeStore.generateCode(anyString(), anyObject(), anyString())).thenReturn(createCode(codeData)); mockMvc.perform(post) .andExpect(status().isFound()) .andExpect(model().attribute("error_message_code", "form_error")) - .andExpect(model().attribute("code", "code")) + .andExpect(model().attribute("code", "thenewcode2")) .andExpect(view().name("redirect:accept")); verify(expiringCodeStore).retrieveCode("thecode"); - verify(expiringCodeStore).generateCode(anyString(),anyObject(),anyString()); + verify(expiringCodeStore, times(2)).generateCode(anyString(),anyObject(),anyString()); verify(invitationsService, never()).acceptInvitation(anyString(), anyString()); }
uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/InvitationsIT.java+17 −0 modified@@ -43,6 +43,7 @@ import org.springframework.security.oauth2.provider.client.BaseClientDetails; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.web.client.DefaultResponseErrorHandler; import org.springframework.web.client.RestTemplate; import java.net.URL; @@ -117,6 +118,22 @@ public void logout_and_clear_cookies() { webDriver.manage().deleteAllCookies(); } + @Test + public void invite_fails() { + RestTemplate uaaTemplate = new RestTemplate(); + uaaTemplate.setErrorHandler(new DefaultResponseErrorHandler() { + @Override + protected boolean hasError(HttpStatus statusCode) { + return statusCode.is5xxServerError(); + } + }); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(APPLICATION_JSON); + HttpEntity<String> request = new HttpEntity<>("{\"emails\":[\"marissa@test.org\"]}", headers); + ResponseEntity<Void> response = uaaTemplate.exchange(uaaUrl + "/invite_users/?client_id=admin&redirect_uri={uri}", POST, request, Void.class, "https://www.google.com"); + assertThat(response.getStatusCode(), is(HttpStatus.UNAUTHORIZED)); + } + @Test public void testInviteUserWithClientRedirect() throws Exception { String userEmail = "user-" + new RandomValueStringGenerator().generate() + "@example.com";
uaa/src/test/java/org/cloudfoundry/identity/uaa/login/InvitationsServiceMockMvcTests.java+52 −0 modified@@ -53,15 +53,18 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; 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.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrlPattern; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; public class InvitationsServiceMockMvcTests extends InjectedMockContextTest { @@ -226,6 +229,55 @@ public void accept_invitation_for_uaa_user_should_expire_invitelink() throws Exc .andExpect(status().isUnprocessableEntity()); } + @Test + public void invalid_code() throws Exception { + String email = new RandomValueStringGenerator().generate().toLowerCase()+"@test.org"; + String invalid = new RandomValueStringGenerator().generate().toLowerCase()+"@test.org"; + URL inviteLink = inviteUser(email, userInviteToken, null, clientId, OriginKeys.UAA); + URL invalidLink = inviteUser(invalid, userInviteToken, null, clientId, OriginKeys.UAA); + + assertFalse("User should not be verified", queryUserForField(email, "verified", Boolean.class)); + assertEquals(OriginKeys.UAA, queryUserForField(email, OriginKeys.ORIGIN, String.class)); + + String code = extractInvitationCode(inviteLink.toString()); + String invalidCode = extractInvitationCode(invalidLink.toString()); + + MvcResult result = getMockMvc().perform(get("/invitations/accept") + .param("code", code) + .accept(MediaType.TEXT_HTML) + ) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("Email: " + email))) + .andReturn(); + + MockHttpSession session = (MockHttpSession) result.getRequest().getSession(false); + result = getMockMvc().perform( + post("/invitations/accept.do") + .session(session) + .param("password", "s3cret") + .param("password_confirmation", "s3cret") + .param("code",invalidCode) + .with(csrf()) + ) + .andExpect(status().isUnprocessableEntity()) + .andExpect(model().attribute("error_message_code", "code_expired")) + .andExpect(view().name("invitations/accept_invite")) + .andReturn(); + + assertFalse("User should be not yet be verified", queryUserForField(email, "verified", Boolean.class)); + assertNull(session.getAttribute("SPRING_SECURITY_CONTEXT")); + + session = (MockHttpSession) result.getRequest().getSession(false); + //not logged in anymore + getMockMvc().perform( + get("/profile") + .session(session) + .accept(MediaType.TEXT_HTML) + ) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("http://localhost/login")); + } + @Test public void accept_invitation_sets_your_password() throws Exception { String email = new RandomValueStringGenerator().generate().toLowerCase()+"@test.org";
uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcTests.java+6 −1 modified@@ -1767,7 +1767,12 @@ public void testCsrfForInvitationAcceptPost() throws Exception { inviteContext.setAuthentication(inviteToken); inviteSession.setAttribute("SPRING_SECURITY_CONTEXT", inviteContext); - ExpiringCode code = getWebApplicationContext().getBean(ExpiringCodeStore.class).generateCode("{ \"origin\" : \"uaa\"}", new Timestamp(System.currentTimeMillis() + 1000 * 60), null); + Map<String, String> codeData = new HashMap(); + codeData.put("user_id", ((UaaPrincipal)marissaContext.getAuthentication().getPrincipal()).getId()); + codeData.put("email", ((UaaPrincipal)marissaContext.getAuthentication().getPrincipal()).getEmail()); + codeData.put("origin", OriginKeys.UAA); + + ExpiringCode code = getWebApplicationContext().getBean(ExpiringCodeStore.class).generateCode(JsonUtils.writeValueAsString(codeData), new Timestamp(System.currentTimeMillis() + 1000 * 60), null); //logged in with valid CSRF MockHttpServletRequestBuilder post = post("/invitations/accept.do")
6 files changed · +223 −11
server/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsController.java+18 −1 modified@@ -239,19 +239,36 @@ public String acceptInvitation(@RequestParam("password") String password, UaaPrincipal principal = (UaaPrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + final ExpiringCode expiringCode = expiringCodeStore.retrieveCode(code); + + if (expiringCode == null || expiringCode.getData() == null) { + logger.debug("Failing invitation. Code not found."); + SecurityContextHolder.clearContext(); + return handleUnprocessableEntity(model, response, "error_message_code", "code_expired", "invitations/accept_invite"); + } + Map<String,String> data = JsonUtils.readValue(expiringCode.getData(), new TypeReference<Map<String,String>>() {}); + if (principal == null || data.get("user_id") == null || !data.get("user_id").equals(principal.getId())) { + logger.debug("Failing invitation. Code and user ID mismatch."); + SecurityContextHolder.clearContext(); + return handleUnprocessableEntity(model, response, "error_message_code", "code_expired", "invitations/accept_invite"); + } + + final String newCode = expiringCodeStore.generateCode(expiringCode.getData(), new Timestamp(System.currentTimeMillis() + (10 * 60 * 1000)), expiringCode.getIntent()).getCode(); if (!validation.valid()) { + model.addAttribute("code", newCode); model.addAttribute("email", principal.getEmail()); return handleUnprocessableEntity(model, response, "error_message_code", validation.getMessageCode(), "invitations/accept_invite"); } try { passwordValidator.validate(password); } catch (InvalidPasswordException e) { + model.addAttribute("code", newCode); model.addAttribute("email", principal.getEmail()); return handleUnprocessableEntity(model, response, "error_message", e.getMessagesAsOneString(), "invitations/accept_invite"); } AcceptedInvitation invitation; try { - invitation = invitationsService.acceptInvitation(code, password); + invitation = invitationsService.acceptInvitation(newCode, password); } catch (HttpClientErrorException e) { return handleUnprocessableEntity(model, response, "error_message_code", "code_expired", "invitations/accept_invite"); }
server/src/main/resources/login-ui.xml+3 −2 modified@@ -113,7 +113,7 @@ <intercept-url pattern="/invitations/accept" access="isFullyAuthenticated() or isAnonymous()" method="GET"/> <intercept-url pattern="/invitations/accept.do" access="hasAuthority('uaa.invited')" method="POST"/> <intercept-url pattern="/invitations/accept_enterprise.do" access="hasAuthority('uaa.invited')" method="POST"/> - <intercept-url pattern="/**" access="isFullyAuthenticated()"/> + <intercept-url pattern="/**" access="denyAll"/> <csrf disabled="false"/> <custom-filter ref="acceptInvitationSecurityContextPersistenceFilter" before="FIRST"/> </http> @@ -123,9 +123,10 @@ use-expressions="true" pattern="/invite_users/**" xmlns="http://www.springframework.org/schema/security"> - <intercept-url pattern="/invite_users" + <intercept-url pattern="/**" access="#oauth2.hasAnyScope('scim.invite') or #oauth2.hasScopeInAuthZone('zones.{zone.id}.admin')" method="POST"/> + <intercept-url pattern="**" access="denyAll"/> <expression-handler ref="oauthWebExpressionHandler"/> <access-denied-handler ref="oauthAccessDeniedHandler"/> <custom-filter ref="resourceAgnosticAuthenticationFilter" position="PRE_AUTH_FILTER"/>
server/src/test/java/org/cloudfoundry/identity/uaa/invitations/InvitationsControllerTest.java+105 −7 modified@@ -10,8 +10,8 @@ import org.cloudfoundry.identity.uaa.login.test.ThymeleafConfig; import org.cloudfoundry.identity.uaa.provider.IdentityProvider; import org.cloudfoundry.identity.uaa.provider.IdentityProviderProvisioning; -import org.cloudfoundry.identity.uaa.provider.SamlIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.provider.OIDCIdentityProviderDefinition; +import org.cloudfoundry.identity.uaa.provider.SamlIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.provider.ldap.ExtendedLdapUserDetails; import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning; @@ -66,14 +66,15 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyObject; import static org.mockito.Matchers.anyString; 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.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import static org.springframework.http.HttpStatus.BAD_REQUEST; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -425,15 +426,71 @@ public void testAcceptInvitePageWithExpiredCode() throws Exception { assertNull(SecurityContextHolder.getContext().getAuthentication()); } + @Test + public void missing_code() throws Exception { + MockHttpServletRequestBuilder post = startAcceptInviteFlow("a", "a"); + + when(expiringCodeStore.retrieveCode("thecode")).thenReturn(null); + + IdentityProvider identityProvider = new IdentityProvider(); + identityProvider.setType(OriginKeys.UAA); + when(providerProvisioning.retrieveByOrigin("uaa", "uaa")).thenReturn(identityProvider); + mockMvc.perform(post) + .andExpect(status().isUnprocessableEntity()) + .andExpect(model().attribute("error_message_code", "code_expired")) + .andExpect(view().name("invitations/accept_invite")); + verify(expiringCodeStore).retrieveCode("thecode"); + verify(expiringCodeStore, never()).generateCode(anyString(),anyObject(),anyString()); + verify(invitationsService, never()).acceptInvitation(anyString(), anyString()); + + } + + @Test + public void invalid_principal_id() throws Exception { + MockHttpServletRequestBuilder post = startAcceptInviteFlow("a", "a"); + + Map<String,String> codeData = getInvitationsCode(OriginKeys.UAA); + codeData.put("user_id", "invalid id"); + String codeDataString = JsonUtils.writeValueAsString(codeData); + when(expiringCodeStore.retrieveCode("thecode")).thenReturn(new ExpiringCode("thecode", new Timestamp(1), codeDataString, INVITATION.name()), null); + + IdentityProvider identityProvider = new IdentityProvider(); + identityProvider.setType(OriginKeys.UAA); + when(providerProvisioning.retrieveByOrigin("uaa", "uaa")).thenReturn(identityProvider); + mockMvc.perform(post) + .andExpect(status().isUnprocessableEntity()) + .andExpect(model().attribute("error_message_code", "code_expired")) + .andExpect(view().name("invitations/accept_invite")); + verify(expiringCodeStore).retrieveCode("thecode"); + verify(expiringCodeStore, never()).generateCode(anyString(),anyObject(),anyString()); + verify(invitationsService, never()).acceptInvitation(anyString(), anyString()); + + } + @Test public void testAcceptInviteWithContraveningPassword() throws Exception { doThrow(new InvalidPasswordException(Arrays.asList("Msg 2c", "Msg 1c"))).when(passwordValidator).validate("a"); MockHttpServletRequestBuilder post = startAcceptInviteFlow("a"); + Map<String,String> codeData = getInvitationsCode(OriginKeys.UAA); + String codeDataString = JsonUtils.writeValueAsString(codeData); + when(expiringCodeStore.retrieveCode("thecode")).thenReturn(new ExpiringCode("thecode", new Timestamp(1), codeDataString, INVITATION.name()), null); + when(expiringCodeStore.retrieveCode("thenewcode")).thenReturn(new ExpiringCode("thenewcode", new Timestamp(1), codeDataString, INVITATION.name()), null); + when(expiringCodeStore.generateCode(eq(codeDataString),any(), eq(INVITATION.name()))).thenReturn( + new ExpiringCode("thenewcode", new Timestamp(1), codeDataString, INVITATION.name()), + new ExpiringCode("thenewcode2", new Timestamp(1), codeDataString, INVITATION.name()) + ); + + IdentityProvider identityProvider = new IdentityProvider(); + identityProvider.setType(OriginKeys.UAA); + when(providerProvisioning.retrieveByOrigin("uaa", "uaa")).thenReturn(identityProvider); mockMvc.perform(post) .andExpect(status().isUnprocessableEntity()) .andExpect(model().attribute("error_message", "Msg 1c Msg 2c")) + .andExpect(model().attribute("code", "thenewcode")) .andExpect(view().name("invitations/accept_invite")); + verify(expiringCodeStore).retrieveCode("thecode"); + verify(expiringCodeStore, times(1)).generateCode(anyString(),anyObject(),anyString()); verify(invitationsService, never()).acceptInvitation(anyString(), anyString()); } @@ -443,6 +500,17 @@ public void testAcceptInvite() throws Exception { user.setPrimaryEmail(user.getUserName()); MockHttpServletRequestBuilder post = startAcceptInviteFlow("passw0rd"); + Map<String,String> codeData = getInvitationsCode(OriginKeys.UAA); + String codeDataString = JsonUtils.writeValueAsString(codeData); + ExpiringCode thecode = new ExpiringCode("thecode", new Timestamp(1), codeDataString, INVITATION.name()); + ExpiringCode thenewcode = new ExpiringCode("thenewcode", new Timestamp(1), codeDataString, INVITATION.name()); + ExpiringCode thenewcode2 = new ExpiringCode("thenewcode2", new Timestamp(1), codeDataString, INVITATION.name()); + when(expiringCodeStore.retrieveCode("thecode")).thenReturn(thecode, null); + when(expiringCodeStore.retrieveCode("thenewcode")).thenReturn(thenewcode, null); + when(expiringCodeStore.generateCode(eq(codeDataString),any(), eq(INVITATION.name()))) + .thenReturn(thenewcode) + .thenReturn(thenewcode2); + when(invitationsService.acceptInvitation(anyString(), eq("passw0rd"))).thenReturn(new InvitationsService.AcceptedInvitation("/home", user)); mockMvc.perform(post) @@ -453,14 +521,17 @@ public void testAcceptInvite() throws Exception { } private MockHttpServletRequestBuilder startAcceptInviteFlow(String password) { + return startAcceptInviteFlow(password, password); + } + private MockHttpServletRequestBuilder startAcceptInviteFlow(String password, String confirmPassword) { UaaPrincipal uaaPrincipal = new UaaPrincipal("user-id-001", "user@example.com", "user@example.com", OriginKeys.UAA, null, IdentityZoneHolder.get().getId()); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(uaaPrincipal, null, UaaAuthority.USER_AUTHORITIES); SecurityContextHolder.getContext().setAuthentication(token); return post("/invitations/accept.do") .param("code","thecode") .param("password", password) - .param("password_confirmation", password); + .param("password_confirmation", confirmPassword); } @Test @@ -472,6 +543,10 @@ public void acceptInviteWithValidClientRedirect() throws Exception { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(uaaPrincipal, null, UaaAuthority.USER_AUTHORITIES); SecurityContextHolder.getContext().setAuthentication(token); + Map<String,String> codeData = getInvitationsCode(OriginKeys.UAA); + String codeDataString = JsonUtils.writeValueAsString(codeData); + when(expiringCodeStore.retrieveCode("thecode")).thenReturn(new ExpiringCode("thecode", new Timestamp(1), codeDataString, INVITATION.name()), null); + when(expiringCodeStore.generateCode(eq(codeDataString),any(), eq(INVITATION.name()))).thenReturn(new ExpiringCode("thenewcode", new Timestamp(1), codeDataString, INVITATION.name())); when(invitationsService.acceptInvitation(anyString(), eq("password"))).thenReturn(new InvitationsService.AcceptedInvitation("valid.redirect.com", user)); MockHttpServletRequestBuilder post = post("/invitations/accept.do") @@ -493,6 +568,11 @@ public void acceptInviteWithInvalidClientRedirect() throws Exception { ScimUser user = new ScimUser(uaaPrincipal.getId(), uaaPrincipal.getName(),"fname", "lname"); user.setPrimaryEmail(user.getUserName()); + Map<String,String> codeData = getInvitationsCode(OriginKeys.UAA); + String codeDataString = JsonUtils.writeValueAsString(codeData); + when(expiringCodeStore.retrieveCode("thecode")).thenReturn(new ExpiringCode("thecode", new Timestamp(1), codeDataString, INVITATION.name()), null); + when(expiringCodeStore.generateCode(eq(codeDataString),any(), eq(INVITATION.name()))).thenReturn(new ExpiringCode("thenewcode", new Timestamp(1), codeDataString, INVITATION.name())); + when(invitationsService.acceptInvitation(anyString(), eq("password"))).thenReturn(new InvitationsService.AcceptedInvitation("/home", user)); MockHttpServletRequestBuilder post = post("/invitations/accept.do") @@ -511,6 +591,11 @@ public void invalidCodeOnAcceptPost() throws Exception { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(uaaPrincipal, null, UaaAuthority.USER_AUTHORITIES); SecurityContextHolder.getContext().setAuthentication(token); + Map<String,String> codeData = getInvitationsCode(OriginKeys.UAA); + String codeDataString = JsonUtils.writeValueAsString(codeData); + when(expiringCodeStore.retrieveCode("thecode")).thenReturn(new ExpiringCode("thecode", new Timestamp(1), codeDataString, INVITATION.name()), null); + when(expiringCodeStore.generateCode(eq(codeDataString),any(), eq(INVITATION.name()))).thenReturn(new ExpiringCode("thenewcode", new Timestamp(1), codeDataString, INVITATION.name())); + doThrow(new HttpClientErrorException(BAD_REQUEST)).when(invitationsService).acceptInvitation(anyString(), anyString()); MockHttpServletRequestBuilder post = post("/invitations/accept.do") @@ -530,18 +615,31 @@ public void testAcceptInviteWithoutMatchingPasswords() throws Exception { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(uaaPrincipal, null, UaaAuthority.USER_AUTHORITIES); SecurityContextHolder.getContext().setAuthentication(token); + Map<String,String> codeData = getInvitationsCode(OriginKeys.UAA); + String codeDataString = JsonUtils.writeValueAsString(codeData); + when(expiringCodeStore.retrieveCode("thecode")).thenReturn(new ExpiringCode("thecode", new Timestamp(1), codeDataString, INVITATION.name()), null); + when(expiringCodeStore.retrieveCode("thenewcode")).thenReturn(new ExpiringCode("thenewcode", new Timestamp(1), codeDataString, INVITATION.name()), null); + when(expiringCodeStore.generateCode(eq(codeDataString),any(), eq(INVITATION.name()))).thenReturn( + new ExpiringCode("thenewcode", new Timestamp(1), codeDataString, INVITATION.name()), + new ExpiringCode("thenewcode2", new Timestamp(1), codeDataString, INVITATION.name()) + ); + + + IdentityProvider identityProvider = new IdentityProvider(); + identityProvider.setType(OriginKeys.UAA); + when(providerProvisioning.retrieveByOrigin("uaa", "uaa")).thenReturn(identityProvider); MockHttpServletRequestBuilder post = post("/invitations/accept.do") .param("code", "thecode") .param("password", "password") .param("password_confirmation", "does not match"); - mockMvc.perform(post) .andExpect(status().isUnprocessableEntity()) .andExpect(model().attribute("error_message_code", "form_error")) - .andExpect(model().attribute("email", "user@example.com")) + .andExpect(model().attribute("code", "thenewcode")) .andExpect(view().name("invitations/accept_invite")); - - verifyZeroInteractions(invitationsService); + verify(expiringCodeStore).retrieveCode("thecode"); + verify(expiringCodeStore, times(1)).generateCode(anyString(),anyObject(),anyString()); + verify(invitationsService, never()).acceptInvitation(anyString(), anyString()); }
uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/InvitationsIT.java+17 −0 modified@@ -43,6 +43,7 @@ import org.springframework.security.oauth2.provider.client.BaseClientDetails; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.web.client.DefaultResponseErrorHandler; import org.springframework.web.client.RestTemplate; import java.net.URL; @@ -117,6 +118,22 @@ public void logout_and_clear_cookies() { webDriver.manage().deleteAllCookies(); } + @Test + public void invite_fails() { + RestTemplate uaaTemplate = new RestTemplate(); + uaaTemplate.setErrorHandler(new DefaultResponseErrorHandler() { + @Override + protected boolean hasError(HttpStatus statusCode) { + return statusCode.is5xxServerError(); + } + }); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(APPLICATION_JSON); + HttpEntity<String> request = new HttpEntity<>("{\"emails\":[\"marissa@test.org\"]}", headers); + ResponseEntity<Void> response = uaaTemplate.exchange(uaaUrl + "/invite_users/?client_id=admin&redirect_uri={uri}", POST, request, Void.class, "https://www.google.com"); + assertThat(response.getStatusCode(), is(HttpStatus.UNAUTHORIZED)); + } + @Test public void testInviteUserWithClientRedirect() throws Exception { String userEmail = "user-" + new RandomValueStringGenerator().generate() + "@example.com";
uaa/src/test/java/org/cloudfoundry/identity/uaa/login/InvitationsServiceMockMvcTests.java+69 −0 modified@@ -52,15 +52,18 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; 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.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrlPattern; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; public class InvitationsServiceMockMvcTests extends InjectedMockContextTest { @@ -207,6 +210,72 @@ public void accept_invitation_for_verified_user_sends_redirect() throws Exceptio .andExpect(redirectedUrl(REDIRECT_URI)); } + @Test + public void accept_invitation_for_uaa_user_should_expire_invitelink() throws Exception { + String email = new RandomValueStringGenerator().generate().toLowerCase() + "@test.org"; + URL inviteLink = inviteUser(email, userInviteToken, null, clientId, OriginKeys.UAA); + assertEquals(OriginKeys.UAA, queryUserForField(email, OriginKeys.ORIGIN, String.class)); + + String code = extractInvitationCode(inviteLink.toString()); + MockHttpServletRequestBuilder get = get("/invitations/accept") + .param("code", code) + .accept(MediaType.TEXT_HTML); + getMockMvc().perform(get) + .andExpect(status().isOk()); + + getMockMvc().perform(get) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + public void invalid_code() throws Exception { + String email = new RandomValueStringGenerator().generate().toLowerCase()+"@test.org"; + String invalid = new RandomValueStringGenerator().generate().toLowerCase()+"@test.org"; + URL inviteLink = inviteUser(email, userInviteToken, null, clientId, OriginKeys.UAA); + URL invalidLink = inviteUser(invalid, userInviteToken, null, clientId, OriginKeys.UAA); + + assertFalse("User should not be verified", queryUserForField(email, "verified", Boolean.class)); + assertEquals(OriginKeys.UAA, queryUserForField(email, OriginKeys.ORIGIN, String.class)); + + String code = extractInvitationCode(inviteLink.toString()); + String invalidCode = extractInvitationCode(invalidLink.toString()); + + MvcResult result = getMockMvc().perform(get("/invitations/accept") + .param("code", code) + .accept(MediaType.TEXT_HTML) + ) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("Email: " + email))) + .andReturn(); + + MockHttpSession session = (MockHttpSession) result.getRequest().getSession(false); + result = getMockMvc().perform( + post("/invitations/accept.do") + .session(session) + .param("password", "s3cret") + .param("password_confirmation", "s3cret") + .param("code",invalidCode) + .with(csrf()) + ) + .andExpect(status().isUnprocessableEntity()) + .andExpect(model().attribute("error_message_code", "code_expired")) + .andExpect(view().name("invitations/accept_invite")) + .andReturn(); + + assertFalse("User should be not yet be verified", queryUserForField(email, "verified", Boolean.class)); + assertNull(session.getAttribute("SPRING_SECURITY_CONTEXT")); + + session = (MockHttpSession) result.getRequest().getSession(false); + //not logged in anymore + getMockMvc().perform( + get("/profile") + .session(session) + .accept(MediaType.TEXT_HTML) + ) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("http://localhost/login")); + } + @Test public void accept_invitation_sets_your_password() throws Exception { String email = new RandomValueStringGenerator().generate().toLowerCase()+"@test.org";
uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcTests.java+11 −1 modified@@ -13,6 +13,8 @@ package org.cloudfoundry.identity.uaa.login; 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; @@ -25,9 +27,9 @@ import org.cloudfoundry.identity.uaa.provider.IdentityProviderProvisioning; import org.cloudfoundry.identity.uaa.provider.LdapIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.provider.LockoutPolicy; +import org.cloudfoundry.identity.uaa.provider.OIDCIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.provider.SamlIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.provider.UaaIdentityProviderDefinition; -import org.cloudfoundry.identity.uaa.provider.OIDCIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.provider.saml.BootstrapSamlIdentityProviderConfiguratorTests; import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning; @@ -79,6 +81,7 @@ import java.lang.reflect.Field; import java.net.URL; import java.net.URLEncoder; +import java.sql.Timestamp; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -1583,6 +1586,13 @@ public void testCsrfForInvitationAcceptPost() throws Exception { inviteContext.setAuthentication(inviteToken); inviteSession.setAttribute("SPRING_SECURITY_CONTEXT", inviteContext); + Map<String, String> codeData = new HashMap(); + codeData.put("user_id", ((UaaPrincipal)marissaContext.getAuthentication().getPrincipal()).getId()); + codeData.put("email", ((UaaPrincipal)marissaContext.getAuthentication().getPrincipal()).getEmail()); + codeData.put("origin", OriginKeys.UAA); + + ExpiringCode code = getWebApplicationContext().getBean(ExpiringCodeStore.class).generateCode(JsonUtils.writeValueAsString(codeData), new Timestamp(System.currentTimeMillis() + 1000 * 60), null); + //logged in with valid CSRF MockHttpServletRequestBuilder post = post("/invitations/accept.do") .session(inviteSession)
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
8- github.com/advisories/GHSA-jcmh-x32v-7mgfghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2017-4992ghsaADVISORY
- www.cloudfoundry.org/cve-2017-4992/nvdVendor Advisory
- github.com/cloudfoundry/uaa/commit/1c9c6dd88266cfa7d333e5d8be1031fa31c5c939ghsaWEB
- github.com/cloudfoundry/uaa/commit/3ce42a4c75828cb58287c3c7495dde3f5261f12cghsaWEB
- github.com/cloudfoundry/uaa/commit/4f942064d85454a4bcc4da04cd482d114816c14aghsaWEB
- github.com/cloudfoundry/uaa/commit/96a294013c0c9a13ef32afc49d2b759f5107dc49ghsaWEB
- www.cloudfoundry.org/cve-2017-4992ghsaWEB
News mentions
0No linked articles in our index yet.