MFA & step-up
TOTP + recovery codes, email/SMS OTP factors, WebAuthn as a second factor, step-up MFA, and an adaptive risk engine (bot-score, impossible travel, device reputation).
Qeet ID ships a complete multi-factor system: TOTP with recovery codes, email/SMS OTP factors, WebAuthn as a second factor, and step-up MFA that gates sensitive actions behind a recent verification.
All implemented
TOTP, OTP factors, WebAuthn-2FA, and step-up are live. Any factor verification
refreshes a recent-MFA window that a server-side RequireRecentMFA
middleware checks before sensitive operations.
Factors
| Factor | Endpoints | Notes |
|---|---|---|
| TOTP (RFC 6238) | /v1/mfa/totp/* | 6-digit authenticator codes; enroll → confirm → verify. |
| Recovery codes | /v1/mfa/recovery-codes* | bcrypt-hashed single-use codes; regenerable. |
| Email / SMS OTP | /v1/mfa/otp/factors* | Per-factor challenge + confirm via real senders. |
| WebAuthn 2FA | /v1/mfa/webauthn/* | Reuses the user's registered passkeys as a second factor. |
TOTP
Three-step enrollment, then verification at sign-in.
Start enrollment
Returns the shared secret + provisioning URI to render as a QR code.
/v1/mfa/totp/enroll/startBegin TOTP enrollmentConfirm
The user enters the first 6-digit code to prove the authenticator is synced.
/v1/mfa/totp/enroll/confirmConfirm enrollmentVerify at sign-in
/v1/mfa/totp/verifyVerify a TOTP codeRemove TOTP with DELETE /v1/mfa/totp (this is a sensitive action — see
Step-up).
Recovery codes
Issued at enrollment, single-use, bcrypt-hashed. Users can regenerate the set (another step-up-gated action).
/v1/mfa/recovery-codesList remaining recovery codes/v1/mfa/recovery-codes/regenerateRegenerate the setEmail / SMS OTP factors
Register a contact factor, then challenge + confirm it as a second step.
/v1/mfa/otp/factorsRegister an OTP factor (email/SMS)/v1/mfa/otp/factors/{id}/challengeSend a code/v1/mfa/otp/factors/{id}/confirmConfirm enrollment/v1/mfa/otp/verifyVerify an OTP at sign-inSMS is lower assurance
Prefer TOTP or WebAuthn as the primary second factor; SMS is vulnerable to SIM-swap. SMS/email delivery also depends on a configured sending domain / Twilio account.
WebAuthn as a second factor
Step-up with a registered passkey. Unlike passwordless login, this asserts the known user and issues no new token — it only proves presence to refresh the MFA window.
/v1/mfa/webauthn/challengeGet assertion options/v1/mfa/webauthn/verifyVerify the assertionStep-up MFA
Any successful factor verification refreshes a recent-verification window. The
RequireRecentMFA middleware gates sensitive endpoints (e.g. disabling MFA,
regenerating recovery codes) and returns an MFA challenge if the window has lapsed.
Check whether the user currently satisfies the window:
/v1/mfa/step-up/statusIs a recent MFA present?curl https://api.id.qeet.in/v1/mfa/step-up/status \
-H "Authorization: Bearer $ACCESS_TOKEN"
# → { "recent": false } → prompt the user to re-verify a factorThe pattern: call step-up/status before a sensitive action; if
not recent, run any factor's verify endpoint to refresh the window, then
retry. The server enforces this independently — clients can't bypass it.
Adaptive MFA (risk-based)
On the hosted-login (cookie) flow, a "remembered device" can normally skip the second factor. Qeet ID scores each such login and overrides that skip — forcing MFA anyway — when the score crosses a per-tenant threshold. The base signal is bot-likeness (a User-Agent heuristic); two more are additive, independently toggled, and off by default:
- Impossible travel — a login from a different country than the user's last one, sooner than a configurable minimum plausible travel time later.
- Device reputation — a login from a browser + OS combination never seen before for that user.
/v1/tenants/{tenantID}/security/risk-settingsGet risk settings/v1/tenants/{tenantID}/security/risk-settingsUpdate risk settings{
"medium_threshold": 0.5,
"high_threshold": 0.8,
"force_mfa_at_level": "high",
"impossible_travel_enabled": false,
"min_travel_hours": 3,
"device_reputation_enabled": false
}Impossible travel needs a geo signal
Qeet ID ships no server-side GeoIP lookup. The country comes from a
configurable trusted-upstream-proxy header (e.g. Cloudflare's
CF-IPCountry, set via GEO_COUNTRY_HEADER). With none
configured, enabling this signal is a safe no-op — it never fires (fail-open),
rather than guessing from a raw IP. This is the same "interface ships, live
dependency is external ops" pattern as
KMS BYOK.
Also in the console: Security & Compliance → Threat Protection → Risk Settings.
SSO overview
How Qeet ID does enterprise SSO — SAML 2.0 (SP and IdP), SCIM provisioning, and LDAP — with pointers to the Enterprise section.
Sessions
ES256 access tokens, refresh-token rotation with theft detection, per-account lockout, session list/revoke, and CAEP/SSF-shaped real-time revocation signals.