Configure Forgot Password in Keycloak

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 to edit realm SMTP and login settings; normal user for browser reset tests
Lab environment Keycloak 26.7.3 on http://127.0.0.1:8080/ — Install Keycloak with Podman
Scope Realm SMTP prerequisites, enabling forgot password, user email and action-token lifespan, full reset-credentials flow for local users, enumeration-safe responses, expired and reused links, federated-user limits, and troubleshooting missing links or mail failures. Does not cover production SMTP provider setup, the full required-actions catalog, or custom email themes.
Related guides Install Keycloak with Podman
Keycloak required actions
Keycloak password policy
Create and manage users in Keycloak
Keycloak user federation vs identity brokering
Short on time?Beta Get the gist of this 11-minute read in seconds, then ask follow-up questions

Forgot password lets users recover access without an admin reset. Keycloak sends a signed action-token link, runs the built-in reset-credentials flow, and lands on the Update password screen. An action token is a short-lived signed JWT embedded in the reset link. It authorizes only the specific reset action; it is not an access token for APIs.

A working forgot-password flow depends on four pieces: SMTP, the realm setting, an email address on the user, and the built-in reset-credentials flow. This guide configures each one and then tests both successful and failure paths. The lab uses a local SMTP catcher on 127.0.0.1:1025 with host-network Podman so Keycloak can reach the listener.


Forgot-password flow and dependencies

Before you hunt for a missing link, confirm each dependency:

Dependency Why it matters
Realm SMTP (smtpServer) Without outbound mail, the UI may still say “check your email” but nothing is delivered
resetPasswordAllowed Realm Login setting that exposes Forgot Password? on the password step
User email Keycloak needs a mailbox address on the account to send the reset link
Built-in reset credentials flow Authentication flow Keycloak binds when the user starts a reset
actionTokenGeneratedByUserLifespan Caps how long the emailed /login-actions/action-token link stays valid (seconds)
  ┌──────────────┐  Forgot Password?   ┌─────────────────────┐
  │ Login (pwd)  │ ─────────────────► │ reset credentials    │
  │ step         │  submit username   │ authentication flow  │
  └──────┬───────┘                    └──────────┬──────────┘
         │                                       │ SMTP
         │                                       v
         │                            ┌──────────────────────┐
         │                            │ Email with action-   │
         │                            │ token link           │
         │                            └──────────┬───────────┘
         │                                       │ user opens link
         v                                       v
  ┌──────────────┐                    ┌──────────────────────┐
  │ Generic      │                    │ UPDATE_PASSWORD form │
  │ confirmation │                    │ (new password)       │
  │ (same for    │                    └──────────────────────┘
  │ known or     │
  │ unknown user)│
  └──────────────┘

Lab user forgot-lab-user in realm golinuxcloud has an email address on the account for the walkthrough. Side effects: realm SMTP points at the local catcher for this article only.


Configure SMTP first

Keycloak does not ship a mail relay. Point realm email settings at a relay your container can reach. With host-network Podman, 127.0.0.1:1025 works; bridge networking often needs the host gateway IP instead.

Admin Console: open Realm settings → Email on golinuxcloud and configure:

  • Host — SMTP server hostname or IP (127.0.0.1 in this lab)
  • Port — listener port (1025 for the local catcher)
  • From — envelope sender Keycloak uses in reset mail
  • Encryption — SSL / STARTTLS switches to match your relay
  • Authentication — username and password when the relay requires auth

Click Test connection before you test forgot password. Keycloak sends a probe message to the address you enter and logs SMTP errors if the relay is unreachable.

Admin REST equivalent — export shared lab variables and apply the same profile:

bash
export KC_BASE='http://127.0.0.1:8080'
export REALM='golinuxcloud'

ADMIN_TOKEN=$(curl -sS -X POST "${KC_BASE}/realms/master/protocol/openid-connect/token" \
  -d 'grant_type=password' -d 'client_id=admin-cli' \
  -d 'username=admin' \
  -d 'password=YOUR_ADMIN_PASSWORD' | \
  python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")

The Admin REST examples use admin-cli only for local lab automation; this is not an application login pattern.

Pull the current SMTP block so you know what you are replacing:

bash
curl -sS -H "Authorization: Bearer ${ADMIN_TOKEN}" \
  "${KC_BASE}/admin/realms/${REALM}" | \
  python3 -c "import sys,json; r=json.load(sys.stdin); print(r.get('smtpServer',{}))"

Sample output on a fresh realm:

output
{}

Merge a minimal lab profile (no TLS, open relay on localhost):

bash
curl -sS -o /dev/null -w 'smtp-update:%{http_code}\n' -X PUT \
  -H "Authorization: Bearer ${ADMIN_TOKEN}" -H 'Content-Type: application/json' \
  "${KC_BASE}/admin/realms/${REALM}" \
  -d '{
    "smtpServer": {
      "host": "127.0.0.1",
      "port": "1025",
      "from": "keycloak-noreply@golinuxcloud.lab",
      "fromDisplayName": "GoLinuxCloud Keycloak",
      "ssl": "false",
      "starttls": "false",
      "auth": "false"
    }
  }'

