KEYCLOAK-7970-KEYCLOAK-7222 Add clientId to action tokens

This commit is contained in:
Martin Kanis 2018-08-17 11:59:49 +02:00 committed by Hynek Mlnařík
parent c5f861a522
commit d04791243c
12 changed files with 230 additions and 22 deletions

View file

@ -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() {

View file

@ -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() {

View file

@ -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() {

View file

@ -131,7 +131,7 @@ public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator
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());

View file

@ -91,7 +91,7 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
// 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()

View file

@ -143,7 +143,7 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor
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();

View file

@ -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;
@ -29,6 +30,7 @@ import org.keycloak.events.Errors;
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;
@ -43,9 +45,11 @@ import org.keycloak.testsuite.pages.InfoPage;
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;
@ -384,9 +388,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
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();
@ -629,6 +631,39 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
}
}
@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))

View file

@ -19,9 +19,11 @@ package org.keycloak.testsuite.broker;
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;
@ -36,6 +38,7 @@ import org.keycloak.testsuite.pages.LoginPage;
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;
@ -142,6 +145,22 @@ public abstract class AbstractBaseBrokerTest extends AbstractKeycloakTest {
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)

View file

@ -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;
@ -16,6 +19,7 @@ import org.keycloak.testsuite.Assert;
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;
@ -27,11 +31,14 @@ import java.util.stream.Collectors;
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;
@ -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());
@ -181,15 +192,10 @@ public abstract class AbstractBrokerTest extends AbstractBaseBrokerTest {
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()));
@ -228,8 +234,74 @@ public abstract class AbstractBrokerTest extends AbstractBaseBrokerTest {
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();
}
@ -431,4 +503,17 @@ public abstract class AbstractBrokerTest extends AbstractBaseBrokerTest {
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;
}
}

View file

@ -121,7 +121,20 @@ public class KcOidcBrokerConfiguration implements BrokerConfiguration {
@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

View file

@ -166,6 +166,15 @@ public class KcSamlBrokerConfiguration implements BrokerConfiguration {
.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()
);
}

View file

@ -16,6 +16,8 @@
*/
package org.keycloak.testsuite.forms;
import org.hamcrest.Matchers;
import org.jboss.arquillian.drone.api.annotation.Drone;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.authentication.actiontoken.resetcred.ResetCredentialsActionToken;
import org.jboss.arquillian.graphene.page.Page;
@ -40,9 +42,11 @@ import org.keycloak.testsuite.pages.LoginPasswordResetPage;
import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
import org.keycloak.testsuite.pages.VerifyEmailPage;
import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
import org.keycloak.testsuite.util.GreenMailRule;
import org.keycloak.testsuite.util.MailUtils;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.SecondBrowser;
import org.keycloak.testsuite.util.UserActionTokenBuilder;
import org.keycloak.testsuite.util.UserBuilder;
@ -57,7 +61,11 @@ import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.*;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.*;
/**
@ -68,6 +76,10 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
private String userId;
@Drone
@SecondBrowser
protected WebDriver driver2;
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
}
@ -182,7 +194,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
String changePasswordUrl = resetPassword("login-test");
events.clear();
assertSecondPasswordResetFails(changePasswordUrl, null); // KC_RESTART doesn't exists, it was deleted after first successful reset-password flow was finished
assertSecondPasswordResetFails(changePasswordUrl, oauth.getClientId()); // KC_RESTART doesn't exists, it was deleted after first successful reset-password flow was finished
}
@Test
@ -194,7 +206,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
driver.navigate().to(resetUri); // This is necessary to delete KC_RESTART cookie that is restricted to /auth/realms/test path
driver.manage().deleteAllCookies();
assertSecondPasswordResetFails(changePasswordUrl, null);
assertSecondPasswordResetFails(changePasswordUrl, oauth.getClientId());
}
public void assertSecondPasswordResetFails(String changePasswordUrl, String clientId) {
@ -204,7 +216,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
assertEquals("Action expired. Please continue with login now.", errorPage.getError());
events.expect(EventType.RESET_PASSWORD)
.client("account")
.client(clientId)
.session((String) null)
.user(userId)
.error(Errors.EXPIRED_CODE)
@ -1012,4 +1024,36 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
}
}
@Test
public void resetPasswordLinkNewBrowserSessionPreserveClient() throws IOException, MessagingException {
loginPage.open();
loginPage.resetPassword();
resetPasswordPage.assertCurrent();
resetPasswordPage.changePassword("login-test");
loginPage.assertCurrent();
assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
assertEquals(1, greenMail.getReceivedMessages().length);
MimeMessage message = greenMail.getReceivedMessages()[0];
String changePasswordUrl = MailUtils.getPasswordResetEmailLink(message);
driver2.navigate().to(changePasswordUrl.trim());
final WebElement newPassword = driver2.findElement(By.id("password-new"));
newPassword.sendKeys("resetPassword");
final WebElement confirmPassword = driver2.findElement(By.id("password-confirm"));
confirmPassword.sendKeys("resetPassword");
final WebElement submit = driver2.findElement(By.cssSelector("input[type=\"submit\"]"));
submit.click();
assertThat(driver2.getCurrentUrl(), Matchers.containsString("client_id=test-app"));
assertThat(driver2.getPageSource(), Matchers.containsString("Your account has been updated."));
}
}