Files
redefined-designs/docs/ops/google-sign-in.md
T
synAdminandClaude Opus 5 2c6ac4d2be
Linting / lint (pull_request) Successful in 3m49s
SonarQube Analysis / sonarqube (pull_request) Failing after 30m35s
feat(auth): offer Google sign-in on the login form (#345)
The last of the six, and the first a customer can see. The auth form is the single sign-in implementation rendered by both the route modal and the cart prompt, so the button goes in one place and appears in both.

Below the passkey button, which is below the password form. The order is deliberate and it is not about preference: a passkey is already on the device in front of the customer, while Google is a round trip to somebody else's site, and passwords are how every existing customer signs in. Each step down that list asks more of the person using it.

Absent rather than disabled where it is not configured, which is the same call #41 made for a browser without WebAuthn. It matters more here, because being unconfigured is the normal state rather than the exception: local development has no credentials, and QA cannot have any until #313. The storefront advertises a boolean through the existing public config, never the client id — the browser has no use for one, since the whole flow is a redirect the server builds.

Google's mark is inlined as SVG with their published colours and geometry. A hand-drawn approximation of somebody else's trademark is a compliance problem rather than a style choice, and a second origin on the sign-in path is a second thing that can be down.

The button is a navigation rather than a fetch, which makes it unlike every other control on that form. The flow leaves the application entirely, so there is no promise to await and no error to catch — the callback decides and redirects.

Where to return to is supplied by the caller, because only the caller knows. The route modal renders over a backdrop location and its own path is /login, so reading the current URL there would send the customer back to the form they just left; the router builds it from the backdrop instead. The cart prompt uses the page it interrupted. It cannot resume the interrupted action the way onSuccess does — the redirect leaves the app — so the customer lands back on the page and presses the button again.

That value is validated on the server and not in the browser. It has to be, since anyone can type the URL, and doing it in one place beats doing it twice in two languages.

The end-to-end test asserts the button is ABSENT, which is the behaviour local and QA actually have, and then signs in with the password form to show that its absence changes nothing. That is the point of putting the alternatives below rather than above.

docs/ops/google-sign-in.md records what has to be true outside the repository: the seven sections of the Google Auth Platform, the three scopes that keep publishing out of a verification review, the cutover checklist for #313, and the production smoke test. It states plainly that QA on the Synology hostname is impossible rather than merely unconfigured, because Google will not accept a redirect URI whose domain nobody can prove they own — the same wall #285 hit with Cloudflare.

The failure that document warns about hardest is leaving the consent screen in Testing. Only listed test users can then sign in, the refusal happens on Google's own page, and nothing reaches the storefront at all — so a customer reports a broken button and the logs are silent.

Verified: backend tsc clean for src and tests, 590 unit tests pass, lint at the seven warnings that predate this branch, frontend tsc, lint and build clean. The integration and end-to-end suites need a database this machine has no Docker for.

Closes #345

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 15:13:18 -05:00

5.0 KiB

Google sign-in

What has to be true outside the repository for the Google button to work, and what to do at the domain cutover. The code side is #332 and the six issues under it; this is only the parts that live in a browser tab at Google.

Where it is configured

The Google Auth Platform in the Google Cloud Console, in one project. There is one consent screen per project and every OAuth client in it shares that screen, so what appears there is the production identity even while testing.

Section What it holds
Branding App name, support email, authorized domains, the three app links
Audience External, publishing status, test users
Clients The OAuth client, its redirect URIs, the id and secret
Data Access Exactly openid, email, profile
Verification Center Nothing to submit, and it should stay that way

The constraint that shapes everything

Google refuses a redirect URI whose host is not under an authorized domain, and a domain can only be authorized after ownership is proved by DNS in Search Console. localhost is the only exemption.

qa-redefined-designs.bermudalamb.synology.me therefore cannot ever be used: Synology owns the registrable domain above it, so there is no record to add and nothing to prove. This is the same wall #285 hit with Cloudflare.

The consequence, stated plainly because it changes how the feature is worked on:

Environment Google sign-in
Local, on localhost Works, by exemption
QA on the Synology hostname Impossible, not merely unconfigured
QA on qa.redefined-designs.com Works, after the cutover
Production on redefined-designs.com Works, after the cutover

So this feature is built and exercised locally, and QA cannot see it at all until QA moves onto a subdomain of the real domain. The Search Console property for redefined-designs.com already covers qa.redefined-designs.com, because a Domain property covers every subdomain.

docker-compose.qa.yml sets both credentials to empty deliberately, with a comment saying so, and the storefront then offers no button rather than one that fails at Google.

Scopes, and why publishing needs no review

openid produces the id token carrying the subject claim, which is the identity stored. email carries the address and the email_verified flag the linking policy turns on. profile carries the names used when an account is created.

All three are non-sensitive. Requesting only them is what lets the app publish without verification and without customers seeing an unverified-app warning. Add one sensitive scope and publishing becomes a review with a video walkthrough and a wait measured in weeks. Nothing in this feature needs one.

Uploading an app logo also triggers a brand review, which is why Branding has none.

The cutover checklist, for #313

  1. Point QA at qa.redefined-designs.com and set its PUBLIC_URL to match.
  2. Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in the QA stack. Both or neither — the backend refuses to start on one without the other, because the failure would otherwise arrive the moment a customer presses the button.
  3. In Clients, add the QA callback: https://qa.redefined-designs.com/api/auth/google/callback
  4. Confirm the production callback is registered: https://redefined-designs.com/api/auth/google/callback
  5. In Audience, move the publishing status from Testing to In production. Do it once the domain resolves, so the home page and privacy links Google shows actually answer.

No code changes at any step. The redirect URI is derived from PUBLIC_URL, so the environment variable and the console entry are the whole of it.

Leaving it in Testing is the failure to watch for. Only listed test users can sign in, and the refusal happens on Google's own page, so nothing reaches the storefront and nothing appears in its logs. A customer reports a broken button and the logs are silent.

The production smoke test

The consent screen, the redirect and the domain are all environment-specific, so QA proves the flow and not the configuration. After the cutover:

  1. Sign in with a Google account that has never been used on the site. A new customer is created and lands on the consent step.
  2. Sign in again with the same account. It reaches the same customer rather than a second one.
  3. Check the account page lists Google under connected accounts.

What is not offered, and why

Unlinking. A customer cannot detach their Google account. Removing the only way into an account is guarded for passkeys and the same guard would be needed here first. Worth its own issue when somebody actually asks.

Apple. A separate decision with a materially different cost, set out on #332: a paid developer programme, a client secret that expires every six months, no localhost redirect URIs at all, and a name and email returned exactly once. Apple is required for iOS apps offering third-party sign-in, and this is a website, so that rule does not apply here.