Authentication
:::info Status
Implemented. Source: fastapi-backend/api/v1/endpoints/auth.py, fastapi-backend/core/security.py, fastapi-backend/main.py.
:::
This page covers how a person (member, mosque admin, superadmin) proves who they are to the backend. Devices authenticate differently — see Device Authorization and MQTT Protocol.
Tokens
All authenticated requests carry a JWT bearer token (Authorization: Bearer <token>),
issued by core/security.py:create_access_token (HS256, signed with
JWT_SECRET), valid 7 days (ACCESS_TOKEN_EXPIRE_DAYS). The payload
carries id, email, and role — core/security.py:get_current_user
decodes and trusts it directly; there is no server-side session store for
ordinary logins (contrast with the superadmin 2FA flow below, which does use
one).
Regular login
Three ways to authenticate as a member or mosque admin, all in auth.py:
| Endpoint | Flow |
|---|---|
POST /auth/login | Client sends a hashed user identifier + password hash; server verifies against bcrypt and returns a token immediately. |
POST /auth/login-code → POST /auth/login-code/verify | Passwordless: server emails/SMSes a 6-digit code (stored as a hashed, 15-minute token), client submits it back for a token. |
POST /auth/signup | Creates a user by email and/or phone (a phone-only signup gets a synthetic placeholder email internally), returns a token immediately. |
Password reset (/auth/forgot-password → /auth/reset-password) follows
the same hashed-token pattern as login codes, TTL configurable via
PASSWORD_RESET_TTL_HOURS (default 24h, capped 1–168h).
All of these deliberately return the same response shape whether or not an account exists for the given identifier, to avoid account enumeration.
Superadmin login — hardened 2FA
Superadmin login is a separate, stricter flow — not just /auth/login with
an extra step:
Several details are deliberate hardening, not incidental:
- Client never sends the raw password. The client hashes the password
(SHA-256) before it leaves the browser; the server bcrypt-hashes that
hex digest for storage and verification (
hash_superadmin_password/verify_superadmin_password) — the server never sees or stores the plaintext password at any point. - The OTP itself never crosses the wire back to the server in the clear
on step 2. The client proves it holds the OTP by sending an
HMAC-SHA256(session_token, otp|timestamp)digest; the server recomputes the same HMAC server-side and compares withhmac.compare_digest(constant-time), rather than comparing the OTP value directly. session_tokenis stored server-side only as its SHA-256 hash — a database read alone can't be replayed as a valid session token.- Replay window — the verify step rejects any request more than 300 seconds off server time.
- Single-use —
mark_2fa_session_usedconsumes the session on successful verification; a second/verifywith the same session fails. - A prior pending session is invalidated on every new
/superadmin-2facall — only one 2FA attempt can be outstanding per superadmin at a time.
Superadmin password reset
/auth/superadmin-forgot → /auth/superadmin-forgot/verify →
/auth/superadmin-reset-password mirrors the same OTP/HMAC/single-use
pattern, ending in a separate short-lived (15-minute) reset token that must
be redeemed before a new password takes effect.
Transport hardening on auth responses
main.py's AuthSecurityHeadersMiddleware adds, on every response under
/api/v1/auth/*:
Strict-Transport-Security: max-age=31536000; includeSubDomains(HSTS — also set at the edge by Caddy)X-Content-Type-Options: nosniffCache-Control: no-store, no-cache, must-revalidate+Pragma: no-cache
Roles
role on the JWT is one of superadmin, or a member/mosque-admin role
derived from users/memberships — see
Authorization for how role and mosque
membership together decide what an authenticated request may do.