Improved Reset OTP authenticator (#20572)

* ResetOTP authenticator can now be configured, so that one or all existing OTP configurations are deleted upon reset.

Closes #8753
---------

Co-authored-by: bal1imb <Artur.Baltabayev@bosch.com>
This commit is contained in:
Artur Baltabayev 2023-06-06 13:30:44 +02:00 committed by GitHub
parent cbed5849f5
commit 041441f48f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 460 additions and 5 deletions

View file

@ -28,6 +28,6 @@ public enum LoginFormsPages {
LOGIN_PAGE_EXPIRED, CODE, X509_CONFIRM, SAML_POST_FORM,
LOGIN_OAUTH2_DEVICE_VERIFY_USER_CODE, UPDATE_USER_PROFILE, IDP_REVIEW_USER_PROFILE,
LOGIN_RECOVERY_AUTHN_CODES_INPUT, LOGIN_RECOVERY_AUTHN_CODES_CONFIG,
FRONTCHANNEL_LOGOUT, LOGOUT_CONFIRM, UPDATE_EMAIL;
FRONTCHANNEL_LOGOUT, LOGOUT_CONFIRM, UPDATE_EMAIL, LOGIN_RESET_OTP;
}

View file

@ -62,6 +62,8 @@ public interface LoginFormsProvider extends Provider {
Response createLoginPassword();
Response createOtpReset();
Response createPasswordReset();
Response createLoginTotp();

View file

@ -17,13 +17,28 @@
package org.keycloak.authentication.authenticators.resetcred;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.CredentialValidator;
import org.keycloak.credential.CredentialModel;
import org.keycloak.credential.CredentialProvider;
import org.keycloak.credential.OTPCredentialProvider;
import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.credential.OTPCredentialModel;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import org.keycloak.services.messages.Messages;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static java.util.Arrays.asList;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -33,12 +48,93 @@ public class ResetOTP extends AbstractSetRequiredActionAuthenticator implements
public static final String PROVIDER_ID = "reset-otp";
private static final String ACTION_ON_OTP_RESET_FLAG = "action_on_otp_reset_flag";
private static final String REMOVE_NONE = "Remove none";
private static final String REMOVE_ONE = "Remove one";
private static final String REMOVE_ALL = "Remove all";
@Override
public void authenticate(AuthenticationFlowContext context) {
AuthenticatorConfigModel authenticatorConfigModel = context.getAuthenticatorConfig();
Map<String, String> authenticatorConfig = null;
if (authenticatorConfigModel != null) {
authenticatorConfig = authenticatorConfigModel.getConfig();
}
if (authenticatorConfig != null) {
String selectedOption = authenticatorConfig.get(ACTION_ON_OTP_RESET_FLAG);
List<CredentialModel> otpCredentialModelList = context.getUser().credentialManager()
.getStoredCredentialsByTypeStream(OTPCredentialModel.TYPE).collect(Collectors.toList());
if (REMOVE_ALL.equals(selectedOption)) {
otpCredentialModelList.forEach(otpCredentialModel -> context.getUser().credentialManager()
.removeStoredCredentialById(otpCredentialModel.getId()));
}
else if (REMOVE_ONE.equals(selectedOption) && !otpCredentialModelList.isEmpty()) {
Response challengeResponse = context.form()
.setAttribute("configuredOtpCredentials", otpCredentialModelList)
.createOtpReset();
context.challenge(challengeResponse);
return;
}
}
// To ensure backwards compatability, the required action has to be set even if no configuration is available.
context.getAuthenticationSession().addRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP);
context.success();
}
@Override
public void action(AuthenticationFlowContext context) {
MultivaluedMap<String, String> inputData = context.getHttpRequest().getDecodedFormParameters();
String credentialId = inputData.getFirst("selectedCredentialId");
// This case should never occur. If you there are no OTP credentials available the form will never be displayed in the first place.
// If the form is displayed the first OTP credential is selected by default, and it's not possible to unselect radio buttons.
if (credentialId == null || credentialId.isEmpty()) {
List<CredentialModel> otpCredentialModelList = context.getUser().credentialManager()
.getStoredCredentialsByTypeStream(OTPCredentialModel.TYPE).collect(Collectors.toList());
Response challengeResponse = context.form()
.setAttribute("configuredOtpCredentials", otpCredentialModelList)
.setError(Messages.RESET_OTP_MISSING_ID_ERROR)
.createOtpReset();
context.challenge(challengeResponse);
return;
}
context.getUser().credentialManager().removeStoredCredentialById(credentialId);
context.getAuthenticationSession().addRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP);
context.success();
}
@Override public boolean isConfigurable() {
return true;
}
@Override public List<ProviderConfigProperty> getConfigProperties() {
ProviderConfigurationBuilder builder = ProviderConfigurationBuilder.create();
builder.property(ACTION_ON_OTP_RESET_FLAG,
"Action on OTP reset.",
" If 'Remove none' is chosen, the user will keep all existing OTP configurations (legacy behavior)." +
" If 'Remove one' is chosen, the user will be prompted to choose one OTP configuration which will then be removed." +
" If 'Remove all' is chosen, all existing OTP configurations of the user will be removed." +
" The user will always be prompted to configure a new OTP no matter which option is selected.",
ProviderConfigProperty.LIST_TYPE,
REMOVE_NONE,
asList(REMOVE_NONE, REMOVE_ONE, REMOVE_ALL));
return builder.build();
}
@Override
public OTPCredentialProvider getCredentialProvider(KeycloakSession session) {
return (OTPCredentialProvider)session.getProvider(CredentialProvider.class, "keycloak-otp");
@ -56,7 +152,7 @@ public class ResetOTP extends AbstractSetRequiredActionAuthenticator implements
@Override
public String getHelpText() {
return "Sets the Configure OTP required action.";
return "Removes existing OTP configurations (if chosen) and sets the 'Configure OTP' required action.";
}
@Override

View file

@ -259,6 +259,9 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
case LOGIN_TOTP:
attributes.put("otpLogin", new TotpLoginBean(session, realm, user, (String) this.attributes.get(OTPFormAuthenticator.SELECTED_OTP_CREDENTIAL_ID)));
break;
case LOGIN_RESET_OTP:
attributes.put("configuredOtpCredentials", new TotpLoginBean(session, realm, user, (String) this.attributes.get(OTPFormAuthenticator.SELECTED_OTP_CREDENTIAL_ID)));
break;
case REGISTER:
if(isDynamicUserProfile()) {
page = LoginFormsPages.REGISTER_USER_PROFILE;
@ -549,7 +552,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
public Response createLoginPassword(){
return createResponse(LoginFormsPages.LOGIN_PASSWORD);
};
}
@Override
public Response createPasswordReset() {
@ -560,6 +563,10 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
return createResponse(LoginFormsPages.LOGIN_RESET_PASSWORD);
}
@Override public Response createOtpReset() {
return createResponse(LoginFormsPages.LOGIN_RESET_OTP);
}
@Override
public Response createLoginTotp() {
return createResponse(LoginFormsPages.LOGIN_TOTP);

View file

@ -34,6 +34,8 @@ public class Templates {
return "login-password.ftl";
case LOGIN_TOTP:
return "login-otp.ftl";
case LOGIN_RESET_OTP:
return "login-reset-otp.ftl";
case LOGIN_CONFIG_TOTP:
return "login-config-totp.ftl";
case LOGIN_RECOVERY_AUTHN_CODES_INPUT:

View file

@ -62,6 +62,8 @@ public class Messages {
public static final String MISSING_TOTP = "missingTotpMessage";
public static final String RESET_OTP_MISSING_ID_ERROR = "error-reset-otp-missing-id";
public static final String MISSING_TOTP_DEVICE_NAME = "missingTotpDeviceNameMessage";
public static final String COOKIE_NOT_FOUND = "cookieNotFoundMessage";

View file

@ -0,0 +1,32 @@
package org.keycloak.testsuite.pages;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
public class ResetOtpPage extends AbstractPage {
@FindBy(id = "kc-otp-reset-form-submit")
protected WebElement submitButton;
@FindBy(id = "kc-otp-reset-form-description")
protected WebElement description;
@Override
public boolean isCurrent() {
return description.getText().equals("Which OTP configuration should be removed?");
}
@Override
public void open() throws Exception {
// This page is part of the reset credentials flow, so you shouldn't be able to open it by itself.
}
public void selectOtp(int index) {
driver.findElement(By.id("kc-otp-credential-" + index)).click();
}
public void submitOtpReset() {
submitButton.click();
}
}

View file

@ -227,7 +227,7 @@ public class InitialFlowsTest extends AbstractAuthenticationTest {
addExecInfo(execs, "Reset Password", "reset-password", false, 0, 2, REQUIRED, null, new String[]{REQUIRED, ALTERNATIVE, DISABLED});
addExecInfo(execs, "Reset - Conditional OTP", null, false, 0, 3, CONDITIONAL, true, new String[]{REQUIRED, ALTERNATIVE, DISABLED, CONDITIONAL});
addExecInfo(execs, "Condition - user configured", "conditional-user-configured", false, 1, 0, REQUIRED, null, new String[]{REQUIRED, DISABLED});
addExecInfo(execs, "Reset OTP", "reset-otp", false, 1, 1, REQUIRED, null, new String[]{REQUIRED, ALTERNATIVE, DISABLED});
addExecInfo(execs, "Reset OTP", "reset-otp", true, 1, 1, REQUIRED, null, new String[]{REQUIRED, ALTERNATIVE, DISABLED});
expected.add(new FlowExecutions(flow, execs));
return expected;

View file

@ -184,7 +184,7 @@ public class ProvidersTest extends AbstractAuthenticationTest {
"Just press the button to login.");
addProviderInfo(result, "reset-credential-email", "Send Reset Email", "Send email to user and wait for response.");
addProviderInfo(result, "reset-credentials-choose-user", "Choose User", "Choose a user to reset credentials for");
addProviderInfo(result, "reset-otp", "Reset OTP", "Sets the Configure OTP required action.");
addProviderInfo(result, "reset-otp", "Reset OTP", "Removes existing OTP configurations (if chosen) and sets the 'Configure OTP' required action.");
addProviderInfo(result, "reset-password", "Reset Password", "Sets the Update Password required action if execution is REQUIRED. " +
"Will also set it if execution is OPTIONAL and the password is currently configured for it.");
addProviderInfo(result, "testsuite-dummy-click-through", "Testsuite Dummy Click Thru",

View file

@ -0,0 +1,277 @@
package org.keycloak.testsuite.forms;
import static org.wildfly.common.Assert.assertTrue;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Test;
import org.keycloak.admin.client.CreatedResponseUtil;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.representations.idm.AuthenticationExecutionRepresentation;
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
import org.keycloak.representations.idm.AuthenticatorConfigRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.LoginPasswordResetPage;
import org.keycloak.testsuite.pages.ResetOtpPage;
import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.UserBuilder;
import java.util.Map;
import java.util.stream.Collectors;
public class ResetOtpTest extends AbstractTestRealmKeycloakTest {
@Page
protected LoginPage loginPage;
@Page
protected ResetOtpPage resetOtpPage;
@Page
protected LoginPasswordResetPage resetPasswordPage;
private static RealmResource realmResource;
private static String resetOtpExecutionId;
private static String resetOtpConfigId;
private static String flowId;
private static final String FLOW_ALIAS = "otpResetTestFlow";
private static final String RESET_OTP_TEST_USER_REMOVE_NONE = "reset-otp-test-user-remove-none";
private static final String RESET_OTP_TEST_USER_REMOVE_ONE = "reset-otp-test-user-remove-one";
private static final String RESET_OTP_TEST_USER_REMOVE_ALL = "reset-otp-test-user-remove-all";
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
var secretCredentialData = "{\"digits\":6,\"counter\":0,\"period\":30,\"algorithm\":\"HmacSHA1\",\"subType\":\"totp\"}";
var removeNoneCredential = new CredentialRepresentation();
removeNoneCredential.setId("Otp-RemoveNone");
removeNoneCredential.setType("otp");
removeNoneCredential.setUserLabel("Otp");
removeNoneCredential.setCreatedDate(-1L);
removeNoneCredential.setSecretData("{\"value\":\"DJmQfC73VGFhw7D4QJ8C\"}");
removeNoneCredential.setCredentialData(secretCredentialData);
var removeAllCredential1 = new CredentialRepresentation();
removeAllCredential1.setId("Otp1-RemoveAll");
removeAllCredential1.setType("otp");
removeAllCredential1.setUserLabel("Otp1");
removeAllCredential1.setCreatedDate(-1L);
removeAllCredential1.setSecretData("{\"value\":\"DJmQfC73VGFhw7D4QJ8D\"}");
removeAllCredential1.setCredentialData(secretCredentialData);
var removeAllCredential2 = new CredentialRepresentation();
removeAllCredential2.setId("Otp2-RemoveAll");
removeAllCredential2.setType("otp");
removeAllCredential2.setUserLabel("Otp2");
removeAllCredential2.setCreatedDate(-1L);
removeAllCredential2.setSecretData("{\"value\":\"DJmQfC73VGFhw7D4QJ8E\"}");
removeAllCredential2.setCredentialData(secretCredentialData);
var removeOneCredential1 = new CredentialRepresentation();
removeOneCredential1.setId("Otp1-RemoveOne");
removeOneCredential1.setType("otp");
removeOneCredential1.setUserLabel("Otp1");
removeOneCredential1.setCreatedDate(-1L);
removeOneCredential1.setSecretData("{\"value\":\"DJmQfC73VGFhw7D4QJ8F\"}");
removeOneCredential1.setCredentialData(secretCredentialData);
var removeOneCredential2 = new CredentialRepresentation();
removeOneCredential2.setId("Otp2-RemoveOne");
removeOneCredential2.setType("otp");
removeOneCredential2.setUserLabel("Otp2");
removeOneCredential2.setCreatedDate(-1L);
removeOneCredential2.setSecretData("{\"value\":\"DJmQfC73VGFhw7D4QJ8G\"}");
removeOneCredential2.setCredentialData(secretCredentialData);
var userRemoveNone = UserBuilder.create();
userRemoveNone.username("reset-otp-test-user-remove-none");
userRemoveNone.secret(removeNoneCredential);
var userRemoveOne = UserBuilder.create();
userRemoveOne.username("reset-otp-test-user-remove-one");
userRemoveOne.secret(removeOneCredential1);
userRemoveOne.secret(removeOneCredential2);
var userRemoveAll = UserBuilder.create();
userRemoveAll.username("reset-otp-test-user-remove-all");
userRemoveAll.secret(removeAllCredential1);
userRemoveAll.secret(removeAllCredential2);
RealmBuilder.edit(testRealm).user(userRemoveNone.build()).user(userRemoveOne.build()).user(userRemoveAll.build());
realmResource = adminClient.realm(testRealm.getRealm());
}
@Test
public void noOtpIsRemovedOnResetWithoutConfig_LegacyBehaviour() {
createOrChangeResetOtpFlowConfig(null);
var userRep = realmResource.users().search(RESET_OTP_TEST_USER_REMOVE_NONE).get(0);
var credentials = realmResource.users().get(userRep.getId()).credentials();
var otpCredentials = credentials.stream().filter(credentialRep -> "otp".equals(credentialRep.getType()))
.collect(Collectors.toList());
assertTrue(otpCredentials.size() == 1);
loginPage.open();
loginPage.resetPassword();
resetPasswordPage.changePassword(RESET_OTP_TEST_USER_REMOVE_NONE);
credentials = realmResource.users().get(userRep.getId()).credentials();
otpCredentials = credentials.stream().filter(credentialRep -> "otp".equals(credentialRep.getType()))
.collect(Collectors.toList());
assertTrue(otpCredentials.size() == 1);
}
@Test
public void noOtpIsRemovedOnResetWithConfig() {
createOrChangeResetOtpFlowConfig("Remove none");
var userRep = realmResource.users().search(RESET_OTP_TEST_USER_REMOVE_NONE).get(0);
var credentials = realmResource.users().get(userRep.getId()).credentials();
var otpCredentials = credentials.stream().filter(credentialRep -> "otp".equals(credentialRep.getType()))
.collect(Collectors.toList());
assertTrue(otpCredentials.size() == 1);
loginPage.open();
loginPage.resetPassword();
resetPasswordPage.changePassword(RESET_OTP_TEST_USER_REMOVE_NONE);
credentials = realmResource.users().get(userRep.getId()).credentials();
otpCredentials = credentials.stream().filter(credentialRep -> "otp".equals(credentialRep.getType()))
.collect(Collectors.toList());
assertTrue(otpCredentials.size() == 1);
}
@Test
public void removeOneSpecificOtpOnReset() {
createOrChangeResetOtpFlowConfig("Remove one");
var userRep = realmResource.users().search(RESET_OTP_TEST_USER_REMOVE_ONE).get(0);
var credentials = realmResource.users().get(userRep.getId()).credentials();
var otpCredentials = credentials.stream().filter(credentialRep -> "otp".equals(credentialRep.getType()))
.collect(Collectors.toList());
assertTrue(otpCredentials.size() == 2);
loginPage.open();
loginPage.resetPassword();
resetPasswordPage.changePassword(RESET_OTP_TEST_USER_REMOVE_ONE);
resetOtpPage.selectOtp(1);
resetOtpPage.submitOtpReset();
credentials = realmResource.users().get(userRep.getId()).credentials();
otpCredentials = credentials.stream().filter(credentialRep -> "otp".equals(credentialRep.getType()))
.collect(Collectors.toList());
assertTrue(otpCredentials.size() == 1);
// Since we selected to remove the second OTP, the first one should still be there.
assertTrue("Otp1-RemoveOne".equals(otpCredentials.get(0).getId()));
loginPage.open();
loginPage.resetPassword();
resetPasswordPage.changePassword(RESET_OTP_TEST_USER_REMOVE_ONE);
// Here the last remaining OTP is selected automatically.
resetOtpPage.isCurrent();
resetOtpPage.submitOtpReset();
credentials = realmResource.users().get(userRep.getId()).credentials();
otpCredentials = credentials.stream().filter(credentialRep -> "otp".equals(credentialRep.getType()))
.collect(Collectors.toList());
assertTrue(otpCredentials.isEmpty());
}
@Test
public void removeAllOtpsOnReset() {
createOrChangeResetOtpFlowConfig("Remove all");
var userRep = realmResource.users().search(RESET_OTP_TEST_USER_REMOVE_ALL).get(0);
var credentials = realmResource.users().get(userRep.getId()).credentials();
var otpCredentials = credentials.stream().filter(credentialRep -> "otp".equals(credentialRep.getType()))
.collect(Collectors.toList());
assertTrue(otpCredentials.size() == 2);
loginPage.open();
loginPage.resetPassword();
resetPasswordPage.changePassword(RESET_OTP_TEST_USER_REMOVE_ALL);
credentials = realmResource.users().get(userRep.getId()).credentials();
otpCredentials = credentials.stream().filter(credentialRep -> "otp".equals(credentialRep.getType()))
.collect(Collectors.toList());
assertTrue(otpCredentials.isEmpty());
}
private void createOrChangeResetOtpFlowConfig(String configOption) {
/*
* You can't do the flow/authenticator setup inside the configureTestRealm() method because the LegacyExportImportManager
* won't import the default flows if there is already an authentication flow present (no matter which one it is) inside the realm representation
* (the importAuthenticationFlows() method will skip the migrateFlows() step).
*/
if (flowId == null) {
var resetOtpFlow = new AuthenticationFlowRepresentation();
resetOtpFlow.setAlias(FLOW_ALIAS);
resetOtpFlow.setProviderId("basic-flow");
resetOtpFlow.setBuiltIn(false);
resetOtpFlow.setTopLevel(true);
flowId = CreatedResponseUtil.getCreatedId(realmResource.flows().createFlow(resetOtpFlow));
var chooseUserExecutionRep = new AuthenticationExecutionRepresentation();
chooseUserExecutionRep.setParentFlow(flowId);
chooseUserExecutionRep.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED.name());
chooseUserExecutionRep.setAuthenticator("reset-credentials-choose-user");
var conditionalUserConfiguredExecutionRep = new AuthenticationExecutionRepresentation();
conditionalUserConfiguredExecutionRep.setParentFlow(flowId);
conditionalUserConfiguredExecutionRep.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED.name());
conditionalUserConfiguredExecutionRep.setAuthenticator("conditional-user-configured");
var resetOtpExecutionRep = new AuthenticationExecutionRepresentation();
resetOtpExecutionRep.setParentFlow(flowId);
resetOtpExecutionRep.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED.name());
resetOtpExecutionRep.setAuthenticator("reset-otp");
realmResource.flows().addExecution(chooseUserExecutionRep);
realmResource.flows().addExecution(conditionalUserConfiguredExecutionRep);
resetOtpExecutionId = CreatedResponseUtil.getCreatedId(realmResource.flows().addExecution(resetOtpExecutionRep));
var realmRep = realmResource.toRepresentation();
realmRep.setResetCredentialsFlow(FLOW_ALIAS);
realmRep.setResetPasswordAllowed(true);
realmResource.update(realmRep);
}
var resetOtpAuthConfigRep = new AuthenticatorConfigRepresentation();
resetOtpAuthConfigRep.setAlias("ResetOtpConfig");
if (configOption == null) {
if (resetOtpConfigId != null) {
realmResource.flows().removeAuthenticatorConfig(resetOtpConfigId);
}
return;
}
resetOtpAuthConfigRep.setConfig(Map.of("action_on_otp_reset_flag", configOption));
if(resetOtpConfigId == null) {
resetOtpConfigId = CreatedResponseUtil
.getCreatedId(realmResource.flows().newExecutionConfig(resetOtpExecutionId, resetOtpAuthConfigRep));
}
else {
realmResource.flows().updateAuthenticatorConfig(resetOtpConfigId, resetOtpAuthConfigRep);
}
}
}

View file

@ -349,6 +349,7 @@ auth-username-form-display-name=Benutzername
auth-username-form-help-text=Anmelden durch Eingabe des Benutzernamens
auth-username-password-form-display-name=Benutzername und Passwort
auth-username-password-form-help-text=Anmelden, indem Sie Ihren Benutzernamen und Ihr Passwort eingeben.
error-reset-otp-missing-id=Bitte w\u00E4hlen Sie eine OTP Konfiguration aus.
# Recovery Codes
auth-recovery-authn-code-form-display-name=Wiederherstellungscode

View file

@ -90,6 +90,7 @@ password-help-text=Mit einem Passwort anmelden.
password=Mein Passwort
otp-display-name=Authenticator-Anwendung
otp-help-text=Geben Sie einen Verifizierungscode aus der Authenticator-Anwendung ein.
otp-reset-description=Welche OTP Konfiguration soll entfernt werden?
recovery-authn-code=Meine Wiederherstellungscodes
recovery-authn-codes-display-name=Wiederherstellungscodes
recovery-authn-codes-help-text=Diese Codes k\u00f6nnen verwendet werden, um Ihren Zugang wiederherzustellen, falls Ihre anderen Zwei-Faktor-Mittel nicht verf\u00fcgbar sind.

View file

@ -0,0 +1,33 @@
<#import "template.ftl" as layout>
<@layout.registrationLayout displayMessage=!messagesPerField.existsError('totp'); section>
<#if section="header">
${msg("doLogIn")}
<#elseif section="form">
<form id="kc-otp-reset-form" class="${properties.kcFormClass!}" action="${url.loginAction}"
method="post">
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcInputWrapperClass!}">
<p id="kc-otp-reset-form-description">${msg("otp-reset-description")}</p>
<#list configuredOtpCredentials.userOtpCredentials as otpCredential>
<input id="kc-otp-credential-${otpCredential?index}" class="${properties.kcLoginOTPListInputClass!}" type="radio" name="selectedCredentialId" value="${otpCredential.id}" <#if otpCredential.id == configuredOtpCredentials.selectedCredentialId>checked="checked"</#if>>
<label for="kc-otp-credential-${otpCredential?index}" class="${properties.kcLoginOTPListClass!}" tabindex="${otpCredential?index}">
<span class="${properties.kcLoginOTPListItemHeaderClass!}">
<span class="${properties.kcLoginOTPListItemIconBodyClass!}">
<i class="${properties.kcLoginOTPListItemIconClass!}" aria-hidden="true"></i>
</span>
<span class="${properties.kcLoginOTPListItemTitleClass!}">${otpCredential.userLabel}</span>
</span>
</label>
</#list>
<div class="${properties.kcFormGroupClass!}">
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
<input id="kc-otp-reset-form-submit" class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doSubmit")}"/>
</div>
</div>
</div>
</div>
</form>
</#if>
</@layout.registrationLayout>

View file

@ -250,6 +250,7 @@ error-invalid-date=Invalid date.
error-user-attribute-read-only=This field is read only.
error-username-invalid-character=Value contains invalid character.
error-person-name-invalid-character=Value contains invalid character.
error-reset-otp-missing-id=Please choose an OTP configuration.
invalidPasswordExistingMessage=Invalid existing password.
invalidPasswordBlacklistedMessage=Invalid password: password is blacklisted.
@ -431,6 +432,7 @@ saml.artifactResolutionServiceInvalidResponse=Unable to resolve artifact.
#authenticators
otp-display-name=Authenticator Application
otp-help-text=Enter a verification code from authenticator application.
otp-reset-description=Which OTP configuration should be removed?
password-display-name=Password
password-help-text=Sign in by entering your password.
auth-username-form-display-name=Username