Skip to main content
Petanque Life

Federated Login (OAuth Gateway)

F02.06 16 features Platform+

At a glance

Federated Login is a centralised OAuth gateway that fronts Google, Microsoft, Apple and GitHub for every Petanque Life app — admin, app, web and www. A single redirect URI per provider, per-app routing through an {app} path parameter, PKCE S256, SHA-256 state binding, account linking with fresh-auth requirements, and HTTP-only cookies on .petanque.life keep the auth surface small, consistent and fully auditable across every entry point.

How it works

All providers register exactly one redirect URI, and per-app routing happens through an {app} path parameter so admin, app, web and www can each receive their callback without provisioning multiple Google/Microsoft/Apple/GitHub clients. Every flow is PKCE S256 and uses a one-shot state token whose SHA-256 binding hash is stored in a short-lived pl_oauth_binding cookie, so a callback on a different device or after a state replay is rejected. Web flows complete by setting pl_access_token and pl_refresh_token as Secure; HttpOnly; SameSite=Lax cookies on .petanque.life.

Native flows return a 64-byte base64url OAuthExchangeCode with a 60-second TTL; the mobile client POSTs to /auth/oauth/exchange and the code is consumed in a single find_one_and_delete — tokens never appear in URLs. Provider integrations normalise into the same identity shape but preserve the provider's own trust signals: Google id_tokens are JWKS-verified against accounts.google.com with sub as provider_user_id and email_verified taken from the claim; Microsoft uses the multitenant common endpoint with a documented email priority chain (Graph mail → id_token email → preferred_username → UPN) and trusts work tenants but marks personal MS accounts as email_verified=False; GitHub validates the access token via GET /user, enforces X-OAuth-Scopes, and resolves the primary verified email via GET /user/emails; Apple uses an ES256 JWT client_secret built from team_id+key_id+private_key, verifies id_tokens against Apple's JWKS, supports the iOS ASAuthorizationController flow via POST /auth/oauth/apple/native, and (per Apple policy) marks emails verified including private-relay addresses. compute_email_verified() encodes this matrix as a single function. Account linking lets multiple identities point at one user: POST /auth/oauth/{provider}/link/{app} requires a fresh authentication (auth_time within 300 s) and returns 401 reauth_required if the session is stale.

Unlinking is protected by a last-auth-method policy: DELETE /auth/me/oauth-identities/{provider} counts email + phone + OAuth identities and refuses with 409 last_auth_method if removal would leave the user with zero login methods, and revokes upstream tokens at Google/GitHub/Apple. A provider-discovery endpoint (GET /auth/oauth/providers) drives the connected-accounts UI in both admin (desktop-first) and app (mobile-first), each implementing fresh-auth preflight and explicit unlink confirmation.

Key capabilities

  • Single redirect URI per provider with per-app {app} routing, PKCE S256 and SHA-256 state binding
  • Provider-specific email-verification policy via compute_email_verified() across Google, Microsoft, GitHub, Apple
  • Native exchange-code flow (60 s TTL, single-use) keeping tokens out of URLs
  • Apple Sign In with ES256 client_secret and ASAuthorizationController support
  • Account linking with 5-minute fresh-auth window enforcement
  • Last-auth-method protection on unlink with upstream token revocation
  • HTTP-only cookie sessions on .petanque.life and provider-discovery API

In practice

A user already logged in via Google opens the connected-accounts screen in the mobile app and taps 'Add Apple'. The app calls GET /auth/oauth/providers, surfaces Apple as available, and triggers ASAuthorizationController. Apple returns an id_token; the app POSTs it to /auth/oauth/apple/native with the link intent.

The server verifies the id_token against Apple's JWKS, then checks auth_time on the current session — it is 8 minutes old, so the request is rejected with 401 reauth_required. The app re-prompts Google sign-in, refreshes auth_time, and retries the link, which now succeeds. Months later, on a stolen-phone scare, the user opens the connected-accounts screen on web and unlinks Apple; the server verifies that Google + email still remain as login methods, revokes the upstream Apple refresh token, and removes the identity.

Features in this subsystem

