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:
parent
cbed5849f5
commit
041441f48f
14 changed files with 460 additions and 5 deletions
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
|
@ -62,6 +62,8 @@ public interface LoginFormsProvider extends Provider {
|
|||
|
||||
Response createLoginPassword();
|
||||
|
||||
Response createOtpReset();
|
||||
|
||||
Response createPasswordReset();
|
||||
|
||||
Response createLoginTotp();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue