git:20261013.9703d5e to git:20261020.6a7c3e1

11 added, 9 removed. Audit A to A.

---
name: google-signin
description: >-
- Use when implementing, reviewing or debugging Google login / sign-in / sign-up on a website —
- wiring the GIS button or One Tap, verifying Google ID tokens on the server, linking Google to
- existing password accounts, or fixing "origin is not allowed for the given client ID". Covers
- GCP OAuth client setup, backend ID-token verification, three-way account linking with the pre-
- hijacking guard, login-CSRF defense, nonce/replay protection and a mandatory security
- checklist. Triggers - "sign in with google", "google login", "google sign-in button", "GIS",
- "google.accounts.id", "gsi/client", "verify google token", "one tap", "account linking",
- "login csrf", "g_csrf_token", "вход через Google", "кнопка входа Google", "связать аккаунты",
- "проверить токен Google". For the broader library surface use google-auth instead.
+ Use when implementing or debugging Google sign-in on a website: the GIS button or One Tap,
+ server-side ID token checks, linking Google to password accounts, login CSRF. Not for a
+ server authenticating to Google (google-auth). Opt out: no wiring, без обвязки. Triggers -
+ "sign in with google", "google login", "GIS", "one tap", "account linking", "вход через
+ Google", "связать аккаунты".
license: MIT
---
# Google Sign-In (GIS) — production web login
Condensed operating instructions. Full narrative guide with diagrams, token
anatomy, troubleshooting table, and a copy-paste FastAPI + JS skeleton:
[references/full-guide.md](references/full-guide.md) — read it when
implementing from scratch; its FastAPI + JS skeleton is the reference
implementation this skill ships.
## Pick the right flow first
- Only need "who is this user?" → **GIS ID-token flow** (this skill): load
`https://accounts.google.com/gsi/client`, receive one Google-signed JWT,
verify server-side. No redirect URI, no client secret, no Google-token
storage.
- Need to call Google APIs (Gmail/Drive/Calendar) on the user's behalf →
OAuth 2.0 **authorization-code** flow instead (redirect URI + client
secret + refresh tokens). Never use the code flow just for login.
## Core flow (ID-token)
1. Frontend: fetch a **server-issued nonce** first (`GET /api/auth/google/nonce`
— the server stores it with a TTL, bound to a pre-auth HttpOnly cookie),
then `google.accounts.id.initialize({ client_id, callback, nonce })`
+ `renderButton()`. A client-generated nonce POSTed back proves nothing:
the value would be read out of the very JWT it is meant to check. The GSI
script loads async — retry rendering (e.g. 20 × 150 ms) instead of
silently dropping the button.
2. Callback receives `{ credential }` — a Google-signed ID token (JWT).
POST it alone to your backend (no nonce in the body). Never treat it as a
session.
3. Backend: verify with the official lib — Python
`google.oauth2.id_token.verify_oauth2_token(credential, transport, CLIENT_ID)`,
Node `google-auth-library` `verifyIdToken({ idToken, audience })`. That
checks signature (Google JWKS), `aud`, `iss`, `exp`. Never hand-decode
and trust the payload.
4. Additionally require `email_verified == true` and **pop the expected nonce
from the pre-auth session (one-time consume) and require the token `nonce`
claim to equal it exactly** — a missing claim, a missing expectation, an
expired or an already-consumed nonce all reject. This is what makes a
stolen token non-replayable: a replay from a new session meets a different
expectation, a replay in the same session meets a consumed one.
5. Find-or-create the user, then issue YOUR OWN session (app JWT) as an
**HttpOnly + Secure + SameSite=Strict cookie**. Google's token is
verified once, never stored, never logged.
## Identity & account linking (three-way branch)
Key the user on `sub` (stable Google user ID; store as `google_id`) — never
on email (emails change, `sub` doesn't). On each Google login:
1. Record with this `google_id` exists → login (refresh name/picture).
2. Email matches an existing account without `google_id` → **do NOT auto-link
on `email_verified` alone.** `email_verified` is Google saying it verified
the inbox ONCE, not that this person owns the LOCAL account, and for a
third-party address Google is not even authoritative that they still own the
inbox. Default: link only after a **fresh re-auth of the existing local
account** (its password / existing factor), so the person proves they hold
the account Google is being attached to.
- Auto-link WITHOUT that re-auth is allowed only when Google is
**authoritative for the address** — `email_verified` AND (the address is
`@gmail.com` OR the token carries an `hd` Workspace-domain claim) — AND
the existing account's own email ownership was proven. For any other
(third-party) address, run an **independent challenge** (a link to that
inbox) before linking; Google's one-time verification is not current proof.
- If the existing account is a password account whose email was **never
verified** (a possible pre-registration hijack), send the person into a
**safe account-recovery flow** — never "sign in with the existing
password", which may be the attacker's.
3. No match → create the user. Mark `email_verified=True` only when Google is
authoritative for the address (Gmail or `hd`); for a third-party address
record it unverified and challenge the inbox before granting anything that
trusts the email.
## Login-CSRF (cover BOTH delivery flows)
The two delivery flows are **separate, explicitly typed paths**, chosen by
Content-Type — not one handler that claims both. A skeleton that declares form
support but only binds a JSON body does not actually run the form contract.
- **Form-POST flow** (`login_uri` auto-POST, `application/x-www-form-urlencoded`):
GIS double-submits `g_csrf_token` in the FORM BODY and the cookie — require
BOTH present and equal, constant-time (`hmac.compare_digest`). Missing either
side → reject. This token is the whole authorization for the form path.
- **JS-fetch flow** (`application/json`): no `g_csrf_token`, so require a
trusted same-origin signal — `Sec-Fetch-Site: same-origin` (browser-set,
unforgeable), OR, only when that header is ABSENT, an exact `Origin` in an
allowlist. A **missing `Sec-Fetch-Site` is NOT assumed same-origin**;
`cross-site` and `none` are rejected; and with neither a trusted metadata
value nor an allowed Origin the request **fails closed**.
The CSRF decision runs BEFORE the token is verified — no external call on a
request that has not proven its origin. Without this, an attacker's page can
force-POST the *attacker's* credential and silently log the victim into the
attacker's account.
## Setup (GCP)
1. Cloud Console → APIs & Services → OAuth consent screen: choose the audience.
External Testing normally limits access to test users, but basic identity
scopes (`openid`, `email`, `profile`) are exempt. Workspace policy can still
block access; publishing alone is not the remedy. See the dated setup note
in [the full guide](references/full-guide.md).
2. Credentials → Create OAuth client ID → **Web application** →
**Authorized JavaScript origins** = every origin the button renders on,
incl. `http://localhost:<port>` for dev (scheme+host+port, no path, no
trailing slash). **Authorized redirect URIs: leave empty** for GIS.
3. Ship only `GOOGLE_CLIENT_ID` (public; env var; one source of truth —
inject into HTML server-side, e.g. via a `<meta>` tag). The client
secret is UNUSED in the GIS flow — never put it in the app.
4. CSP if present: allow `https://accounts.google.com` in `script-src`,
`connect-src`, `frame-src`.
5. If `GOOGLE_CLIENT_ID` is empty → hide the button and return
"not configured" server-side (honest degradation, no crash).
## Security checklist (verify ALL before calling it done)
- [ ] Signature verified via official lib against Google JWKS (no
`verify=False`, no manual base64 decode-and-trust)
- [ ] `aud` == your client ID; `exp` enforced
- [ ] `email_verified` required
- [ ] Nonce: fresh per render, checked server-side against the token claim
- [ ] Pre-hijacking guard on email-based linking
- [ ] Login-CSRF: `g_csrf_token` double-submit AND same-origin check for
the JS flow
- [ ] Own session cookie: HttpOnly + Secure + SameSite=Strict
- [ ] `/api/auth/*` rate-limited; auth events logged WITHOUT the credential
- [ ] Session revocation path exists (e.g. an `auth_version`/`ver` claim
check)
- [ ] Logout also calls `google.accounts.id.disableAutoSelect()`
## Debugging quick table
| Symptom | Fix |
|---|---|
| "The given origin is not allowed for the given client ID" | Add the exact origin (port, `www.`) to Authorized JavaScript origins; propagation takes minutes |
| Button never renders | GSI async race (add retry loop), empty client ID, or CSP blocks `accounts.google.com` |
| "Token used too early/expired" | Server clock skew → sync NTP |
| Audience mismatch | Frontend/backend client IDs differ → single env source of truth |
| `access_denied` on consent | Check requested scopes, audience and Workspace policy. Testing allowlisting applies beyond basic identity scopes; investigate before changing publishing status |
| Works locally, breaks in prod | Prod origin missing in GCP, or `Secure` cookie served over plain HTTP |
| One Tap missing / FedCM console warnings | FedCM is mandatory since Aug 2025 and the opt-out is gone (checked 2026-08-06). Load `gsi/client` from Google and keep it current — a vendored or pinned copy is the real break. The rendered button flow is unaffected; code branching on the old `isNotDisplayed()` moment callbacks is not |
## Degradation
- **Not Claude Code** (Cursor, Codex, the skills CLI, the API container): the pack's
`PreToolUse` gate does not run — the rules here hold, the refusal does not.
- **No browser channel**: the sign-in button and its redirect are verified by reading
the code, not by watching it work. Record that as the weaker claim it is.
- **No console access**: creating the OAuth client is a human step; state it once, do
the rest, and say what is waiting on it.
+
+ ## Triggers
+
+ More phrases that route here: "google sign-in button", "google.accounts.id", "gsi/client",
+ "verify google token", "login csrf", "g_csrf_token", "кнопка входа Google", "проверить токен
+ Google", "origin is not allowed for the given client ID".