16
ID Status Features
F02.06.01 Shipped OAuth gateway — single redirect URI per provider, per-app routing via {app} path parameter, PKCE S256, one-shot state with SHA-256 binding hash, web (cookie) and native (exchange code) flows ✅ PL-1801 (infra), ✅ PL-F0206a (Apple provider + account linking)
F02.06.02 Shipped Sign in with Microsoft (Entra ID + personal MS account) — multitenant common endpoint, id_token JWKS-verifiering, email priority chain (Graph mail → id_token email → preferred_username → UPN), personal tenant → email_verified=False ✅ PL-1802, ✅ PL-F0206a
F02.06.03 Shipped Sign in with Google — id_token JWKS-verifiering mot accounts.google.com, sub som provider_user_id, email_verified trustas från id_token claim ✅ PL-1803, ✅ PL-F0206a
F02.06.04 Shipped Sign in with GitHub — read:user user:email scope, access token-validering via GET /user, X-OAuth-Scopes-enforcement, GET /user/emails primary+verified-policy, str(user["id"]) som provider_user_id ✅ PL-1804, ✅ PL-F0206a
F02.06.05 Shipped Apple Sign In för iOS — ES256 JWT client_secret (team_id+key_id+private_key), id_token-verifiering mot Apple JWKS, POST /auth/oauth/apple/native för ASAuthorizationController-flödet, email_verified=True (Apple-policy), private relay email-stöd ✅ PL-F0206a
F02.06.06 Shipped Account linking — flera identiteter per user, POST /auth/oauth/{provider}/link/{app} med 5-min fresh-auth-fönster (auth_time claim), GET /auth/me session-introspection, GET /auth/me/oauth-identities (safe fields only), DELETE /auth/me/oauth-identities/{provider} med last_auth_method-policy och upstream token-revokation ✅ PL-1806, ✅ PL-F0206a
F02.06.07 Shipped Säker avlänkning med last-auth-method-skydd — DELETE /auth/me/oauth-identities/{provider} räknar email + phone + OAuth-identiteter, blockerar med 409 last_auth_method om borttagning skulle lämna noll metoder, upstream token-revokation (Google/GitHub/Apple) ✅ PL-F0206b
F02.06.08 Shipped Connected accounts UI i app och admin — admin/app/(dashboard)/connected-accounts.tsx (desktop-first, slate/indigo), app/app/settings/connected-accounts.tsx (mobile-first, i18n), fresh-auth-preflight, unlink-bekräftelse, provider-discovery via GET /auth/oauth/providers ✅ PL-F0206b
F02.06.09 Shipped Fresh re-auth-krav (≤5 min) vid linking — is_auth_time_fresh() med FRESH_AUTH_WINDOW_SECONDS=300, auth_time-claim i JWT, POST /link/{app} returnerar 401 reauth_required vid stale session, GET /auth/me exponerar auth_time_fresh-flagga ✅ PL-F0206b
F02.06.10 Shipped HTTP-only cookie session på .petanque.life — pl_access_token/pl_refresh_token med Secure; HttpOnly; SameSite=Lax; Domain=.petanque.life; Path=/, pl_oauth_binding-cookie med SHA-256-hash binding, cookie-cleanup efter callback ✅ PL-F0206b
F02.06.11 Shipped Native exchange code-flöde för mobil — OAuthExchangeCode med 64-byte base64url, 60s TTL, POST /auth/oauth/exchange med find_one_and_delete (single-use), POST /auth/oauth/apple/native för iOS ASAuthorizationController, tokens aldrig i URL ✅ PL-F0206b
F02.06.12 Shipped Per-provider email verification policy — compute_email_verified() med Google (trust claim), Microsoft (work=trust, personal=false), GitHub (primary+verified from /user/emails), Apple (alltid true), konservativ default false ✅ PL-F0206b
F02.06.13 Shipped Per-tenant OAuth-provider-overrides — TenantConfig.oauth_providers: list[OAuthProviderConfig] (provider+enabled+applications-tuple), ?application=app admin|web-query på GET /auth/oauth/providers filtrerar listan mot aktiv tenant så en federation kan stänga av specifika providers per surface utan globalt avhopp | ✅ PL-T206
F02.06.14 Shipped Sign in with Apple på web — Service ID + private-key + JWKS-verifiering återanvänds från native-flödet; /.well-known/apple-developer-domain-association(.txt) serveras från Settings.APPLE_DOMAIN_ASSOCIATION så ägaren kan rotera filen utan deploy ✅ PL-T206
F02.06.15 Shipped OAuth-knappar i tenant-CMS-login (/account/login) — web/src/app/account/login/page.tsx + AccountLoginClient.tsx använder shared fetchOAuthProviders med application=web, renderar PROVIDER_META-färgade knappar inom tenantens BookingThemeStyle-brand, hanterar ?error= via auth.error.<code>-i18n med fallback till auth.error.generic ✅ PL-T206
F02.06.16 Shipped Apple på admin-konsolen — apple i ADMIN_PROVIDER_WHITELIST + ADMIN_PROVIDER_DEFAULTS; admin/src/lib/oauth.ts.fetchProviders() skickar ?application=admin så per-tenant-overrides respekteras ✅ PL-T206