/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.actions;
import org.keycloak.authentication.actiontoken.verifyemail.VerifyEmailActionToken;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventType;
import org.keycloak.models.Constants;
import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.auth.page.AuthRealm;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.ErrorPage;
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.util.GreenMailRule;
import org.keycloak.testsuite.util.MailUtils;
import org.keycloak.testsuite.util.UserBuilder;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.internet.MimeMessage;
import java.io.IOException;
import org.hamcrest.Matchers;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeycloakTest {
@Rule
public AssertEvents events = new AssertEvents(this);
@Rule
public GreenMailRule greenMail = new GreenMailRule();
@Page
protected AppPage appPage;
@Page
protected LoginPage loginPage;
@Page
protected VerifyEmailPage verifyEmailPage;
@Page
protected RegisterPage registerPage;
@Page
protected InfoPage infoPage;
@Page
protected ErrorPage errorPage;
private String testUserId;
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
testRealm.setVerifyEmail(Boolean.TRUE);
ActionUtil.findUserInRealmRep(testRealm, "test-user@localhost").setEmailVerified(Boolean.FALSE);
}
@Before
public void before() {
ApiUtil.removeUserByUsername(testRealm(), "test-user@localhost");
UserRepresentation user = UserBuilder.create().enabled(true)
.username("test-user@localhost")
.email("test-user@localhost").build();
testUserId = ApiUtil.createUserAndResetPasswordWithAdminClient(testRealm(), user, "password");
}
/**
* see KEYCLOAK-4163
*/
@Test
public void verifyEmailConfig() throws IOException, MessagingException {
loginPage.open();
loginPage.login("test-user@localhost", "password");
verifyEmailPage.assertCurrent();
Assert.assertEquals(1, greenMail.getReceivedMessages().length);
MimeMessage message = greenMail.getLastReceivedMessage();
// see testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json
Assert.assertEquals("<auto+bounces@keycloak.org>", message.getHeader("Return-Path")[0]);
// displayname <email@example.org>
Assert.assertEquals("Keycloak SSO <auto@keycloak.org>", message.getHeader("From")[0]);
Assert.assertEquals("Keycloak no-reply <reply-to@keycloak.org>", message.getHeader("Reply-To")[0]);
}
@Test
public void verifyEmailExisting() throws IOException, MessagingException {
loginPage.open();
loginPage.login("test-user@localhost", "password");
verifyEmailPage.assertCurrent();
Assert.assertEquals(1, greenMail.getReceivedMessages().length);
MimeMessage message = greenMail.getReceivedMessages()[0];
String verificationUrl = getPasswordResetEmailLink(message);
AssertEvents.ExpectedEvent emailEvent = events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL).detail("email", "test-user@localhost");
EventRepresentation sendEvent = emailEvent.assertEvent();
String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID);
driver.navigate().to(verificationUrl.trim());
events.expectRequiredAction(EventType.VERIFY_EMAIL)
.user(testUserId)
.detail(Details.USERNAME, "test-user@localhost")
.detail(Details.EMAIL, "test-user@localhost")
.detail(Details.CODE_ID, mailCodeId)
.assertEvent();
appPage.assertCurrent();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectLogin().user(testUserId).session(mailCodeId).detail(Details.USERNAME, "test-user@localhost").assertEvent();
}
@Test
public void verifyEmailRegister() throws IOException, MessagingException {
loginPage.open();
loginPage.clickRegister();
registerPage.register("firstName", "lastName", "email@mail.com", "verifyEmail", "password", "password");
String userId = events.expectRegister("verifyEmail", "email@mail.com").assertEvent().getUserId();
verifyEmailPage.assertCurrent();
Assert.assertEquals(1, greenMail.getReceivedMessages().length);
MimeMessage message = greenMail.getReceivedMessages()[0];
EventRepresentation sendEvent = events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL).user(userId).detail(Details.USERNAME, "verifyemail").detail("email", "email@mail.com").assertEvent();
String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID);
String verificationUrl = getPasswordResetEmailLink(message);
driver.navigate().to(verificationUrl.trim());
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectRequiredAction(EventType.VERIFY_EMAIL)
.user(userId)
.detail(Details.USERNAME, "verifyemail")
.detail(Details.EMAIL, "email@mail.com")
.detail(Details.CODE_ID, mailCodeId)
.assertEvent();
events.expectLogin().user(userId).session(mailCodeId).detail(Details.USERNAME, "verifyemail").assertEvent();
}
@Test
public void verifyEmailResend() throws IOException, MessagingException {
loginPage.open();
loginPage.login("test-user@localhost", "password");
verifyEmailPage.assertCurrent();
Assert.assertEquals(1, greenMail.getReceivedMessages().length);
EventRepresentation sendEvent = events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL)
.detail("email", "test-user@localhost")
.assertEvent();
String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID);
verifyEmailPage.clickResendEmail();
verifyEmailPage.assertCurrent();
events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL)
.detail(Details.CODE_ID, mailCodeId)
.detail("email", "test-user@localhost")
.assertEvent();
Assert.assertEquals(2, greenMail.getReceivedMessages().length);
MimeMessage message = greenMail.getLastReceivedMessage();
String verificationUrl = getPasswordResetEmailLink(message);
driver.navigate().to(verificationUrl.trim());
appPage.assertCurrent();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectRequiredAction(EventType.VERIFY_EMAIL)
.user(testUserId)
.detail(Details.USERNAME, "test-user@localhost")
.detail(Details.EMAIL, "test-user@localhost")
.detail(Details.CODE_ID, mailCodeId)
.assertEvent();
events.expectLogin().user(testUserId).session(mailCodeId).detail(Details.USERNAME, "test-user@localhost").assertEvent();
}
@Test
public void verifyEmailResendWithRefreshes() throws IOException, MessagingException {
loginPage.open();
loginPage.login("test-user@localhost", "password");
verifyEmailPage.assertCurrent();
driver.navigate().refresh();
Assert.assertEquals(1, greenMail.getReceivedMessages().length);
EventRepresentation sendEvent = events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL)
.detail("email", "test-user@localhost")
.assertEvent();
String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID);
verifyEmailPage.clickResendEmail();
verifyEmailPage.assertCurrent();
driver.navigate().refresh();
events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL)
.detail(Details.CODE_ID, mailCodeId)
.detail("email", "test-user@localhost")
.assertEvent();
Assert.assertEquals(2, greenMail.getReceivedMessages().length);
MimeMessage message = greenMail.getLastReceivedMessage();
String verificationUrl = getPasswordResetEmailLink(message);
driver.navigate().to(verificationUrl.trim());
appPage.assertCurrent();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectRequiredAction(EventType.VERIFY_EMAIL)
.user(testUserId)
.detail(Details.USERNAME, "test-user@localhost")
.detail(Details.EMAIL, "test-user@localhost")
.detail(Details.CODE_ID, mailCodeId)
.assertEvent();
events.expectLogin().user(testUserId).session(mailCodeId).detail(Details.USERNAME, "test-user@localhost").assertEvent();
}
@Test
public void verifyEmailResendFirstStillValidEvenWithSecond() throws IOException, MessagingException {
// Email verification can be performed any number of times
loginPage.open();
loginPage.login("test-user@localhost", "password");
verifyEmailPage.clickResendEmail();
verifyEmailPage.assertCurrent();
Assert.assertEquals(2, greenMail.getReceivedMessages().length);
MimeMessage message1 = greenMail.getReceivedMessages()[0];
String verificationUrl1 = getPasswordResetEmailLink(message1);
driver.navigate().to(verificationUrl1.trim());
appPage.assertCurrent();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
MimeMessage message2 = greenMail.getReceivedMessages()[1];
String verificationUrl2 = getPasswordResetEmailLink(message2);
driver.navigate().to(verificationUrl2.trim());
infoPage.assertCurrent();
Assert.assertEquals("You are already logged in.", infoPage.getInfo());
}
@Test
public void verifyEmailResendFirstAndSecondStillValid() throws IOException, MessagingException {
// Email verification can be performed any number of times
loginPage.open();
loginPage.login("test-user@localhost", "password");
verifyEmailPage.clickResendEmail();
verifyEmailPage.assertCurrent();
Assert.assertEquals(2, greenMail.getReceivedMessages().length);
MimeMessage message1 = greenMail.getReceivedMessages()[0];
String verificationUrl1 = getPasswordResetEmailLink(message1);
driver.navigate().to(verificationUrl1.trim());
appPage.assertCurrent();
appPage.logout();
MimeMessage message2 = greenMail.getReceivedMessages()[1];
String verificationUrl2 = getPasswordResetEmailLink(message2);
driver.navigate().to(verificationUrl2.trim());
infoPage.assertCurrent();
assertEquals("Your email address has been verified.", infoPage.getInfo());
}
@Test
public void verifyEmailNewBrowserSession() 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);
AssertEvents.ExpectedEvent emailEvent = events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL).detail("email", "test-user@localhost");
EventRepresentation sendEvent = emailEvent.assertEvent();
String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID);
driver.manage().deleteAllCookies();
driver.navigate().to(verificationUrl.trim());
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()
.detail(Details.REDIRECT_URI, Matchers.any(String.class))
.assertEvent();
infoPage.assertCurrent();
assertEquals("Your email address has been verified.", infoPage.getInfo());
loginPage.open();
loginPage.assertCurrent();
}
@Test
public void verifyEmailInvalidKeyInVerficationLink() 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);
verificationUrl = KeycloakUriBuilder.fromUri(verificationUrl).replaceQueryParam(Constants.KEY, "foo").build().toString();
events.poll();
driver.navigate().to(verificationUrl.trim());
errorPage.assertCurrent();
assertEquals("An error occurred, please login again through your application.", errorPage.getError());
events.expectRequiredAction(EventType.EXECUTE_ACTION_TOKEN_ERROR)
.error(Errors.INVALID_CODE)
.client((String)null)
.user((String)null)
.session((String)null)
.clearDetails()
.assertEvent();
}
@Test
public void verifyEmailExpiredCode() 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);
events.poll();
try {
setTimeOffset(3600);
driver.navigate().to(verificationUrl.trim());
loginPage.assertCurrent();
assertEquals("You took too long to login. Login process starting from beginning.", loginPage.getError());
events.expectRequiredAction(EventType.EXECUTE_ACTION_TOKEN_ERROR)
.error(Errors.EXPIRED_CODE)
.client((String)null)
.user(testUserId)
.session((String)null)
.clearDetails()
.detail(Details.ACTION, VerifyEmailActionToken.TOKEN_TYPE)
.assertEvent();
} finally {
setTimeOffset(0);
}
}
@Test
public void verifyEmailExpiredCodeAndExpiredSession() 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);
events.poll();
try {
setTimeOffset(3600);
driver.manage().deleteAllCookies();
driver.navigate().to(verificationUrl.trim());
errorPage.assertCurrent();
assertEquals("The link you clicked is a old stale link and is no longer valid. Maybe you have already verified your email?", errorPage.getError());
events.expectRequiredAction(EventType.EXECUTE_ACTION_TOKEN_ERROR)
.error(Errors.EXPIRED_CODE)
.client((String)null)
.user(testUserId)
.session((String)null)
.clearDetails()
.detail(Details.ACTION, VerifyEmailActionToken.TOKEN_TYPE)
.assertEvent();
} finally {
setTimeOffset(0);
}
}
public static String getPasswordResetEmailLink(MimeMessage message) throws IOException, MessagingException {
Multipart multipart = (Multipart) message.getContent();
final String textContentType = multipart.getBodyPart(0).getContentType();
assertEquals("text/plain; charset=UTF-8", textContentType);
final String textBody = (String) multipart.getBodyPart(0).getContent();
final String textChangePwdUrl = MailUtils.getLink(textBody);
final String htmlContentType = multipart.getBodyPart(1).getContentType();
assertEquals("text/html; charset=UTF-8", htmlContentType);
final String htmlBody = (String) multipart.getBodyPart(1).getContent();
final String htmlChangePwdUrl = MailUtils.getLink(htmlBody);
assertEquals(htmlChangePwdUrl, textChangePwdUrl);
return htmlChangePwdUrl;
}
}