Skip to content

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:

  1. A known identity (the same Google account was linked before): that user signs in, and the stored email is refreshed.
  2. The email of a password account: nothing is linked automatically. The user is sent to /auth/link-google and must enter that account’s password first (section 3).
  3. The email of a deleted user: OAUTH_FAILED. Nothing is created.
  4. 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.
  5. Open registration: a new verified user without a password.
  6. 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.

  1. In Google Cloud Console, APIs & Services > Credentials, create an OAuth client of type Web application.
  2. Set up the OAuth consent screen, with the scopes openid, email and profile. These are non-sensitive, so no Google verification is needed.
  3. Add the authorised redirect URI ${APP_URL}/api/auth/google/callback. In dev that is http://localhost:4200/api/auth/google/callback, through the client’s proxy. In a split-origin deploy use GOOGLE_REDIRECT_URI instead (section 5).
  4. Copy the client id and secret into apps/<product>/api/.env as GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET.
  5. Restart the API. GET /api/auth/providers now 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 plain http only for localhost. A production APP_URL, or GOOGLE_REDIRECT_URI, must be an https URL, and registered at Google exactly.
VariableDefaultNotes
GOOGLE_CLIENT_IDnoneset both or neither
GOOGLE_CLIENT_SECRETnonekeep it in the secret store
GOOGLE_REDIRECT_URI${APP_URL}/api/auth/google/callbackonly for a split-origin deploy
GOOGLE_ISSUERhttps://accounts.google.comfor 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_SET until 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

SituationCodeWhere the user landsAudit
Cancelled at Google (error=access_denied)OAUTH_CANCELLEDlogin (quiet info text, not an error colour)none
Any other error= from GoogleOAUTH_FAILEDloginnone
State cookie missing, unreadable, expired or mismatchedOAUTH_STATE_INVALIDlogin, “Sign-in took too long. Try again”none
Discovery, JWKS or token endpoint unreachable, 429 or 5xxOAUTH_PROVIDER_UNAVAILABLElogin, “Google is not reachable right now”none; logged at error
ID token fails validationOAUTH_FAILEDloginAUTH_LOGIN_FAILED OAUTH_ERROR
email_verified false or no emailOAUTH_EMAIL_NOT_VERIFIEDloginOAUTH_EMAIL_NOT_VERIFIED
Invite-only, no live invitationREGISTRATION_CLOSEDlogin (the existing text)REGISTRATION_CLOSED
Identity’s user or the email’s user is soft-deletedOAUTH_FAILEDlogin: the same generic text as any failure (G9)ACCOUNT_UNAVAILABLE
Google account linked to another user, or a P2002 raceOAUTH_ACCOUNT_IN_USElogin or profilenone
Profile link with a Google account of another emailOAUTH_EMAIL_MISMATCHprofilenone
2FA failure budget exhaustedTOO_MANY_REQUESTSloginAUTH_2FA_FAILED LOCKED, as today
Link token unknown, used, expired or exhausted400 INVALID_OR_EXPIRED_TOKENthe link page’s “expired” statenone
Wrong password on link400 BAD_REQUESTstays on the link pageAUTH_LOGIN_FAILED INVALID_CREDENTIALS
Google not configured404 NOT_FOUND(the button is hidden, so only a hand-typed URL)none
Password action on an account without a password409 PASSWORD_NOT_SETinline messagenone

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_URI to the API’s own host, or /auth/callback’s refresh fails with OAUTH_FAILED.
  • GOOGLE_CLIENT_SECRET lives in the secret store.
  • JWT_SECRET rotation 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 (startFakeOidc in @core/api-core/e2e) and points GOOGLE_ISSUER at it. http: is accepted only on loopback.
  • Run pnpm nx test core-api-auth and PORT=3001 pnpm nx e2e keelmint-api-e2e.

7. Troubleshooting by error code

CodeUsual 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_INVALIDThe state cookie expired (10 minutes), cookies are blocked, the callback was replayed, or JWT_SECRET changed.
OAUTH_PROVIDER_UNAVAILABLEThe API cannot reach Google (network, proxy, DNS) or Google answered 5xx or 429. See the API’s error log.
OAUTH_FAILEDInvalid ID token, a wrong client secret, a deleted account, or a split-origin deploy without GOOGLE_REDIRECT_URI.
OAUTH_EMAIL_NOT_VERIFIEDThe Google account has no verified email.
REGISTRATION_CLOSEDThe product is invite-only and the email has no live invitation. Invite the user first.
OAUTH_ACCOUNT_IN_USEThat Google account is linked to another user.
OAUTH_EMAIL_MISMATCHThe Google account’s email differs from the profile’s email.
OAUTH_CANCELLEDThe 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_SETA password action on a Google-only account. Set a password through the emailed link.
404 on /api/auth/googleThe product has no GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET.