Overview
A system where users can log in using their email Id /Username + Email OTP instead of username/password, by Keycloak.
Make Email OTP Java SPI
# Run this to find the Keycloak container:
docker ps | grep -i keycloak Result will look like 2550aa1a95b7 quay.io/keycloak/keycloak:26.1.0 "/opt/keycloak/bin/k…" 7 days ago Up 7 days 8443/tcp, 0.0.0.0:8080->8080/tcp, [::]:8080->8080/tcp, 9000/tcp keycloak_app fcca38958118 postgres:16 "docker-entrypoint.s…" 7 days ago Up 7 days (healthy) 5432/tcp
Run this in the droplet console to find the exact JAR download URL:
curl -s https://api.github.com/repos/for-keycloak/email-otp-authenticator/releases/latest | grep browser_download_url
We need email-otp-authenticator JAR if it is not available
1. Download the email-otp-authenticator JAR
curl -L -o email-otp-authenticator.jar https://github.com/for-keycloak/email-otp-authenticator/releases/download/v1.3.5/email-otp-authenticator-v1.3.5-kc-26.2.5.jar
2. Copy into the running container
docker cp email-otp-authenticator.jar keycloak_app:/opt/keycloak/providers/
3. Run build inside the container (registers the provider)
# Verify it's there docker exec keycloak_app ls /opt/keycloak/providers/
docker exec keycloak_app /opt/keycloak/bin/kc.sh build
4. Restart the container
docker restart keycloak_app
# Now let's set up the Email OTP flow. Go to Keycloak Admin Console at https://64.227.190.56/:
1. First configure SMTP (if not already done)
Realm Settings → Email
Host: smtp.gmail.com,
Port: 587
From: from email id
Username: your username,
Password: your app
password Enable StartTLS → Save → Test connection
2. Create Email OTP Authentication Flow
Go to Authentication → Flows → Create flow Name: Browser Email OTP → Save Add step → Username Password Form → Required Add step → Email OTP → Required
3. Bind the flow
Client → account → Advance Override realm authentication flow bindings. →Browser Flow → Browser email otp
Customize email content
python3 -c "import zipfile; [print(f) for f in zipfile.ZipFile('email-otp-authenticator.jar').namelist()]"
# check current email template
python3 -c "
import zipfile
with zipfile.ZipFile('email-otp-authenticator.jar') as z:
print(z.read('theme-resources/messages/messages_en.properties').decode())
"
# To customize the email text, create a custom Keycloak theme. Run these commands on the droplet:
Step 1: Create theme directory \structure
docker exec keycloak_app mkdir -p /opt/keycloak/themes/cotrav/email/messages
Step 2: Create theme.\properties
docker exec keycloak_app sh -c 'cat> /opt/keycloak/themes/cotrav/email/theme.properties <<\EOF parent=\base EOF'
Step 3: Create custom messages (edit the text as you like)
docker exec keycloak_app sh -c 'cat> /opt/keycloak/themes/cotrav/email/messages/messages_en.properties <<\EOF
emailOtpSubject=Your Cotrav OTP \Code
emailOtpYourAccessCode=Your one-time login code is:
emailOtpExpiration=This code will expire in {0} minutes. Do not share it with anyone.
EOF'
Step 4: Set realm to use this \theme
TOKEN=$(curl -s -X POST http://64.227.190.56:8080/realms/master/protocol/openid-connect/token
-d "client_id=admin-cli&grant_type=password&username=superadmin_username&password=superadminpassword"
| grep -o '"access_token":"[^"]*' | cut -d'"' -f4)
curl -s -X PUT http://64.227.190.56:8080/admin/realms/master \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"emailTheme":"cotrav"}'
Then test by logging in again — you should see your custom text in the OTP email.
# Invalid otp issue
The OTP field name sent by our server might not match what the extension expects. Let me check:
python3 -c "\
import zipfile\
with zipfile.ZipFile('email-otp-authenticator.jar') as z:\
print(z.read('theme-resources/templates/login-email-otp.ftl').decode())\
"
# Browser email otp Flow order should be
Username Form → Required (first)
Email OTP Form → Required (second)
# Dont do this
- No required user action available in user details
- Set Email & password