mirror of
https://github.com/keycloak/keycloak.git
synced 2026-05-06 23:19:35 -05:00
[KEYCLOAK-12168] Various setup TOTP screen usability improvements (#6709)
On both the TOTP account and TOTP login screens perform the following: * Make the "Device name" label optional if user registers the first TOTP credential. Make it mandatory otherwise, * Denote the "Authenticator code" with asterisk, so it's clear it's required field (always), * Add sentence to Step 3 of configuring TOTP credential explaining the user to provide device name label, Also perform other CSS & locale / messages file changes, so the UX is identical when creating OTP credentials on both of these pages Add a corresponding testcase Also address issues pointed out by mposolda's review. Thanks, Marek! Signed-off-by: Jan Lieskovsky <jlieskov@redhat.com>
This commit is contained in:
@@ -38,6 +38,9 @@ import org.keycloak.services.validation.Validation;
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
@@ -88,6 +91,17 @@ public class UpdateTotp implements RequiredActionProvider, RequiredActionFactory
|
||||
return;
|
||||
}
|
||||
OTPCredentialProvider otpCredentialProvider = (OTPCredentialProvider) context.getSession().getProvider(CredentialProvider.class, "keycloak-otp");
|
||||
final List<CredentialModel> otpCredentials = (otpCredentialProvider.isConfiguredFor(context.getRealm(), context.getUser()))
|
||||
? context.getSession().userCredentialManager().getStoredCredentialsByType(context.getRealm(), context.getUser(), OTPCredentialModel.TYPE)
|
||||
: Collections.EMPTY_LIST;
|
||||
if (otpCredentials.size() >= 1 && Validation.isBlank(userLabel)) {
|
||||
Response challenge = context.form()
|
||||
.setAttribute("mode", mode)
|
||||
.setError(Messages.MISSING_TOTP_DEVICE_NAME)
|
||||
.createResponse(UserModel.RequiredAction.CONFIGURE_TOTP);
|
||||
context.challenge(challenge);
|
||||
return;
|
||||
}
|
||||
CredentialModel createdCredential = otpCredentialProvider.createCredential(context.getRealm(), context.getUser(), credentialModel);
|
||||
UserCredentialModel credential = new UserCredentialModel(createdCredential.getId(), otpCredentialProvider.getType(), challengeResponse);
|
||||
//If the type is HOTP, call verify once to consume the OTP used for registration and increase the counter.
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
*/
|
||||
package org.keycloak.forms.login.freemarker.model;
|
||||
|
||||
import org.keycloak.credential.CredentialModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.OTPPolicy;
|
||||
import org.keycloak.models.RealmModel;
|
||||
@@ -25,6 +26,8 @@ import org.keycloak.models.utils.HmacOTP;
|
||||
import org.keycloak.utils.TotpUtils;
|
||||
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Used for UpdateTotp required action
|
||||
@@ -39,11 +42,17 @@ public class TotpBean {
|
||||
private final String totpSecretQrCode;
|
||||
private final boolean enabled;
|
||||
private UriBuilder uriBuilder;
|
||||
private final List<CredentialModel> otpCredentials;
|
||||
|
||||
public TotpBean(KeycloakSession session, RealmModel realm, UserModel user, UriBuilder uriBuilder) {
|
||||
this.realm = realm;
|
||||
this.uriBuilder = uriBuilder;
|
||||
this.enabled = session.userCredentialManager().isConfiguredFor(realm, user, OTPCredentialModel.TYPE);
|
||||
if (enabled) {
|
||||
otpCredentials = session.userCredentialManager().getStoredCredentialsByType(realm, user, OTPCredentialModel.TYPE);
|
||||
} else {
|
||||
otpCredentials = Collections.EMPTY_LIST;
|
||||
}
|
||||
this.totpSecret = HmacOTP.generateSecret(20);
|
||||
this.totpSecretEncoded = TotpUtils.encode(totpSecret);
|
||||
this.totpSecretQrCode = TotpUtils.qrCode(totpSecret, realm, user);
|
||||
@@ -77,6 +86,8 @@ public class TotpBean {
|
||||
return realm.getOTPPolicy();
|
||||
}
|
||||
|
||||
public List<CredentialModel> getOtpCredentials() {
|
||||
return otpCredentials;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,8 @@ public class Messages {
|
||||
|
||||
public static final String MISSING_TOTP = "missingTotpMessage";
|
||||
|
||||
public static final String MISSING_TOTP_DEVICE_NAME = "missingTotpDeviceNameMessage";
|
||||
|
||||
public static final String NOTMATCH_PASSWORD = "notMatchPasswordMessage";
|
||||
|
||||
public static final String INVALID_PASSWORD_EXISTING = "invalidPasswordExistingMessage";
|
||||
|
||||
+2
-2
@@ -569,8 +569,8 @@ public class KcinitTest extends AbstractTestRealmKeycloakTest {
|
||||
exe.sendLine("password");
|
||||
exe.waitForStderr("One Time Password:");
|
||||
|
||||
Pattern p = Pattern.compile("Open the application and enter the key\\s+(.+)\\s+Use the following configuration values");
|
||||
//Pattern p = Pattern.compile("Open the application and enter the key");
|
||||
Pattern p = Pattern.compile("Open the application and enter the key:\\s+(.+)\\s+Use the following configuration values");
|
||||
//Pattern p = Pattern.compile("Open the application and enter the key:");
|
||||
|
||||
String stderr = exe.stderrString();
|
||||
//System.out.println("***************");
|
||||
|
||||
+173
@@ -24,10 +24,13 @@ import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.models.UserManager;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.DefaultAuthenticationFlows;
|
||||
import org.keycloak.models.utils.TimeBasedOTP;
|
||||
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
@@ -43,6 +46,7 @@ import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
|
||||
import org.keycloak.testsuite.pages.LoginTotpPage;
|
||||
import org.keycloak.testsuite.pages.LoginUsernameOnlyPage;
|
||||
import org.keycloak.testsuite.pages.PasswordPage;
|
||||
import org.keycloak.testsuite.pages.RegisterPage;
|
||||
import org.keycloak.testsuite.util.FlowUtil;
|
||||
import org.keycloak.testsuite.util.GreenMailRule;
|
||||
import org.keycloak.testsuite.util.MailUtils;
|
||||
@@ -52,6 +56,7 @@ import org.keycloak.testsuite.util.UserBuilder;
|
||||
import javax.mail.internet.MimeMessage;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE;
|
||||
@@ -79,6 +84,9 @@ public class ResetCredentialsAlternativeFlowsTest extends AbstractTestRealmKeycl
|
||||
@Page
|
||||
protected PasswordPage passwordPage;
|
||||
|
||||
@Page
|
||||
protected RegisterPage registerPage;
|
||||
|
||||
@Page
|
||||
protected LoginPasswordResetPage resetPasswordPage;
|
||||
|
||||
@@ -380,4 +388,169 @@ public class ResetCredentialsAlternativeFlowsTest extends AbstractTestRealmKeycl
|
||||
revertFlows();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// KEYCLOAK-12168 Verify the 'Device Name' label is optional for the first OTP credential created
|
||||
// (either via Account page or by registering new user), but required for each next created OTP credential
|
||||
@Test
|
||||
public void deviceNameOptionalForFirstOTPCredentialButRequiredForEachNextOne() {
|
||||
// Enable 'Default Action' on 'Configure OTP' RA for the 'test' realm
|
||||
RequiredActionProviderRepresentation otpRequiredAction = testRealm().flows().getRequiredAction("CONFIGURE_TOTP");
|
||||
otpRequiredAction.setDefaultAction(true);
|
||||
testRealm().flows().updateRequiredAction("CONFIGURE_TOTP", otpRequiredAction);
|
||||
|
||||
try {
|
||||
// Make a copy of the default Reset Credentials flow, but:
|
||||
// * Without 'Send Reset Email' authenticator,
|
||||
// * Without 'Reset Password' authenticator
|
||||
final String newFlowAlias = "resetcred - KEYCLOAK-12168 - firstOTP - account - test";
|
||||
configureResetCredentialsRemoveExecutionsAndBindTheFlow(
|
||||
newFlowAlias,
|
||||
Arrays.asList("reset-credential-email", "reset-password")
|
||||
);
|
||||
|
||||
/* Verify the 'Device Name' is optional when creating new OTP credential via the Account page */
|
||||
|
||||
// Login & set up the initial OTP code for the user
|
||||
loginPage.open();
|
||||
loginPage.login("login@test.com", "password");
|
||||
accountTotpPage.open();
|
||||
Assert.assertTrue(accountTotpPage.isCurrent());
|
||||
|
||||
String pageSource = driver.getPageSource();
|
||||
// Check the One-time code label is followed by asterisk character (since always required)
|
||||
final String oneTimeCodeLabelFollowedByAsterisk = "(?s)<label for=\"totp\"((?!</span>).)+((?=<span class=\"required\">\\*).)*";
|
||||
Assert.assertTrue(Pattern.compile(oneTimeCodeLabelFollowedByAsterisk).matcher(pageSource).find());
|
||||
|
||||
// Check the Device Name label is not followed by asterisk character (since optional if no OTP credential defined yet)
|
||||
final String asteriskPrecededByDeviceNameLabel = "(?s)((?<=<label for=\"userLabel\").)+.*<span class=\"required\">\\s+\\*";
|
||||
Assert.assertFalse(Pattern.compile(asteriskPrecededByDeviceNameLabel).matcher(pageSource).find());
|
||||
|
||||
// Create OTP credential with empty label
|
||||
final String emptyOtpLabel = "";
|
||||
accountTotpPage.configure(totp.generateTOTP(accountTotpPage.getTotpSecret()), emptyOtpLabel);
|
||||
|
||||
// Get the updated Account TOTP page source post OTP credential creation
|
||||
pageSource = driver.getPageSource();
|
||||
|
||||
// Check if OTP credential with empty label was created successfully
|
||||
final String emptyOtpLabelPresentInAuthenticatorTable = "(?s)<td class=\"provider\"/>";
|
||||
Assert.assertTrue(Pattern.compile(emptyOtpLabelPresentInAuthenticatorTable).matcher(pageSource).find());
|
||||
accountTotpPage.removeTotp();
|
||||
|
||||
// Logout
|
||||
oauth.openLogout();
|
||||
|
||||
/* Verify the 'Device Name' is optional when creating the first OTP credential via the login config TOTP page */
|
||||
|
||||
// Register new user
|
||||
loginPage.open();
|
||||
loginPage.clickRegister();
|
||||
registerPage.assertCurrent();
|
||||
|
||||
registerPage.register("Bruce", "Wilson", "bwilson@keycloak.org", "bwilson", "password", "password");
|
||||
Assert.assertTrue(totpPage.isCurrent());
|
||||
pageSource = driver.getPageSource();
|
||||
|
||||
// Check the One-time code label is required
|
||||
Assert.assertTrue(Pattern.compile(oneTimeCodeLabelFollowedByAsterisk).matcher(pageSource).find());
|
||||
// Check the Device Name label is optional
|
||||
Assert.assertFalse(Pattern.compile(asteriskPrecededByDeviceNameLabel).matcher(pageSource).find());
|
||||
|
||||
// Create OTP credential with empty label
|
||||
totpPage.configure(totp.generateTOTP(accountTotpPage.getTotpSecret()), emptyOtpLabel);
|
||||
try {
|
||||
Assert.assertTrue(totpPage.getError().isEmpty());
|
||||
} catch (org.openqa.selenium.NoSuchElementException nsee) {
|
||||
// OK to ignore if 'alert-error' element wasn't found
|
||||
}
|
||||
|
||||
// Assert user authenticated
|
||||
appPage.assertCurrent();
|
||||
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||
|
||||
accountTotpPage.open();
|
||||
Assert.assertTrue(accountTotpPage.isCurrent());
|
||||
|
||||
// Get the updated Account TOTP page source post OTP credential creation
|
||||
pageSource = driver.getPageSource();
|
||||
|
||||
// Check if OTP credential with empty label was created successfully
|
||||
Assert.assertTrue(Pattern.compile(emptyOtpLabelPresentInAuthenticatorTable).matcher(pageSource).find());
|
||||
|
||||
// Logout
|
||||
oauth.openLogout();
|
||||
|
||||
/* Verify the 'Device Name' is required for each next OTP credential created via the login config TOTP page */
|
||||
|
||||
// Click "Forgot password" to define another OTP credential
|
||||
loginPage.open();
|
||||
loginPage.resetPassword();
|
||||
|
||||
// Should be on reset password page now. Provide email of previously registered user & click Submit button
|
||||
Assert.assertTrue(resetPasswordPage.isCurrent());
|
||||
resetPasswordPage.changePassword("bwilson@keycloak.org");
|
||||
|
||||
pageSource = driver.getPageSource();
|
||||
|
||||
// Check the One-time code label is required
|
||||
Assert.assertTrue(Pattern.compile(oneTimeCodeLabelFollowedByAsterisk).matcher(pageSource).find());
|
||||
|
||||
// Check the Device Name label is required (since one OTP credential already defined)
|
||||
final String deviceNameLabelFollowedByAsterisk = "(?s)<label for=\"userLabel\"((?!</span>).)+((?=<span class=\"required\">\\*).)*";
|
||||
Assert.assertTrue(Pattern.compile(deviceNameLabelFollowedByAsterisk).matcher(pageSource).find());
|
||||
|
||||
// Try to create another OTP credential with empty label again. This
|
||||
// should fail with error since OTP label is required in this case already
|
||||
final String deviceNameLabelRequiredErrorMessage = "Please specify device name.";
|
||||
totpPage.configure(totp.generateTOTP(accountTotpPage.getTotpSecret()), emptyOtpLabel);
|
||||
Assert.assertTrue(totpPage.getError().equals(deviceNameLabelRequiredErrorMessage));
|
||||
|
||||
// Create 2nd OTP credential with valid (non-empty) Device Name label. This should pass
|
||||
final String secondOtpLabel = "My 2nd OTP device";
|
||||
totpPage.configure(totp.generateTOTP(accountTotpPage.getTotpSecret()), secondOtpLabel);
|
||||
try {
|
||||
Assert.assertTrue(totpPage.getError().isEmpty());
|
||||
} catch (org.openqa.selenium.NoSuchElementException nsee) {
|
||||
// OK to ignore if 'alert-error' element wasn't found
|
||||
}
|
||||
|
||||
// Assert user authenticated
|
||||
appPage.assertCurrent();
|
||||
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||
|
||||
accountTotpPage.open();
|
||||
Assert.assertTrue(accountTotpPage.isCurrent());
|
||||
|
||||
// Get the updated Account TOTP page source after both the OTP credentials were created
|
||||
pageSource = driver.getPageSource();
|
||||
|
||||
// Verify 2nd OTP credential was successfully created too
|
||||
Assert.assertTrue(pageSource.contains(secondOtpLabel));
|
||||
|
||||
// Remove both OTP credentials
|
||||
accountTotpPage.removeTotp();
|
||||
accountTotpPage.removeTotp();
|
||||
|
||||
// Logout
|
||||
oauth.openLogout();
|
||||
|
||||
// Undo setup changes performed within the test
|
||||
} finally {
|
||||
revertFlows();
|
||||
// Disable 'Default Action' on 'Configure OTP' RA for the 'test' realm
|
||||
otpRequiredAction.setDefaultAction(false);
|
||||
testRealm().flows().updateRequiredAction("CONFIGURE_TOTP", otpRequiredAction);
|
||||
// Remove the within test registered 'bwilson' user
|
||||
testingClient.server("test").run(session -> {
|
||||
UserManager um = new UserManager(session);
|
||||
UserModel user = session.users().getUserByUsername("bwilson", session.getContext().getRealm());
|
||||
if (user != null) {
|
||||
um.removeUser(session.getContext().getRealm(), user);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,12 +130,13 @@ revoke=Revoke Grant
|
||||
|
||||
configureAuthenticators=Configured Authenticators
|
||||
mobile=Mobile
|
||||
totpStep1=Install one of the following applications on your mobile
|
||||
totpStep2=Open the application and scan the barcode
|
||||
totpStep1=Install one of the following applications on your mobile:
|
||||
totpStep2=Open the application and scan the barcode:
|
||||
totpStep3=Enter the one-time code provided by the application and click Save to finish the setup.
|
||||
totpStep3DeviceName=Provide a Device Name to help you manage your OTP devices.
|
||||
|
||||
totpManualStep2=Open the application and enter the key
|
||||
totpManualStep3=Use the following configuration values if the application allows setting them
|
||||
totpManualStep2=Open the application and enter the key:
|
||||
totpManualStep3=Use the following configuration values if the application allows setting them:
|
||||
totpUnableToScan=Unable to scan?
|
||||
totpScanBarcode=Scan barcode?
|
||||
|
||||
@@ -147,6 +148,7 @@ totpAlgorithm=Algorithm
|
||||
totpDigits=Digits
|
||||
totpInterval=Interval
|
||||
totpCounter=Counter
|
||||
totpDeviceName=Device Name
|
||||
|
||||
missingUsernameMessage=Please specify username.
|
||||
missingFirstNameMessage=Please specify first name.
|
||||
@@ -158,6 +160,7 @@ notMatchPasswordMessage=Passwords don''t match.
|
||||
invalidUserMessage=Invalid user
|
||||
|
||||
missingTotpMessage=Please specify authenticator code.
|
||||
missingTotpDeviceNameMessage=Please specify device name.
|
||||
invalidPasswordExistingMessage=Invalid existing password.
|
||||
invalidPasswordConfirmMessage=Password confirmation doesn''t match.
|
||||
invalidTotpMessage=Invalid authenticator code.
|
||||
@@ -300,7 +303,7 @@ authenticatorBackupCodesFinishSetUpMessage=12 backup codes were generated at thi
|
||||
authenticatorMobileSetupTitle=Mobile Authenticator Setup
|
||||
smscodeIntroMessage=Enter your phone number and a verification code will be sent to your phone.
|
||||
mobileSetupStep1=Install an authenticator application on your phone. The applications listed here are supported.
|
||||
mobileSetupStep2=Open the application and scan the barcode.
|
||||
mobileSetupStep2=Open the application and scan the barcode:
|
||||
mobileSetupStep3=Enter the one-time code provided by the application and click Save to finish the setup.
|
||||
scanBarCode=Want to scan the barcode?
|
||||
enterBarCode=Enter the one-time code
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
<#import "template.ftl" as layout>
|
||||
<@layout.mainLayout active='totp' bodyClass='totp'; section>
|
||||
|
||||
<h2>${msg("authenticatorTitle")}</h2>
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<h2>${msg("authenticatorTitle")}</h2>
|
||||
</div>
|
||||
<#if totp.otpCredentials?size == 0>
|
||||
<div class="col-md-2 subtitle">
|
||||
<span class="subtitle"><span class="required">*</span> ${msg("requiredFields")}</span>
|
||||
</div>
|
||||
</#if>
|
||||
</div>
|
||||
|
||||
<#if totp.enabled>
|
||||
<table class="table table-bordered table-striped">
|
||||
<thead>
|
||||
@@ -80,6 +90,7 @@
|
||||
</#if>
|
||||
<li>
|
||||
<p>${msg("totpStep3")}</p>
|
||||
<p>${msg("totpStep3DeviceName")}</p>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
@@ -89,7 +100,7 @@
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
|
||||
<div class="form-group">
|
||||
<div class="col-sm-2 col-md-2">
|
||||
<label for="totp" class="control-label">${msg("authenticatorCode")}</label>
|
||||
<label for="totp" class="control-label">${msg("authenticatorCode")}</label> <span class="required">*</span>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-10 col-md-10">
|
||||
@@ -100,9 +111,9 @@
|
||||
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-group" ${messagesPerField.printIfExists('userLabel',properties.kcFormGroupErrorClass!)}">
|
||||
<div class="col-sm-2 col-md-2">
|
||||
<label for="userLabel" class="control-label">Device Name</label>
|
||||
<label for="userLabel" class="control-label">${msg("totpDeviceName")}</label> <#if totp.otpCredentials?size gte 1><span class="required">*</span></#if>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-10 col-md-10">
|
||||
@@ -115,10 +126,12 @@
|
||||
<div class="">
|
||||
<button type="submit"
|
||||
class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}"
|
||||
name="submitAction" value="Save">${msg("doSave")}</button>
|
||||
id="saveTOTPBtn" name="submitAction" value="Save">${msg("doSave")}
|
||||
</button>
|
||||
<button type="submit"
|
||||
class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}"
|
||||
name="submitAction" value="Cancel">${msg("doCancel")}</button>
|
||||
id="cancelTOTPBtn" name="submitAction" value="Cancel">${msg("doCancel")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<#import "template.ftl" as layout>
|
||||
<@layout.registrationLayout displayInfo=true; section>
|
||||
<@layout.registrationLayout displayInfo=true displayRequiredFields=true; section>
|
||||
|
||||
<#if section = "header">
|
||||
${msg("loginTotpTitle")}
|
||||
<#elseif section = "form">
|
||||
|
||||
<#elseif section = "form">
|
||||
|
||||
<ol id="kc-totp-settings">
|
||||
<li>
|
||||
@@ -46,11 +47,15 @@
|
||||
</#if>
|
||||
<li>
|
||||
<p>${msg("loginTotpStep3")}</p>
|
||||
<p>${msg("loginTotpStep3DeviceName")}</p>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<form action="${url.loginAction}" class="${properties.kcFormClass!}" id="kc-totp-settings-form" method="post">
|
||||
<div class="${properties.kcFormGroupClass!}">
|
||||
<div class="${properties.kcInputWrapperClass!}">
|
||||
<label for="totp" class="control-label">${msg("authenticatorCode")}</label> <span class="required">*</span>
|
||||
</div>
|
||||
<div class="${properties.kcInputWrapperClass!}">
|
||||
<input type="text" id="totp" name="totp" autocomplete="off" class="${properties.kcInputClass!}" />
|
||||
</div>
|
||||
@@ -58,9 +63,9 @@
|
||||
<#if mode??><input type="hidden" id="mode" name="mode" value="${mode}"/></#if>
|
||||
</div>
|
||||
|
||||
<div class="${properties.kcFormGroupClass!}">
|
||||
<div class="${properties.kcFormGroupClass!}" ${messagesPerField.printIfExists('userLabel',properties.kcFormGroupErrorClass!)}">
|
||||
<div class="${properties.kcInputWrapperClass!}">
|
||||
<label for="userLabel" class="control-label">Device Name</label>
|
||||
<label for="userLabel" class="control-label">${msg("loginTotpDeviceName")}</label> <#if totp.otpCredentials?size gte 1><span class="required">*</span></#if>
|
||||
</div>
|
||||
|
||||
<div class="${properties.kcInputWrapperClass!}">
|
||||
@@ -69,10 +74,19 @@
|
||||
</div>
|
||||
|
||||
<#if isAppInitiatedAction??>
|
||||
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doSubmit")}" />
|
||||
<button class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!} ${properties.kcButtonLargeClass!}" type="submit" name="cancel-aia" value="true" />${msg("doCancel")}</button>
|
||||
<input type="submit"
|
||||
class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}"
|
||||
id="saveTOTPBtn" value="${msg("doSubmit")}"
|
||||
/>
|
||||
<button type="submit"
|
||||
class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!} ${properties.kcButtonLargeClass!}"
|
||||
id="cancelTOTPBtn" name="cancel-aia" value="true" />${msg("doCancel")}
|
||||
</button>
|
||||
<#else>
|
||||
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doSubmit")}" />
|
||||
<input type="submit"
|
||||
class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"
|
||||
id="saveTOTPBtn" value="${msg("doSubmit")}"
|
||||
/>
|
||||
</#if>
|
||||
</form>
|
||||
</#if>
|
||||
|
||||
@@ -86,11 +86,12 @@ rolesScopeConsentText=User roles
|
||||
restartLoginTooltip=Restart login
|
||||
|
||||
loginTotpIntro=You need to set up a One Time Password generator to access this account
|
||||
loginTotpStep1=Install one of the following applications on your mobile
|
||||
loginTotpStep2=Open the application and scan the barcode
|
||||
loginTotpStep3=Enter the one-time code provided by the application and click Submit to finish the setup
|
||||
loginTotpManualStep2=Open the application and enter the key
|
||||
loginTotpManualStep3=Use the following configuration values if the application allows setting them
|
||||
loginTotpStep1=Install one of the following applications on your mobile:
|
||||
loginTotpStep2=Open the application and scan the barcode:
|
||||
loginTotpStep3=Enter the one-time code provided by the application and click Submit to finish the setup.
|
||||
loginTotpStep3DeviceName=Provide a Device Name to help you manage your OTP devices.
|
||||
loginTotpManualStep2=Open the application and enter the key:
|
||||
loginTotpManualStep3=Use the following configuration values if the application allows setting them:
|
||||
loginTotpUnableToScan=Unable to scan?
|
||||
loginTotpScanBarcode=Scan barcode?
|
||||
loginCredential=Credential
|
||||
@@ -100,6 +101,7 @@ loginTotpAlgorithm=Algorithm
|
||||
loginTotpDigits=Digits
|
||||
loginTotpInterval=Interval
|
||||
loginTotpCounter=Counter
|
||||
loginTotpDeviceName=Device Name
|
||||
|
||||
loginTotp.totp=Time-based
|
||||
loginTotp.hotp=Counter-based
|
||||
@@ -159,6 +161,8 @@ client_admin-cli=Admin CLI
|
||||
client_realm-management=Realm Management
|
||||
client_broker=Broker
|
||||
|
||||
requiredFields=Required fields
|
||||
|
||||
invalidUserMessage=Invalid username or password.
|
||||
invalidUsernameMessage=Invalid username.
|
||||
invalidUsernameOrEmailMessage=Invalid username or email.
|
||||
@@ -177,6 +181,7 @@ missingEmailMessage=Please specify email.
|
||||
missingUsernameMessage=Please specify username.
|
||||
missingPasswordMessage=Please specify password.
|
||||
missingTotpMessage=Please specify authenticator code.
|
||||
missingTotpDeviceNameMessage=Please specify device name.
|
||||
notMatchPasswordMessage=Passwords don''t match.
|
||||
|
||||
invalidPasswordExistingMessage=Invalid existing password.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<#macro registrationLayout bodyClass="" displayInfo=false displayMessage=true displayWide=false>
|
||||
<#macro registrationLayout bodyClass="" displayInfo=false displayMessage=true displayRequiredFields=false displayWide=false>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" class="${properties.kcHtmlClass!}">
|
||||
|
||||
@@ -52,22 +52,55 @@
|
||||
</div>
|
||||
</div>
|
||||
</#if>
|
||||
<#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())>
|
||||
<h1 id="kc-page-title"><#nested "header"></h1>
|
||||
<#else>
|
||||
<#nested "show-username">
|
||||
<div class="${properties.kcFormGroupClass!}">
|
||||
<div id="kc-username">
|
||||
<label id="kc-attempted-username">${auth.attemptedUsername}</label>
|
||||
<a id="reset-login" href="${url.loginRestartFlowUrl}">
|
||||
<div class="kc-login-tooltip">
|
||||
<i class="${properties.kcResetFlowIcon!}"></i>
|
||||
<span class="kc-tooltip-text">${msg("restartLoginTooltip")}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</#if>
|
||||
<#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())>
|
||||
<#if displayRequiredFields>
|
||||
<div class="${properties.kcContentWrapperClass!}">
|
||||
<div class="${properties.kcLabelWrapperClass!} subtitle">
|
||||
<span class="subtitle"><span class="required">*</span> ${msg("requiredFields")}</span>
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<h1 id="kc-page-title"><#nested "header"></h1>
|
||||
</div>
|
||||
</div>
|
||||
<#else>
|
||||
<h1 id="kc-page-title"><#nested "header"></h1>
|
||||
</#if>
|
||||
<#else>
|
||||
<#if displayRequiredFields>
|
||||
<div class="${properties.kcContentWrapperClass!}">
|
||||
<div class="${properties.kcLabelWrapperClass!} subtitle">
|
||||
<span class="subtitle"><span class="required">*</span> ${msg("requiredFields")}</span>
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<#nested "show-username">
|
||||
<div class="${properties.kcFormGroupClass!}">
|
||||
<div id="kc-username">
|
||||
<label id="kc-attempted-username">${auth.attemptedUsername}</label>
|
||||
<a id="reset-login" href="${url.loginRestartFlowUrl}">
|
||||
<div class="kc-login-tooltip">
|
||||
<i class="${properties.kcResetFlowIcon!}"></i>
|
||||
<span class="kc-tooltip-text">${msg("restartLoginTooltip")}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<#else>
|
||||
<#nested "show-username">
|
||||
<div class="${properties.kcFormGroupClass!}">
|
||||
<div id="kc-username">
|
||||
<label id="kc-attempted-username">${auth.attemptedUsername}</label>
|
||||
<a id="reset-login" href="${url.loginRestartFlowUrl}">
|
||||
<div class="kc-login-tooltip">
|
||||
<i class="${properties.kcResetFlowIcon!}"></i>
|
||||
<span class="kc-tooltip-text">${msg("restartLoginTooltip")}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</#if>
|
||||
</#if>
|
||||
</header>
|
||||
<div id="kc-content">
|
||||
<div id="kc-content-wrapper">
|
||||
|
||||
@@ -159,6 +159,16 @@ div.kc-logo-text span {
|
||||
|
||||
/* TOTP */
|
||||
|
||||
.subtitle {
|
||||
text-align: right;
|
||||
margin-top: 30px;
|
||||
color: #909090;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: #CB2915;
|
||||
}
|
||||
|
||||
ol#kc-totp-settings {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
|
||||
Reference in New Issue
Block a user