Architecture: Backend-mediated (BFF), with Express owning the session
You have a backend, so don't keep tokens in the browser. Pick one of two flows.
Option A: Next.js calls Express (recommended for a custom UI)
- Load GIS (
https://accounts.google.com/gsi/client) in Next.js with<Script strategy="afterInteractive">or anonloadloader. Don't poll. - Generate a random
nonceserver-side. Store it in the session, or in a short-lived HttpOnly cookie, and pass it togoogle.accounts.id.initialize({ client_id, nonce, callback }). - In the callback,
POSTthecredential(the ID token) to/api/auth/googlewithcredentials: 'include'. - Express verifies the token, creates a server session, and sets an
HttpOnly; Secure; SameSite=Laxcookie. - The ID token is never stored in
localStorageorsessionStorage. The browser holds only the session cookie.
Option B: login_uri redirect
GIS POSTs the credential straight to Express (ux_mode: "redirect"). Express must check the double-submit CSRF value: the g_csrf_token cookie has to equal the g_csrf_token body field. Then it verifies the token, sets the cookie, and returns a 302. No JavaScript ever touches the token.
If Next.js and Express are on different origins, put them under one site or proxy /api through Next.js. That keeps the cookie first-party and makes SameSite=Lax work. Otherwise you need CORS with credentials, which is more fragile.
What the server must verify
Use OAuth2Client.verifyIdToken({ idToken, audience: CLIENT_ID }) from google-auth-library. Don't hand-roll JWT parsing. It checks the signature against Google's JWKS (https://www.googleapis.com/oauth2/v3/certs) and the standard claims. Make sure these hold:
| Check | Requirement |
|---|---|
| Signature | Valid against Google's current public keys (the library caches and rotates them). |
iss |
https://accounts.google.com or accounts.google.com. |
aud |
Exactly your OAuth client ID. This is the most commonly missed check. |
exp |
Not expired. |
nonce |
Matches the value you issued for this session. It's one-time use, so delete it after the check (this blocks replay). |
email_verified |
true before you trust or link on email. |
sub |
Use this as the stable user key. Never use email as the primary identifier, because emails can change. |
hd |
If you restrict to a Workspace domain, check the hd claim server-side. Client-side hd hints aren't enforcement. |
For the Option B flow, also check the g_csrf_token cookie against the body field, as described above.
Session and hardening
- Regenerate the session ID on login, to prevent session fixation.
- Use CSRF protection on your own state-changing endpoints.
SameSite=Laxhelps but isn't enough on its own. - Send
Cross-Origin-Opener-Policy: same-origin-allow-popups. Plainsame-originbreaks the GIS popup silently. - Set a CSP that allows
script-src https://accounts.google.com/gsi/client,frame-src https://accounts.google.com/gsi/, andconnect-src https://accounts.google.com/gsi/. - On logout, call
google.accounts.id.disableAutoSelect()and destroy the server session. Otherwise One Tap signs the user straight back in. - Don't use the legacy
gapi.auth2orgapi.signin2. They're retired. Use GIS only. - Don't trust any user info the client sends alongside the token. Take identity only from the verified payload.
Express sketch
const { OAuth2Client } = require('google-auth-library');
const client = new OAuth2Client(CLIENT_ID);
app.post('/api/auth/google', async (req, res) => {
const ticket = await client.verifyIdToken({
idToken: req.body.credential,
audience: CLIENT_ID,
});
const p = ticket.getPayload();
if (!p.email_verified) return res.sendStatus(401);
if (p.nonce !== req.session.nonce) return res.sendStatus(401);
delete req.session.nonce;
// if (p.hd !== 'yourdomain.com') return res.sendStatus(403);
const user = await upsertUserBySub(p.sub, p.email);
req.session.regenerate(() => {
req.session.userId = user.id;
res.sendStatus(204);
});
});
This is a sketch. Wrap verifyIdToken in try/catch and return 401 on failure. Also make sure regenerate doesn't lose the nonce check order.
I can wire this into your project if you'd like. It would help to know whether Next.js and Express share an origin.
Pure-instruction skill, so the trial gave the model only the Skill and Read tools. The generated Express sketch was not run or tested; the model itself says to wrap verification in try/catch and check the nonce handling order. Review all authentication code before using it.
Qué hace
Guía al modelo por Google Identity Services (GIS) para la web. Prioriza diseños mediados por el backend en los que los tokens nunca llegan al JavaScript del navegador y advierte contra la biblioteca retirada gapi.auth2.
Cómo funciona
- Elige una arquitectura: Backend For Frontend (recomendada), redirección mediante formulario POST a
login_uri(recomendada) o alternativas solo de cliente para sitios estáticos y PWA sin conexión. - Enumera los ajustes necesarios del navegador: CSP,
Cross-Origin-Opener-Policy: same-origin-allow-popups, comportamiento de FedCM y One Tap,disableAutoSelect()al cerrar sesión y permisoidentity-credentials-getpara iframes. - Ofrece ejemplos de referencia: verificación de tokens de ID con FastAPI (firma, audiencia, nonce y
hdde Workspace), CSRF de doble envío para redirección, un patrón React en memoria, un patrón IndexedDB cifrado y el iframe intermedio.
Para qué sirve
Añadir acceso con Google a sitios React, Next.js, Angular o HTML simple y revisar integraciones existentes.
Límites
Solo web: no cubre Google Sign-In antiguo, cuentas de servicio ni Android/iOS nativo. Los ejemplos de servidor están en Python; debes adaptarlos para Node, Go o Java.
Riesgo bajo: solo contiene instrucciones y ejemplos de código del repositorio oficial de Skills de Google (Apache-2.0). Sin scripts, acceso propio a la red ni escritura de archivos. Necesitas tu propio ID de cliente OAuth de Google; los ejemplos solo indican dónde ponerlo. Son puntos de partida, no código auditado: en el ejemplo FastAPI, el nonce solo se comprueba si existe una cookie de nonce. Haz obligatoria esa comprobación en tu código y revisa todo el código de autenticación antes de publicarlo.