Skip to content

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

Operator
create in console
GCP Secret Manager
ESO sync
Dex + Grafana auth
Console-driven OAuth client → GCP SM → K8s secrets in 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:

  1. GCP Console → APIs & Services → OAuth consent screen → configure. Internal user type limits every client to the tomoda.life Workspace; External opens 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).
  2. APIs & Services → Credentials → Create credentials → OAuth client ID (Web application).
  3. Add authorized redirect URIs: https://argo-app.tomoda.life/api/dex/callback and https://auth.tomoda.life/oauth2/callback.
  4. Copy the resulting client ID and client secret to GCP Secret Manager as tomoda-google-client-id and tomoda-google-client-secret.
  5. ESO projects them into both namespaces via the ExternalSecret resources in k8s/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 to tomoda.life Workspace 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.life at 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:

  1. The mobile app (frontend/utils/google-auth.ts, WEB_CLIENT_ID).
  2. oauth2-proxy (Grafana / pgAdmin / redis-insight).
  3. 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:

  1. GCP Console → APIs & Services → OAuth consent screen (project development-485000).
  2. Change User type: Internal → External, publish (set publishing status to "In production" so it isn't stuck in testing-user-only mode).
  3. 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:

  1. Console → Credentials → create a new Web application OAuth client, e.g. oauth2-proxy-internal. Redirect URIs: https://auth.tomoda.life/oauth2/callback and https://argo-app.tomoda.life/api/dex/callback (Dex can share it, or get its own).
  2. Store its id/secret in GCP Secret Manager (tomoda-google-client-id / tomoda-google-client-secret already feed both argocd and monitoring via ESO — update those entries, ESO re-syncs).
  3. 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).