Skip to content

Commit

Permalink
Allow App Context Specific Invitations
Browse files Browse the repository at this point in the history
[#100343056] https://www.pivotaltracker.com/story/show/100343056

Signed-off-by: Jonathan Lo <jlo@us.ibm.com>
  • Loading branch information
mbhave authored and jlo committed Aug 28, 2015
1 parent a827e77 commit 0c6350c
Show file tree
Hide file tree
Showing 10 changed files with 274 additions and 92 deletions.
Expand Up @@ -14,7 +14,6 @@
import org.springframework.security.oauth2.common.util.RandomValueStringGenerator;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.stereotype.Service;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestClientException;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import org.thymeleaf.context.Context;
Expand Down Expand Up @@ -85,12 +84,13 @@ private String getEmailHtml(String currentUser, String code) {
}

@Override
public void inviteUser(String email, String currentUser) {
public void inviteUser(String email, String currentUser, String redirectUri) {
try {
ScimUser user = accountCreationService.createUser(email, new RandomValueStringGenerator().generate());
Map<String,String> data = new HashMap<>();
data.put("user_id", user.getId());
data.put("email", email);
data.put("redirect_uri", redirectUri);
String code = expiringCodeService.generateCode(data, INVITATION_EXPIRY_DAYS, TimeUnit.DAYS);
sendInvitationEmail(email, currentUser, code);
} catch (ScimResourceAlreadyExistsException e) {
Expand All @@ -102,6 +102,7 @@ public void inviteUser(String email, String currentUser) {
Map<String,String> data = new HashMap<>();
data.put("user_id", existingUserResponse.getUserId());
data.put("email", email);
data.put("redirect_uri", redirectUri);
String code = expiringCodeService.generateCode(data, INVITATION_EXPIRY_DAYS, TimeUnit.DAYS);
sendInvitationEmail(email, currentUser, code);
} catch (JsonUtils.JsonUtilException ioe) {
Expand Down
Expand Up @@ -42,21 +42,22 @@ public InvitationsController(InvitationsService invitationsService) {
}

@RequestMapping(value = "/new", method = GET)
public String newInvitePage(Model model) {
public String newInvitePage(Model model, @RequestParam(required = false, value = "redirect_uri") String redirectUri) {
model.addAttribute("redirect_uri", redirectUri);
return "invitations/new_invite";
}


@RequestMapping(value = "/new.do", method = POST, params = {"email"})
public String sendInvitationEmail(@Valid @ModelAttribute("email") ValidEmail email, BindingResult result, Model model, HttpServletResponse response) {
public String sendInvitationEmail(@Valid @ModelAttribute("email") ValidEmail email, BindingResult result, @RequestParam("redirect_uri") String redirectUri, Model model, HttpServletResponse response) {
if (result.hasErrors()) {
return handleUnprocessableEntity(model, response, "error_message_code", "invalid_email", "invitations/new_invite");
}

UaaPrincipal p = ((UaaPrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal());
String currentUser = p.getName();
try {
invitationsService.inviteUser(email.getEmail(), currentUser);
invitationsService.inviteUser(email.getEmail(), currentUser, redirectUri);
} catch (UaaException e) {
return handleUnprocessableEntity(model, response, "error_message_code", "existing_user", "invitations/new_invite");
}
Expand All @@ -75,8 +76,8 @@ public String acceptInvitePage(@RequestParam String code, Model model, HttpServl
UaaPrincipal uaaPrincipal = new UaaPrincipal(codeData.get("user_id"), codeData.get("email"), codeData.get("email"), Origin.UAA, null, IdentityZoneHolder.get().getId());
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(uaaPrincipal, null, UaaAuthority.USER_AUTHORITIES);
SecurityContextHolder.getContext().setAuthentication(token);
model.addAllAttributes(codeData);
return "invitations/accept_invite";
model.addAllAttributes(codeData);
return "invitations/accept_invite";
} catch (CodeNotFoundException e) {
return handleUnprocessableEntity(model, response, "error_message_code", "code_expired", "invitations/accept_invite");
}
Expand All @@ -86,6 +87,7 @@ public String acceptInvitePage(@RequestParam String code, Model model, HttpServl
public String acceptInvitation(@RequestParam("password") String password,
@RequestParam("password_confirmation") String passwordConfirmation,
@RequestParam("client_id") String clientId,
@RequestParam("redirect_uri") String redirectUri,
Model model, HttpServletResponse servletResponse) throws IOException {

PasswordConfirmationValidation validation = new PasswordConfirmationValidation(password, passwordConfirmation);
Expand All @@ -104,6 +106,9 @@ public String acceptInvitation(@RequestParam("password") String password,
}
String redirectLocation = invitationsService.acceptInvitation(principal.getId(), principal.getEmail(), password, clientId);

if (!redirectUri.equals("")) {
return "redirect:" + redirectUri;
}
if (redirectLocation != null) {
return "redirect:" + redirectLocation;
}
Expand Down
@@ -1,7 +1,7 @@
package org.cloudfoundry.identity.uaa.login;

public interface InvitationsService {
void inviteUser(String email, String currentUser);
void inviteUser(String email, String currentUser, String redirectUri);

String acceptInvitation(String userId, String email, String password, String clientId);
}
Expand Up @@ -23,6 +23,7 @@ <h1>Create your <th:block th:text="${pivotal and isUaa ? 'Pivotal ID' : 'account
<input name="client_id" type="hidden" value="login" th:value="${client_id}"/>
<input name="password" type="password" placeholder="Password" autocomplete="off" class="form-control"/>
<input name="password_confirmation" type="password" placeholder="Confirm" autocomplete="off" class="form-control"/>
<input name="redirect_uri" type="hidden" th:value="${redirect_uri}"/>
<input type="submit" th:value="${pivotal and isUaa ? 'Create Pivotal ID' : 'Create account'}" class="island-button"/>
</form>
</div>
Expand Down
Expand Up @@ -19,6 +19,7 @@ <h1>Send an invite</h1>
<p th:text="#{'new_invite.' + ${error_message_code}}">Error Message</p>
</div>
<input name="email" type="email" placeholder="Enter email" autofocus="autofocus" class="form-control"/>
<input name="redirect_uri" type="hidden" th:value="${redirect_uri}"/>
<input type="submit" value="Send invite" class="island-button"/>
</form>
</div>
Expand Down
Expand Up @@ -102,10 +102,11 @@ public void testSendInviteEmail() throws Exception {
ArgumentCaptor<Map<String,String>> captor = ArgumentCaptor.forClass((Class)Map.class);

when(expiringCodeService.generateCode(captor.capture(), anyInt(), eq(TimeUnit.DAYS))).thenReturn("the_secret_code");
emailInvitationsService.inviteUser("user@example.com", "current-user");
emailInvitationsService.inviteUser("user@example.com", "current-user", "blah.example.com");

Map<String,String> data = captor.getValue();
assertEquals("existing-user-id", data.get("user_id"));
assertEquals("blah.example.com", data.get("redirect_uri"));

ArgumentCaptor<String> emailBodyArgument = ArgumentCaptor.forClass(String.class);
Mockito.verify(messageService).sendMessage(
Expand All @@ -127,7 +128,7 @@ public void testSendInviteEmailToUserThatIsAlreadyVerified() throws Exception {
request.setProtocol("http");
request.setContextPath("/login");

emailInvitationsService.inviteUser("alreadyverified@example.com", "current-user");
emailInvitationsService.inviteUser("alreadyverified@example.com", "current-user", "");
}

@Test
Expand All @@ -143,10 +144,11 @@ public void testSendInviteEmailToUnverifiedUser() throws Exception {
ArgumentCaptor<Map<String,String>> captor = ArgumentCaptor.forClass((Class)Map.class);

when(expiringCodeService.generateCode(captor.capture(), anyInt(), eq(TimeUnit.DAYS))).thenReturn("the_secret_code");
emailInvitationsService.inviteUser("existingunverified@example.com", "current-user");
emailInvitationsService.inviteUser("existingunverified@example.com", "current-user", "blah.example.com");

Map<String,String> data = captor.getValue();
assertEquals("existing-user-id", data.get("user_id"));
assertEquals("blah.example.com", data.get("redirect_uri"));

ArgumentCaptor<String> emailBodyArgument = ArgumentCaptor.forClass(String.class);
Mockito.verify(messageService).sendMessage(
Expand All @@ -173,7 +175,7 @@ public void testSendInviteEmailWithOSSBrand() throws Exception {
ArgumentCaptor<Map<String,String>> captor = ArgumentCaptor.forClass((Class)Map.class);

when(expiringCodeService.generateCode(captor.capture(), anyInt(), eq(TimeUnit.DAYS))).thenReturn("the_secret_code");
emailInvitationsService.inviteUser("user@example.com", "current-user");
emailInvitationsService.inviteUser("user@example.com", "current-user", "");

Map<String,String> data = captor.getValue();
assertEquals("existing-user-id", data.get("user_id"));
Expand Down
Expand Up @@ -8,6 +8,7 @@
import org.cloudfoundry.identity.uaa.scim.exception.InvalidPasswordException;
import org.cloudfoundry.identity.uaa.scim.validate.PasswordValidator;
import org.cloudfoundry.identity.uaa.user.UaaAuthority;
import org.cloudfoundry.identity.uaa.util.SetServerNameRequestPostProcessor;
import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder;
import org.junit.After;
import org.junit.Before;
Expand All @@ -18,6 +19,7 @@
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
Expand All @@ -37,9 +39,12 @@
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import static com.google.common.collect.Lists.newArrayList;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
Expand All @@ -50,8 +55,10 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
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.status;
Expand Down Expand Up @@ -113,14 +120,50 @@ public void testSendInvitationEmail() throws Exception {
);

MockHttpServletRequestBuilder post = post("/invitations/new.do")
.param("email", "user1@example.com");
.param("email", "user1@example.com")
.param("redirect_uri", "");

mockMvc.perform(post)
.andExpect(status().isFound())
.andExpect(redirectedUrl("sent"));
verify(invitationsService).inviteUser("user1@example.com", "marissa");
verify(invitationsService).inviteUser("user1@example.com", "marissa", "");
}

@Test
public void newInvitePageWithRedirectUri() throws Exception {
MockHttpServletRequestBuilder get = get("/invitations/new?redirect_uri=blah.example.com");

mockMvc.perform(get)
.andExpect(model().attribute("redirect_uri", "blah.example.com"))
.andExpect(status().isOk())
.andExpect(view().name("invitations/new_invite"))
.andExpect(xpath("//*[@type='hidden' and @value='blah.example.com']").exists());
}

@Test
public void sendInvitationWithRedirectUri() throws Exception {
UaaPrincipal p = new UaaPrincipal("123","marissa","marissa@test.org", Origin.UAA,"", IdentityZoneHolder.get().getId());
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(p, "", UaaAuthority.USER_AUTHORITIES);
assertTrue(auth.isAuthenticated());
MockSecurityContext mockSecurityContext = new MockSecurityContext(auth);
SecurityContextHolder.setContext(mockSecurityContext);
MockHttpSession session = new MockHttpSession();
session.setAttribute(
HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY,
mockSecurityContext
);

MockHttpServletRequestBuilder post = post("/invitations/new.do")
.param("email", "user1@example.com")
.param("redirect_uri", "blah.example.com");

mockMvc.perform(post)
.andExpect(status().isFound())
.andExpect(redirectedUrl("sent"));
verify(invitationsService).inviteUser("user1@example.com", "marissa", "blah.example.com");
}


@Test
public void testSendInvitationEmailToExistingVerifiedUser() throws Exception {
UaaPrincipal p = new UaaPrincipal("123","marissa","marissa@test.org", Origin.UAA,"", IdentityZoneHolder.get().getId());
Expand All @@ -135,9 +178,10 @@ public void testSendInvitationEmailToExistingVerifiedUser() throws Exception {
);

MockHttpServletRequestBuilder post = post("/invitations/new.do")
.param("email", "user1@example.com");
.param("email", "user1@example.com")
.param("redirect_uri", "");

doThrow(new UaaException("",409)).when(invitationsService).inviteUser("user1@example.com", "marissa");
doThrow(new UaaException("",409)).when(invitationsService).inviteUser("user1@example.com", "marissa", "");
mockMvc.perform(post)
.andExpect(status().isUnprocessableEntity())
.andExpect(view().name("invitations/new_invite"))
Expand All @@ -158,7 +202,8 @@ public void testSendInvitationWithInvalidEmail() throws Exception {
);

MockHttpServletRequestBuilder post = post("/invitations/new.do")
.param("email", "not_a_real_email");
.param("email", "not_a_real_email")
.param("redirect_uri", "");

mockMvc.perform(post)
.andExpect(status().isUnprocessableEntity())
Expand All @@ -173,6 +218,7 @@ public void testAcceptInvitationsPage() throws Exception {
Map<String,String> codeData = new HashMap<>();
codeData.put("user_id", "user-id-001");
codeData.put("email", "user@example.com");
codeData.put("redirect_uri", "blah.test.com");
when(expiringCodeService.verifyCode("the_secret_code")).thenReturn(codeData);
MockHttpServletRequestBuilder get = get("/invitations/accept")
.param("code", "the_secret_code");
Expand All @@ -181,7 +227,8 @@ public void testAcceptInvitationsPage() throws Exception {
.andExpect(status().isOk())
.andExpect(model().attribute("user_id", "user-id-001"))
.andExpect(model().attribute("email", "user@example.com"))
.andExpect(view().name("invitations/accept_invite"));
.andExpect(view().name("invitations/accept_invite"))
.andExpect(xpath("//*[@type='hidden' and @value='blah.test.com']").exists());
UaaPrincipal principal = ((UaaPrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal());
assertEquals("user-id-001", principal.getId());
assertEquals("user@example.com", principal.getName());
Expand All @@ -208,9 +255,9 @@ public void testAcceptInviteWithContraveningPassword() throws Exception {
MockHttpServletRequestBuilder post = startAcceptInviteFlow("a");

mockMvc.perform(post)
.andExpect(status().isUnprocessableEntity())
.andExpect(model().attribute("error_message", "Msg 1c Msg 2c"))
.andExpect(view().name("invitations/accept_invite"));
.andExpect(status().isUnprocessableEntity())
.andExpect(model().attribute("error_message", "Msg 1c Msg 2c"))
.andExpect(view().name("invitations/accept_invite"));
verify(invitationsService, never()).acceptInvitation(anyString(), anyString(), anyString(), anyString());
}

Expand All @@ -233,11 +280,12 @@ public MockHttpServletRequestBuilder startAcceptInviteFlow(String password) {
return post("/invitations/accept.do")
.param("password", password)
.param("password_confirmation", password)
.param("client_id", "");
.param("client_id", "")
.param("redirect_uri", "");
}

@Test
public void testAcceptInviteWithClientRedirect() throws Exception {
public void testAcceptInviteWithGlobalClientRedirect() 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);
Expand All @@ -247,13 +295,33 @@ public void testAcceptInviteWithClientRedirect() throws Exception {
MockHttpServletRequestBuilder post = post("/invitations/accept.do")
.param("password", "password")
.param("password_confirmation", "password")
.param("client_id", "app");
.param("client_id", "app")
.param("redirect_uri", "");

mockMvc.perform(post)
.andExpect(status().isFound())
.andExpect(redirectedUrl("http://localhost:8080/app"));
}

@Test
public void acceptInviteWithAppSpecificClientRedirect() 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);

when(invitationsService.acceptInvitation("user-id-001", "user@example.com", "password", "app")).thenReturn("http://localhost:8080/app");

MockHttpServletRequestBuilder post = post("/invitations/accept.do")
.param("password", "password")
.param("password_confirmation", "password")
.param("client_id", "app")
.param("redirect_uri", "blah.test.com");

mockMvc.perform(post)
.andExpect(status().isFound())
.andExpect(redirectedUrl("blah.test.com"));
}

@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());
Expand All @@ -263,7 +331,8 @@ public void testAcceptInviteWithoutMatchingPasswords() throws Exception {
MockHttpServletRequestBuilder post = post("/invitations/accept.do")
.param("password", "password")
.param("password_confirmation", "does not match")
.param("client_id", "");
.param("client_id", "")
.param("redirect_uri", "");

mockMvc.perform(post)

This comment has been minimized.

Copy link
@fhanik

fhanik Aug 31, 2015

Contributor

this test should not have a redirect_uri param instead run it as it was before...backwards compatibility.
You can do

@RequestParam(value = "redirect_uri", defaultValue = "")
.andExpect(status().isUnprocessableEntity())
Expand Down

0 comments on commit 0c6350c

Please sign in to comment.