Sample output:

output
smtp-update:204

HTTP 204 means Keycloak accepted the partial realm update. Production relays need TLS, credentials, and DNS-aligned from addresses; this article stops at proving Keycloak can hand a message to SMTP.


Enable forgot password

Turn on self-service reset at the realm level. In the Admin Console, open Realm settings → Login and enable Forgot password.

Keycloak golinuxcloud realm Login settings with Forgot password enabled

The same flag through Admin REST is resetPasswordAllowed:

bash
curl -sS -H "Authorization: Bearer ${ADMIN_TOKEN}" \
  "${KC_BASE}/admin/realms/${REALM}" | python3 <<'PY'
import json, os, sys, urllib.request
realm = json.load(sys.stdin)
realm['resetPasswordAllowed'] = True
realm['loginWithEmailAllowed'] = True
req = urllib.request.Request(
    f"{os.environ['KC_BASE']}/admin/realms/{os.environ['REALM']}",
    data=json.dumps(realm).encode(),
    headers={
        'Authorization': f"Bearer {os.environ['ADMIN_TOKEN']}",
        'Content-Type': 'application/json',
    },
    method='PUT',
)
urllib.request.urlopen(req)
print('resetPasswordAllowed=true')
PY

Sample output:

output
resetPasswordAllowed=true

Keycloak 26 uses a username-first login page. Forgot Password? appears on the password step after you submit a username, not on the first screen.

Keycloak login password step showing Forgot Password link

If the link is still missing, confirm resetPasswordAllowed is true and that you reached the password form (enter a username, click Sign In).


Reset email targets the email field on the user record. Create or update the account with a reachable address — see Create and manage users in Keycloak for User Profile fields such as employeeId on this lab realm.

Check reset-link timing before you blame “broken” links:

bash
curl -sS -H "Authorization: Bearer ${ADMIN_TOKEN}" \
  "${KC_BASE}/admin/realms/${REALM}" | \
  python3 -c "
import sys, json
r = json.load(sys.stdin)
print('actionTokenGeneratedByUserLifespan', r.get('actionTokenGeneratedByUserLifespan'))"

Sample output:

output
actionTokenGeneratedByUserLifespan 300
Setting Meaning
actionTokenGeneratedByUserLifespan Maximum lifetime of the user-generated reset link (seconds)

In the Admin Console this corresponds to the user-initiated action lifespan under realm token settings. Shorter values help security testing; raise them only when mail latency or mobile clients need more time.


Run a complete password reset

Walk the flow the way an end user would, using the built-in account client (standard browser login, no PKCE):

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

Enter forgot-lab-user, continue to the password step, and click Forgot Password?

Keycloak Forgot Your Password form asking for username or email

Submit the username again on the reset form. Keycloak shows the same confirmation whether or not it found a mailbox to target (covered in the next section):

Keycloak confirmation after forgot-password submit

The plain-text mail contains a link shaped like:

text
http://127.0.0.1:8080/realms/golinuxcloud/login-actions/action-token?key=eyJhbGciOiJIUzUxMiIs...

Open the link in a browser. Keycloak signs the user in for that action only and shows Update password:

Keycloak Update password form opened from reset email action token

Set and confirm a new password, then submit. A successful change returns you toward the client redirect (here the Account Console).

Keycloak 26.x also controls what happens after the reset through Authentication → Flows → reset credentials → Send Reset Email → Force login after reset. The current default is only-federated: users stored directly in Keycloak can continue in the same authentication session, while federated users are required to sign in again. Set it to true if you want every user to authenticate again after resetting credentials.

After reset, sign out if necessary and log in again using forgot-lab-user and the new password.

The new password must still satisfy the realm password policy when rules are configured.


How Keycloak prevents user enumeration

Submitting no-such-user-9999 on the forgot-password form returns the same banner as a real account:

text
You should receive an email shortly with further instructions.

Keycloak forgot-password confirmation for an unknown username

No email is captured for the unknown name in this lab, but the browser message does not reveal that fact. That is deliberate: different text would let attackers learn which usernames exist.

Trade-off: help-desk tickets from users who mistyped an address look the same as successful sends. Combine safe UI copy with mail-server logs or Keycloak events when you need to diagnose delivery.


Reset-credentials authentication flow

Forgot password is not a separate microservice. It is the built-in reset credentials authentication flow (alias reset credentials, builtIn=true).

Keycloak reset credentials authentication flow showing Choose User, Send Reset Email, Reset Password, and Reset Conditional OTP

The default tree runs Choose User, Send Reset Email, Reset Password, and Reset - Conditional OTP. List flows when you need to confirm the alias or clone it:

bash
curl -sS -H "Authorization: Bearer ${ADMIN_TOKEN}" \
  "${KC_BASE}/admin/realms/${REALM}/authentication/flows" | \
  python3 -c "
