Public social signup into OAUTH_DEFAULT_ORG (role user, seat-checked); email-match links existing active user instead of duplicating. Server-side provider token validation via stdlib urllib only (no new dep): Google tokeninfo (aud + email_verified) and Facebook app/debug-token/me (is_valid, app_id, me.id==user_id). Fail-closed when creds unconfigured, rate-limited per-IP + per-email, /oauth/config leaks no secrets. Frontend: login buttons (only enabled providers), GSI + FB SDK on-demand, monochrome glyphs, TH/EN. Login page shows social buttons only when backend reports provider enabled. 348 backend tests pass (337 + 11 new OAuth), frontend build + 4/4 unit clean, manual security review PASS. Not pushed (push auto-deploys).
4.4 KiB
4.4 KiB
OAuth (Google + Facebook) — Design & Plan
Status: in progress (2026-08-19)
Owner decision (clarify): public signup — new users from Google/FB join a single default
org (OAUTH_DEFAULT_ORG), role user, seat-checked. If the verified email matches an existing
user, log them in (link) instead of creating a duplicate.
Model chosen
- Supersedes "no self-registration" ONLY for social login. Username/password auth + admin provisioning remain unchanged.
- New OAuth-registered users go to
OAUTH_DEFAULT_ORG(configurable tenant id). Org is active (created if missing) so new signups pass the SaaS tenant gate. Seats apply via existingUserStore.create_userseat check. - Email-match linking: if
by_email(verified_email)finds an active user, issue a token for them (OAuth sign-in); else create a new user.
Security invariants (MUST preserve)
- Users stay keyed by
usernamewithid == username,USERNAME_RE [a-zA-Z0-9_.-]{2,64}, matching_token_identity+require_auth. - OAuth username derivation must satisfy
USERNAME_REand be collision-safe: e.g.g_<google_sub>/fb_<facebook_id>(subs are numeric → valid). Must not collide with an existing username; if it does, suffix with a counter until unique. - Server-side token validation only. Backend validates the provider token over HTTPS —
never trusts a client-declared identity. Use stdlib
urllib.request(no new dep, avoidsrequirements.lock.txtchurn).- Google: verify ID token via
https://oauth2.googleapis.com/tokeninfo?id_token=...; checkemail_verified == "true", audience ==OAUTH_GOOGLE_CLIENT_ID, takesub+email. - Facebook: exchange client token for app token
GET /oauth/access_token?client_id=..&client_secret=..&grant_type=client_credentials, thenGET /me?fields=id,email,name&access_token=<user_token>, then verify the user token viaGET /debug_token?input_token=<user_token>&access_token=<app_token>checkingdata.is_valid,data.app_id == OAUTH_FACEBOOK_APP_ID,data.user_id, and that the Graphmeresult'sid == data.user_id. Only accept verified email.
- Google: verify ID token via
- Provider tokens are single-use inputs, never stored. Secret keys live in
.envonly, never logged or returned. - Rate-limit the OAuth exchange endpoint (per-IP + per-email) like login.
- Reject tokens with invalid issuer/app/audience →
ApiError401/400, never 500. - All outbound provider calls must set a timeout and fail closed (
AuthError→ 401/400).
Config (backend/app/config.py)
New env vars (all optional; OAuth disabled unless configured):
OAUTH_GOOGLE_CLIENT_ID,OAUTH_GOOGLE_CLIENT_SECRETOAUTH_FACEBOOK_APP_ID,OAUTH_FACEBOOK_APP_SECRETOAUTH_DEFAULT_ORG(tenant id for new signups)OAUTH_ENABLED-style toggles derived from presence of creds (fail closed: endpoint 404/disabled when creds absent).
API
POST /api/auth/oauthbody{provider: "google"|"facebook", token: "..."}→ validates token, resolves/creates user, returns{token, user, must_setup}(same shape as login).- No new deps.
Frontend
frontend/src/views/Login.vue+frontend/src/api/index.js: addapi.oauth(provider, token).- Login screen adds "เข้าสู่ระบบด้วย Google / Facebook" buttons (single-color line icons, no colored emoji per brand rule).
- Provider SDK: Google Identity Services (GID) loaded from CDN on demand →
google.accounts.idto get an ID token; FB SDK (fbLogin FB.getLoginStatus / FB.login scope email) to get an access token. Send the token to/api/auth/oauth. Handlemust_setuplike login. - Keep OAuth buttons hidden/disabled when provider creds are not configured (backend drives via a small public config endpoint or by the presence of the button config in the login page response).
Tests (backend, mock/fake provider — no live network)
- New
backend/tests/test_oauth.py: fake provider responses via monkeypatched validation function; assert: new-user creation (org=OAUTH_DEFAULT_ORG, role user, seat-checked), email-match login-links existing user, invalid token → 401, unverified email rejected, disabled provider (creds absent) → 404/disabled, username collision gets a unique suffix, rate-limit enforced, response shape matches login.
Deliverable
Backend routes + validation + store integration + config; frontend buttons + api method + provider SDK wiring; tests; handoff docs.