Authentication Flow Setup
Wire the Email OTP authenticator into a Keycloak authentication flow and configure realm email settings.
Step 1 — Configure Realm Email (SMTP)
- Login as admin to your Keycloak installation
- Switch to your target realm
- Navigate to Realm Settings → Email tab
- Fill in your SMTP server details:
| Field | Example |
|---|---|
| SMTP Host | smtp.gmail.com |
| SMTP Port | 587 |
| From Email | noreply@yourdomain.com |
| Enable StarTLS | ✅ |
| Username / Password | your credentials |
See Email Provider Configuration for more details on these settings.
Step 2 — Create the Authentication Flow
a) Copy the Browser Flow
- Navigate to Authentication
- Inside the Flows tab, locate the Browser flow
- Click the "⋮" (overflow) menu
- Select "Duplicate"

- Name it
"Browser with Email OTP" - Click "Duplicate" to create the new flow

b) Add the OTP Execution
Inside the copied flow, locate the "Browser with Email OTP Forms - Conditional 2FA" sub-flow. This is where the OTP step lives alongside username/password.
- Click "+" Add on the right side of the Forms row
- Click "Add execution"

Choose your execution type depending on whether you want OTP for all users or only some:
- Email OTP
- Conditional Email OTP
Use this when: you want OTP enforced for every user, every login — no conditions.
- Search for and select "Email OTP"
- Click "Add"

- On the "Email OTP" row, set the requirement:
| Requirement | Behaviour |
|---|---|
| REQUIRED | Every login always prompts for an OTP code |
| ALTERNATIVE | OTP is offered but skipped if another factor already satisfies the flow |
| DISABLED | OTP step is never triggered |

Use REQUIRED for the strictest security. Use ALTERNATIVE only if you have other authenticators in the same flow that can substitute for OTP.
Use this when: you want OTP triggered only for certain users — for example, users with a specific role, or using a specific attribute, etc.
The settings details will be covered in the next section.
- Search for and select "Conditional Email OTP"
- Click "Add"
A sub-flow appears with the condition executions inside it.
- Click "Add condition" inside the Conditional Email OTP sub-flow
- Choose a condition type:
| Condition | Behaviour |
|---|---|
| Condition - User Configured | OTP only fires for users who have already set up this authenticator |
| Condition - User Role | OTP only fires for users belonging to a specific realm or client role |
- Click "Add", then click ⚙️ on the condition row to configure it (e.g. select the target role)
If you need OTP for all users with no conditions, use the Email OTP tab instead — it is simpler to configure and maintain.
c) Bind the Flow
- At the top right of the flow editor, click the "Actions" menu
- Select "Bind flow"

- Set Binding type to Browser flow
- Click "Save"

After binding, the new flow will replace the default Browser flow for all browser-based logins.

Resulting Flow Structure
After completing the above steps, when users log in through the browser, after successfully entering their username and password, they will be prompted to enter an OTP code sent to their email (if they have one configured and the requirement is met).

Step 3 — Configure Email OTP Settings
Click the ⚙️ Settings icon on the "Email OTP" row to open the authenticator configuration panel.
Settings reference
| Setting | Description | Default | Recommended (prod) |
|---|---|---|---|
| Code length | Number of digits in the OTP code | 6 | 6 |
| Time-to-live | Seconds before the code expires | 300 | 300 |
| Simulation mode | Log codes to server logs instead of sending emails | false | false |
| Resend cooldown | Seconds a user must wait before requesting a new code | 30 | 60 |
| Max code attempts | Wrong attempts allowed before the code is invalidated | 5 | 3 |
| Skip setup | Skip enrollment for users who already have an email address | true | true |
Setting details
Code length
Controls how many digits are in the OTP. 6 is the industry standard and what users expect. Increasing to 8 adds marginal security at the cost of more user errors.
Time-to-live
How long (in seconds) the code remains valid after being sent. 300 seconds (5 minutes) is a good balance between security and usability. If your users report codes expiring too quickly (e.g. they check email on a slow device), increase to 600.
Simulation mode
When true, no email is sent — the OTP code is written to the Keycloak server log instead. Use this during development and local testing to avoid needing a real mail server.
docker logs keycloak | grep "SIMULATION MODE"
# ***** SIMULATION MODE ***** Email code send to test@example.com for user testuser is: 123456
Never enable simulation mode in production — codes are visible in plain text in the logs.
Resend cooldown
Prevents users from spamming the "Resend code" button. The default of 30 seconds is fine for development; increase to 60 in production to reduce email abuse.
Max code attempts
After this many wrong attempts, the current code is invalidated and the user must request a new one. Lower values (e.g. 3) make brute-forcing harder but may frustrate users who mistype frequently.
Skip setup
When true, users who already have an email address on their Keycloak account skip the enrollment required action and go straight to the OTP prompt. This is the recommended setting when an admin has enforced 2FA for existing users — they are not forced through a setup wizard on their next login.
When false, every user must explicitly enroll before they can use email OTP, even if they already have an email on their account.
What's Next?
- Email Provider Configuration — configure SendGrid, AWS SES, Mailgun, or SMTP
- Template Customization — customize the OTP email appearance