google-signin
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.
How do I install this agent skill?
npx skills add https://github.com/ssheleg/sheleg-dev --skill google-signinIs this agent skill safe to install?
- Gen Agent Trust Hubpass
The skill provides comprehensive and secure guidance for implementing Google Sign-In (GIS). It emphasizes industry best practices such as backend token verification using official libraries, nonce-based replay protection, and CSRF defense mechanisms. No malicious patterns, obfuscation, or unauthorized data access were detected.
- Socketpass
No alerts
- Snykpass
Risk: LOW · No issues
What does this agent skill do?
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 — 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)
- 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), thengoogle.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.
- 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. - Backend: verify with the official lib — Python
google.oauth2.id_token.verify_oauth2_token(credential, transport, CLIENT_ID), Nodegoogle-auth-libraryverifyIdToken({ idToken, audience }). That checks signature (Google JWKS),aud,iss,exp. Never hand-decode and trust the payload. - Additionally require
email_verified == trueand pop the expected nonce from the pre-auth session (one-time consume) and require the tokennonceclaim 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. - 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:
- Record with this
google_idexists → login (refresh name/picture). - Email matches an existing account without
google_id→ do NOT auto-link onemail_verifiedalone.email_verifiedis 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_verifiedAND (the address is@gmail.comOR the token carries anhdWorkspace-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.
- Auto-link WITHOUT that re-auth is allowed only when Google is
authoritative for the address —
- No match → create the user. Mark
email_verified=Trueonly when Google is authoritative for the address (Gmail orhd); 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_uriauto-POST,application/x-www-form-urlencoded): GIS double-submitsg_csrf_tokenin 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): nog_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 exactOriginin an allowlist. A missingSec-Fetch-Siteis NOT assumed same-origin;cross-siteandnoneare 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)
- Cloud Console → APIs & Services → OAuth consent screen (External; publish it — in "Testing" only allowlisted users can sign in).
- 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. - 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. - CSP if present: allow
https://accounts.google.cominscript-src,connect-src,frame-src. - If
GOOGLE_CLIENT_IDis 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;expenforced -
email_verifiedrequired - Nonce: fresh per render, checked server-side against the token claim
- Pre-hijacking guard on email-based linking
- Login-CSRF:
g_csrf_tokendouble-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/verclaim 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 | Consent screen still in Testing → publish the app |
| 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
PreToolUsegate 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.
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/ssheleg/sheleg-dev/google-signin">View google-signin on skillZs</a>