Skip to content

test(e2e): OIDC sign-in against a mock IdP (compose override + Playwright spec) - #440

Merged
github-actions[bot] merged 2 commits into
mainfrom
auto/oidc-e2e-mock-idp
Nov 10, 2026
Merged

github-actions[bot] merged 2 commits into
mainfrom
auto/oidc-e2e-mock-idp

Conversation

@grumpy-miner

@grumpy-miner grumpy-miner commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Board card: E2E: OIDC sign-in against a mock IdP (compose override + Playwright test) (PVTI_lADOElDmdM4Bn50jzhHLapk)

Draft card, so no Closes. Closes the last AC of the parent card "SSO + multi-company user membership" ("OIDC login works against a test IdP in e2e"), following the owner decision of 2026-11-09 (option b in #430: mock IdP from a separate, E2E-only compose file).

Research

  • Code: generic OIDC (feat(auth): generic OpenID Connect sign-in for existing accounts #429) validates the discovery issuer against GANEK_OIDC_ISSUER, requires https endpoints except on localhost/127.0.0.1, checks aud/azp = client id, nonce, and does PKCE. Invite accept through OIDC (feat(auth): accept a team invite through the instance's OIDC provider #430) needs the IdP to vouch for the invited email (email_verified). Team invite links are only emailed, never returned by the API.
  • navikt/mock-oauth2-server, read at tag 6.1.3: with no JSON_CONFIG the standalone server enables an interactive login page (username becomes sub, a claims JSON box adds claims), verifies PKCE, echoes the nonce, sets the ID token aud to the client id and azp to the client id, and derives the issuer (http://<host>/<issuerId>) from the request host. 6.1.3 (2026-10-23) is pinned; its release workflow published ghcr.io/navikt/mock-oauth2-server:6.1.3 successfully.
  • How others do it: Vaultwarden's Playwright SSO suite and most self-hosted apps run a full Keycloak in compose (slow JVM boot, realm import to maintain); lighter projects use navikt's mock or oidc-provider-mock. The recurring pain point in self-hosted SSO threads (Vikunja, BookStack, Zitadel issues) is the issuer mismatch between the hostname the browser sees and the one the container sees. This override avoids it by construction: the mock IdP shares the backend's network namespace, so http://localhost:8080/ganek is the same IdP for the backend (discovery, token exchange) and for the browser (login page, through a port published on the backend), and it satisfies our "https or localhost" endpoint rule without loosening it.

Plan and scope

  • docker-compose.oidc-e2e.yml: mock-idp service (pinned image, network_mode: service:backend), plus GANEK_OIDC_* for the backend (issuer http://localhost:8080/ganek, client ganek-e2e, display name "Mock IdP", email domain e2e.ganek.dev) and a loopback-only 127.0.0.1:8080 port on the backend for the browser.
  • frontend/e2e/sso.spec.ts (runs last, alphabetical, workers=1):
    1. /login -> "Continue with Mock IdP" -> mock login as the suite admin (verified email) -> lands on /admin; /api/v1/auth/me shows the admin with sso_linked. First run links by verified email, reruns sign in by (issuer, sub).
    2. Admin creates an invite over the API, the spec opens /invite/<token> -> "Continue with Mock IdP" -> lands on /admin as the new teammate (role member, sso_linked, no password). Unique email per run, so reruns on the same stack work.
  • frontend/e2e/invite-token.ts: signs the invite token exactly like create_invite_token (itsdangerous URLSafeTimedSerializer, salt ganek-team-invite) with the stack's GANEK_SECRET_KEY (from E2E_SECRET_KEY, else the repo-root .env that compose gave the backend; CI copies .env.example). Needed because invite links are never exposed by the API.
  • The spec skips unless E2E_OIDC=1; when set it waits for the IdP's discovery document (JVM start) and then requires the SSO button.

In scope: the above. Out of scope: SAML (own card), production docker-compose.yml / .env.example (untouched), scripts/ci_local.sh (can mirror the lines below once the workflow has them), .github/workflows/ (owner edits, below).

Acceptance:

  • With the override and the workflow lines below, the E2E browser job runs the two SSO tests.
  • Without them, existing E2E is unchanged and sso.spec.ts is skipped (the default docker compose up stack has no GANEK_OIDC_* and E2E_OIDC is unset).
  • docker-compose.yml and .env.example untouched.

Workflow lines for the owner (.github/workflows/e2e.yml)

Task PRs never edit workflows. To turn the spec on, add a job-level env to the browser job only (leave compose-smoke as is). COMPOSE_FILE makes every docker compose call in that job (up, ps, logs) use both files, so the failure log dump also covers the IdP:

  browser:
    name: playwright golden paths
    runs-on: ubuntu-latest
    env:
      COMPOSE_FILE: docker-compose.yml:docker-compose.oidc-e2e.yml
      E2E_OIDC: "1"
    steps:
      # ... unchanged

Testing

  • Cloud container: pnpm typecheck OK; pnpm lint 0 errors (3 pre-existing warnings); pnpm exec playwright test --list lists the two SSO tests next to the existing specs. Vitest only collects src/** and is unaffected.
  • signInviteToken cross-checked against Python itsdangerous: URLSafeTimedSerializer(..., salt="ganek-team-invite").loads() accepts the Node-minted token, and dumps() of the same id in the same second gives a byte-identical token.
  • docker compose -f docker-compose.yml -f docker-compose.oidc-e2e.yml config resolves: backend gets the five GANEK_OIDC_* vars and ports 8000 + 8080 on 127.0.0.1, mock-idp runs in service:backend; plain docker compose config has no mock-idp.
  • Not run end to end here: the agent container cannot pull from ghcr.io or Docker Hub, so the full stack + mock IdP could not be started locally. The spec's IdP selectors (input[name=username], textarea[name=claims], "Sign-in" button) and claim behaviour were read from the 6.1.3 source (login.ftl, AuthorizationCodeHandler, OAuth2TokenProvider). The first real run is the E2E job once the lines above are added; in this PR's CI the spec is skipped by design.
  • Same deterministic edit script in both clones: script md5 60c29f6b..., git diff --cached | md5sum 59e0b206... on both sides.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RPybyvkbTrgrgAQHfjyJQ3

Status (run 2026-11-09 ~17:35 UTC) - blocked, needs the owner

CI (all 7 jobs), E2E (the new SSO spec skips by design until the workflow lines above are added) and the auto-merge workflow are green and auto_merge is enabled, but GitHub has not merged: the ruleset requires the cla-assistant check and the cla workflow ended in startup_failure with no jobs (run 42662750137). Same blocker as #439: every cla run since 16:15 UTC has failed to start, the last success was 15:16 UTC on #438. Not merged by hand and no workflow edited, per the task rules. To unblock: open the cla run page for the startup error (or comment recheck), then the PR merges itself once the check is green. Card set to In review.

…ight spec)

docker-compose.oidc-e2e.yml adds navikt/mock-oauth2-server 6.1.3 in the
backend's network namespace, so http://localhost:8080/ganek is the same
issuer for the backend and the browser, and points GANEK_OIDC_* at it.
frontend/e2e/sso.spec.ts signs the suite's admin in through "Continue
with Mock IdP" and accepts a team invite through SSO; it skips unless
E2E_OIDC=1, so the default E2E run is unchanged. Production compose and
.env.example are untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RPybyvkbTrgrgAQHfjyJQ3
The 2026-11-09 16:00-18:00 UTC GitHub brownout blocked the cla workflow at startup; an empty commit re-sends the pull_request_target event.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions
github-actions Bot merged commit f852e56 into main Nov 10, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant