Skip to main content

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 rolecore/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:

EndpointFlow
POST /auth/loginClient sends a hashed user identifier + password hash; server verifies against bcrypt and returns a token immediately.
POST /auth/login-codePOST /auth/login-code/verifyPasswordless: server emails/SMSes a 6-digit code (stored as a hashed, 15-minute token), client submits it back for a token.
POST /auth/signupCreates 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 with hmac.compare_digest (constant-time), rather than comparing the OTP value directly.
  • session_token is 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-usemark_2fa_session_used consumes the session on successful verification; a second /verify with the same session fails.
  • A prior pending session is invalidated on every new /superadmin-2fa call — 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: nosniff
  • Cache-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.