Google login
A product can offer “Continue with Google” on its login, register and invitation pages. It is off until the product’s API has a Google OAuth client. A user can also link or unlink Google in their profile (“Sign-in methods”).
The session, 2FA and cookies work as in Authentication and Two-factor authentication.
1. How it works
The client sends the browser to GET /api/auth/google. The API starts an
OpenID Connect authorization code flow with PKCE and a nonce, and keeps the
state in a short-lived encrypted cookie. Google sends the browser back to the
API’s callback, the API exchanges the code, validates the ID token and
redirects to the client.
sequenceDiagram participant B as Browser participant A as API participant G as Google B->>A: GET /api/auth/google A-->>B: 302 to Google + Set-Cookie oauth_google (10 min, encrypted) B->>G: consent (select account) G-->>B: 302 /api/auth/google/callback?code&state B->>A: GET callback (+ oauth_google) A->>G: code + PKCE verifier + client secret G-->>A: ID token (validated: signature, iss, aud, exp, nonce) A-->>B: 302 /auth/callback (+ refresh cookie) | #challenge= | /auth/link-google#token= | ?error=Once the ID token is valid and Google says the email is verified, the API resolves the person in this order:
- A known identity (the same Google account was linked before): that user signs in, and the stored email is refreshed.
- The email of a password account: nothing is linked automatically. The
user is sent to
/auth/link-googleand must enter that account’s password first (section 3). - The email of a deleted user:
OAUTH_FAILED. Nothing is created. - A pending invitation for the email: the invitation wins. The user is created with the invitation’s roles and language, in an open or an invite-only product.
- Open registration: a new verified user without a password.
- Invite-only registration, no invitation:
REGISTRATION_CLOSED.
2FA and sessions are as for a password login: a user with 2FA gets a
challenge and enters the code, then the session starts and is audited as
AUTH_LOGIN_SUCCEEDED with method google. Tokens travel only in URL
fragments (#challenge=, #token=), never in a query string. A session needs
none: the refresh cookie is set on the callback’s redirect.
2. Google Cloud Console setup
Do this once per product, with that product’s own OAuth client.
- In Google Cloud Console, APIs & Services > Credentials, create an OAuth client of type Web application.
- Set up the OAuth consent screen, with the scopes
openid,emailandprofile. These are non-sensitive, so no Google verification is needed. - Add the authorised redirect URI
${APP_URL}/api/auth/google/callback. In dev that ishttp://localhost:4200/api/auth/google/callback, through the client’s proxy. In a split-origin deploy useGOOGLE_REDIRECT_URIinstead (section 5). - Copy the client id and secret into
apps/<product>/api/.envasGOOGLE_CLIENT_IDandGOOGLE_CLIENT_SECRET. - Restart the API.
GET /api/auth/providersnow answers{ "google": true }and the button shows.
Before launch, two things that a dev setup never hits:
- Publish the app. An External consent screen whose publishing status
is Testing lets only the listed test users sign in. Everyone else is
stopped at Google, and what they see there depends on Google: usually an
“access blocked” page, or a return to our login page with the quiet
“cancelled” notice (
OAUTH_CANCELLED), which points nowhere near the cause. Move the app from Testing to In production (the three scopes need no Google verification), or add every user as a test user. - Production redirect URIs must be
https. Google accepts plainhttponly forlocalhost. A productionAPP_URL, orGOOGLE_REDIRECT_URI, must be anhttpsURL, and registered at Google exactly.
| Variable | Default | Notes |
|---|---|---|
GOOGLE_CLIENT_ID | none | set both or neither |
GOOGLE_CLIENT_SECRET | none | keep it in the secret store |
GOOGLE_REDIRECT_URI | ${APP_URL}/api/auth/google/callback | only for a split-origin deploy |
GOOGLE_ISSUER | https://accounts.google.com | for tests; http: only on localhost, 127.0.0.1, [::1] |
3. Linking, unlinking and accounts without a password
- Linking needs the account’s password, and only Google’s verified email equal to the account’s email can be linked (D8). Nothing is linked automatically. A link is started from the login flow (branch 2 above) or from the profile’s Sign-in methods card.
- Unlinking is in the same card and needs the password.
- A Google-only account has no password. It sets one with “Email me a link to set a password”, which is the normal reset link (D9). Reset also lets the user sign in with a password afterwards.
- Change password, delete account and turning on 2FA answer
409 PASSWORD_NOT_SETuntil a password is set. - A Google row stays visible for an account that has a linked Google identity, even when the product has no Google client, so it can still be unlinked.
4. Failure modes
| Situation | Code | Where the user lands | Audit |
|---|---|---|---|
Cancelled at Google (error=access_denied) | OAUTH_CANCELLED | login (quiet info text, not an error colour) | none |
Any other error= from Google | OAUTH_FAILED | login | none |
| State cookie missing, unreadable, expired or mismatched | OAUTH_STATE_INVALID | login, “Sign-in took too long. Try again” | none |
| Discovery, JWKS or token endpoint unreachable, 429 or 5xx | OAUTH_PROVIDER_UNAVAILABLE | login, “Google is not reachable right now” | none; logged at error |
| ID token fails validation | OAUTH_FAILED | login | AUTH_LOGIN_FAILED OAUTH_ERROR |
email_verified false or no email | OAUTH_EMAIL_NOT_VERIFIED | login | OAUTH_EMAIL_NOT_VERIFIED |
| Invite-only, no live invitation | REGISTRATION_CLOSED | login (the existing text) | REGISTRATION_CLOSED |
| Identity’s user or the email’s user is soft-deleted | OAUTH_FAILED | login: the same generic text as any failure (G9) | ACCOUNT_UNAVAILABLE |
| Google account linked to another user, or a P2002 race | OAUTH_ACCOUNT_IN_USE | login or profile | none |
| Profile link with a Google account of another email | OAUTH_EMAIL_MISMATCH | profile | none |
| 2FA failure budget exhausted | TOO_MANY_REQUESTS | login | AUTH_2FA_FAILED LOCKED, as today |
| Link token unknown, used, expired or exhausted | 400 INVALID_OR_EXPIRED_TOKEN | the link page’s “expired” state | none |
| Wrong password on link | 400 BAD_REQUEST | stays on the link page | AUTH_LOGIN_FAILED INVALID_CREDENTIALS |
| Google not configured | 404 NOT_FOUND | (the button is hidden, so only a hand-typed URL) | none |
| Password action on an account without a password | 409 PASSWORD_NOT_SET | inline message | none |
The codes and texts never reveal which branch an unknown or deleted email took,
and never echo Google’s own error_description.
5. Production checklist
- The redirect URI registered at Google matches exactly, and is
https. - The consent screen is published (In production), not left in Testing, or only the listed test users can sign in.
- Split-origin deploys set
GOOGLE_REDIRECT_URIto the API’s own host, or/auth/callback’s refresh fails withOAUTH_FAILED. GOOGLE_CLIENT_SECRETlives in the secret store.JWT_SECRETrotation also ends Google sign-ins started in the last 10 minutes (the state cookie key derives from it).- Admins should ask Google-only users to set a password, so a Google outage does not lock them out.
- The per-IP throttle is per instance.
- Before the first release that turns Google on, run a manual smoke test with a real Google OAuth client: a new user, an existing user’s link, and a user with 2FA. The e2e suite uses a fake provider, so Google’s live behaviour (issuer, consent screen, publishing status) is not covered by tests.
6. Local setup and tests
- Google is off unless both keys are set.
- The e2e suite starts a fake OpenID provider (
startFakeOidcin@core/api-core/e2e) and pointsGOOGLE_ISSUERat it.http:is accepted only on loopback. - Run
pnpm nx test core-api-authandPORT=3001 pnpm nx e2e keelmint-api-e2e.
7. Troubleshooting by error code
| Code | Usual cause and fix |
|---|---|
redirect_uri_mismatch (shown by Google) | The URI at Google differs from ${APP_URL}/api/auth/google/callback or GOOGLE_REDIRECT_URI. |
OAUTH_STATE_INVALID | The state cookie expired (10 minutes), cookies are blocked, the callback was replayed, or JWT_SECRET changed. |
OAUTH_PROVIDER_UNAVAILABLE | The API cannot reach Google (network, proxy, DNS) or Google answered 5xx or 429. See the API’s error log. |
OAUTH_FAILED | Invalid ID token, a wrong client secret, a deleted account, or a split-origin deploy without GOOGLE_REDIRECT_URI. |
OAUTH_EMAIL_NOT_VERIFIED | The Google account has no verified email. |
REGISTRATION_CLOSED | The product is invite-only and the email has no live invitation. Invite the user first. |
OAUTH_ACCOUNT_IN_USE | That Google account is linked to another user. |
OAUTH_EMAIL_MISMATCH | The Google account’s email differs from the profile’s email. |
OAUTH_CANCELLED | The user pressed Cancel at Google, or Google refused the sign-in (for example a Testing-status app and a user who is not a listed test user). |
PASSWORD_NOT_SET | A password action on a Google-only account. Set a password through the emailed link. |
404 on /api/auth/google | The product has no GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET. |