/*
* *****************************************************************************
* Cloud Foundry
* Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved.
* This product is licensed to you under the Apache License, Version 2.0 (the "License").
* You may not use this product except in compliance with the License.
*
* This product includes a number of subcomponents with
* separate copyright notices and license terms. Your use of these
* subcomponents is subject to the terms and conditions of the
* subcomponent's license, as noted in the LICENSE file.
* *****************************************************************************
*/
package org.cloudfoundry.identity.uaa.login;
import org.cloudfoundry.identity.uaa.constants.OriginKeys;
import org.cloudfoundry.identity.uaa.message.EmailService;
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.resources.jdbc.LimitSqlAdapterFactory;
import org.cloudfoundry.identity.uaa.resources.jdbc.SQLServerLimitSqlAdapter;
import org.cloudfoundry.identity.uaa.zone.IdentityZone;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.http.MediaType;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.security.oauth2.common.util.OAuth2Utils;
import org.springframework.security.oauth2.common.util.RandomValueStringGenerator;
import org.springframework.security.oauth2.provider.client.BaseClientDetails;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.utils;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.endsWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
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;
public class InvitationsServiceMockMvcTests extends InjectedMockContextTest {
public static final String REDIRECT_URI = "http://invitation.redirect.test";
private JavaMailSender originalSender;
private FakeJavaMailSender fakeJavaMailSender = new FakeJavaMailSender();
private MockMvcUtils utils = MockMvcUtils.utils();
private RandomValueStringGenerator generator = new RandomValueStringGenerator();
private String clientId;
private String clientSecret;
private String adminToken;
private String authorities;
private String userInviteToken;
public ZoneScimInviteData createZoneForInvites() throws Exception {
return utils().createZoneForInvites(getMockMvc(), getWebApplicationContext(), clientId, REDIRECT_URI);
}
@Before
public void setUp() throws Exception {
adminToken = MockMvcUtils.utils().getClientCredentialsOAuthAccessToken(getMockMvc(), "admin", "adminsecret", "clients.admin clients.read clients.write clients.secret scim.read scim.write", null);
clientId = generator.generate().toLowerCase();
clientSecret = generator.generate().toLowerCase();
authorities = "scim.read,scim.invite";
MockMvcUtils.utils().createClient(this.getMockMvc(), adminToken, clientId, clientSecret, Collections.singleton("oauth"), Arrays.asList("scim.read","scim.invite"), Arrays.asList(new String[]{"client_credentials", "password"}), authorities, Collections.singleton(REDIRECT_URI), IdentityZone.getUaa());
userInviteToken = MockMvcUtils.utils().getScimInviteUserToken(getMockMvc(), clientId, clientSecret, null);
getWebApplicationContext().getBean(JdbcTemplate.class).update("delete from expiring_code_store");
}
@Before
public void setUpFakeMailServer() throws Exception {
originalSender = getWebApplicationContext().getBean("emailService", EmailService.class).getMailSender();
getWebApplicationContext().getBean("emailService", EmailService.class).setMailSender(fakeJavaMailSender);
}
@After
public void restoreMailServer() throws Exception {
getWebApplicationContext().getBean("emailService", EmailService.class).setMailSender(originalSender);
}
@Before
@After
public void clearOutCodeTable() throws Exception {
getWebApplicationContext().getBean(JdbcTemplate.class).update("DELETE FROM expiring_code_store");
fakeJavaMailSender.clearMessage();
}
@Test
public void inviteUser_Correct_Origin_Set() throws Exception {
String email = new RandomValueStringGenerator().generate().toLowerCase()+"@test.org";
inviteUser(email, userInviteToken, null, clientId, OriginKeys.UAA);
}
protected <T> T queryUserForField(String email, String field, Class<T> type) throws Exception {
return getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("SELECT "+field+" FROM users WHERE email=?",type, email);
}
@Test
public void test_authorize_with_invitation_login() throws Exception {
String email = new RandomValueStringGenerator().generate().toLowerCase()+"@test.org";
URL inviteLink = inviteUser(email, userInviteToken, null, clientId, OriginKeys.UAA);
assertEquals(OriginKeys.UAA, getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("select origin from users where username=?", new Object[]{email}, String.class));
String code = extractInvitationCode(inviteLink.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 inviteSession = (MockHttpSession) result.getRequest().getSession(false);
assertNotNull(inviteSession);
assertNotNull(inviteSession.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY));
String redirectUri = "https://example.com/dashboard/?appGuid=app-guid";
String clientId = "authclient-"+new RandomValueStringGenerator().generate();
BaseClientDetails client = new BaseClientDetails(clientId, "", "openid","authorization_code","",redirectUri);
client.setClientSecret("secret");
String adminToken = utils().getClientCredentialsOAuthAccessToken(getMockMvc(), "admin", "adminsecret", "", null);
MockMvcUtils.utils().createClient(getMockMvc(), adminToken, client);
String state = new RandomValueStringGenerator().generate();
MockHttpServletRequestBuilder authRequest = get("/oauth/authorize")
.session(inviteSession)
.param(OAuth2Utils.RESPONSE_TYPE, "code")
.param(OAuth2Utils.SCOPE, "openid")
.param(OAuth2Utils.STATE, state)
.param(OAuth2Utils.CLIENT_ID, clientId)
.param(OAuth2Utils.REDIRECT_URI, redirectUri);
result = getMockMvc()
.perform(authRequest)
.andExpect(status().is3xxRedirection())
.andReturn();
String location = result.getResponse().getHeader("Location");
assertThat(location, endsWith("/login"));
assertEquals(-1, location.indexOf("code"));
}
@Test
public void accept_invitation_should_not_log_you_in() throws Exception {
String email = new RandomValueStringGenerator().generate().toLowerCase()+"@test.org";
URL inviteLink = inviteUser(email, userInviteToken, null, clientId, OriginKeys.UAA);
assertEquals(OriginKeys.UAA, getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("select origin from users where username=?", new Object[]{email}, String.class));
String code = extractInvitationCode(inviteLink.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);
getMockMvc().perform(
get("/profile")
.session(session)
.accept(MediaType.TEXT_HTML)
)
.andExpect(status().isFound())
.andExpect(redirectedUrlPattern("**/login"));
}
@Test
public void accept_invitation_for_verified_user_sends_redirect() throws Exception {
String email = new RandomValueStringGenerator().generate().toLowerCase() + "@test.org";
URL inviteLink = inviteUser(email, userInviteToken, null, clientId, OriginKeys.UAA);
String dbTrueString = LimitSqlAdapterFactory.getLimitSqlAdapter().getClass().equals(SQLServerLimitSqlAdapter.class) ? "1" : "true";
getWebApplicationContext().getBean(JdbcTemplate.class).update("UPDATE users SET verified="+dbTrueString+" WHERE email=?",email);
assertTrue("User should not be verified", queryUserForField(email, "verified", Boolean.class));
assertEquals(OriginKeys.UAA, queryUserForField(email, OriginKeys.ORIGIN, String.class));
String code = extractInvitationCode(inviteLink.toString());
getMockMvc().perform(
get("/invitations/accept")
.param("code", code)
.accept(MediaType.TEXT_HTML)
)
.andExpect(status().isFound())
.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 accept_invitation_sets_your_password() throws Exception {
String email = new RandomValueStringGenerator().generate().toLowerCase()+"@test.org";
URL inviteLink = inviteUser(email, 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());
MvcResult result = getMockMvc().perform(get("/invitations/accept")
.param("code", code)
.accept(MediaType.TEXT_HTML)
)
.andExpect(status().isOk())
.andExpect(content().string(containsString("Email: " + email)))
.andReturn();
code = getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("select code from expiring_code_store", String.class);
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",code)
.with(csrf())
)
.andExpect(status().isFound())
.andExpect(redirectedUrl(REDIRECT_URI))
.andReturn();
assertTrue("User should be verified after password reset", queryUserForField(email, "verified", Boolean.class));
session = (MockHttpSession) result.getRequest().getSession(false);
getMockMvc().perform(
get("/profile")
.session(session)
.accept(MediaType.TEXT_HTML)
)
.andExpect(status().isOk());
}
@Test
public void invite_ldap_users_verifies_and_redirects() throws Exception {
ZoneScimInviteData zone = createZoneForInvites();
LdapIdentityProviderDefinition definition = LdapIdentityProviderDefinition.searchAndBindMapGroupToScopes("", "", "", "", "", "", "", "", "", false, false, false, 1, true);
String domain = generator.generate().toLowerCase()+".com";
definition.setEmailDomain(Arrays.asList(domain));
IdentityProvider provider = createIdentityProvider(zone.getZone(), OriginKeys.LDAP, definition);
String email = new RandomValueStringGenerator().generate().toLowerCase()+"@"+domain;
URL inviteLink = inviteUser(email, zone.getAdminToken(), zone.getZone().getIdentityZone().getSubdomain(), zone.getScimInviteClient().getClientId(), provider.getOriginKey());
String code = extractInvitationCode(inviteLink.toString());
assertFalse("User should not be verified", queryUserForField(email, "verified", Boolean.class));
assertEquals(OriginKeys.LDAP, queryUserForField(email, OriginKeys.ORIGIN, String.class));
ResultActions actions = getMockMvc().perform(get("/invitations/accept")
.param("code", code)
.accept(MediaType.TEXT_HTML)
.header("Host", zone.getZone().getIdentityZone().getSubdomain() + ".localhost")
);
actions
.andExpect(status().isOk())
.andExpect(content().string(containsString("Email: "+email)));
assertFalse("LDAP user should not be verified after accepting invite until logging in", queryUserForField(email, "verified", Boolean.class));
}
@Test
public void invite_saml_user_will_redirect_upon_accept() throws Exception {
ZoneScimInviteData zone = createZoneForInvites();
String entityID = generator.generate();
String originKey = "invite1-"+generator.generate().toLowerCase();
String domain = generator.generate().toLowerCase()+".com";
SamlIdentityProviderDefinition definition = getSamlIdentityProviderDefinition(zone.getZone(), entityID);
definition.setEmailDomain(Arrays.asList(domain));
definition.setIdpEntityAlias(originKey);
IdentityProvider provider = createIdentityProvider(zone.getZone(), originKey, definition);
String email = new RandomValueStringGenerator().generate().toLowerCase()+"@"+domain;
URL inviteLink = inviteUser(email,zone.getAdminToken(), zone.getZone().getIdentityZone().getSubdomain(), zone.getScimInviteClient().getClientId(), provider.getOriginKey());
String code = extractInvitationCode(inviteLink.toString());
assertFalse("User should not be verified", queryUserForField(email, "verified", Boolean.class));
assertEquals(originKey, queryUserForField(email, OriginKeys.ORIGIN, String.class));
//should redirect to saml provider
getMockMvc().perform(
get("/invitations/accept")
.param("code", code)
.accept(MediaType.TEXT_HTML)
.header("Host", zone.getZone().getIdentityZone().getSubdomain() + ".localhost")
)
.andExpect(status().is3xxRedirection())
.andExpect(
redirectedUrl(
String.format("/saml/discovery?returnIDParam=idp&entityID=%s.cloudfoundry-saml-login&idp=%s&isPassive=true",
zone.getZone().getIdentityZone().getId(),
originKey)
)
);
assertEquals(provider.getOriginKey(), queryUserForField(email, OriginKeys.ORIGIN, String.class));
assertFalse("Saml user should not yet be verified after clicking on the accept link", queryUserForField(email, "verified", Boolean.class));
}
protected IdentityProvider createIdentityProvider(IdentityZoneCreationResult zone, String nameAndOriginKey, AbstractIdentityProviderDefinition definition) throws Exception {
return utils().createIdentityProvider(getMockMvc(), zone, nameAndOriginKey, definition);
}
protected SamlIdentityProviderDefinition getSamlIdentityProviderDefinition(IdentityZoneCreationResult zone, String entityID) {
return new SamlIdentityProviderDefinition()
.setMetaDataLocation(String.format(utils.IDP_META_DATA, entityID))
.setIdpEntityAlias(entityID)
.setNameID("urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress")
.setLinkText("Test Saml Provider")
.setZoneId(zone.getIdentityZone().getId());
}
public URL inviteUser(String email, String userInviteToken, String subdomain, String clientId, String expectedOrigin) throws Exception {
return utils().inviteUser(getWebApplicationContext(), getMockMvc(), email, userInviteToken, subdomain, clientId, expectedOrigin,REDIRECT_URI);
}
private String extractInvitationCode(String inviteLink) throws Exception {
return utils().extractInvitationCode(inviteLink);
}
}