VYPR
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.

PackageAffected versionsPatched versions
org.cloudfoundry.identity:cloudfoundry-identity-serverMaven
>= 2.0.0, < 2.7.4.172.7.4.17
org.cloudfoundry.identity:cloudfoundry-identity-serverMaven
>= 3.0.0, < 3.6.113.6.11
org.cloudfoundry.identity:cloudfoundry-identity-serverMaven
>= 3.7.0, < 3.9.133.9.13
org.cloudfoundry.identity:cloudfoundry-identity-serverMaven
>= 3.10.0, < 4.2.04.2.0

Affected products

73
  • cpe:2.3:a:cloudfoundry:cf-release:*:*:*:*:*:*:*:*
    Range: <=260
  • cpe: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

4
96a294013c0c

Improve invitations

https://github.com/cloudfoundry/uaaFilip HanikMay 17, 2017via ghsa
7 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();
    
3ce42a4c7582

Improve invitations

https://github.com/cloudfoundry/uaaFilip HanikMay 17, 2017via ghsa
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)
    
4f942064d854

Improve invitations

https://github.com/cloudfoundry/uaaFilip HanikMay 17, 2017via ghsa
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")
    
1c9c6dd88266

Improve invitations

https://github.com/cloudfoundry/uaaFilip HanikMay 17, 2017via ghsa
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

News mentions

0

No linked articles in our index yet.