8000 Registration with email verification - corner case scenario for pre-a… by mposolda · Pull Request #48208 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@
import org.keycloak.userprofile.UserProfileProvider;
import org.keycloak.userprofile.ValidationException;

import static org.keycloak.services.managers.AuthenticationManager.NEW_USER_REGISTERED;

/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
Expand Down Expand Up @@ -153,6 +155,7 @@ public void success(FormContext context) {

UserProfile profile = getOrCreateUserProfile(context, formData);
UserModel user = profile.create();
context.getAuthenticationSession().setAuthNote(NEW_USER_REGISTERED, "true");

addOrganizationMember(context, user);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

package org.keycloak.authentication.requiredactions;

import java.net.URI;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -58,6 +59,8 @@

import org.jboss.logging.Logger;

import static org.keycloak.services.managers.AuthenticationManager.NEW_USER_REGISTERED;

/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
Expand All @@ -66,6 +69,9 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor
public static final String EMAIL_RESEND_COOLDOWN_KEY_PREFIX = "verify-email-cooldown-";
private static final Logger logger = Logger.getLogger(VerifyEmail.class);

// Auth note to set that verifyEmail is triggered during registration
private static final String VERIFY_EMAIL_DURING_REGISTRATION = "VERIFY_EMAIL_DURING_REGISTRATION";

@Override
public void evaluateTriggers(RequiredActionContext context) {
if (context.getRealm().isVerifyEmail() && !context.getUser().isEmailVerified()) {
Expand Down Expand Up @@ -93,11 +99,26 @@ private void process(RequiredActionContext context, boolean isChallenge) {
AuthenticationSessionModel authSession = context.getAuthenticationSession();

if (context.getUser().isEmailVerified()) {
if ("true".equals(context.getAuthenticationSession().getAuthNote(NEW_USER_REGISTERED))) {
// If registration of the user happened in this authSession, but email was later verified in different authSession, this authSession should not continue
URI redirectToRestartUrl = Urls.realmLoginRestartPage(context.getUriInfo().getBaseUri(), context.getRealm().getName(),
authSession.getClient().getClientId(),
authSession.getTabId(),
AuthenticationProcessor.getClientData(context.getSession(), authSession), false);
Response response = Response.status(302).location(redirectToRestartUrl).build();
context.challenge(response);
return;
}
context.success();
authSession.removeAuthNote(Constants.VERIFY_EMAIL_KEY);
return;
}

// When triggered during registration, make sure to add requiredAction also to the authSession
if ("true".equals(context.getAuthenticationSession().getAuthNote(NEW_USER_REGISTERED))) {
context.getAuthenticationSession().addRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL);
}

String email = context.getUser().getEmail();
if (Validation.isBlank(email)) {
context.ignore();
Expand Down
9 changes: 9 additions & 0 deletions services/src/main/java/org/keycloak/services/Urls.java
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,15 @@ public static URI realmLoginRestartPage(URI baseUri, String realmId, boolean ski
.build(realmId);
}

public static URI realmLoginRestartPage(URI baseUri, String realmName, String clientId, String tabId, String clientData, boolean skipLogout) {
return loginActionsBase(baseUri).path(LoginActionsService.class, "restartSession")
.replaceQueryParam(Constants.CLIENT_ID, clientId)
.replaceQueryParam(Constants.CLIENT_DATA, clientData)
.replaceQueryParam(Constants.TAB_ID, tabId)
.queryParam(Constants.SKIP_LOGOUT, String.valueOf(skipLogout))
.build(realmName);
}

private static UriBuilder realmLogout(URI baseUri) {
return tokenBase(baseUri).path(OIDCLoginProtocolService.class, "logout");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ public class AuthenticationManager {
// authSession note with flag that is true if the user's password has been correctly validated
public static final String PASSWORD_VALIDATED = "PASSWORD_VALIDATED";

// authSession note with flag that registration of new user happened in this authSession
public static final String NEW_USER_REGISTERED = "NEW_USER_REGISTERED";

// state checker identity token claim
private static final String STATE_CHECKER = "state_checker";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.events.EventAssertion;
import org.keycloak.testframework.events.Events;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.mail.MailServer;
import org.keycloak.testframework.mail.annotations.InjectMailServer;
import org.keycloak.testframework.oauth.OAuthClient;
Expand All @@ -36,6 +37,7 @@
import org.keycloak.testframework.ui.annotations.InjectPage;
import org.keycloak.testframework.ui.annotations.InjectWebDriver;
import org.keycloak.testframework.ui.page.LoginPage;
import org.keycloak.testframework.ui.page.LoginPasswordResetPage;
import org.keycloak.testframework.ui.page.LoginPasswordUpdatePage;
import org.keycloak.testframework.ui.page.RegisterPage;
import org.keycloak.testframework.ui.page.VerifyEmailPage;
Expand All @@ -60,13 +62,13 @@
@KeycloakIntegrationTest
public class RegisterWithEmailVerificationTest {

@InjectWebDriver
@InjectWebDriver(ref = "driver", lifecycle = LifeCycle.CLASS)
ManagedWebDriver driver;

@InjectRealm(config = RegisterTestRealmConfig.class)
ManagedRealm realm;

@InjectOAuthClient
@InjectOAuthClient(ref = "oauth", webDriverRef = "driver")
OAuthClient oauth;

@InjectEvents
Expand All @@ -75,21 +77,39 @@ public class RegisterWithEmailVerificationTest {
@InjectMailServer
MailServer mailServer;

@InjectPage
@InjectPage(ref = "loginPage", webDriverRef = "driver")
LoginPage loginPage;

@InjectPage
@InjectPage(ref = "registerPage", webDriverRef = "driver")
RegisterPage registerPage;

@InjectPage
@InjectPage(ref = "changePasswordPage", webDriverRef = "driver")
protected LoginPasswordUpdatePage changePasswordPage;

@InjectPage
@InjectPage(ref = "verifyEmailPage", webDriverRef = "driver")
VerifyEmailPage verifyEmailPage;

@InjectTimeOffSet
TimeOffSet timeOffSet;

@InjectWebDriver(ref = "driver2", lifecycle = LifeCycle.CLASS)
ManagedWebDriver driver2;

@InjectOAuthClient(ref = "oauth2", webDriverRef = "driver2")
OAuthClient oauth2;

@InjectPage(ref = "loginPage2", webDriverRef = "driver2")
LoginPage loginPage2;

@InjectPage(ref = "resetPasswordPage2", webDriverRef = "driver2")
LoginPasswordResetPage resetPasswordPage2;

@InjectPage(ref = "verifyEmailPage2", webDriverRef = "driver2")
VerifyEmailPage verifyEmailPage2;

@InjectPage(ref = "changePasswordPage2", webDriverRef = "driver2")
protected LoginPasswordUpdatePage changePasswordPage2;

@Test
public void registerUserSuccessWithEmailVerification() {
realm.updateWithCleanup((realmm) -> realmm.verifyEmail(true));
Expand Down Expand Up @@ -154,6 +174,93 @@ public void registerUserSuccessWithEmailVerificationWithResend() {
});
}

@Test
public void registerUserSuccessWithEmailVerificationWithForgetPassword() throws Exception {
realm.updateWithCleanup((realmm) -> {
realmm.verifyEmail(true);
realmm.resetPasswordAllowed(true);
return realmm;
});

registerUserSuccessWithEmailVerificationWithForgetPasswordImpl();
}

@Test
public void registerUserSuccessWithEmailVerificationWithForgetPassword_emailVerifyDefaultAction() throws Exception {
realm.updateWithCleanup((realmm) -> {
// Don't enable "Verify email" realm switch, but rather switch VERIFY_EMAIL as a default required action
AuthenticationManagementResource authMgmt = realm.admin().flows();
RequiredActionProviderRepresentation reqAction = authMgmt.getRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL.name());
reqAction.setDefaultAction(true);
authMgmt.updateRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL.name(), reqAction);

realmm.resetPasswordAllowed(true);
return realmm;
});

registerUserSuccessWithEmailVerificationWithForgetPasswordImpl();
}

// Issue 48206
private void registerUserSuccessWithEmailVerificationWithForgetPasswordImpl() throws Exception {
oauth.openLoginForm();
loginPage.clickRegister();
registerPage.assertCurrent();

// Password not shown initially on the registration page since verify-email is required
Assert.assertFalse(registerPage.isPasswordPresent());
registerPage.registerWithoutPassword("firstName", "lastName", "john@email.cz", "john");
verifyEmailPage.assertCurrent();

EventRepresentation registerEvent = events.poll();
EventAssertion.assertSuccess(registerEvent)
.clientId("test-app-oauth")
.details(Details.USERNAME, "john")
.details(Details.EMAIL, "john@email.cz")
.details(Details.REGISTER_METHOD, "form")
.type(EventType.REGISTER);
String userId = registerEvent.getUserId();

try {
EventRepresentation sendVerifyEmailEvent = events.poll();
EventAssertion.assertSuccess(sendVerifyEmailEvent)
.details(Details.EMAIL, "john@email.cz")
.userId(userId)
.type(EventType.SEND_VERIFY_EMAIL);

// Browser2 - open login, click "Forget password" and fill username
oauth2.openLoginForm();
loginPage2.resetPassword();
resetPasswordPage2.assertCurrent();
resetPasswordPage2.changePassword("john@email.cz");

// Receive the email and click it on browser2
MimeMessage message = mailServer.getLastReceivedMessage();
String forgetPasswordEmailLink = MailUtils.getPasswordResetEmailLink(message);
driver2.open(forgetPasswordEmailLink);

// Need to verify email now
verifyEmailPage2.assertCurrent();
message = mailServer.getLastReceivedMessage();
String verifyEmailLink = MailUtils.getPasswordResetEmailLink(message);
driver2.open(verifyEmailLink);

// Browser 2 - update password and authenticate
changePasswordPage2.assertCurrent();
String password = generatePassword();
changePasswordPage2.changePassword(password, password);

String code = oauth2.parseLoginResponse().getCode();
assertNotNull(code);

// Browser 1 - refresh. Should be still on verifyEmail
driver.navigate().refresh();
loginPage.assertCurrent();
} finally {
realm.admin().users().delete(userId).close();
}
}

/**
* @param receiveEmailFunction Income is userId. Outcome is link to password reset
* @throws Exception
Expand All @@ -170,7 +277,7 @@ private void registerUserSuccessWithEmailVerification(Function<String, String> r

EventRepresentation registerEvent = events.poll();
EventAssertion.assertSuccess(registerEvent)
.clientId("test-app")
.clientId("test-app-oauth")
.details(Details.USERNAME, "registerUserSuccessWithEmailVerification")
.details(Details.EMAIL, "registerUserSuccessWithEmailVerification@email")
.details(Details.REGISTER_METHOD, "form")
Expand Down Expand Up @@ -221,7 +328,7 @@ public void registerUserSuccessWithEmailVerification_passwordOnRegisterForm() th

EventRepresentation registerEvent = events.poll();
EventAssertion.assertSuccess(registerEvent)
.clientId("test-app")
.clientId("test-app-oauth")
.details(Details.USERNAME, "registerUserSuccessWithEmailVerification")
.details(Details.EMAIL, "registerUserSuccessWithEmailVerification@email")
.details(Details.REGISTER_METHOD, "form")
Expand Down
Loading
0