Keycloak TOTP Two-Factor Authentication

Tested on Red Hat Enterprise Linux 10.2 (Coughlan)
Package podman-5.8.2-5.el10_2.x86_64
quay.io/keycloak/keycloak:26.7.3
Applies to Any Linux host with Podman or Docker
Privilege Keycloak admin access for OTP policy and credential management; normal user for browser enrollment and login tests
Lab environment Keycloak 26.7.3 on http://127.0.0.1:8080/ — Install Keycloak with Podman
Scope How TOTP works in Keycloak, realm OTP policy fields, forcing Configure OTP, authenticator app enrollment, browser login with OTP Form, selective enforcement patterns, admin credential removal for lost devices, recovery-code relationship, clock skew and invalid-code troubleshooting. Does not cover custom conditional-flow wiring, the full recovery-codes walkthrough, or WebAuthn passkeys.
Related guides Install Keycloak with Podman
Keycloak required actions
Create and manage users in Keycloak
Keycloak authentication flows explained
Keycloak recovery codes
Short on time?Beta Get the gist of this 11-minute read in seconds, then ask follow-up questions

When a user enrolls an authenticator app, Keycloak creates an otp credential for that user. That credential contains the secret Keycloak uses to verify the time-based codes submitted during login.

You configure the TOTP period, algorithm, digits, and validation window at realm level, while each enrolled user has their own OTP credential. The built-in Browser - Conditional 2FA subflow prompts for a code only when that credential exists.

This guide walks enrollment and login on Keycloak 26.7.3 with user totp-lab-user in realm golinuxcloud. You will set OTP policy, assign Configure OTP, scan the QR code (or copy the manual key), verify a second factor at login, reject stale codes when the look-ahead window is tight, and remove a lost OTP credential from the Admin Console.


How TOTP works in Keycloak

TOTP combines a shared secret with the current time interval to generate a short numeric code.

In this lab, Keycloak uses a 30-second period and 6-digit codes, so both Keycloak and the authenticator app calculate a new six-digit value every 30 seconds from the same secret. Keycloak accepts codes within the realm look-ahead window around the current step.

  Enrollment                         Login (browser flow)
  ┌──────────────┐  secret + QR       ┌──────────────┐
  │ Configure OTP│ ────────────────► │ OTP credential│
  │ required act │                   │ on user record│
  └──────┬───────┘                   └──────┬───────┘
         │ user scans app                  │
         v                                 v
  ┌──────────────┐   6-digit code   ┌──────────────┐
  │ Authenticator│ ◄──────────────► │ OTP Form in  │
  │ app          │   each 30s step  │ browser flow │
  └──────────────┘                  └──────────────┘

Keycloak never shows the raw secret again after enrollment. Admin API responses list an otp credential with algorithm metadata only. For enrollment triggers and email links, see Keycloak required actions — this page focuses on the OTP policy, credential, and login path.


Configure the realm OTP policy

Realm OTP policy defines what enrolled apps must generate: type, digits, period, algorithm, and how many adjacent windows Keycloak accepts.

Field Lab default What it controls
otpPolicyType totp Time-based OTP (not counter-based HOTP)
otpPolicyDigits 6 Code length
otpPolicyPeriod 30 Seconds per step
otpPolicyAlgorithm HmacSHA1 HMAC algorithm (matches most apps)
otpPolicyLookAheadWindow 1 Accept codes from ±1 window when clocks drift
otpPolicyCodeReusable false Whether the same code works twice in one window

With reusable codes disabled, a TOTP that has already been accepted during its current validity window cannot simply be submitted again for another authentication.

Open Authentication → Policies → OTP policy in the Admin Console:

Keycloak realm OTP policy tab showing digits, period, algorithm, and look-ahead window

Read the live policy through the Admin REST API when you automate deployments:

bash
curl -sS -H "Authorization: Bearer ${ADMIN_TOKEN}" \
  "${KC_BASE}/admin/realms/${REALM}" | \
  python3 -c "import sys,json; r=json.load(sys.stdin); print('digits', r['otpPolicyDigits'], 'period', r['otpPolicyPeriod'], 'lookAhead', r['otpPolicyLookAheadWindow'])"

