Define done before changing the login screen
Google login is ready for coworkers only when all of these statements are true:
- the production origin and login endpoint are registered exactly;
- the backend rejects a missing or mismatched CSRF token;
- the backend verifies the ID-token signature, audience, issuer, and expiry;
- any Workspace restriction checks the verified
hdclaim; - Google's stable
submaps to one local account; - the local account supplies status and roles;
- the application creates and expires its own session;
- disabling a local account blocks access even if Google still authenticates;
- unlinking Google consent and ending local sessions are treated separately.
The sign-in result answers “which Google identity presented this credential?” It does not answer “may this person read payroll records?”
Step 1: register the exact web application boundary
Create or select a Google Cloud project and configure an OAuth 2.0 web client. Google's current setup guide requires the client ID, application branding, authorized JavaScript origins, and—when using a redirect—the login endpoint URI.
Record the values as an environment-specific configuration item:
Environment: production
Origin: https://internal.example.com
Login endpoint: https://internal.example.com/auth/google
Web client ID: stored in approved configuration
Owner: identity-platform owner
Do not use a preview origin as the production origin. Do not put a client secret in browser code. The web client ID is expected to be visible; backend trust comes from verifying the returned signed token for that audience.
Step 2: render Google's current button
Load https://accounts.google.com/gsi/client and render the Sign in with Google
button using Google's HTML or JavaScript API. For a backend POST flow, the HTML
shape is approximately:
<script src="https://accounts.google.com/gsi/client" async></script>
<div
id="g_id_onload"
data-client_id="YOUR_WEB_CLIENT_ID"
data-login_uri="https://internal.example.com/auth/google"
></div>
<div class="g_id_signin" data-type="standard"></div>
This is an illustrative integration shape, not a browser-tested copy-and-paste fixture. Generate and review the actual button settings against Google's current button guidance, content-security policy, origins, and user experience.
Step 3: reject the request before decoding claims
When Google posts to the login endpoint, it supplies the ID token in the
credential field. Google's server-side guidance also describes a
double-submit CSRF value: the g_csrf_token cookie must be present and must
match the value in the POST body.
Reject the request if either value is missing or they differ. Logging the credential is not a debugging shortcut; it is a bearer credential containing identity claims.
Only after the request passes the CSRF check should the backend verify the ID
token with a maintained Google client library. For Node, the documented shape
uses google-auth-library:
const ticket = await googleClient.verifyIdToken({
idToken: request.body.credential,
audience: process.env.GOOGLE_WEB_CLIENT_ID,
});
const claims = ticket.getPayload();
The production library checks the signature and core claims. The backend must also apply the application's hosted-domain policy when one exists.
Step 4: verify the identity claims you actually depend on
Google's server-side verification guide names the important checks:
audidentifies the client for which the token was issued;issmust be a documented Google issuer;expmust still be in the future;subis the stable Google account identifier;hd, when present, identifies a hosted Google domain.
If the internal app is restricted to a Workspace domain, compare the verified
hd value with the allowlist. An @example.com email string is not a substitute
for the hosted-domain claim.
Use sub as the external identity key. Email is useful display and contact
information, but it can change and should not be the durable account key.
Step 5: make a separate local account decision
After token verification, look up a local identity link such as:
provider: google
provider_subject: google-sub-123
local_account_id: acct-42
local_status: active
local_role: operations-viewer
An unknown sub should enter an explicit invitation, linking, or signup flow.
It should not inherit a role from an email suffix. A disabled local account
must remain disabled even when a valid Google token arrives.
Enforce access to data and actions at the trusted server or database boundary. Hiding a button in React does not authorize the corresponding backend request.
Step 6: own the local session and offboarding path
The application creates its own session after all identity and account checks pass. Define cookie settings, session duration, renewal, logout, revocation, and reauthentication for sensitive actions in the application's threat model.
Google's revocation guidance explains how a user can revoke consent to share an ID token. That does not prove the application deleted its local account, ended every existing local session, or removed its roles. Your offboarding operation must do those local jobs and verify the result.
Reproduce the nine boundary tests
Run the local evidence model:
npm test --prefix sites/howtox.com/evidence/P20
On Node.js 22.18.0, all nine tests passed. They cover a successful local session and rejection of bad CSRF, changed signed content, wrong audience, wrong issuer, expiry, missing hosted-domain evidence, a disabled local account, and an unknown subject.
The fixture uses a local Ed25519 test key. It does not contact Google or replace Google's production verification library.
Common failure modes
- Decode without verification: readable JWT claims are not trusted claims.
- Check only email: email is not the stable external identity and its suffix does not prove a hosted Google domain.
- Put roles in browser state: the backend must authorize every protected operation from trusted account data.
- Treat Google logout as application logout: the local session can outlive the browser's Google state.
- Disable only at Google: the internal app still needs a local suspension and session-revocation path.
- Test only success: wrong audience and disabled-account cases are part of the feature, not optional security polish.
Verification checklist
- Exact production origin and login URI are registered.
- CSRF cookie and body values are both required and compared.
- A maintained backend library verifies signature, audience, issuer, and expiry.
- Workspace restriction uses the verified
hdclaim when required. - Local identity links use
sub, not email, as the provider key. - Local account status and roles control the application session.
- Protected requests enforce authorization on the backend.
- Logout, local session revocation, account disabling, and Google consent revocation have separate tests.
Frequently asked questions
Does Google login authorize access to company data?
No. It supplies an authenticated Google identity after verification. The application still owns account status, roles, record authorization, and audit behavior.
Should an internal app allow every account in the company domain?
Not automatically. A hosted-domain restriction can be one admission condition, but the application should still require an explicit local account or invitation policy and a way to disable access.
Is the local fixture a production Google integration test?
No. It tests a synthetic signed-token decision boundary. A real release also needs an approved Google project, browser and backend integration tests, session review, and account-lifecycle exercise.