Conditional OTP authentication:
... try the various options of the Conditional OTP Authenticator.
I recommend the chrome ModHeader Plugin to test the header based patterns.
Conditional OTP authentication:
... try the various options of the Conditional OTP Authenticator.
I recommend the chrome ModHeader Plugin to test the header based patterns.
| package org.keycloak.authentication.authenticators.browser; | |
| import org.keycloak.authentication.AuthenticationFlowContext; | |
| import org.keycloak.models.RoleModel; | |
| import org.keycloak.models.UserModel; | |
| import javax.ws.rs.core.MultivaluedMap; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.regex.Pattern; | |
| import static org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticator.OtpDecision.*; | |
| import static org.keycloak.models.utils.KeycloakModelUtils.getRoleFromString; | |
| import static org.keycloak.models.utils.KeycloakModelUtils.hasRole; | |
| /** | |
| * An {@link OTPFormAuthenticator} that can conditionally require OTP authentication. | |
| * <p> | |
| * <p> | |
| * The decision for whether or not to require OTP authentication can be made based on multiple conditions | |
| * which are evaluated in the following order. The first matching condition determines the outcome. | |
| * </p> | |
| * <ol> | |
| * <li>User Attribute</li> | |
| * <li>Role</li> | |
| * <li>Request Header</li> | |
| * <li>Configured Default</li> | |
| * </ol> | |
| * <p> | |
| * If no condition matches, the {@link ConditionalOtpFormAuthenticator} fallback is to require OTP authentication. | |
| * </p> | |
| * <p> | |
| * <h2>User Attribute</h2> | |
| * A User Attribute like <code>otp_auth</code> can be used to control OTP authentication on individual user level. | |
| * The supported values are <i>skip</i> and <i>force</i>. If the value is set to <i>skip</i> then the OTP auth is skipped for the user, | |
| * otherwise if the value is <i>force</i> then the OTP auth is enforced. The setting is ignored for any other value. | |
| * </p> | |
| * <p> | |
| * <h2>Role</h2> | |
| * A role can be used to control the OTP authentication. If the user has the specified role the OTP authentication is forced. | |
| * Otherwise if no role is selected the setting is ignored. | |
| * <p> | |
| * </p> | |
| * <p> | |
| * <h2>Request Header</h2> | |
| * <p> | |
| * Request Headers are matched via regex {@link Pattern}s and can be specified as a whitelist and blacklist. | |
| * <i>No OTP for Header</i> specifies the pattern for which OTP authentication <b>is not</b> required. | |
| * This can be used to specify trusted networks, e.g. via: <code>X-Forwarded-Host: (1.2.3.4|1.2.3.5)</code> where | |
| * The IPs 1.2.3.4, 1.2.3.5 denote trusted machines. | |
| * <i>Force OTP for Header</i> specifies the pattern for which OTP authentication <b>is</b> required. Whitelist entries take | |
| * precedence before blacklist entries. | |
| * </p> | |
| * <p> | |
| * <h2>Configured Default</h2> | |
| * A default fall-though behaviour can be specified to handle cases where all previous conditions did not lead to a conclusion. | |
| * An OTP authentication is required in case no default is configured. | |
| * </p> | |
| * | |
| * @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a> | |
| */ | |
| public class ConditionalOtpFormAuthenticator extends OTPFormAuthenticator { | |
| public static final String SKIP = "skip"; | |
| public static final String FORCE = "force"; | |
| public static final String OTP_CONTROL_USER_ATTRIBUTE = "otpControlAttribute"; | |
| public static final String FORCE_OTP_ROLE = "forceOtpRole"; | |
| public static final String NO_OTP_REQUIRED_FOR_HTTP_HEADER = "noOtpRequiredForHeaderPattern"; | |
| public static final String FORCE_OTP_FOR_HTTP_HEADER = "forceOtpForHeaderPattern"; | |
| public static final String DEFAULT_OTP_OUTCOME = "defaultOtpOutcome"; | |
| enum OtpDecision { | |
| SKIP_OTP, SHOW_OTP, ABSTAIN | |
| } | |
| @Override | |
| public void authenticate(AuthenticationFlowContext context) { | |
| Map<String, String> config = context.getAuthenticatorConfig().getConfig(); | |
| if (tryConcludeBasedOn(voteForUserOtpControlAttribute(context, config), context)) { | |
| return; | |
| } | |
| if (tryConcludeBasedOn(voteForUserForceOtpRole(context, config), context)) { | |
| return; | |
| } | |
| if (tryConcludeBasedOn(voteForHttpHeaderMatchesPattern(context, config), context)) { | |
| return; | |
| } | |
| if (tryConcludeBasedOn(voteForDefaultFallback(context, config), context)) { | |
| return; | |
| } | |
| showOtpForm(context); | |
| } | |
| private OtpDecision voteForDefaultFallback(AuthenticationFlowContext context, Map<String, String> config) { | |
| if (!config.containsKey(DEFAULT_OTP_OUTCOME)) { | |
| return ABSTAIN; | |
| } | |
| switch (config.get(DEFAULT_OTP_OUTCOME)) { | |
| case SKIP: | |
| return SKIP_OTP; | |
| case FORCE: | |
| return SHOW_OTP; | |
| default: | |
| return ABSTAIN; | |
| } | |
| } | |
| private boolean tryConcludeBasedOn(OtpDecision state, AuthenticationFlowContext context) { | |
| switch (state) { | |
| case SHOW_OTP: | |
| showOtpForm(context); | |
| return true; | |
| case SKIP_OTP: | |
| context.success(); | |
| return true; | |
| default: | |
| return false; | |
| } | |
| } | |
| private void showOtpForm(AuthenticationFlowContext context) { | |
| super.authenticate(context); | |
| } | |
| private OtpDecision voteForUserOtpControlAttribute(AuthenticationFlowContext context, Map<String, String> config) { | |
| if (!config.containsKey(OTP_CONTROL_USER_ATTRIBUTE)) { | |
| return ABSTAIN; | |
| } | |
| String attributeName = config.get(OTP_CONTROL_USER_ATTRIBUTE); | |
| if (attributeName == null) { | |
| return ABSTAIN; | |
| } | |
| List<String> values = context.getUser().getAttribute(attributeName); | |
| if (values.isEmpty()) { | |
| return ABSTAIN; | |
| } | |
| String value = values.get(0).trim(); | |
| switch (value) { | |
| case SKIP: | |
| return SKIP_OTP; | |
| case FORCE: | |
| return SHOW_OTP; | |
| default: | |
| return ABSTAIN; | |
| } | |
| } | |
| private OtpDecision voteForHttpHeaderMatchesPattern(AuthenticationFlowContext context, Map<String, String> config) { | |
| if (!config.containsKey(FORCE_OTP_FOR_HTTP_HEADER) && !config.containsKey(NO_OTP_REQUIRED_FOR_HTTP_HEADER)) { | |
| return ABSTAIN; | |
| } | |
| MultivaluedMap<String, String> requestHeaders = context.getHttpRequest().getHttpHeaders().getRequestHeaders(); | |
| //Inverted to allow white-lists, e.g. for specifying trusted remote hosts: X-Forwarded-Host: (1.2.3.4|1.2.3.5) | |
| if (containsMatchingRequestHeader(requestHeaders, config.get(NO_OTP_REQUIRED_FOR_HTTP_HEADER))) { | |
| return SKIP_OTP; | |
| } | |
| if (containsMatchingRequestHeader(requestHeaders, config.get(FORCE_OTP_FOR_HTTP_HEADER))) { | |
| return SHOW_OTP; | |
| } | |
| return ABSTAIN; | |
| } | |
| private boolean containsMatchingRequestHeader(MultivaluedMap<String, String> requestHeaders, String headerPattern) { | |
| if (headerPattern == null) { | |
| return false; | |
| } | |
| //TODO cache RequestHeader Patterns | |
| //TODO how to deal with pattern syntax exceptions? | |
| Pattern pattern = Pattern.compile(headerPattern, Pattern.DOTALL); | |
| for (Map.Entry<String, List<String>> entry : requestHeaders.entrySet()) { | |
| String key = entry.getKey(); | |
| for (String value : entry.getValue()) { | |
| String headerEntry = key.trim() + ": " + value.trim(); | |
| if (pattern.matcher(headerEntry).matches()) { | |
| return true; | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| private OtpDecision voteForUserForceOtpRole(AuthenticationFlowContext context, Map<String, String> config) { | |
| if (!config.containsKey(FORCE_OTP_ROLE)) { | |
| return ABSTAIN; | |
| } | |
| RoleModel forceOtpRole = getRoleFromString(context.getRealm(), config.get(FORCE_OTP_ROLE)); | |
| UserModel user = context.getUser(); | |
| if (hasRole(user.getRoleMappings(), forceOtpRole)) { | |
| return SHOW_OTP; | |
| } | |
| return ABSTAIN; | |
| } | |
| } |
| package org.keycloak.authentication.authenticators.browser; | |
| import org.keycloak.Config; | |
| import org.keycloak.authentication.Authenticator; | |
| import org.keycloak.authentication.AuthenticatorFactory; | |
| import org.keycloak.models.AuthenticationExecutionModel; | |
| import org.keycloak.models.KeycloakSession; | |
| import org.keycloak.models.KeycloakSessionFactory; | |
| import org.keycloak.models.UserCredentialModel; | |
| import org.keycloak.provider.ProviderConfigProperty; | |
| import java.util.List; | |
| import static java.util.Arrays.asList; | |
| import static org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticator.*; | |
| import static org.keycloak.provider.ProviderConfigProperty.*; | |
| /** | |
| * An {@link AuthenticatorFactory} for {@link ConditionalOtpFormAuthenticator}s. | |
| * | |
| * @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a> | |
| */ | |
| public class ConditionalOtpFormAuthenticatorFactory implements AuthenticatorFactory { | |
| public static final String PROVIDER_ID = "auth-conditional-otp-form"; | |
| public static final ConditionalOtpFormAuthenticator SINGLETON = new ConditionalOtpFormAuthenticator(); | |
| public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = { | |
| AuthenticationExecutionModel.Requirement.REQUIRED, | |
| AuthenticationExecutionModel.Requirement.OPTIONAL, | |
| AuthenticationExecutionModel.Requirement.DISABLED}; | |
| @Override | |
| public Authenticator create(KeycloakSession session) { | |
| return SINGLETON; | |
| } | |
| @Override | |
| public void init(Config.Scope config) { | |
| //NOOP | |
| } | |
| @Override | |
| public void postInit(KeycloakSessionFactory factory) { | |
| //NOOP | |
| } | |
| @Override | |
| public void close() { | |
| //NOOP | |
| } | |
| @Override | |
| public String getId() { | |
| return PROVIDER_ID; | |
| } | |
| @Override | |
| public String getReferenceCategory() { | |
| return UserCredentialModel.TOTP; | |
| } | |
| @Override | |
| public boolean isConfigurable() { | |
| return true; | |
| } | |
| @Override | |
| public boolean isUserSetupAllowed() { | |
| return true; | |
| } | |
| @Override | |
| public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { | |
| return REQUIREMENT_CHOICES; | |
| } | |
| @Override | |
| public String getDisplayType() { | |
| return "Conditional OTP Form"; | |
| } | |
| @Override | |
| public String getHelpText() { | |
| return "Validates a OTP on a separate OTP form. Only shown if required based on the configured conditions."; | |
| } | |
| @Override | |
| public List<ProviderConfigProperty> getConfigProperties() { | |
| ProviderConfigProperty forceOtpUserAttribute = new ProviderConfigProperty(); | |
| forceOtpUserAttribute.setType(STRING_TYPE); | |
| forceOtpUserAttribute.setName(OTP_CONTROL_USER_ATTRIBUTE); | |
| forceOtpUserAttribute.setLabel("OTP control User Attribute"); | |
| forceOtpUserAttribute.setHelpText("The name of the user attribute to explicitly control OTP auth. " + | |
| "If attribute value is 'force' then OTP is always required. " + | |
| "If value is 'skip' the OTP auth is skipped. Otherwise this check is ignored."); | |
| ProviderConfigProperty forceOtpRole = new ProviderConfigProperty(); | |
| forceOtpRole.setType(ROLE_TYPE); | |
| forceOtpRole.setName(FORCE_OTP_ROLE); | |
| forceOtpRole.setLabel("Force OTP for Role"); | |
| forceOtpRole.setHelpText("OTP is always required if user has the given Role."); | |
| ProviderConfigProperty noOtpRequiredForHttpHeader = new ProviderConfigProperty(); | |
| noOtpRequiredForHttpHeader.setType(STRING_TYPE); | |
| noOtpRequiredForHttpHeader.setName(NO_OTP_REQUIRED_FOR_HTTP_HEADER); | |
| noOtpRequiredForHttpHeader.setLabel("No OTP for Header"); | |
| noOtpRequiredForHttpHeader.setHelpText("OTP required if a HTTP request header does not match the given pattern." + | |
| "Can be used to specify trusted networks via: X-Forwarded-Host: (1.2.3.4|1.2.3.5)." + | |
| "In this case requests from 1.2.3.4 and 1.2.3.5 come from a trusted source."); | |
| noOtpRequiredForHttpHeader.setDefaultValue(""); | |
| ProviderConfigProperty forceOtpForHttpHeader = new ProviderConfigProperty(); | |
| forceOtpForHttpHeader.setType(STRING_TYPE); | |
| forceOtpForHttpHeader.setName(FORCE_OTP_FOR_HTTP_HEADER); | |
| forceOtpForHttpHeader.setLabel("Force OTP for Header"); | |
| forceOtpForHttpHeader.setHelpText("OTP required if a HTTP request header matches the given pattern."); | |
| forceOtpForHttpHeader.setDefaultValue(""); | |
| ProviderConfigProperty defaultOutcome = new ProviderConfigProperty(); | |
| defaultOutcome.setType(LIST_TYPE); | |
| defaultOutcome.setName(DEFAULT_OTP_OUTCOME); | |
| defaultOutcome.setLabel("Fallback OTP handling"); | |
| defaultOutcome.setDefaultValue(asList(SKIP, FORCE)); | |
| defaultOutcome.setHelpText("What to do in case of every check abstains. Defaults to force OTP authentication."); | |
| return asList(forceOtpUserAttribute, forceOtpRole, noOtpRequiredForHttpHeader, forceOtpForHttpHeader, defaultOutcome); | |
| } | |
| } |
| // Used in various role mappers | |
| public static RoleModel getRoleFromString(RealmModel realm, String roleName) { | |
| String[] parsedRole = parseRole(roleName); | |
| RoleModel role = null; | |
| if (parsedRole[0] == null) { | |
| role = realm.getRole(parsedRole[1]); | |
| } else { | |
| ClientModel client = realm.getClientByClientId(parsedRole[0]); | |
| if (client != null) { | |
| role = client.getRole(parsedRole[1]); | |
| } | |
| } | |
| return role; | |
| } | |
| // Used for hardcoded role mappers | |
| public static String[] parseRole(String role) { | |
| int scopeIndex = role.lastIndexOf('.'); | |
| if (scopeIndex > -1) { | |
| String appName = role.substring(0, scopeIndex); | |
| role = role.substring(scopeIndex + 1); | |
| String[] rtn = {appName, role}; | |
| return rtn; | |
| } else { | |
| String[] rtn = {null, role}; | |
| return rtn; | |
| } | |
| } |
| org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticatorFactory |
Hi Thomas,
I have this setup configured on earlier version of Keyclaok and it worked fine. Recently, I upgraded to 10.0.1 and this workflow breaks down.
When I try to login after setting up the local Keyclok, I get:
13:30:36,766 WARN [org.keycloak.services] (default task-17) KC-SERVICES0013: Failed authentication: org.keycloak.authentication.AuthenticationFlowException
at org.keycloak.keycloak-services@10.0.1//org.keycloak.authentication.AuthenticationProcessor.authenticationAction(AuthenticationProcessor.java:942)
at org.keycloak.keycloak-services@10.0.1//org.keycloak.services.resources.LoginActionsService.processFlow(LoginActionsService.java:311)
at org.keycloak.keycloak-services@10.0.1//org.keycloak.services.resources.LoginActionsService.processAuthentication(LoginActionsService.java:282)
at org.keycloak.keycloak-services@10.0.1//org.keycloak.services.resources.LoginActionsService.authenticate(LoginActionsService.java:266)
at org.keycloak.keycloak-services@10.0.1//org.keycloak.services.resources.LoginActionsService.authenticateForm(LoginActionsService.java:339)
at jdk.internal.reflect.GeneratedMethodAccessor876.invoke(Unknown Source)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
13:30:36,767 WARN [org.keycloak.events] (default task-17) type=LOGIN_ERROR, realmId=OTPRealm, clientId=account, userId=null, ipAddress=172.17.0.1, error=invalid_user_credentials, auth_method=openid-connect, auth_type=code, redirect_uri=http://0.0.0.0:8087/auth/realms/OTPRealm/account/login-redirect, code_id=6e5a0ae8-6e63-480f-bb11-2ede8d5d1d48, username=username@user.name, authSessionParentId=6e5a0ae8-6e63-480f-bb11-2ede8d5d1d48, authSessionTabId=sYEQPf288Ow
Should there be any changes in the setup for the latest or is this a bug?
Note that this code here is outdated. The ConditionaOtpAuthenticator was adopted a while ago by Keycloak and got adapted over time.
I am not using the code. In 4.5.0.FINAL the 2FA works just by following the set up steps but not in 10.0.1.
Instead of Browser Dynamic Otp Forms use Browser Dynamic Otp Browser - Conditional OTP to add the execution. May be you can make this small update.
Your gist is the only place where I found this configuration. Cheers! :)
Hi!
Thanks a lot for your work developing the ConditionalOtpFormAuthenticator! This is exactly what I was looking for!
I was also following the steps documented here and I couldn't make it work in 10.0.1.
I was wondering if there is other documentation about this that I could follow, or maybe I'm doing something wrong?
The other docs that I found are the javadocs and the Keycloak Server Administration.
Would upgrading to 10.0.2 fix this? (It's not mentioned in the release notes/Jira).
Thanks!
Hi
I want to OTP for few specific functions in my application. To start/finish the transaction(Function) keycloak should ask for OTP(Google Authenticator).If OTP is valid ,it should allow to complete the transaction.
Need help to implement this on keycloak 12.0.4
Note that this code here is outdated. The ConditionaOtpAuthenticator was adopted a while ago by Keycloak and got adapted over time.
Shouldn't this statement be at the top of the readme? "This code was adopted into Keycloak in version X and remains here only as an example of extending a service provider." Just to save everyone time wondering "does this repo do what I need?"
Hii,
When i click on send otp then i'm getting info 422 in console. Can someone help me
More clear version 😄
https://access.redhat.com/solutions/6976632
Issue
Resolution
Root Cause
Diagnostic Steps
Browser flow:
- Browser - Conditional OTP
Condition - User Role = 'REQUIRED'
- Config: Alias=[alias-name], user role: [select client role to users]
At least the above provided solutions showed for me with latest keycloak versions (26.5.0 - but i believe this always was the case) the problem that the resolution of @myonaingwinn has the disadvantage that users who voluntarily setup otp are not requested for otp any longer once i disable the OTP form.
But enabling OTP form and setting it to previous default value actually of ALTERNATIVE triggers during authentication flow the OTP execution twice which is first not communicated to a user doing the login and once entering the same code a second time u'll receive the error that the code already was used. So users are required to wait until the 30 seconds have past and then it actually fulfills the second step of authentication flow and user get's signed in.
I tried out many things but finally got it working to:
with configuration for conditional credential config like:

I even automated it for easier adaption to multiple customers with the keycloak terraform provider and despite managing flows is quite fragile and error prone due to changing ID's once renaming a flow or changing priority i added some additional comments in the file as well.
So maybe this helps someone else as well:
###############################################
# Keycloak OTP Authentication Flow with Role Enforcement
# This configuration sets up a custom authentication flow in Keycloak
# that requires users assigned to a specific role to authenticate using
# OTP (One-Time Password). The flow is based on the default Browser flow,
# with modifications to include conditional OTP enforcement.
# BEWARE: Changing configuration here has serious impact on users AND confuses tofu a LOT and destroys resources randomly repeatedly without reason.
# Only workaround is to rename all resources while keeping the old flow name to avoid keycloak 500 errors.
# hence rename the local variable version whenever changing anything here.
###############################################
locals {
version = "v3"
}
resource "keycloak_role" "require_otp_auth" {
name = "require_otp_auth"
realm_id = keycloak_realm.realm.id
description = "Role to enforce OTP authentication for users or groups assigned to this role via Browser Login Flow."
}
resource "keycloak_authentication_flow" "browser_otp_v3" {
realm_id = keycloak_realm.realm.id
description = "browser dynamic otp flow for everyone with role require_otp_auth"
alias = "browser_otp_${local.version}"
}
# old flow kept as otherwise keycloak error with still bound flow pops up
# TODO delete after migration on all realms is done
resource "keycloak_authentication_flow" "browser_otp_v2" {
realm_id = keycloak_realm.realm.id
description = "browser dynamic otp flow for everyone with role require_otp_auth"
alias = "browser_otp_v2"
}
resource "keycloak_authentication_flow" "browser_otp" {
realm_id = keycloak_realm.realm.id
description = "browser dynamic otp flow for everyone with role require_otp_auth"
alias = "browser_otp"
}
# first execution, rebuild from Browser Flow
resource "keycloak_authentication_execution" "execution_one" {
realm_id = keycloak_realm.realm.id
parent_flow_alias = keycloak_authentication_flow.browser_otp_v3.alias
authenticator = "auth-cookie"
requirement = "ALTERNATIVE"
priority = 10
}
# second execution, rebuild from Browser Flow
resource "keycloak_authentication_execution" "execution_two" {
realm_id = keycloak_realm.realm.id
parent_flow_alias = keycloak_authentication_flow.browser_otp_v3.alias
authenticator = "auth-spnego" # Kerberos
requirement = "DISABLED"
priority = 20
}
# third execution, rebuild from Browser Flow
resource "keycloak_authentication_execution" "execution_three" {
realm_id = keycloak_realm.realm.id
parent_flow_alias = keycloak_authentication_flow.browser_otp_v3.alias
authenticator = "identity-provider-redirector"
requirement = "ALTERNATIVE"
priority = 30
}
resource "keycloak_authentication_subflow" "form" {
realm_id = keycloak_realm.realm.id
alias = "otp-form-alias_${local.version}"
description = "Username, password, otp and other auth forms."
parent_flow_alias = keycloak_authentication_flow.browser_otp_v3.alias
provider_id = "basic-flow"
requirement = "ALTERNATIVE"
priority = 40
}
# inside subflow form, first execution: username-password form
resource "keycloak_authentication_execution" "form_execution_one" {
realm_id = keycloak_realm.realm.id
parent_flow_alias = keycloak_authentication_subflow.form.alias
authenticator = "auth-username-password-form"
requirement = "REQUIRED"
priority = 10
}
# inside subflow form, second execution: conditional otp form flow
resource "keycloak_authentication_subflow" "form_flow_nested" {
realm_id = keycloak_realm.realm.id
description = "Flow to determine if the OTP is required for the authentication"
alias = "otp-form-alias-browser-conditional-otp_${local.version}"
parent_flow_alias = keycloak_authentication_subflow.form.alias
authenticator = "auth-conditional-otp-form"
requirement = "CONDITIONAL"
priority = 20
}
# inside nested subflow form, first condition user configured for otp
# inside subflow form, first execution: username-password form
resource "keycloak_authentication_execution" "conditional_user_configured" {
realm_id = keycloak_realm.realm.id
parent_flow_alias = keycloak_authentication_subflow.form_flow_nested.alias
authenticator = "conditional-user-configured"
requirement = "REQUIRED"
priority = 10
}
# inside nested subflow form, second execution: otp form
resource "keycloak_authentication_execution" "sub_form_otp" {
realm_id = keycloak_realm.realm.id
parent_flow_alias = keycloak_authentication_subflow.form_flow_nested.alias
authenticator = "auth-otp-form"
requirement = "ALTERNATIVE"
priority = 20
}
# inside subflow form, second execution: conditional otp auth check
resource "keycloak_authentication_subflow" "require_otp_auth_check" {
realm_id = keycloak_realm.realm.id
description = "Flow to determine if the OTP is to be enforced for the authentication, only if not already provided before."
alias = "otp-required-flow-for-role_${local.version}"
parent_flow_alias = keycloak_authentication_subflow.form.alias
authenticator = "auth-conditional-otp-form"
requirement = "CONDITIONAL"
priority = 30
}
# inside subflow form for otp required users, first execution: Credential OTP check to disable a second otp prompt if already provided by previous flow
resource "keycloak_authentication_execution" "conditional_credential_otp_check" {
realm_id = keycloak_realm.realm.id
parent_flow_alias = keycloak_authentication_subflow.require_otp_auth_check.alias
authenticator = "conditional-credential"
requirement = "REQUIRED"
priority = 10
}
# configure conditional credential for existing otp to skip further otp prompt
resource "keycloak_authentication_execution_config" "check_otp_already_provided" {
realm_id = keycloak_realm.realm.id
execution_id = keycloak_authentication_execution.conditional_credential_otp_check.id
alias = "otp-already-provided-check_${local.version}"
config = {
credentials = "otp",
included = "false"
}
}
# inside subflow form for otp required users, second execution: conditional otp form to enforce otp only for users with role
resource "keycloak_authentication_execution" "conditional_otp_form" {
realm_id = keycloak_realm.realm.id
parent_flow_alias = keycloak_authentication_subflow.require_otp_auth_check.alias
authenticator = "auth-conditional-otp-form"
requirement = "REQUIRED"
priority = 20
}
# configure conditional otp form to require otp only for users with specified role
resource "keycloak_authentication_execution_config" "require_otp_flow_for_role" {
realm_id = keycloak_realm.realm.id
execution_id = keycloak_authentication_execution.conditional_otp_form.id
alias = "require_otp_flow_for_role_${local.version}"
config = {
forceOtpRole = keycloak_role.require_otp_auth.name,
defaultOtpOutcome = "skip"
}
}
resource "keycloak_authentication_bindings" "browser_authentication_binding" {
realm_id = keycloak_realm.realm.id
browser_flow = keycloak_authentication_flow.browser_otp_v3.alias
}
How I can edit the UI or Template for OTP form on keycloak theme?