fix(oidc): reconcile lets-chat client per-environment (LC-448) #217

Merged
nrupard merged 1 commit from fix/LC-448-letschat-oidc-env-reconcile into main 2026-06-24 18:17:41 +02:00
Owner

Tracking: LC-448 (subtask of LC-446 "Deploy to prod").

Problem

Production lets-chat-psa (nc-01, chat.psa.systems, issuer api.psa.systems) cannot complete login. The lets-chat confidential OIDC client (client_id b0000000-0000-4000-8000-00000000000c) is seeded by static migration 20260618032217_register_lets_chat_oidc_client.sql with redirect_uris = ['https://chat.a8n.systems/auth/bunyip/callback'] (the staging host). The same image runs on every bunyip-api, so the prod client carries the staging redirect and rejects the prod callback.

This is exactly the class BUNYIP-57 fixed for the SPA clients (mokosh-apps, drillmark) via the env-driven startup upsert (upsert_spa_oidc_client), whose comment notes the static migration "seeds env-blind staging (a8n.systems) URIs that break the PKCE flow on every other host." lets-chat was never added to that list.

Change

Add upsert_lets_chat_oidc_client(&pool) after the drillmark upsert. A confidential analogue of upsert_spa_oidc_client:

  • Keyed on the fixed client_id; UPDATEs the row the migration pre-seeds (the migrator at main.rs runs before this), writing redirect_uris / post_logout_redirect_uris / audience from per-environment LETS_CHAT_REDIRECT_URIS / LETS_CHAT_POST_LOGOUT_REDIRECT_URIS / LETS_CHAT_AUDIENCE.
  • client_type and token_endpoint_auth_method are left at their migration values (confidential / client_secret_basic).
  • Optional LETS_CHAT_CLIENT_SECRET_HASH (Argon2id PHC string) pins a dedicated per-environment client secret via COALESCE (unset keeps the migration's shared hash), so production no longer shares a secret with staging.
  • Gated on LETS_CHAT_REDIRECT_URIS + LETS_CHAT_AUDIENCE present (skip + log otherwise), mirroring the SPA path. Zero rows updated logs a warning rather than inserting a malformed client.

lets-chat validates id_token.aud == client_id (not the audience column), so redirect_uris is what blocked the prod flow; audience is set for consistency but is not load-bearing for lets-chat.

Operator action (new env vars per host)

Set in each bunyip-api compose (companion docker PR):

  • nc-01: LETS_CHAT_REDIRECT_URIS=https://chat.psa.systems/auth/bunyip/callback, LETS_CHAT_POST_LOGOUT_REDIRECT_URIS=https://chat.psa.systems, LETS_CHAT_AUDIENCE=https://chat.psa.systems, LETS_CHAT_CLIENT_SECRET_HASH=<argon2id hash of the dedicated prod secret>.
  • c-01: the chat.a8n.systems equivalents, no hash var (keeps the migration's shared hash).

Verification

cargo fmt --all + cargo clippy --workspace --all-targets -- -D warnings green in the pinned rust-builder-glibc image. Change is bin-only (bunyip-api/src/main.rs); cargo test --workspace --lib compiles lib targets only and is unaffected. Runtime query (not the query! macro), so no .sqlx cache entry needed.

Tracking: LC-448 (subtask of LC-446 "Deploy to prod"). ## Problem Production lets-chat-psa (nc-01, `chat.psa.systems`, issuer `api.psa.systems`) cannot complete login. The lets-chat confidential OIDC client (`client_id b0000000-0000-4000-8000-00000000000c`) is seeded by static migration `20260618032217_register_lets_chat_oidc_client.sql` with `redirect_uris = ['https://chat.a8n.systems/auth/bunyip/callback']` (the staging host). The same image runs on every bunyip-api, so the prod client carries the staging redirect and rejects the prod callback. This is exactly the class BUNYIP-57 fixed for the SPA clients (`mokosh-apps`, `drillmark`) via the env-driven startup upsert (`upsert_spa_oidc_client`), whose comment notes the static migration "seeds env-blind staging (a8n.systems) URIs that break the PKCE flow on every other host." lets-chat was never added to that list. ## Change Add `upsert_lets_chat_oidc_client(&pool)` after the drillmark upsert. A confidential analogue of `upsert_spa_oidc_client`: - Keyed on the fixed `client_id`; `UPDATE`s the row the migration pre-seeds (the migrator at `main.rs` runs before this), writing `redirect_uris` / `post_logout_redirect_uris` / `audience` from per-environment `LETS_CHAT_REDIRECT_URIS` / `LETS_CHAT_POST_LOGOUT_REDIRECT_URIS` / `LETS_CHAT_AUDIENCE`. - `client_type` and `token_endpoint_auth_method` are left at their migration values (confidential / client_secret_basic). - Optional `LETS_CHAT_CLIENT_SECRET_HASH` (Argon2id PHC string) pins a **dedicated per-environment** client secret via `COALESCE` (unset keeps the migration's shared hash), so production no longer shares a secret with staging. - Gated on `LETS_CHAT_REDIRECT_URIS` + `LETS_CHAT_AUDIENCE` present (skip + log otherwise), mirroring the SPA path. Zero rows updated logs a warning rather than inserting a malformed client. lets-chat validates `id_token.aud == client_id` (not the `audience` column), so `redirect_uris` is what blocked the prod flow; audience is set for consistency but is not load-bearing for lets-chat. ## Operator action (new env vars per host) Set in each bunyip-api compose (companion docker PR): - nc-01: `LETS_CHAT_REDIRECT_URIS=https://chat.psa.systems/auth/bunyip/callback`, `LETS_CHAT_POST_LOGOUT_REDIRECT_URIS=https://chat.psa.systems`, `LETS_CHAT_AUDIENCE=https://chat.psa.systems`, `LETS_CHAT_CLIENT_SECRET_HASH=<argon2id hash of the dedicated prod secret>`. - c-01: the `chat.a8n.systems` equivalents, no hash var (keeps the migration's shared hash). ## Verification `cargo fmt --all` + `cargo clippy --workspace --all-targets -- -D warnings` green in the pinned `rust-builder-glibc` image. Change is bin-only (`bunyip-api/src/main.rs`); `cargo test --workspace --lib` compiles lib targets only and is unaffected. Runtime query (not the `query!` macro), so no `.sqlx` cache entry needed.
fix(oidc): reconcile lets-chat client per-environment (LC-448)
All checks were successful
E2E / Playwright against deployment (pull_request) Successful in 46s
Check / fmt + clippy + build + tests (pull_request) Successful in 4m50s
Create release / Create release from merged PR (pull_request) Has been skipped
6355e6f91a
The static migration 20260618032217_register_lets_chat_oidc_client.sql seeds env-blind staging (chat.a8n.systems) redirect/audience for the lets-chat confidential client. The same image runs on every bunyip-api, so production (api.psa.systems) carries the staging redirect and rejects the chat.psa.systems callback. This is the class BUNYIP-57 fixed for the SPA clients (mokosh-apps, drillmark) via the env-driven startup upsert; lets-chat was never added to it.

Add upsert_lets_chat_oidc_client: a confidential analogue of upsert_spa_oidc_client that UPDATEs the migration-seeded row from per-environment LETS_CHAT_REDIRECT_URIS / LETS_CHAT_POST_LOGOUT_REDIRECT_URIS / LETS_CHAT_AUDIENCE, leaving client_type and token_endpoint_auth_method at their migration values. Optional LETS_CHAT_CLIENT_SECRET_HASH (Argon2id PHC string) lets an environment pin a dedicated client secret via COALESCE (unset keeps the existing migration hash), so production no longer has to share a secret with staging. Gated on redirect + audience present (skip + log otherwise), mirroring the SPA path.

Deployments set the new vars in the bunyip-api compose per host. lets-chat only validates id_token.aud == client_id, so the audience column is not load-bearing; redirect_uris is what blocked the prod flow.

#LC-448

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nrupard deleted branch fix/LC-448-letschat-oidc-env-reconcile 2026-06-24 18:17:41 +02:00
nrupard referenced this pull request from a commit 2026-06-24 18:21:18 +02:00
vas2000-work referenced this pull request from a commit 2026-07-01 06:55:05 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!217
No description provided.