Sample output:

output
digits 6 period 30 lookAhead 1

Keep the default look-ahead window of 1 unless you have a specific reason to change it.

A value of 0 requires tighter clock alignment. Higher values tolerate more clock drift but also widen the window in which a code can be accepted, so fix time synchronization rather than increasing the window unnecessarily.

Tighten otpPolicyLookAheadWindow to 0 in a lab when you want to reproduce clock-skew rejection.

Important: Configuring OTP Policy does not automatically enable 2FA for every user. The policy defines how OTP works once a user has enrolled.

To make a user enroll an authenticator, assign the Configure OTP required action, enable it as a default action for new users, or enforce enrollment through a custom authentication flow.


Require users to configure OTP

Keycloak separates enrollment (Configure OTP required action) from login-time verification (OTP Form in the browser flow). Users must finish enrollment before the conditional 2FA subflow can challenge them.

Three common enforcement patterns:

Pattern When to use How
Per-user One account or a pilot group Set requiredActions to include CONFIGURE_TOTP
Default action Every new user at creation Enable Configure OTP and turn on Default action
Email link Remote enrollment execute-actions-email with ["CONFIGURE_TOTP"] (needs SMTP)

Ensure Configure OTP is enabled under Authentication → Required actions:

Keycloak Admin Console Required actions list with Configure OTP enabled

Assign Configure OTP to the lab user through the API (the golinuxcloud realm also requires employeeId on create — see Create and manage users in Keycloak):

bash
TOTP_USER_ID=$(curl -sS -H "Authorization: Bearer ${ADMIN_TOKEN}" \
  "${KC_BASE}/admin/realms/${REALM}/users?username=totp-lab-user&exact=true" | \
  python3 -c "import sys,json; u=json.load(sys.stdin); print(u[0]['id'])")

curl -sS -o /dev/null -w 'assign-configure-otp:%{http_code}\n' -X PUT \
  -H "Authorization: Bearer ${ADMIN_TOKEN}" -H 'Content-Type: application/json' \
  "${KC_BASE}/admin/realms/${REALM}/users/${TOTP_USER_ID}" \
  -d '{"requiredActions":["CONFIGURE_TOTP"],"enabled":true,"attributes":{"employeeId":["EMP-5501"]}}'

Sample output:

output
assign-configure-otp:204

HTTP 204 means the user will see the Mobile Authenticator Setup screen after the next successful password validation.


Enroll an authenticator app

Sign in through the Account Console client so Configure OTP runs as a required action:

text
http://127.0.0.1:8080/realms/golinuxcloud/protocol/openid-connect/auth?client_id=account&redirect_uri=http://127.0.0.1:8080/realms/golinuxcloud/account/&response_type=code&scope=openid

Use totp-lab-user / TotpLab!2026. Keycloak shows a QR code for FreeOTP, Google Authenticator, or Microsoft Authenticator:

The QR code contains the OTP enrollment information, including the shared secret and the realm's TOTP parameters. Scanning it saves those values in the authenticator app so the app and Keycloak can generate matching codes.

Keycloak Mobile Authenticator Setup screen with QR code for TOTP enrollment

The enrollment secret is intentionally redacted. Treat the QR code and manual key like a password.

Click Unable to scan? when you need the plain secret for a manual entry or lab automation. Copy the grouped key from the page (not the hidden form field) into your app:

Keycloak manual TOTP setup showing secret key, type, algorithm, digits, and interval

Enter the current six-digit code from the app, add a device label such as lab-phone, and submit. Keycloak stores an otp credential and clears CONFIGURE_TOTP from requiredActions.

List credentials to confirm enrollment without reading the secret back:

bash
curl -sS -H "Authorization: Bearer ${ADMIN_TOKEN}" \
  "${KC_BASE}/admin/realms/${REALM}/users/${TOTP_USER_ID}/credentials" | \
  python3 -c "import sys,json; [print(c['type'], c.get('userLabel','')) for c in json.load(sys.stdin)]"

