OAuth & Dex¶
The Google OAuth client used by Argo CD's Dex (and by Grafana's auth-proxy flow) is provisioned manually in the GCP Console. The credentials are stored in GCP Secret Manager and pulled into the argocd and monitoring namespaces by External Secrets Operator.
Why manual, not Terraform
The google_iap_brand and google_iap_client Terraform resources were deprecated by Google. The underlying IAP OAuth Admin APIs stop accepting new client creation on Jan 19, 2026 and are fully shut down on Mar 19, 2026. We removed oauth.tf in favor of console-driven OAuth client setup. The client + secret are still stored in GCP SM (tomoda-google-client-id, tomoda-google-client-secret) and synced to K8s via ESO; only the creation step is now manual.
Flow¶
argocd + monitoring namespaces (consumed by Dex and Grafana respectively).Provisioning¶
For step-by-step instructions including consent screen setup, authorized redirect URIs, and the gcloud commands to store the values in Secret Manager, see Security → Secrets Management → Provisioning the Google OAuth client.
Short version:
- GCP Console → APIs & Services → OAuth consent screen → configure.
Internaluser type limits every client to thetomoda.lifeWorkspace;Externalopens sign-in to any Google account (see Opening the app to all domains for when to choose which, and why the admin UIs stay gated either way). - APIs & Services → Credentials → Create credentials → OAuth client ID (Web application).
- Add authorized redirect URIs:
https://argo-app.tomoda.life/api/dex/callbackandhttps://auth.tomoda.life/oauth2/callback. - Copy the resulting client ID and client secret to GCP Secret Manager as
tomoda-google-client-idandtomoda-google-client-secret. - ESO projects them into both namespaces via the
ExternalSecretresources ink8s/envs/platform/manifests/external-secrets-config.yaml.
Domain restriction¶
Two things are true and must not be confused:
- The consent screen (project-level "brand") is
User type: Internal, which by itself limits every OAuth client in the project totomoda.lifeWorkspace accounts. There is exactly one brand per GCP project, so all clients inherit the same Internal/External setting. - The internal admin UIs do not depend on that brand setting. Each enforces
tomoda.lifeat the application layer, independently:
| Surface | Domain gate | Where |
|---|---|---|
| Argo CD (Dex) | hostedDomains: ["tomoda.life"] + RBAC (policy.default: readonly, admin allow-list) |
infrastructure/gcp/argocd.tf |
| Grafana, pgAdmin, redis-insight | oauth2-proxy --email-domain=tomoda.life |
k8s/envs/platform/oauth2-proxy/application.yaml |
Both layers must hold. Don't rely on the consent screen alone: flipping the brand to External does not open the admin UIs, because their own gates still reject non-tomoda.life identities.
Client coupling (important)¶
The web OAuth client 287267207777-7d0cdo9ueh4ir6jd90ge411crcmfvs92 is shared by three consumers:
- The mobile app (
frontend/utils/google-auth.ts,WEB_CLIENT_ID). - oauth2-proxy (Grafana / pgAdmin / redis-insight).
- Argo CD Dex (
argocd.tf).
The iOS (…-b03ijlh…) and Android (…-nfb8r1g…) clients are app-only. All live in project development-485000 under the single Internal brand.
Because the app and the internal admin UIs share a client (and always share a consent screen), you cannot make the app External while keeping the admin client Internal by client alone — Internal/External is a brand property. See the runbook below for the supported way to open the app without weakening the admin UIs.
Opening the app to all domains (External)¶
Goal: let any Google account sign in to the mobile app, while the internal admin UIs stay tomoda.life-only.
This is safe because the admin gate is oauth2-proxy --email-domain + Dex hostedDomains, not the consent screen. Flipping the brand to External leaves those gates intact (verified: every admin ingress carries its own domain gate; xavia-ota uses a password, not Google).
Step 1 — flip the consent screen to External (org admin, GCP Console).
The IAP OAuth Admin API that once managed the brand is deprecated and no longer accepts changes, so this is a Console-only action:
- GCP Console → APIs & Services → OAuth consent screen (project
development-485000). - Change User type: Internal → External, publish (set publishing status to "In production" so it isn't stuck in testing-user-only mode).
- No client IDs change. The app can now accept any Google account; the admin UIs are unaffected.
Step 2 — decouple the shared client (recommended follow-up, not required for step 1).
Sharing one client between the public app and the internal admin UIs couples their secret rotation, redirect-URI list, and blast radius (an app-client incident would take down Argo/Grafana login). Give the internal consumers their own client:
- Console → Credentials → create a new Web application OAuth client, e.g.
oauth2-proxy-internal. Redirect URIs:https://auth.tomoda.life/oauth2/callbackandhttps://argo-app.tomoda.life/api/dex/callback(Dex can share it, or get its own). - Store its id/secret in GCP Secret Manager (
tomoda-google-client-id/tomoda-google-client-secretalready feed bothargocdandmonitoringvia ESO — update those entries, ESO re-syncs). - The app keeps
287267207777-7d0c…; the internal UIs move to the new client. No manifest logic changes — only the secret values.
After decoupling, the app client and the internal client are independent credentials; both still sit under the same External brand, but the admin gates (--email-domain, hostedDomains) continue to enforce tomoda.life.
What about the other OAuth clients?¶
The iOS and Android app clients are app-only (created in the same Console flow, IDs injected into the frontend bundle via Cloud Build substitutions). See Cloud Build and Secrets Management for the per-client GCP SM entries (tomoda-google-android-client-id, tomoda-google-ios-client-id).