-
Notifications
You must be signed in to change notification settings - Fork 1
api 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 chain spans three files:
-
app/core/jwt_verification.py- local JWT verification against the Supabase JWKS -
app/core/auth.py- Supabase client wrapper, failure classification, admin operations -
app/api/api_v1/dependencies/auth.py- FastAPI dependencies that resolve the current user
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.
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.
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 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.
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 localUserrow viaget_or_create_user_from_supabase, setsrequest.state.user_id, and tags the Sentry user context. Raises 401 on bad tokens, 503 on provider outage. -
get_current_active_user(current_user)- extendsget_current_userto reject inactive users with 403 (USER_INACTIVE). -
get_current_user_optional(request, authorization, db) -> User | None- returnsNoneinstead of raising when no token is present or verification fails. Used by public endpoints that personalize for logged-in users (property feeds, share previews). OnPROVIDER_UNREACHABLEit returnsNonerather than 503, so a Supabase outage degrades gracefully to anonymous mode. -
get_current_agent(current_user)- ensuresrole == UserRole.agent, else 403 (AGENT_REQUIRED). -
get_current_admin(current_user)- ensuresrole == UserRole.admin, else 403 (ADMIN_REQUIRED). -
get_current_user_sse(request, authorization, token)- SSE-specific. BrowserEventSourcecannot set custom headers, so this dependency accepts the token as a?token=query parameter and falls back to theAuthorizationheader. It uses a short-lived background-pool session instead ofDepends(get_db)so long-running SSE streams do not exhaust the main connection pool.
- No
/api/v1/auth/login,/auth/refresh,/auth/logout, or/auth/registerendpoints. 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/deleteroute. Account deletion is exposed atDELETE /api/v1/users/me(canonical, returnsMessageResponse) andPOST /api/v1/auth/delete-account(alternate for mobile clients, returns 204). Thedelete_user_accountservice hard-deletes the Supabase Auth user, anonymizes all PII, and soft-deletes the local row.
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.
- Features overview
- Ghar Core (marketplace)
- 360 Stays (bookings)
- 360 Flatmates
- Property Management
- 360 Virtual Tours
- 360 Data Hub
- MCP servers and widgets
- AI agent
- Blog and SEO
- Notifications
- Vastu analyzer