From a263f742c0e8f3694ee700424939946a458fe1ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leynier=20Guti=C3=A9rrez=20Gonz=C3=A1lez?= Date: Fri, 9 Oct 2026 23:43:08 -0600 Subject: [PATCH 1/2] fix: use a google web oauth client for mcp and device sign-in The web sign-in redirects to https://api.alera.build/oauth/callback, which the Google desktop client cannot accept, so Google answered redirect_uri_mismatch. Production now sets the new Alera Web client id; its secret is in alera-web-google-oauth-client-secret. GitHub keeps the desktop OAuth App, which now lists the web callback as a second redirect URI. --- docs/remote-mcp.md | 2 +- infra/production/production.auto.tfvars | 23 ++++++++++++----------- infra/production/readme.md | 1 + 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/docs/remote-mcp.md b/docs/remote-mcp.md index 9551af433..28348b580 100644 --- a/docs/remote-mcp.md +++ b/docs/remote-mcp.md @@ -68,7 +68,7 @@ Rules: - OAuth endpoints use the standard snake_case field names and `{ error, error_description }` errors; every other route keeps camelCase and `{ error: { code, message } }`. - Unknown scopes such as `offline_access` are ignored, and `mcp:read` is always granted. A token request may omit `redirect_uri`; when present it must match. - `ALERA_MCP_ENABLED=false` removes the metadata, registration, authorize, token, revoke, and gateway routes. Device sign-in, grant listing, and runtime naming keep working. -- The web login uses `{ALERA_PUBLIC_BASE_URL}/oauth/callback`. Production needs web OAuth clients for Google and GitHub that admit that redirect: `ALERA_WEB_GOOGLE_CLIENT_ID`, `ALERA_WEB_GOOGLE_CLIENT_SECRET`, `ALERA_WEB_GITHUB_CLIENT_ID`, and `ALERA_WEB_GITHUB_CLIENT_SECRET`. Without them the native client credentials are reused. +- The web login uses `{ALERA_PUBLIC_BASE_URL}/oauth/callback`. Each provider needs a client that admits that redirect. Google desktop clients only accept loopback redirects, so production sets a separate Google web client (`ALERA_WEB_GOOGLE_CLIENT_ID` from `web_google_oauth_client_id`, `ALERA_WEB_GOOGLE_CLIENT_SECRET` from the `alera-web-google-oauth-client-secret` secret). A GitHub OAuth App accepts several redirect URIs, so production reuses the desktop app with that callback added and leaves `ALERA_WEB_GITHUB_CLIENT_ID` and `ALERA_WEB_GITHUB_CLIENT_SECRET` unset. Without web credentials a provider reuses the native client. ### Device Authorization diff --git a/infra/production/production.auto.tfvars b/infra/production/production.auto.tfvars index 09c36c545..4bf08541b 100644 --- a/infra/production/production.auto.tfvars +++ b/infra/production/production.auto.tfvars @@ -1,11 +1,12 @@ -gcp_project_id = "alera-production" -gcp_region = "us-central1" -cloudflare_zone_id = "d5bb590bba0a461c5a5f699b40b2c95f" -api_hostname = "api.alera.build" -google_oauth_client_id = "850463913236-eun5inavt29h0cpanc3utkvpdu4tari5.apps.googleusercontent.com" -github_oauth_client_id = "Ov23lihNhXgxBYUsBk3N" -neon_project_id = "empty-glade-74978232" -signing_key_id = "alera-production-v1" -kms_key_version = "1" -kms_public_key_b64url = "4OHKJMPgzVh_4XSvY11WVLqTSNPG_opDR53rPMp-3y4" -previous_jwks_json = "{\"keys\":[]}" +gcp_project_id = "alera-production" +gcp_region = "us-central1" +cloudflare_zone_id = "d5bb590bba0a461c5a5f699b40b2c95f" +api_hostname = "api.alera.build" +google_oauth_client_id = "850463913236-eun5inavt29h0cpanc3utkvpdu4tari5.apps.googleusercontent.com" +github_oauth_client_id = "Ov23lihNhXgxBYUsBk3N" +web_google_oauth_client_id = "850463913236-s6laqmn715qj97rjjjp863epsn87325d.apps.googleusercontent.com" +neon_project_id = "empty-glade-74978232" +signing_key_id = "alera-production-v1" +kms_key_version = "1" +kms_public_key_b64url = "4OHKJMPgzVh_4XSvY11WVLqTSNPG_opDR53rPMp-3y4" +previous_jwks_json = "{\"keys\":[]}" diff --git a/infra/production/readme.md b/infra/production/readme.md index a82b5b6a9..2d3acdd9e 100644 --- a/infra/production/readme.md +++ b/infra/production/readme.md @@ -47,6 +47,7 @@ gcloud secrets versions add alera-edge-origin-token --data-file=- gcloud secrets versions add alera-github-oauth-client-secret --data-file=- gcloud secrets versions add alera-google-oauth-client-secret --data-file=- gcloud secrets versions add alera-tombstone-pepper --data-file=- +gcloud secrets versions add alera-web-google-oauth-client-secret --data-file=- ``` Use independent random values for the origin token and tombstone pepper. Never reuse an OAuth client secret. From c4782a525310571ae0830e489d5fb79efda6d996 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leynier=20Guti=C3=A9rrez=20Gonz=C3=A1lez?= Date: Fri, 9 Oct 2026 23:59:34 -0600 Subject: [PATCH 2/2] ci: require web oauth secrets in the production preflight A web client id mounts its secret on the Cloud Run revision, so the preflight now checks that secret has an enabled version before the image build, and the setup and operations docs list it. The setup guide also records the API callback as the GitHub OAuth App's second redirect URI. --- .github/workflows/cloud-deploy.yml | 8 ++++++++ docs/cloud-operations.md | 1 + docs/cloud-setup.md | 16 ++++++++++++++-- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cloud-deploy.yml b/.github/workflows/cloud-deploy.yml index 365ea905c..a99800c7e 100644 --- a/.github/workflows/cloud-deploy.yml +++ b/.github/workflows/cloud-deploy.yml @@ -219,6 +219,14 @@ jobs: alera-google-oauth-client-secret alera-tombstone-pepper ) + # A web client id mounts its secret on the Cloud Run revision, so that + # secret must have a version before the image build starts. + for provider in google github; do + if grep -Eq "^web_${provider}_oauth_client_id[[:space:]]*=[[:space:]]*\"[^\"]+\"" \ + infra/production/production.auto.tfvars; then + required_secrets+=("alera-web-${provider}-oauth-client-secret") + fi + done for secret in "${required_secrets[@]}"; do enabled_version="$( gcloud secrets versions list "$secret" \ diff --git a/docs/cloud-operations.md b/docs/cloud-operations.md index 5c82d2667..4487e0ca2 100644 --- a/docs/cloud-operations.md +++ b/docs/cloud-operations.md @@ -104,6 +104,7 @@ gcloud secrets versions add alera-edge-origin-token --data-file=- gcloud secrets versions add alera-github-oauth-client-secret --data-file=- gcloud secrets versions add alera-google-oauth-client-secret --data-file=- gcloud secrets versions add alera-tombstone-pepper --data-file=- +gcloud secrets versions add alera-web-google-oauth-client-secret --data-file=- ``` Generate independent high-entropy values for the edge token and tombstone pepper. Do not reuse an OAuth client secret or copy local development values. diff --git a/docs/cloud-setup.md b/docs/cloud-setup.md index abebb4a00..ad1f951f7 100644 --- a/docs/cloud-setup.md +++ b/docs/cloud-setup.md @@ -160,6 +160,14 @@ Record: - Client id: public configuration in `terraform.tfvars` - Client secret: secret value added to `alera-google-oauth-client-secret` +MCP and device sign-in run in a browser and return to `https://api.alera.build/oauth/callback`, which a desktop client cannot accept. Create a second OAuth client for them: + +- Application type: Web application +- Name: `Alera Web` +- Authorized redirect URI: `https://api.alera.build/oauth/callback` + +Record its client id as `web_google_oauth_client_id` and add its secret to `alera-web-google-oauth-client-secret` before the id is applied. + The backend verifies the Google ID token, including signature, issuer, audience, authorized presenter, expiry, and nonce. Provider tokens are discarded after identity resolution. ## GitHub OAuth Registration @@ -171,9 +179,10 @@ Configuration: - Application name: `Alera` - Homepage URL: `https://alera.build` - Authorization callback URL: `http://127.0.0.1/callback` +- Second redirect URI: `https://api.alera.build/oauth/callback` - Device Flow: disabled -The runtime supplies the actual loopback port. GitHub permits a loopback redirect to vary the port while preserving the registered host and path. +The runtime supplies the actual loopback port. GitHub permits a loopback redirect to vary the port while preserving the registered host and path. The second URI serves MCP and device sign-in, so production reuses this app for them and leaves `web_github_oauth_client_id` empty. The application requests only: @@ -293,7 +302,7 @@ The value is public signing-key material and may appear in OpenTofu state. The p ## Secret Manager Values -OpenTofu creates six secret containers. Five require initial values: +OpenTofu creates eight secret containers. Five require initial values, and a web OAuth secret is required once its client id is set: | Secret | Value | | --- | --- | @@ -303,6 +312,8 @@ OpenTofu creates six secret containers. Five require initial values: | `alera-google-oauth-client-secret` | Google desktop OAuth client secret | | `alera-tombstone-pepper` | Independent random value with at least 32 characters | | `alera-edge-previous-origin-token` | Leave without a version until an edge-token rotation | +| `alera-web-google-oauth-client-secret` | Google web OAuth client secret, required while `web_google_oauth_client_id` is set | +| `alera-web-github-oauth-client-secret` | Leave without a version while `web_github_oauth_client_id` is empty | Add values through standard input: @@ -312,6 +323,7 @@ gcloud secrets versions add alera-edge-origin-token --data-file=- gcloud secrets versions add alera-github-oauth-client-secret --data-file=- gcloud secrets versions add alera-google-oauth-client-secret --data-file=- gcloud secrets versions add alera-tombstone-pepper --data-file=- +gcloud secrets versions add alera-web-google-oauth-client-secret --data-file=- ``` Use a password manager or secure random generator for the origin token and tombstone pepper. They must be unrelated values and must not reuse either OAuth secret.