Skip to content

Commit

Permalink
KEYCLOAK-7970-KEYCLOAK-7222 Add clientId to action tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
martin-kanis authored and hmlnarik committed Aug 20, 2018
1 parent c5f861a commit d047912
Show file tree
Hide file tree
Showing 12 changed files with 230 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,11 @@ public class IdpVerifyAccountLinkActionToken extends DefaultActionToken {


public IdpVerifyAccountLinkActionToken(String userId, int absoluteExpirationInSecs, String compoundAuthenticationSessionId,
String identityProviderUsername, String identityProviderAlias) {
String identityProviderUsername, String identityProviderAlias, String clientId) {
super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, compoundAuthenticationSessionId);
this.identityProviderUsername = identityProviderUsername;
this.identityProviderAlias = identityProviderAlias;
this.issuedFor = clientId;
}

private IdpVerifyAccountLinkActionToken() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ public class ResetCredentialsActionToken extends DefaultActionToken {

public static final String TOKEN_TYPE = "reset-credentials";

public ResetCredentialsActionToken(String userId, int absoluteExpirationInSecs, String compoundAuthenticationSessionId) {
public ResetCredentialsActionToken(String userId, int absoluteExpirationInSecs, String compoundAuthenticationSessionId, String clientId) {
super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, compoundAuthenticationSessionId);
this.issuedFor = clientId;
}

private ResetCredentialsActionToken() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ public class VerifyEmailActionToken extends DefaultActionToken {
@JsonProperty(value = JSON_FIELD_ORIGINAL_AUTHENTICATION_SESSION_ID)
private String originalAuthenticationSessionId;

public VerifyEmailActionToken(String userId, int absoluteExpirationInSecs, String compoundAuthenticationSessionId, String email) {
public VerifyEmailActionToken(String userId, int absoluteExpirationInSecs, String compoundAuthenticationSessionId, String email, String clientId) {
super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, compoundAuthenticationSessionId);
this.email = email;
this.issuedFor = clientId;
}

private VerifyEmailActionToken() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ private void sendVerifyEmail(KeycloakSession session, AuthenticationFlowContext
String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId();
IdpVerifyAccountLinkActionToken token = new IdpVerifyAccountLinkActionToken(
existingUser.getId(), absoluteExpirationInSecs, authSessionEncodedId,
brokerContext.getUsername(), brokerContext.getIdpConfig().getAlias()
brokerContext.getUsername(), brokerContext.getIdpConfig().getAlias(), authSession.getClient().getClientId()
);
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo),
authSession.getClient().getClientId(), authSession.getTabId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public void authenticate(AuthenticationFlowContext context) {

// We send the secret in the email in a link as a query param.
String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authenticationSession).getEncodedId();
ResetCredentialsActionToken token = new ResetCredentialsActionToken(user.getId(), absoluteExpirationInSecs, authSessionEncodedId);
ResetCredentialsActionToken token = new ResetCredentialsActionToken(user.getId(), absoluteExpirationInSecs, authSessionEncodedId, authenticationSession.getClient().getClientId());
String link = UriBuilder
.fromUri(context.getActionTokenUrl(token.serialize(context.getSession(), context.getRealm(), context.getUriInfo())))
.build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ private Response sendVerifyEmail(KeycloakSession session, LoginFormsProvider for
int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;

String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId();
VerifyEmailActionToken token = new VerifyEmailActionToken(user.getId(), absoluteExpirationInSecs, authSessionEncodedId, user.getEmail());
VerifyEmailActionToken token = new VerifyEmailActionToken(user.getId(), absoluteExpirationInSecs, authSessionEncodedId, user.getEmail(), authSession.getClient().getClientId());
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo),
authSession.getClient().getClientId(), authSession.getTabId());
String link = builder.build(realm.getName()).toString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.keycloak.testsuite.actions;

import org.jboss.arquillian.drone.api.annotation.Drone;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.authentication.actiontoken.verifyemail.VerifyEmailActionToken;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Assert;
Expand All @@ -29,6 +30,7 @@
import org.keycloak.events.EventType;
import org.keycloak.models.Constants;
import org.keycloak.models.UserModel.RequiredAction;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
Expand All @@ -43,9 +45,11 @@
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.RegisterPage;
import org.keycloak.testsuite.pages.VerifyEmailPage;
import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
import org.keycloak.testsuite.updaters.UserAttributeUpdater;
import org.keycloak.testsuite.util.GreenMailRule;
import org.keycloak.testsuite.util.MailUtils;
import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.SecondBrowser;
import org.keycloak.testsuite.util.UserActionTokenBuilder;
import org.keycloak.testsuite.util.UserBuilder;
Expand Down Expand Up @@ -384,9 +388,7 @@ public void verifyEmailNewBrowserSession() throws IOException, MessagingExceptio
events.expectRequiredAction(EventType.VERIFY_EMAIL)
.user(testUserId)
.detail(Details.CODE_ID, Matchers.not(Matchers.is(mailCodeId)))
.client(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID) // as authentication sessions are browser-specific,
// the client and redirect_uri is unrelated to
// the "test-app" specified in loginPage.open()
.client(oauth.getClientId()) // the "test-app" client specified in loginPage.open() is expected
.detail(Details.REDIRECT_URI, Matchers.any(String.class))
.assertEvent();

Expand Down Expand Up @@ -629,6 +631,39 @@ public void verifyEmailNewBrowserSessionWithClientRedirect() throws IOException,
}
}