Sample output:

output
password 
otp lab-phone

Test two-factor login

After enrollment, the built-in browser flow runs Browser - Conditional 2FA when the user already has an OTP credential. Password validation succeeds first; then OTP Form asks for the current code.

Start a fresh browser session (clear cookies or use a private window) and open the same Account Console auth URL. After username and password, Keycloak stops on the OTP step:

Keycloak login OTP form asking for authenticator code after password validation

Enter a deliberately incorrect code—for example, change one digit from the code currently displayed by your authenticator app—to confirm Keycloak rejects bad codes before issuing tokens:

Keycloak OTP login form showing Invalid authenticator code feedback

The UI reports Invalid authenticator code. and keeps you on the OTP step. Sign in again with a fresh session and enter the live code from the app to reach the Account Console.

The OTP Form execution lives inside the browser authentication flow. For how conditions and subflows fit together, see Keycloak authentication flows explained — this article does not rewire those executions.


Apply TOTP selectively

Keycloak lets you decide which users must enroll and when the OTP step should run:

  • Conditional 2FA (default): OTP runs only when the user has enrolled. Unenrolled users stay password-only until you assign Configure OTP.

  • Default required action: New users enroll before first app access; turn the default off after onboarding waves complete.

  • Per-user assignment: Add or remove CONFIGURE_TOTP on individual accounts through the Admin Console or API.

  • Group- or role-gated MFA: Requires a copied browser flow with extra conditions — keep that work in a dedicated conditional-flow guide rather than here.

Pick one primary pattern per realm stage. Mixing default actions with ad-hoc assignments without cleanup leaves some users on password-only login while others see OTP every session.


Lost device and credential reset

When a user loses the phone that held their enrolled OTP credential, an admin must delete the otp credential and re-assign Configure OTP.

The Admin Console can list the OTP credential, but it does not expose the enrollment secret again. Reset therefore means deleting the old credential and enrolling a new one rather than retrieving the old QR code.

List credentials and note the OTP credential ID:

bash
curl -sS -H "Authorization: Bearer ${ADMIN_TOKEN}" \
  "${KC_BASE}/admin/realms/${REALM}/users/${TOTP_USER_ID}/credentials" | \
  python3 -c "import sys,json; [print(c['id'], c['type'], c.get('userLabel')) for c in json.load(sys.stdin)]"

Sample output:

output
69d90af0-29b9-4580-9e0c-6f19982b89f2 password None
3f3eb615-1ca1-4cdd-a418-98e2daea410c otp lab-phone

Capture the OTP credential ID from the list, then delete only that row:

bash
OTP_CRED_ID=$(curl -sS -H "Authorization: Bearer ${ADMIN_TOKEN}" \
  "${KC_BASE}/admin/realms/${REALM}/users/${TOTP_USER_ID}/credentials" | \
  python3 -c "import sys,json; print(next(c['id'] for c in json.load(sys.stdin) if c['type']=='otp'))")

curl -sS -o /dev/null -w 'delete-otp:%{http_code}\n' -X DELETE \
  -H "Authorization: Bearer ${ADMIN_TOKEN}" \
  "${KC_BASE}/admin/realms/${REALM}/users/${TOTP_USER_ID}/credentials/${OTP_CRED_ID}"

Sample output:

output
delete-otp:204

Re-assign Configure OTP so the user can enroll a replacement device on next login. The Admin Console Credentials tab should show password only:

Keycloak user Credentials tab after admin removed the OTP credential

After deleting the lost OTP credential, re-assign Configure OTP.

On the user's next login:

  1. The password is validated.
  2. Keycloak sees the pending CONFIGURE_TOTP required action.
  3. The user must enroll a new authenticator before completing login.

If you delete the OTP credential without assigning Configure OTP again, the default conditional browser flow can fall back to password-only login because the user no longer has an OTP credential.


TOTP with recovery codes

Recovery codes are a separate credential type from TOTP. The default browser flow ships with Recovery Authentication Code Form disabled.

