Sign-in & Sessions
Everything that happens after registration: the sign-in screen, two-factor challenge, forgot-password recovery, magic-link entry, "new device detected" alerts, and active session management. The whole post-registration auth surface — owner side and public side share the same components, only the routing changes.
UI — sign-in screen (admin console)
Sign-in flow
User submits credentials
Client validates email format and minimum password length, then POSTs to
/api/auth/signin. Honeypot field included, hCaptcha token attached if risk score is elevated.Server verifies password
Argon2id verify against stored hash. Failed attempts go to a per-account rate limiter (5 / 10 min) and per-IP limiter (20 / hour).
Risk assessment
Compare IP geo, device fingerprint and time-of-day against the user's history. High deviation → 2FA required even if 2FA isn't permanently enabled.
If 2FA is enabled or required
Return
{ status: "2fa_required", challenge_token }. Client renders the 2FA screen; no session cookie issued yet.Issue session
Set HTTP-only secure session cookie (15-min access + 30-day refresh). Audit-log
auth.signed_inwith device + IP.Redirect
Owners → property dashboard. Public users → their previous URL or home.
UI — two-factor challenge
Can't access your authenticator?
UI — forgot password (3 steps)
UI — magic link sign-in
UI — "new device detected"
If a sign-in succeeds from an IP/device the user hasn't seen before, we send an alert and surface a banner on next sign-in.
Active sessions screen
Every signed-in device is visible to the user under Settings → Security → Sessions. The list updates every 30 seconds. Manual sign-out is one click. Sign-out-everywhere bumps the JWT version and kills everything including the current tab.
| Device | Location · IP | Last active | |
|---|---|---|---|
|
Chrome on macOS
Sonoma 14.4
|
Manali, IN 103.21.x.x |
Just now | Current |
|
Safari on iPhone
iOS 18.1
|
Manali, IN 103.21.x.x |
2 hours ago | |
|
Chrome on Windows New
Windows 11
|
Delhi, IN 122.176.x.x |
3 days ago |
API contract
// Request { "email": "rohan@parkviewhotel.in", "password": "••••••••••", "remember": true, "device_fingerprint": "d4f8a…", "hcaptcha_token": "…" // only when risk score > 0.5 } // Response — 200 (no 2FA) { "status": "signed_in", "user": { "id": "usr_…", "role": "owner" }, "redirect": "/properties" } // Response — 200 (2FA required) { "status": "2fa_required", "challenge_token": "ch_01HW…", // short-lived, 10 min "method": "totp", "fallback_methods": ["sms", "recovery_code"] } // Response — 401 (bad credentials) { "error": "invalid_credentials", "message": "Wrong email or password." } // Response — 429 (rate limited) { "error": "rate_limited", "retry_after": 600 }
Cookies & tokens
| Cookie | Purpose | Lifetime | Flags |
|---|---|---|---|
abc_session | Short-lived access token (JWT) | 15 minutes | HttpOnly · Secure · SameSite=Lax |
abc_refresh | Refresh token for silent re-auth | 30 days (90 if "remember") | HttpOnly · Secure · SameSite=Strict |
abc_device | Device fingerprint, low-PII | 1 year | HttpOnly · Secure · SameSite=Lax |
abc_csrf | Double-submit CSRF token | Session | Secure · SameSite=Strict (readable by JS) |
Why we don't show a generic "invalid credentials" error
If we said "no account with that email" vs "wrong password", an attacker could discover whether your email is registered. We always return the same friendly "Wrong email or password" string, regardless of which is wrong, with a consistent response time (we run the Argon2id hash even for non-existent emails to avoid timing leaks).
Edge cases handled
- Account locked after 10 failed attempts — auto-unlock after 30 minutes, but user is emailed.
- 2FA recovery code use — single-use, the user gets a fresh batch of 8 codes after using one.
- Magic-link reuse attempt — second click returns "link already used" page, prompts sign-in normally.
- Sign-in from suspended account — credentials accepted but user is shown the "account on hold" screen, not the dashboard.
- Concurrent sign-in from two devices — fine, both get sessions; user can see both in the sessions screen.
- Stolen refresh-token detection — refresh tokens are rotated on every use; reusing an old one signs the user out everywhere and alerts.