Two-Factor Authentication
hxEASM supports optional two-factor authentication for interactive username/password logins.
2FA is disabled by default after upgrades. Existing users continue signing in with password + JWT until an administrator enables a 2FA policy.
API keys are not interactive logins and are not challenged with 2FA.
Supported Methods
| Method | Description |
|---|---|
| Authenticator App | RFC 6238 TOTP, compatible with Google Authenticator, Microsoft Authenticator, Authy, and similar apps |
| Email OTP | Six-digit one-time code sent to the user's registered email address |
SMS is not supported.
Global Policy
Administrators configure 2FA under:
Settings -> Administration -> Two-Factor Authentication
Modes:
| Mode | Behavior |
|---|---|
| Disabled | Current login behavior. No user is challenged. |
| Optional | Users may enroll. Enrolled users are challenged after password login. |
| Required | Users must enroll and use 2FA on fresh login. Existing sessions are not immediately invalidated. |
Required mode cannot be enabled unless at least one method is allowed.
Email OTP cannot be enabled unless SMTP is configured.
Global Policy vs User Enrollment
The global policy and a user's enrollment state are separate concepts.
| Global Policy | User Enrollment | Login Behavior |
|---|---|---|
| Required | Not enrolled | User must configure an allowed 2FA method after password login before receiving JWT tokens |
| Required | Enabled | User must complete the enrolled second factor after password login |
| Optional | Not enrolled | User can login with password only |
| Optional | Enabled | User must complete the enrolled second factor after password login |
| Disabled | Not enrolled or enabled | 2FA is not used for login while the policy remains disabled |
The account settings page displays both values separately:
- Global policy:
disabled,optional, orrequired - Your 2FA:
not_enrolledorenabled - Method:
email,totp, or empty
Required mode does not mean a user is already enrolled. It means the user must enroll before a fresh login can complete.
User Enrollment
Users manage their own enrollment under:
Settings -> General -> Two-Factor Authentication
Authenticator App enrollment:
- User starts setup.
- Backend generates a TOTP secret.
- Frontend displays a QR code and manual setup key.
- User scans the QR code with an authenticator app.
- User enters a six-digit code.
- Backend verifies the code and only then enables TOTP.
- Recovery codes are shown once.
Email OTP enrollment:
- User starts email setup.
- Backend sends a six-digit code.
- User enters the code.
- Backend verifies the code and only then enables email 2FA.
Login Flow
Without 2FA:
POST /api/v1/auth/login
-> password valid
-> JWT access/refresh tokens returned
With 2FA:
POST /api/v1/auth/login
-> password valid
-> short-lived 2FA challenge returned
-> no JWT tokens returned
POST /api/v1/auth/2fa/verify
-> code valid
-> challenge consumed
-> JWT access/refresh tokens returned
The backend never issues normal JWT credentials before the second factor succeeds.
Recovery Codes
TOTP enrollment generates one-time recovery codes. They are shown once and stored only as bcrypt hashes.
Each recovery code can be used once. Users can regenerate recovery codes from account settings.
API Endpoints
Public auth endpoints:
POST /api/v1/auth/2fa/verify
POST /api/v1/auth/2fa/resend
POST /api/v1/auth/2fa/totp/setup
POST /api/v1/auth/2fa/totp/confirm
POST /api/v1/auth/2fa/email/enable
Authenticated account endpoints:
GET /api/v1/me/2fa
POST /api/v1/me/2fa/totp/setup
POST /api/v1/me/2fa/totp/confirm
POST /api/v1/me/2fa/email/enable
POST /api/v1/me/2fa/disable
POST /api/v1/me/2fa/recovery-codes/regenerate
GET /api/v1/me/2fa returns user-safe policy and enrollment state:
{
"global_mode": "required",
"allowed_methods": ["email", "totp"],
"enabled": false,
"method": null,
"email_available": true,
"totp_available": true
}
Admin endpoints:
GET /api/v1/admin/auth/2fa
PUT /api/v1/admin/auth/2fa
SMTP Configuration
Email OTP requires SMTP configuration:
smtp:
host: smtp.example.com
port: 587
username: hxeasm@example.com
password: secret
from: hxeasm@example.com
use_tls: true
Environment variables:
EASM_SMTP_HOST
EASM_SMTP_PORT
EASM_SMTP_USERNAME
EASM_SMTP_PASSWORD
EASM_SMTP_FROM
EASM_SMTP_USE_TLS
Security Design
- TOTP uses six digits and a 30-second period.
- TOTP verification accepts a small ±1 step drift window.
- TOTP secrets are encrypted with AES-GCM before storage.
- Encryption key material comes from
two_factor.encryption_key, orjwt.secretwhen the dedicated key is empty. - Email OTP codes are generated with
crypto/rand. - OTP codes are stored as keyed hashes, not plaintext.
- Challenges are short-lived and single-use.
- Challenges are limited to five attempts.
- Email resend is throttled.
- Recovery codes are stored as bcrypt hashes.
- API keys are unaffected.
Troubleshooting
If Email OTP cannot be enabled, check that SMTP host, port, and from address are configured.
If Required mode is enabled and a user is not enrolled, the next fresh password login starts enrollment instead of returning JWT tokens.
If a TOTP code fails, check server clock drift and confirm the authenticator app is using the current code for the hxEASM issuer.