Skip to content

api authentication

github-actions[bot] edited this page Jun 19, 2026 · 1 revision

Authentication

360Ghar does not run its own auth server. Clients authenticate directly with Supabase Auth using the Supabase SDK, then send the resulting Supabase access token as a bearer JWT to the API. The backend verifies the token, syncs the Supabase user into a local User row, and attaches the user to the request. There are no /api/v1/auth/login, /api/v1/auth/refresh, or /api/v1/auth/logout endpoints. Clients own the entire session lifecycle.

Active contributors: Saksham, Ravi

The verification path

The verification chain spans three files:

  1. app/core/jwt_verification.py - local JWT verification against the Supabase JWKS
  2. app/core/auth.py - Supabase client wrapper, failure classification, admin operations
  3. app/api/api_v1/dependencies/auth.py - FastAPI dependencies that resolve the current user

Local JWKS verification

app/core/jwt_verification.py verifies the Supabase access-token signature, iss, aud, and exp claims locally using the cached JWKS public key set. This avoids a per-request HTTP round-trip to /auth/v1/user. The JWKS is fetched from {SUPABASE_URL}/auth/v1/.well-known/jwks.json, cached with a 1-hour TTL (JWKS_TTL_SECONDS = 3600), and refreshed on-demand when a kid is missing. A short-TTL positive cache (token hash to claims, 60s, max 5000 entries) avoids re-verifying identical tokens within the cache window. If the JWKS endpoint is unreachable, JWKSUnavailable is raised and the caller falls back to introspection.

Failure classification

app/core/auth.py defines an AuthFailureReason enum with three values:

  • INVALID_TOKEN - the JWT signature, claims, or expiry failed verification. Maps to HTTP 401.
  • PROVIDER_UNREACHABLE - Supabase could not be reached (network error, DNS failure, timeout). Maps to HTTP 503.
  • PROVIDER_ERROR - Supabase returned an error response. Maps to HTTP 401.

The verify_supabase_token function returns either the user payload or a tagged failure dict. The dependency layer inspects _is_failure(result) and maps the reason to the right status code. Transient network errors (connection resets, gaierror, httpx timeouts) are retried twice with a 0.3s flat wait via tenacity before being classified as PROVIDER_UNREACHABLE.

The 503 distinction

This is the key design decision. A bad token returns 401. A Supabase outage returns 503 with Retry-After: 5. The response body carries a code of AUTH_PROVIDER_UNREACHABLE and a human-readable message. Clients can distinguish "my token is bad, log me out" from "Supabase is down, retry in 5 seconds" without parsing error strings. The constant _RETRY_AFTER_SECONDS = "5" lives in app/api/api_v1/dependencies/auth.py.

Phone-first identity

Phone is the primary identifier for Indian users. The User model has a unique phone column with a partial unique index. The user service's get_user_by_phone tries an exact match first, then falls back to a normalized last-10-digits match (handling +91, 0091, and bare-digit formats). Email is a secondary identity-linking key with its own partial unique index uq_users_email (unique only when not null).

last_auth_method and last_auth_method_at on the User model mirror the client login state machine. The column is stored as a String with a DB-level CHECK constraint and typed via the AuthMethod enum. Google OAuth client IDs (GOOGLE_WEB_CLIENT_ID, GOOGLE_IOS_CLIENT_ID, GOOGLE_ANDROID_CLIENT_ID) are surfaced to clients via GET /api/v1/auth/config so mobile and web clients can render the right sign-in buttons.

FastAPI dependencies

All auth flows are exposed as FastAPI dependencies in app/api/api_v1/dependencies/auth.py:

  • get_current_user(request, authorization, db) -> User - the primary dependency. Parses the bearer token, verifies it, syncs the Supabase user into a local User row via get_or_create_user_from_supabase, sets request.state.user_id, and tags the Sentry user context. Raises 401 on bad tokens, 503 on provider outage.
  • get_current_active_user(current_user) - extends get_current_user to reject inactive users with 403 (USER_INACTIVE).
  • get_current_user_optional(request, authorization, db) -> User | None - returns None instead of raising when no token is present or verification fails. Used by public endpoints that personalize for logged-in users (property feeds, share previews). On PROVIDER_UNREACHABLE it returns None rather than 503, so a Supabase outage degrades gracefully to anonymous mode.
  • get_current_agent(current_user) - ensures role == UserRole.agent, else 403 (AGENT_REQUIRED).
  • get_current_admin(current_user) - ensures role == UserRole.admin, else 403 (ADMIN_REQUIRED).
  • get_current_user_sse(request, authorization, token) - SSE-specific. Browser EventSource cannot set custom headers, so this dependency accepts the token as a ?token= query parameter and falls back to the Authorization header. It uses a short-lived background-pool session instead of Depends(get_db) so long-running SSE streams do not exhaust the main connection pool.

What is not here

  • No /api/v1/auth/login, /auth/refresh, /auth/logout, or /auth/register endpoints. Clients use the Supabase SDK directly.
  • No session store on the backend. The backend is stateless with respect to auth - every request re-verifies the JWT.
  • No /users/me/delete route. Account deletion is exposed at DELETE /api/v1/users/me (canonical, returns MessageResponse) and POST /api/v1/auth/delete-account (alternate for mobile clients, returns 204). The delete_user_account service hard-deletes the Supabase Auth user, anonymizes all PII, and soft-deletes the local row.

MCP auth

The MCP servers at /mcp and /mcp-admin use OAuth 2.1 with PKCE, not bearer JWTs. The SupabaseTokenVerifier in app/mcp/auth_provider.py validates both Supabase JWT access tokens and first-party OAuth access tokens issued by this backend, returning a FastMCP AccessToken with rich claims. It implements audience validation per RFC 8707 to prevent token passthrough attacks. See security.md and features/mcp-servers.md.

Clone this wiki locally