Recovery codes are not enabled for login just by generating them. The Recovery Authentication Codes required action must be enabled, and Recovery Authentication Code Form must also participate in the browser flow, normally as an Alternative second-factor method.

Pair TOTP with recovery codes in production: enroll OTP for daily login, issue recovery codes once, and store them offline. The dedicated Keycloak recovery codes walkthrough covers enrollment and rotation — this page only removes and re-issues TOTP.


Troubleshoot invalid OTP

Symptom Likely cause Fix
Invalid authenticator code right after enrollment Typo, expired step, or wrong secret scanned Re-open manual setup; wait for the next 30s window
Valid phone code rejected intermittently Clock skew beyond look-ahead window Sync NTP on app host and Keycloak nodes; keep otpPolicyLookAheadWindow at 1 unless you have a documented reason to change it
Valid code rejected after policy change Digits, period, or algorithm no longer match the enrolled credential Remove OTP credential and re-enroll
OTP prompt never appears User not enrolled or conditional subflow disabled Confirm otp credential exists; check browser flow bindings
User locked out after many attempts Brute-force detection (when enabled) Clear brute-force flags in Admin Console or temporarily relax realm lockout policy

Simulate clock skew by setting otpPolicyLookAheadWindow to 0 and submitting a code from an earlier window. A stale code produces the same Invalid authenticator code message as a mistyped code, so the UI alone does not tell you which condition failed.

Restore otpPolicyLookAheadWindow to 1 after the test. When brute-force protection is enabled on the realm, repeated wrong OTP attempts count toward temporary lockout the same way failed passwords do — plan recovery through credential removal plus Configure OTP rather than guessing codes.


Summary

Realm OTP policy sets the time step, digits, and look-ahead window every authenticator app must follow. Assign Configure OTP per user, as a default action, or through execute-actions email, then let users scan the QR code or paste the manual key to create an otp credential.

Successful enrollment switches the browser flow to OTP Form on the next login. Wrong or stale codes return Invalid authenticator code without issuing tokens; sync NTP when clocks drift rather than widening the look-ahead window without cause.

When a device is lost, delete the OTP credential in the Admin API or Console, re-assign Configure OTP, and point users toward recovery codes for backup access. Deleting the credential alone can leave password-only login until Configure OTP is assigned again. Selective MFA for subsets of users belongs in authentication-flow customization — keep this page focused on standard TOTP enrollment, login, and reset.


References


Frequently Asked Questions

Which authenticator apps work with Keycloak TOTP?

Any app that supports standard TOTP with SHA-1, six digits, and a 30-second period. Keycloak lists FreeOTP, Google Authenticator, and Microsoft Authenticator in the enrollment screen; other RFC 6238-compatible apps work when they match the realm OTP policy.

How do I force every user to set up OTP?

Enable Configure OTP under Required actions, set Default action on for new users, or assign CONFIGURE_TOTP through automation on existing accounts. Users complete enrollment at next login or through an execute-actions email link when SMTP is configured.

Why is a valid authenticator code rejected?

Common causes are clock drift outside the look-ahead window, a code from the previous 30-second step, or an OTP policy change after enrollment. Sync time with NTP, try the next code, or remove the OTP credential and enroll again. Fix clock synchronization before raising the look-ahead window.

How do I reset TOTP when a user loses their phone?

Delete the otp credential through the Admin Console Credentials tab or DELETE .../credentials/{id}, then assign Configure OTP again. The user must complete enrollment before login finishes. Without Configure OTP, conditional 2FA may allow password-only login. Issue recovery codes beforehand if you need backup access between removal and re-enrollment.

TOTP vs passkeys — which should I use?

TOTP works on any smartphone authenticator app and is quick to roll out. Passkeys (WebAuthn) use device-bound keys and resist phishing differently. Keycloak supports both; choose TOTP for broad app compatibility and add passkeys when you are ready for WebAuthn enrollment flows.

Finished reading?Beta Get the key takeaways in seconds, then ask follow-up questions
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with 15+ years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)