Skip to content

feat(auth): Sign in with Google on the web login page (S1)#297

Closed
oratis wants to merge 1 commit into
mainfrom
feat/s1-google-signin
Closed

feat(auth): Sign in with Google on the web login page (S1)#297
oratis wants to merge 1 commit into
mainfrom
feat/s1-google-signin

Conversation

@oratis

@oratis oratis commented Jul 24, 2026

Copy link
Copy Markdown
Owner

Part of PLAN_WEB_SIGNUP_v1.0 — milestone S1.

What

Adds the third sign-in provider to LISA Cloud's login page, mirroring the Apple channel's zero-dependency posture:

  • src/web/googleAuth.ts — pure RS256/JWKS verifier for Google ID tokens: dual issuer spellings, aud = OAuth web client id, clock tolerance, 1h-cached JWKS, typed GoogleAuthError. Default-OFF: lights up only when LISA_CLOUD_GOOGLE_SIGNIN and LISA_CLOUD_GOOGLE_CLIENT_ID are both set.
  • POST /api/auth/google — pre-gate access-minting endpoint (same posture as /api/auth/apple), sharing the per-IP auth rate bucket; upserts the account, mints the per-uid HMAC session, pins the cookie.
  • Merge policy (plan §3, D2 verdict): a Google sign-in whose email Google asserts verified links onto an existing verified same-email account — one uid, one Lisa across providers. Unverified squatter accounts never inherit a Google identity. Covered by tests.
  • Login page: GIS button drawn only when /api/auth/config advertises googleWeb; script-load failure (e.g. mainland China) silently hides the button instead of erroring.
  • LOGIN_HTML added to the inline-script syntax regression guard (it wasn't covered).
  • deploy/deploy.sh plumbs the two new env vars.

Replay posture

GIS button-flow tokens are minted per page-load by Google's script with no client-mintable raw nonce, so the ~1h token lifetime is the replay window — the same stance the Apple channel takes for pre-nonce clients. Documented in the module header.

Tests

npm test: 1296 pass / 0 fail. New: googleAuth.test.ts (verifier + config, incl. alg-confusion and tampered-payload cases), 4 merge-policy cases in accounts.test.ts.

Operator steps (not in this PR)

Create the OAuth web client in GCP console (authorized JS origin https://cloud.meetlisa.ai), then deploy with LISA_CLOUD_GOOGLE_SIGNIN=1 LISA_CLOUD_GOOGLE_CLIENT_ID=….

🤖 Generated with Claude Code

Mirrors the Apple channel's zero-dep posture (PLAN_WEB_SIGNUP S1):

- src/web/googleAuth.ts — pure RS256/JWKS verifier for Google ID tokens
  (dual-issuer, aud = OAuth web client id, clock-tolerant, cached JWKS),
  default-OFF config gated on LISA_CLOUD_GOOGLE_SIGNIN + _CLIENT_ID.
- POST /api/auth/google — pre-gate access-minting endpoint sharing the
  per-IP auth rate bucket; upserts the account and mints a per-uid session.
- Account merge policy: a Google sign-in whose email Google asserts
  verified links onto an existing VERIFIED same-email account (same uid,
  same Lisa); unverified squatters never inherit an identity.
- Login page draws the GIS button only when /api/auth/config advertises
  it; unreachable networks (e.g. mainland China) silently hide it.
- LOGIN_HTML joins the inline-script syntax regression guard.
- deploy.sh plumbs the two new env vars.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@oratis

oratis commented Jul 25, 2026

Copy link
Copy Markdown
Owner Author

Review — merge-ready after one fix ⚠️ (adversarial pass)

The two things that must be right in an auth foundation are both correct: ID-token verification (RSA signature verified before any claim is trusted, alg pinned to RS256 so alg:none/HS-confusion are dead, kid from Google's JWKS over TLS, iss/aud/exp/iat all checked) and the account-takeover defense — a Google login only merges onto an existing account when both Google asserts email_verified and the local account is already verified:true (reached only via the mailed-token flow). Every takeover path reduces to "already controls the email." Session minting is byte-identical to the password path. No client secret in deploy.sh (none needed). Nicely done.

MUST-FIX — login-CSRF (Medium). readJsonBody (server.ts) never checks Content-Type and tolerantly JSON.parses any body. POST /api/auth/google has no state/nonce and no Origin check. An attacker submits their own valid Google credential cross-site via <form enctype="text/plain"> (a CORS "simple request" → no preflight); the server sets lisa_token=<attacker session> in the victim's jar, and on the victim's next same-site visit they silently operate inside the attacker's account (session fixation → their chats/uploads/spend land in the attacker's Lisa). SameSite=Strict doesn't help (it governs sending, not storing).

Fix I'll push: enforce Content-Type: application/json centrally in readJsonBody (forms can't forge it → forces a preflight that fails without CORS). One change covers /api/auth/google + the shared siblings (apple, login, and S2's new OTP routes). Optional GIS nonce (mirror the Apple expectedNonce from #261) as follow-up.

Non-blocking: verified: emailVerified !== false defaults a missing claim to true — prefer === true; JWKS unknown-kid could refresh-once before rejecting.

@oratis

oratis commented Jul 25, 2026

Copy link
Copy Markdown
Owner Author

Superseded by the A-series auth that landed on main via #305 (Land auth OTP + Google + mail A1–A6) — main already has Google sign-in + OTP, so this now conflicts on 7 files with no new content. The two review findings that were LIVE on main (login-CSRF via readJsonBody, and — for #301 — the birth crash-window) are fixed in #306. Closing as superseded per the plan; reopen if any piece here isn't actually covered by the A-series.

@oratis oratis closed this Jul 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant