feat(dev): mock OIDC OP can authenticate for the local dev stack (LC-577) #549

Merged
longjacksonle merged 1 commit from feat/lc577-mock-oidc-auth into main 2026-07-13 17:24:36 +02:00

What

Gives dev/mock-oidc.py a real, self-contained authorization-code flow so just dev-web-local-mock can complete a login end to end. Every authenticated page (home dashboard, room view, /settings, thread/details panels) now renders and is screenshot-verifiable without standing up the full bunyip dev-sso stack.

Attribution

The implementation is @vas2000-work's commit (feat(dev): make the mock OIDC OP able to actually authenticate), cherry-picked onto current main with authorship preserved. It applied cleanly (the two files' bases were identical to main). I verified it end to end against current main and opened this PR.

How it works (no server changes)

  • The mock generates a real Ed25519 keypair from a FIXED seed and publishes the public half at /jwks.json (same kid). Fixed rather than per-process because the server caches JWKS at boot; a random key would invalidate that cache on every mock restart and force an app restart in lockstep.
  • GET /authorize: no login form or consent (it is a stub); mints a single-use code and 302s straight back to redirect_uri with the caller's state, retaining the nonce.
  • POST /token: exchanges the code for an EdDSA-signed id_token carrying iss/aud/sub/email/preferred_username/iat/exp and the retained nonce.
  • GET /userinfo: serves the same identity, so the callback's userinfo.sub == id_token.sub consistency check passes.
  • PKCE S256 is accepted but not verified (local-dev stub, not a conformance target).
  • Identity is fixed and env-overridable (MOCK_SSO_SUB/EMAIL/USERNAME), so the same account is reused across boots.
  • Image moves python:3-alpine -> python:3-slim so pyjwt[crypto] / cryptography land as glibc wheels instead of compiling against musl. The mock port is published and the overlay documents the chrome --host-resolver-rules="MAP mock-oidc 127.0.0.1" rule so one issuer string stays valid for both the server (compose DNS) and a host browser.

The server's OIDC path is untouched and fully exercised: the id_token is really signed and really verified. No dev bypass was added to the shipped binary.

Verification (live, against current main)

Booted just dev-web-local-mock and completed a real login via headless chrome with the documented resolver rule:

  • Login completes -> landed authenticated as devuser on the home dashboard.
  • /settings renders (Profile: username devuser, role user, email dev@example.test).
  • Room view renders: created an enclave + #general room and posted a message as the same identity (via a curl cookie jar, --resolve mock-oidc:9000:127.0.0.1); the room composes correctly with the sidebar, the active-row glyph, and the right-hand Details panel.
  • ID token verifies with no server-side change (git diff touches only dev/mock-oidc.py + the compose overlay; login succeeded against the untouched OIDC path).
  • Stable sub across restart: restarting only the mock container, a fresh login still completed with NO app restart (cached JWKS stayed valid) and the post-restart session saw the enclave/room the pre-restart session had created - same account, not a new one.
  • Warnings kept and updated; the stale "CANNOT authenticate a real login" comment in the overlay is corrected.

Stack torn down cleanly; the separate long-chat dev-web stack was unaffected.

Acceptance criteria

  • just dev-web-local-mock boots; visiting / and completing the SSO redirect lands on an authenticated session.
  • Room view, /settings, and the home dashboard all render for that session.
  • ID token verifies against the server's existing OIDC path with no server-side changes.
  • Mock user is stable across restarts (same sub).
  • dev/mock-oidc.py and the overlay keep their LOCAL-DEV-ONLY warnings, updated to reflect that login now works.
  • The stale "CANNOT authenticate" comment is corrected.

Closes LC-577.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GJbJChgMXj7m1Q8GdnTPyT

