Skip to content

Commit 690b0e4

Browse files
authored
VERIFY_EMAIL as supported Application Initiated Action
Closes #25154 Signed-off-by: Alexander Schwartz <[email protected]>
1 parent 8828ca5 commit 690b0e4

File tree

9 files changed

+167
-22
lines changed

9 files changed

+167
-22
lines changed

docs/documentation/server_admin/topics/users/con-aia.adoc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ the client can still request re-authentication when some AIA is requested. Exce
4747

4848
* The action `delete_account` will always require the user to actively re-authenticate
4949

50-
* The action `update_password` might require the user to actively re-authenticate according to the configured <<maximum-authentication-age,Maximum Authentication Age Password policy>>.
50+
* The action `UPDATE_PASSWORD` might require the user to actively re-authenticate according to the configured <<maximum-authentication-age,Maximum Authentication Age Password policy>>.
5151
In case the policy is not configured, it is also possible to configure it on the required action itself in the <<proc-setting-default-required-actions_{context}, Required actions tab>>
5252
when configuring the particular required action. If the policy is not configured in any of those places, it defaults to five minutes.
5353

@@ -65,7 +65,7 @@ In the same manner, deleting an existing 2nd-factor credential (`otp` or `webaut
6565

6666
Some AIA can require the parameter to be sent together with the action name. For instance, the `Delete Credential` action can be triggered only by AIA and it requires a parameter to be sent together with the name
6767
of the action, which points to the ID of the removed credential. So the URL for this example would be `kc_action=delete_credential:ce1008ac-f811-427f-825a-c0b878d1c24b`. In this case, the
68-
part after the colon character (`ce1008ac-f811-427f-825a-c0b878d1c24b`) contains the ID of the credential of the particular user, which is to be deleted. The `Delete Credential` action
68+
part after the colon character (`ce1008ac-f811-427f-825a-c0b878d1c24b`) contains the ID of the credential of the particular user, which is to be deleted. The `Delete Credential` action
6969
displays the confirmation screen where the user can confirm agreement to delete the credential.
7070

7171
NOTE: The <<_account-service,{project_name} Account Console>> typically uses the `Delete Credential` action when deleting a 2nd-factor credential. You can check the Account Console for examples if you want

services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.jboss.logging.Logger;
2525
import org.keycloak.Config;
2626
import org.keycloak.authentication.AuthenticationProcessor;
27+
import org.keycloak.authentication.InitiatedActionSupport;
2728
import org.keycloak.authentication.RequiredActionContext;
2829
import org.keycloak.authentication.RequiredActionFactory;
2930
import org.keycloak.authentication.RequiredActionProvider;
@@ -64,8 +65,18 @@ public void evaluateTriggers(RequiredActionContext context) {
6465
logger.debug("User is required to verify email");
6566
}
6667
}
68+
69+
@Override
70+
public InitiatedActionSupport initiatedActionSupport() {
71+
return InitiatedActionSupport.SUPPORTED;
72+
}
73+
6774
@Override
6875
public void requiredActionChallenge(RequiredActionContext context) {
76+
process(context, true);
77+
}
78+
79+
public void process(RequiredActionContext context, boolean isChallenge) {
6980
AuthenticationSessionModel authSession = context.getAuthenticationSession();
7081

7182
if (context.getUser().isEmailVerified()) {
@@ -81,11 +92,12 @@ public void requiredActionChallenge(RequiredActionContext context) {
8192
}
8293

8394
LoginFormsProvider loginFormsProvider = context.form();
95+
loginFormsProvider.setAuthenticationSession(context.getAuthenticationSession());
8496
Response challenge;
8597
authSession.setClientNote(AuthorizationEndpointBase.APP_INITIATED_FLOW, null);
8698

8799
// Do not allow resending e-mail by simple page refresh, i.e. when e-mail sent, it should be resent properly via email-verification endpoint
88-
if (! Objects.equals(authSession.getAuthNote(Constants.VERIFY_EMAIL_KEY), email)) {
100+
if (!Objects.equals(authSession.getAuthNote(Constants.VERIFY_EMAIL_KEY), email) && !(isCurrentActionTriggeredFromAIA(context) && isChallenge)) {
89101
authSession.setAuthNote(Constants.VERIFY_EMAIL_KEY, email);
90102
EventBuilder event = context.getEvent().clone().event(EventType.SEND_VERIFY_EMAIL).detail(Details.EMAIL, email);
91103
challenge = sendVerifyEmail(context, event);
@@ -96,6 +108,9 @@ public void requiredActionChallenge(RequiredActionContext context) {
96108
context.challenge(challenge);
97109
}
98110

111+
private boolean isCurrentActionTriggeredFromAIA(RequiredActionContext context) {
112+
return Objects.equals(context.getAuthenticationSession().getClientNote(Constants.KC_ACTION), getId());
113+
}
99114

100115
@Override
101116
public void processAction(RequiredActionContext context) {
@@ -104,7 +119,7 @@ public void processAction(RequiredActionContext context) {
104119
// This will allow user to re-send email again
105120
context.getAuthenticationSession().removeAuthNote(Constants.VERIFY_EMAIL_KEY);
106121

107-
requiredActionChallenge(context);
122+
process(context, false);
108123
}
109124

110125

services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,9 @@ public Response createResponse(UserModel.RequiredAction action) {
196196
case VERIFY_EMAIL:
197197
UpdateProfileContext userBasedContext1 = new UserUpdateProfileContext(realm, user);
198198
attributes.put("user", new ProfileBean(userBasedContext1, formData));
199+
if (authenticationSession.getAuthNote(Constants.VERIFY_EMAIL_KEY) != null) {
200+
attributes.put("verifyEmail", authenticationSession.getAuthNote(Constants.VERIFY_EMAIL_KEY));
201+
}
199202
actionMessage = Messages.VERIFY_EMAIL;
200203
page = LoginFormsPages.LOGIN_VERIFY_EMAIL;
201204
break;

testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/VerifyEmailPage.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ public class VerifyEmailPage extends AbstractPage {
3333
@FindBy(linkText = "Click here")
3434
private WebElement resendEmailLink;
3535

36+
@FindBy(name = "cancel-aia")
37+
private WebElement cancelAIAButton;
38+
3639
@Override
3740
public void open() {
3841
}
@@ -49,4 +52,8 @@ public String getResendEmailLink() {
4952
return resendEmailLink.getAttribute("href");
5053
}
5154

55+
public void cancel() {
56+
cancelAIAButton.click();
57+
}
58+
5259
}

testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionTest.java

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
import org.keycloak.testsuite.AssertEvents;
3030
import org.keycloak.testsuite.pages.AppPage;
3131
import org.keycloak.testsuite.pages.LoginPage;
32-
import org.keycloak.testsuite.pages.VerifyEmailPage;
32+
import org.keycloak.testsuite.pages.TermsAndConditionsPage;
3333
import org.keycloak.testsuite.updaters.UserAttributeUpdater;
3434
import org.keycloak.testsuite.util.GreenMailRule;
3535

@@ -58,7 +58,7 @@ public void configureTestRealm(RealmRepresentation testRealm) {
5858
protected LoginPage loginPage;
5959

6060
@Page
61-
protected VerifyEmailPage verifyEmailPage;
61+
protected TermsAndConditionsPage termsAndConditionsPage;
6262

6363
@Test
6464
public void executeUnknownAction() {
@@ -106,17 +106,19 @@ public void executeDisabledAction() {
106106
}
107107

108108
@Test
109-
public void executeActionWithVerifyEmailUnsupportedAIA() throws IOException {
109+
public void executeActionWithTermsAndConditionsUnsupportedAIA() throws IOException {
110110
RealmResource realm = testRealm();
111-
RequiredActionProviderRepresentation model = realm.flows().getRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL.name());
112-
int prevPriority = model.getPriority();
111+
RequiredActionProviderRepresentation termsAndConditions = realm.flows().getRequiredAction(TermsAndConditions.PROVIDER_ID);
112+
int prevPriority = termsAndConditions.getPriority();
113+
boolean prevEnabled = termsAndConditions.isEnabled();
113114

114115
try (UserAttributeUpdater userUpdater = UserAttributeUpdater
115116
.forUserByUsername(realm, "test-user@localhost")
116-
.setRequiredActions(UserModel.RequiredAction.VERIFY_EMAIL).update()) {
117-
// Set max priority for verify email (AIA not supported) to be executed before update password
118-
model.setPriority(1);
119-
realm.flows().updateRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL.name(), model);
117+
.setRequiredActions(UserModel.RequiredAction.TERMS_AND_CONDITIONS).update()) {
118+
// Set max priority for terms and conditions (AIA not supported) to be executed before update password
119+
termsAndConditions.setPriority(1);
120+
termsAndConditions.setEnabled(true);
121+
realm.flows().updateRequiredAction(TermsAndConditions.PROVIDER_ID, termsAndConditions);
120122

121123
oauth.kcAction(UserModel.RequiredAction.UPDATE_PASSWORD.name()).openLoginForm();
122124
loginPage.login("test-user@localhost", "password");
@@ -125,11 +127,12 @@ public void executeActionWithVerifyEmailUnsupportedAIA() throws IOException {
125127
passwordUpdatePage.assertCurrent();
126128
passwordUpdatePage.changePassword("password", "password");
127129

128-
// once the AIA password is executed the verify profile should be displayed for the login
129-
verifyEmailPage.assertCurrent();
130+
// once the AIA password is executed the terms and conditions should be displayed for the login
131+
termsAndConditionsPage.assertCurrent();
130132
} finally {
131-
model.setPriority(prevPriority);
132-
realm.flows().updateRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL.name(), model);
133+
termsAndConditions.setPriority(prevPriority);
134+
termsAndConditions.setEnabled(prevEnabled);
135+
realm.flows().updateRequiredAction(TermsAndConditions.PROVIDER_ID, termsAndConditions);
133136
}
134137
}
135138
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright 2019 Red Hat, Inc. and/or its affiliates
3+
* and other contributors as indicated by the @author tags.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.keycloak.testsuite.actions;
18+
19+
import org.jboss.arquillian.graphene.page.Page;
20+
import org.junit.Before;
21+
import org.junit.Test;
22+
import org.keycloak.models.UserModel;
23+
import org.keycloak.representations.idm.RealmRepresentation;
24+
import org.keycloak.representations.idm.UserRepresentation;
25+
import org.keycloak.testsuite.admin.ApiUtil;
26+
import org.keycloak.testsuite.pages.ErrorPage;
27+
import org.keycloak.testsuite.pages.VerifyEmailPage;
28+
import org.keycloak.testsuite.util.UserBuilder;
29+
30+
/**
31+
* Only covers basic use cases for App Initialized actions. Complete dynamic user profile behavior is tested in {@link RequiredActionUpdateProfileWithUserProfileTest} as it shares same code as the App initialized action.
32+
*
33+
* @author Stan Silvert
34+
*/
35+
public class AppInitiatedActionVerifyEmailTest extends AbstractAppInitiatedActionTest {
36+
37+
@Override
38+
public String getAiaAction() {
39+
return UserModel.RequiredAction.VERIFY_EMAIL.name();
40+
}
41+
42+
@Page
43+
protected VerifyEmailPage verifyEmailPage;
44+
45+
@Page
46+
protected ErrorPage errorPage;
47+
48+
@Override
49+
public void configureTestRealm(RealmRepresentation testRealm) {
50+
}
51+
52+
@Before
53+
public void beforeTest() {
54+
ApiUtil.removeUserByUsername(testRealm(), "test-user@localhost");
55+
UserRepresentation user = UserBuilder.create().enabled(true)
56+
.username("test-user@localhost")
57+
.email("test-user@localhost")
58+
.firstName("Tom")
59+
.lastName("Brady")
60+
.build();
61+
ApiUtil.createUserAndResetPasswordWithAdminClient(testRealm(), user, "password");
62+
}
63+
64+
@Test
65+
public void sendVerifyEmail() {
66+
doAIA();
67+
68+
loginPage.login("test-user@localhost", "password");
69+
70+
verifyEmailPage.assertCurrent();
71+
72+
}
73+
74+
@Test
75+
public void cancelUpdateProfile() {
76+
doAIA();
77+
78+
loginPage.login("test-user@localhost", "password");
79+
80+
verifyEmailPage.assertCurrent();
81+
verifyEmailPage.cancel();
82+
83+
assertKcActionStatus(CANCELLED);
84+
85+
}
86+
87+
}

themes/src/main/resources/theme/base/login/login-verify-email.ftl

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,34 @@
33
<#if section = "header">
44
${msg("emailVerifyTitle")}
55
<#elseif section = "form">
6-
<p class="instruction">${msg("emailVerifyInstruction1",user.email)}</p>
7-
<#elseif section = "info">
86
<p class="instruction">
9-
${msg("emailVerifyInstruction2")}
10-
<br/>
11-
<a href="${url.loginAction}">${msg("doClickHere")}</a> ${msg("emailVerifyInstruction3")}
7+
<#if verifyEmail??>
8+
${msg("emailVerifyInstruction1",verifyEmail)}
9+
<#else>
10+
${msg("emailVerifyInstruction4",user.email)}
11+
</#if>
1212
</p>
13+
<#if isAppInitiatedAction??>
14+
<form id="kc-verify-email-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
15+
<div class="${properties.kcFormGroupClass!}">
16+
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
17+
<#if verifyEmail??>
18+
<input class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("emailVerifyResend")}" />
19+
<#else>
20+
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("emailVerifySend")}" />
21+
</#if>
22+
<button class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" type="submit" name="cancel-aia" value="true" formnovalidate/>${msg("doCancel")}</button>
23+
</div>
24+
</div>
25+
</form>
26+
</#if>
27+
<#elseif section = "info">
28+
<#if !isAppInitiatedAction??>
29+
<p class="instruction">
30+
${msg("emailVerifyInstruction2")}
31+
<br/>
32+
<a href="${url.loginAction}">${msg("doClickHere")}</a> ${msg("emailVerifyInstruction3")}
33+
</p>
34+
</#if>
1335
</#if>
1436
</@layout.registrationLayout>

themes/src/main/resources/theme/base/login/messages/messages_en.properties

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,9 @@ oauth2DeviceAuthorizationGrantDisabledMessage=Client is not allowed to initiate
168168
emailVerifyInstruction1=An email with instructions to verify your email address has been sent to your address {0}.
169169
emailVerifyInstruction2=Haven''t received a verification code in your email?
170170
emailVerifyInstruction3=to re-send the email.
171+
emailVerifyInstruction4=To verify your email address, we are about to send you email with instructions to the address {0}.
172+
emailVerifyResend=Re-send verification email
173+
emailVerifySend=Send verification email
171174

172175
emailLinkIdpTitle=Link {0}
173176
emailLinkIdp1=An email with instructions to link {0} account {1} with your {2} account has been sent to you.

themes/src/main/resources/theme/keycloak.v2/login/resources/css/styles.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ div.kc-logo-text span {
7979
overflow-wrap: break-word
8080
}
8181

82+
#kc-verify-email-form {
83+
margin-top: 24px;
84+
margin-bottom: 24px;
85+
}
86+
8287
#kc-header-wrapper {
8388
font-size: 29px;
8489
text-transform: uppercase;

0 commit comments

Comments
 (0)