import sys, json
for f in json.load(sys.stdin):
    if 'reset' in f.get('alias','').lower():
        print(f['alias'], 'builtIn=', f.get('builtIn'))"

Sample output:

output
reset credentials builtIn= True

Custom copies belong in Authentication → Flows when you need extra steps (for example reCAPTCHA). Keep executions aligned with authentication flows concepts: the reset flow only runs after the user chooses Forgot Password? and submits an identifier.

Action links are single-use. Opening the same /login-actions/action-token URL twice after a successful password change shows an error page instead of the form again.

Password reset can also reset OTP

Keycloak's built-in reset credentials flow is not limited to the password. If the user already has OTP configured, the flow can also require the user to configure OTP again.

The default flow includes a Reset - Conditional OTP subflow. If you want forgot-password to change only the password, open Authentication → Flows → reset credentials and disable Reset - Conditional OTP.

reset credentials
    |
    +-- Choose User
    |
    +-- Send Reset Email
    |
    +-- Reset Password
    |
    +-- Reset - Conditional OTP

Federated user behavior

If the user storage provider is read-only, Keycloak cannot write the new password to the external directory. The reset flow may reach the password-update step, but the credential update fails when Keycloak attempts to write through the provider. Those users should use the directory's own password-reset mechanism unless the federation provider is configured for password writes.

Keycloak 26.x also treats federated users differently after reset: with Force login after reset at the default only-federated setting, federated users must sign in again when the reset completes in the same browser session.

See Keycloak user federation vs identity brokering for provider modes. Writable LDAP password sync is a separate integration decision; do not assume forgot password replaces IdP workflows.


Symptom Likely cause Fix
No Forgot Password? link resetPasswordAllowed false, or still on username-only step Enable the realm switch; enter username first, then open the password screen
Confirmation shown, no email SMTP host/port wrong from container network, relay down, or user has no email Fix smtpServer.host (127.0.0.1 with host networking), verify listener, set an email address on the user
HTTP 500 on submit SMTP unreachable (common when using bridge IP instead of host gateway) Match SMTP host to a route Keycloak can open; read podman logs for SMTPTransport stack traces
Link shows Action expired. actionTokenGeneratedByUserLifespan elapsed, or clock skew Resend reset; widen lifespan temporarily; sync NTP on host and containers
Link invalid after first use Token consumed Request a fresh reset; old links cannot be replayed
Weak password rejected at end Realm password policy Follow policy hints on the form; see password policy guide

Keycloak error page when reset action token expired

When SMTP is broken, Keycloak still renders the success banner but logs a connection failure. Troubleshoot SMTP delivery from Keycloak logs after a failed send:

bash
podman logs --tail 20 keycloak-groups-lab 2>&1 | grep -iE 'SMTP|mail|19999'

Sample output (port intentionally wrong for the test):

output
	at org.eclipse.angus.mail.smtp.SMTPTransport.openServer(SMTPTransport.java:2193)

Enable Realm settings → Events (already on in this lab) and filter for SEND_RESET_PASSWORD or RESET_PASSWORD when you need an audit trail in the Admin Console.

execute-actions-email with UPDATE_PASSWORD is an admin-driven cousin to forgot password — different trigger, same action form. See Keycloak required actions for that path.


Summary

Forgot password in Keycloak is a chain: SMTP that the container can reach, resetPasswordAllowed on the realm, a user account with an email address, and the built-in reset-credentials flow that emails an action-token link. The login UI stays enumeration-safe by showing the same confirmation for unknown usernames, which means you diagnose delivery with logs and events rather than the banner text.

I walked a local user from Forgot Password? through the Update password form, signed in again with the new password, and reproduced expired and reused link errors by tightening actionTokenGeneratedByUserLifespan. The default reset flow can also reconfigure OTP through Reset - Conditional OTP; federated read-only accounts need directory-side resets instead.

For production, pair this flow with a real relay, sensible token lifespans, and password policy rules so self-service resets meet the same strength bar as admin-initiated changes.


References


Frequently Asked Questions

Why is Forgot Password not visible?

Keycloak shows the link on the password step after username entry when resetPasswordAllowed is true. If you only see the username form, submit a username first. Also confirm the realm Login switch is enabled and you are not using a client whose theme hides recovery links.

Why is no email sent?

The UI confirmation does not prove delivery. Check that smtpServer points at a host Keycloak can open from its network namespace, the relay is listening, and the user has a valid email attribute. Container logs often show SMTPTransport connection errors when the host or port is wrong.

By default actionTokenGeneratedByUserLifespan is 300 seconds (five minutes) for user-initiated resets. Admins can change the realm value; expired tokens show Action expired.

Can users reset LDAP passwords through Keycloak?

Only when the user storage provider allows password writes to the directory. Read-only LDAP or AD federation blocks Keycloak-side password updates; users must use directory self-service or an admin reset instead.

Why does Keycloak show the same message for unknown users?

To prevent account enumeration. Attackers cannot distinguish “unknown username” from “email dispatched” using the browser text alone. Use mail logs or events when you need to trace a specific request.

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)