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.
它做什么
指导模型使用 Google Identity Services(GIS)在网页上接入 Google 登录。它优先推荐令牌不进入浏览器 JavaScript 的后端中转方案,并提醒不要再用已停用的 gapi.auth2。
工作方式
- 选择架构:后端代理前端(BFF,推荐)、
login_uri表单 POST 跳转(推荐),以及给纯静态站和离线 PWA 用的纯前端备选方案。 - 列出必须配置的浏览器侧设置:CSP、
Cross-Origin-Opener-Policy: same-origin-allow-popups、FedCM 与 One Tap 的行为、退出时调用disableAutoSelect(),以及 iframe 里的identity-credentials-get权限。 - 提供参考实现:FastAPI 校验 ID 令牌(签名、audience、nonce、Workspace 的
hd)、跳转模式的双重提交 CSRF、React 内存存储方案、加密 IndexedDB 方案和 intermediate iframe。
适合
给 React、Next.js、Angular 或纯 HTML 网站加 Google 登录,或审查已有的接入代码。
局限
只管网页:不含旧版 Google Sign-In、服务账号和 Android/iOS 原生。服务端示例是 Python,用 Node、Go、Java 时需要自己改写。
低风险:纯指令和示例代码文件,来自 Google 官方 Skills 仓库(Apache-2.0)。没有脚本,自身不联网、不写文件。你需要自己的 Google OAuth 客户端 ID,示例只演示它放在哪里。示例只是起点,不是审计过的代码:FastAPI 示例里的 nonce 只在带有 nonce cookie 时才校验,请在自己的代码里把这一步改成必须校验;所有认证相关的代码上线前都要自己审一遍。