## What Gives `dev/mock-oidc.py` a real, self-contained authorization-code flow so `just dev-web-local-mock` can complete a login end to end. Every authenticated page (home dashboard, room view, `/settings`, thread/details panels) now renders and is screenshot-verifiable without standing up the full bunyip dev-sso stack. ## Attribution The implementation is @vas2000-work's commit (`feat(dev): make the mock OIDC OP able to actually authenticate`), cherry-picked onto current main with authorship preserved. It applied cleanly (the two files' bases were identical to main). I verified it end to end against current main and opened this PR. ## How it works (no server changes) - The mock generates a real Ed25519 keypair from a FIXED seed and publishes the public half at `/jwks.json` (same `kid`). Fixed rather than per-process because the server caches JWKS at boot; a random key would invalidate that cache on every mock restart and force an app restart in lockstep. - `GET /authorize`: no login form or consent (it is a stub); mints a single-use code and 302s straight back to `redirect_uri` with the caller's `state`, retaining the `nonce`. - `POST /token`: exchanges the code for an EdDSA-signed `id_token` carrying `iss/aud/sub/email/preferred_username/iat/exp` and the retained `nonce`. - `GET /userinfo`: serves the same identity, so the callback's `userinfo.sub == id_token.sub` consistency check passes. - PKCE `S256` is accepted but not verified (local-dev stub, not a conformance target). - Identity is fixed and env-overridable (`MOCK_SSO_SUB/EMAIL/USERNAME`), so the same account is reused across boots. - Image moves `python:3-alpine` -> `python:3-slim` so `pyjwt[crypto]` / `cryptography` land as glibc wheels instead of compiling against musl. The mock port is published and the overlay documents the `chrome --host-resolver-rules="MAP mock-oidc 127.0.0.1"` rule so one issuer string stays valid for both the server (compose DNS) and a host browser. The server's OIDC path is untouched and fully exercised: the id_token is really signed and really verified. No dev bypass was added to the shipped binary. ## Verification (live, against current main) Booted `just dev-web-local-mock` and completed a real login via headless chrome with the documented resolver rule: - **Login completes** -> landed authenticated as `devuser` on the home dashboard. - **`/settings`** renders (Profile: username `devuser`, role `user`, email `dev@example.test`). - **Room view** renders: created an enclave + `#general` room and posted a message as the same identity (via a curl cookie jar, `--resolve mock-oidc:9000:127.0.0.1`); the room composes correctly with the sidebar, the active-row glyph, and the right-hand Details panel. - **ID token verifies with no server-side change** (git diff touches only `dev/mock-oidc.py` + the compose overlay; login succeeded against the untouched OIDC path). - **Stable sub across restart**: restarting only the mock container, a fresh login still completed with NO app restart (cached JWKS stayed valid) and the post-restart session saw the enclave/room the pre-restart session had created - same account, not a new one. - Warnings kept and updated; the stale "CANNOT authenticate a real login" comment in the overlay is corrected. Stack torn down cleanly; the separate long-chat dev-web stack was unaffected. ## Acceptance criteria - [x] `just dev-web-local-mock` boots; visiting `/` and completing the SSO redirect lands on an authenticated session. - [x] Room view, `/settings`, and the home dashboard all render for that session. - [x] ID token verifies against the server's existing OIDC path with no server-side changes. - [x] Mock user is stable across restarts (same `sub`). - [x] `dev/mock-oidc.py` and the overlay keep their LOCAL-DEV-ONLY warnings, updated to reflect that login now works. - [x] The stale "CANNOT authenticate" comment is corrected. Closes LC-577. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01GJbJChgMXj7m1Q8GdnTPyT
feat(dev): make the mock OIDC OP able to actually authenticate
All checks were successful
check-secrets / TruffleHog (push) Successful in 17s
check-secrets / Kingfisher (push) Successful in 19s
check-secrets / Nosey parker (push) Successful in 19s
check-secrets / Kingfisher (pull_request) Successful in 10s
check-secrets / Nosey parker (pull_request) Successful in 10s
check-secrets / TruffleHog (pull_request) Successful in 12s
Check / clippy + fmt + tests (pull_request) Successful in 7m44s
Create release / Create release from merged PR (pull_request) Has been skipped
1135afd654
The mock OP served only discovery + JWKS, with a dummy key of 32 zero bytes and no authorize/token flow, so it could not complete a login. Every authenticated page was therefore unreachable in the mock stack: the room view, the sidebar, the thread and details panels, /settings, the home dashboard. That is nearly the whole product, and it meant UI work on those pages could not be visually verified without standing up the full bunyip dev-sso stack. The workaround had been to mirror components into /dev/theme-gallery, which only proves a component renders in isolation, never that the real page composes correctly.

It now implements a real authorization-code flow: a genuine Ed25519 keypair whose public half is published at /jwks.json, /authorize minting a code and bouncing straight back, /token returning an EdDSA-signed id_token, and /userinfo serving the same identity (the callback rejects the login if the two subs disagree). The server's own OIDC path is untouched and fully exercised: the token is really signed and really verified. No dev bypass was added to the shipped binary.

The signing key is derived from a fixed seed rather than generated per process, because the server caches JWKS at boot: a random key would invalidate that cache on every mock restart and force an app restart in lockstep.

PKCE is accepted but not verified. This is a local-dev stub, not a conformance target.

The image moves from alpine to slim because `cryptography` has no musl wheel and would otherwise compile from source on every boot. The mock's port is published so a host browser can follow the /authorize redirect, since the issuer is a compose-internal hostname; a browser resolver rule maps it, keeping one issuer string valid on both sides.

#LC-577
longjacksonle scheduled this pull request to auto merge when all checks succeed 2026-07-13 17:18:59 +02:00
longjacksonle deleted branch feat/lc577-mock-oidc-auth 2026-07-13 17:24:37 +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/lets-chat!549
No description provided.