@Test
public void verifyEmailNewBrowserSessionPreserveClient() throws IOException, MessagingException {
loginPage.open();
loginPage.login("test-user@localhost", "password");

verifyEmailPage.assertCurrent();

Assert.assertEquals(1, greenMail.getReceivedMessages().length);

MimeMessage message = greenMail.getLastReceivedMessage();

String verificationUrl = getPasswordResetEmailLink(message);

// open link in the second browser without the session
driver2.navigate().to(verificationUrl.trim());

// follow the link
final WebElement proceedLink = driver2.findElement(By.linkText("» Click here to proceed"));
assertThat(proceedLink, Matchers.notNullValue());

// check if the initial client is preserved
String link = proceedLink.getAttribute("href");
assertThat(link, Matchers.containsString("client_id=test-app"));
proceedLink.click();

// confirmation in the second browser
assertThat(driver2.getPageSource(), Matchers.containsString("kc-info-message"));
assertThat(driver2.getPageSource(), Matchers.containsString("Your email address has been verified."));

final WebElement backToApplicationLink = driver2.findElement(By.linkText("« Back to Application"));
assertThat(backToApplicationLink, Matchers.notNullValue());
}

@Test
public void verifyEmailDuringAuthFlow() throws IOException, MessagingException {
try (Closeable u = new UserAttributeUpdater(testRealm().users().get(testUserId))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@

import java.util.List;

import org.hamcrest.Matchers;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.After;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
Expand All @@ -36,6 +38,7 @@
import org.keycloak.testsuite.pages.UpdateAccountInformationPage;
import org.openqa.selenium.TimeoutException;

import static org.junit.Assert.assertThat;
import static org.keycloak.testsuite.broker.BrokerTestTools.encodeUrl;
import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage;

Expand Down Expand Up @@ -142,6 +145,22 @@ protected String getAccountPasswordUrl(String realmName) {
return BrokerTestTools.getAuthRoot(suiteContext) + "/auth/realms/" + realmName + "/account/password";
}

/**
* Get the login page for an existing client in provided realm
* @param realmName Name of the realm
* @param clientId ClientId of a client. Client has to exists in the realm.
* @return Login URL
*/
protected String getLoginUrl(String realmName, String clientId) {
List<ClientRepresentation> clients = adminClient.realm(realmName).clients().findByClientId(clientId);

assertThat(clients, Matchers.is(Matchers.not(Matchers.empty())));

String redirectURI = clients.get(0).getBaseUrl();

return BrokerTestTools.getAuthRoot(suiteContext) + "/auth/realms/" + realmName + "/protocol/openid-connect/auth?client_id=" +
clientId + "&redirect_uri=" + redirectURI + "&response_type=code&scope=openid";
}

protected void logoutFromRealm(String realm) {
driver.navigate().to(BrokerTestTools.getAuthRoot(suiteContext)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package org.keycloak.testsuite.broker;

import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers;
import org.jboss.arquillian.drone.api.annotation.Drone;
import org.junit.Before;
import org.junit.Test;

Expand All @@ -16,6 +19,7 @@
import org.keycloak.testsuite.pages.ConsentPage;
import org.keycloak.testsuite.util.*;

import org.openqa.selenium.By;
import org.openqa.selenium.TimeoutException;

import java.util.Collections;
Expand All @@ -27,11 +31,14 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.keycloak.testsuite.admin.ApiUtil.createUserWithAdminClient;
import static org.keycloak.testsuite.admin.ApiUtil.removeUserByUsername;
import static org.keycloak.testsuite.admin.ApiUtil.resetUserPassword;
import static org.keycloak.testsuite.broker.BrokerTestConstants.USER_EMAIL;
import static org.keycloak.testsuite.util.MailAssert.assertEmailAndGetUrl;

import org.jboss.arquillian.graphene.page.Page;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;

import javax.ws.rs.core.Response;

Expand All @@ -49,6 +56,10 @@ public abstract class AbstractBrokerTest extends AbstractBaseBrokerTest {

protected IdentityProviderResource identityProviderResource;

@Drone
@SecondBrowser
protected WebDriver driver2;

@Before
public void beforeBrokerTest() {
log.debug("creating user for realm " + bc.providerRealmName());
Expand Down Expand Up @@ -181,15 +192,10 @@ public void testLinkAccountWithEmailVerified() {
MailServer.createEmailAccount(USER_EMAIL, "password");

try {
//configure smpt server in the realm
RealmRepresentation master = adminClient.realm(bc.consumerRealmName()).toRepresentation();
master.setSmtpServer(suiteContext.getSmtpServer());
adminClient.realm(bc.consumerRealmName()).update(master);

configureSMPTServer();

//create user on consumer's site who should be linked later
UserRepresentation newUser = UserBuilder.create().username("consumer").email(USER_EMAIL).enabled(true).build();
String userId = createUserWithAdminClient(adminClient.realm(bc.consumerRealmName()), newUser);
resetUserPassword(adminClient.realm(bc.consumerRealmName()).users().get(userId), "password", false);
String linkedUserId = createUser("consumer");

//test
driver.navigate().to(getAccountUrl(bc.consumerRealmName()));
Expand Down Expand Up @@ -228,8 +234,74 @@ public void testLinkAccountWithEmailVerified() {
assertEquals(accountPage.buildUri().toASCIIString().replace("master", "consumer") + "/", driver.getCurrentUrl());

//test if the user has verified email
assertTrue(adminClient.realm(bc.consumerRealmName()).users().get(userId).toRepresentation().isEmailVerified());
assertTrue(adminClient.realm(bc.consumerRealmName()).users().get(linkedUserId).toRepresentation().isEmailVerified());
} finally {
removeUserByUsername(adminClient.realm(bc.consumerRealmName()), "consumer");
// stop mail server
MailServer.stop();
}
}

@Test
public void testVerifyEmailInNewBrowserWithPreserveClient() {
//start mail server
MailServer.start();
MailServer.createEmailAccount(USER_EMAIL, "password");

try {
configureSMPTServer();

//create user on consumer's site who should be linked later
String linkedUserId = createUser("consumer");

driver.navigate().to(getLoginUrl(bc.consumerRealmName(), "broker-app"));

log.debug("Clicking social " + bc.getIDPAlias());
accountLoginPage.clickSocial(bc.getIDPAlias());

waitForPage(driver, "log in to", true);

Assert.assertTrue("Driver should be on the provider realm page right now",
driver.getCurrentUrl().contains("/auth/realms/" + bc.providerRealmName() + "/"));

log.debug("Logging in");
accountLoginPage.login(bc.getUserLogin(), bc.getUserPassword());

waitForPage(driver, "update account information", false);

Assert.assertTrue(updateAccountInformationPage.isCurrent());
Assert.assertTrue("We must be on correct realm right now",
driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/"));

log.debug("Updating info on updateAccount page");
updateAccountInformationPage.updateAccountInformation("Firstname", "Lastname");

//link account by email
waitForPage(driver, "account already exists", false);
idpConfirmLinkPage.clickLinkAccount();

String url = assertEmailAndGetUrl(MailServerConfiguration.FROM, USER_EMAIL,
"Someone wants to link your ", false);

log.info("navigating to url from email in second browser: " + url);

// navigate to url in the second browser
driver2.navigate().to(url);

final WebElement proceedLink = driver2.findElement(By.linkText("» Click here to proceed"));
MatcherAssert.assertThat(proceedLink, Matchers.notNullValue());

// check if the initial client is preserved
String link = proceedLink.getAttribute("href");
MatcherAssert.assertThat(link, Matchers.containsString("client_id=broker-app"));
proceedLink.click();

assertThat(driver2.getPageSource(), Matchers.containsString("You successfully verified your email. Please go back to your original browser and continue there with the login."));

//test if the user has verified email
assertTrue(adminClient.realm(bc.consumerRealmName()).users().get(linkedUserId).toRepresentation().isEmailVerified());
} finally {
removeUserByUsername(adminClient.realm(bc.consumerRealmName()), "consumer");
// stop mail server
MailServer.stop();
}
Expand Down Expand Up @@ -431,4 +503,17 @@ public void testExpiredCode() {
String link = errorPage.getBackToApplicationLink();
Assert.assertTrue(link.endsWith("/auth/realms/consumer/account"));
}

private void configureSMPTServer() {
RealmRepresentation master = adminClient.realm(bc.consumerRealmName()).toRepresentation();
master.setSmtpServer(suiteContext.getSmtpServer());
adminClient.realm(bc.consumerRealmName()).update(master);
}

private String createUser(String username) {
UserRepresentation newUser = UserBuilder.create().username(username).email(USER_EMAIL).enabled(true).build();
String userId = createUserWithAdminClient(adminClient.realm(bc.consumerRealmName()), newUser);
resetUserPassword(adminClient.realm(bc.consumerRealmName()).users().get(userId), "password", false);
return userId;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,20 @@ public List<ClientRepresentation> createProviderClients(SuiteContext suiteContex

@Override
public List<ClientRepresentation> createConsumerClients(SuiteContext suiteContext) {
return null;
ClientRepresentation client = new ClientRepresentation();
client.setId("broker-app");
client.setClientId("broker-app");
client.setName("broker-app");
client.setSecret("broker-app-secret");
client.setEnabled(true);

client.setRedirectUris(Collections.singletonList(getAuthRoot(suiteContext) +
"/auth/*"));

client.setBaseUrl(getAuthRoot(suiteContext) +
"/auth/realms/" + REALM_CONS_NAME + "/app");

return Collections.singletonList(client);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,15 @@ public List<ClientRepresentation> createConsumerClients(SuiteContext suiteContex
.addRedirectUri("http://localhost:8080/sales-post/*")
.attribute(SamlConfigAttributes.SAML_AUTHNSTATEMENT, SamlProtocol.ATTRIBUTE_TRUE_VALUE)
.attribute(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE, SamlProtocol.ATTRIBUTE_FALSE_VALUE)
.build(),
ClientBuilder.create()
.id("broker-app")
.clientId("broker-app")
.name("broker-app")
.secret("broker-app-secret")
.enabled(true)
.addRedirectUri(getAuthRoot(suiteContext) + "/auth/*")
.baseUrl(getAuthRoot(suiteContext) + "/auth/realms/" + REALM_CONS_NAME + "/app")
.build()
);
}
Expand Down
Loading

0 comments on commit d047912

Please sign in to comment.