test(e2e): cover real password sign-in through the Security API + Authentik - #280
Merged
Conversation
startSocialLogin sent the browser to the generic OIDC authorize endpoint, which requires an authenticated session. With none, Authentik fell back to the brand's authentication flow and rendered its own identification page (/if/flow/...) with a "Google" button — stranding the user on the IdP's UI instead of Google. Launch the Google source directly instead: /source/oauth/login/google/?next=<authorize>. The source-redirect view 302s straight to accounts.google.com; after the callback the source flow runs (silent enrollment first time, login when returning), then `next` resumes authorize, which issues the code silently. Same cookie/state/PKCE round-trip — one hop inserted ahead of authorize, so no /if/flow/ ever renders. This also lets devops drop /if from the IdP ingress. Also corrects the stale source-google.yaml comment (the removed auth.fuzefront.com -> app.fuzefront.com); the callback is host-derived, not a settable field. Co-Authored-By: Claude <claude-opus-4-8> <noreply@anthropic.com> Claude-Session-Id: cf830721-b1ef-4fe0-a024-035ad280dcf7
…ly uses
The security cutover moved the SPA onto /api/v1/security/* (POST /session to
log in, GET /methods for capabilities, /social/google/start, /session/exchange),
but the e2e suite still waited on /api/auth/login and mocked /api/auth/method.
The frontend never calls those anymore, so page.waitForResponse() hung until
timeout — this is why "E2E (sign-in)" has been red on master since the cutover.
The tests were asserting an endpoint we deleted from the client, not a real bug.
- auth-simple / clock-load / mobile-layout / pages/login-page: wait on
POST /api/v1/security/session. Match the METHOD too — GET /session ("me")
shares the URL and would otherwise satisfy the wait before login completes.
- google-signin: mock /api/v1/security/methods with the neutral descriptor
(`social: ['google']` replaces the vendor-flavoured `oidcConfigured`), and
route the real endpoints — /social/google/start and /session/exchange.
- federated-apps-register-activate: "me" is GET /api/v1/security/session.
- live-smoke (post-prod): smoke /methods and sign in via /session — the same
surface prod serves browsers, so it fails if real sign-in breaks.
Also ADDS the missing coverage of the new surface to oidc-plumbing (it only
tested the deprecated /api/auth shim): capability descriptor, password sign-in,
wrong-password 401, plus a boundary assertion that no response leaks the vendor.
The /api/auth shim assertions stay — it is still mounted for one release.
Co-Authored-By: Claude <claude-opus-4-8> <noreply@anthropic.com>
Claude-Session-Id: cf830721-b1ef-4fe0-a024-035ad280dcf7
The source path was hardcoded to google. The `provider !== 'google'` guard above means nothing else can reach it today, so this is not a live bug — but it is a trap: widening that guard would silently route every new provider through Google's source. Track `provider` so the slug cannot drift from the guard. Co-Authored-By: Claude <claude-opus-4-8> <noreply@anthropic.com> Claude-Session-Id: cf830721-b1ef-4fe0-a024-035ad280dcf7
…v1/security The SPA was migrated onto the provider-agnostic Security API (/api/v1/security/*), served ONLY by backend/security. But no e2e stack ran that service: this compose builds backend/Dockerfile (the MONOLITH), which mounts /api/auth, /api/apps, /api/organizations and /api/v1/app-registry — not /api/v1/security. So in CI the SPA's login POST 404s and the sign-in e2e times out waiting for a response that can never arrive. That is why "E2E (sign-in)" has been red on master since the cutover, and red on EVERY pr since — including PRs that touch only a shell script and markdown. The tests were not flaky and the failure was not pre-existing noise: the security service, which now serves all SPA auth, has had ZERO e2e coverage. Adds the `security` service (Authentik + DB env — password sign-in is brokered server-side through the flow-executor, so it needs Authentik even for the plain email/password path) and routes /api/v1/security/ → security:3002 in the e2e nginx, mirroring the prod Ingress. The frontend now depends on it: nginx resolves upstreams at startup and refuses to boot on an unresolvable one. Prod was never affected — the k8s Ingress routes /api/v1/security → fuzefront-security ahead of nginx. Verified: docker compose -f docker-compose.e2e.yml config -> PARSE OK, `security` resolved nginx -t (upstreams host-mapped) -> syntax is ok, test successful Co-Authored-By: Claude <claude-opus-4-8> <noreply@anthropic.com> Claude-Session-Id: cf830721-b1ef-4fe0-a024-035ad280dcf7
Self-review catch: I had pointed the security container at http://localhost:4173/api/v1/security/social/callback, which is not in the OIDC provider's redirect_uris. Authentik matches those strict, so it would have rejected the authorize call and password login would still have failed — the container would be present but sign-in still broken. Password sign-in is a full authorize→code→exchange (services/authentikPassword.ts drives the flow-executor, then GETs the authorize URL with the session cookies and lifts `code` out of the 302 Location). The redirect_uri therefore has to be REGISTERED, but never has to be browser-reachable — the server follows the redirect itself. So reuse the shared, already-registered /api/auth/oidc/callback, which is exactly what prod's security service does (authentik.oidc.redirectUri → https://app.fuzefront.com/api/auth/oidc/callback). Registered set: authentik/blueprints/provider-oidc.yaml. Co-Authored-By: Claude <claude-opus-4-8> <noreply@anthropic.com> Claude-Session-Id: cf830721-b1ef-4fe0-a024-035ad280dcf7
…the backend
Regression I introduced in this PR: adding the security container made the
OIDC-plumbing stack fail with
dependency failed to start: container fuzefront-e2e-backend-1 exited (1)
which the control run (same branch, before this container existed) did NOT do —
there the stack came up and the test failed on its own merits (access_denied).
Cause: backend/security/src/index.ts runs the SAME 001-009 migration chain
against the SAME `knex_migrations` table as the monolith. Compose started both
at once, they raced for the migration lock, and the loser exited 1 — taking the
whole stack down, since frontend depends on it.
Serialise: security now waits for backend to be healthy, so the chain is already
applied and security's own run is a harmless no-op. Chain is
postgres → backend → security → frontend (no cycle; compose config parses).
Co-Authored-By: Claude <claude-opus-4-8> <noreply@anthropic.com>
Claude-Session-Id: cf830721-b1ef-4fe0-a024-035ad280dcf7
… claude/e2e-real-signin-coverage
…hentik
Sign-in has had NO honest CI coverage since the Security API cutover.
.github/workflows/e2e.yml ("Playwright sign-in flow") cannot provide it, by
construction: it starts the monolith with NO Authentik and seeds the account as
a local bcrypt row. But the SPA signs in via /api/v1/security/session, brokered
through Authentik — a bcrypt row in the platform DB is not a credential Authentik
will accept, and the monolith does not even serve that route. So that job has
been failing since the cutover for reasons no test change can fix.
The plumbing workflow already has what is needed — full compose stack, Authentik
provisioned, a real Authentik user — and (with the security service now in that
stack) is the only place real sign-in CAN be exercised. So run it here instead of
duplicating ~300 lines of Authentik provisioning into the other workflow.
- auth-simple: take credentials from E2E_USER_EMAIL/E2E_USER_PASSWORD (defaults
unchanged) so it can target the account that exists in the provider.
- Seed the platform projection (users + personal org + owner membership) for that
account. WorkspaceProvisioningGate only renders the shell once a personal org
exists; a real login provisions it asynchronously, but Permit is a no-op and
Kafka is absent in CI, so the gate would spin and the spec would fail for
reasons unrelated to auth. Seeding by EMAIL is what makes this work:
syncUserToDatabase matches on email and mints its own uuid, so the row is
adopted by the login rather than duplicated. No password_hash — the credential
lives in Authentik.
- Verify the seed and fail loudly: a missing org looks identical to broken auth.
A pass here means password sign-in genuinely works end to end: browser →
Security API → Authentik flow-executor → code exchange → session → dashboard.
Stacked on the security-service compose change and the endpoint repointing; both
are prerequisites, so this branch carries them.
Co-Authored-By: Claude <claude-opus-4-8> <noreply@anthropic.com>
Claude-Session-Id: cf830721-b1ef-4fe0-a024-035ad280dcf7
Contributor
Automated code review (gate-code-review)Credit balance is too low Report-only — this check never blocks merge. |
…ity API
Root cause of the UI sign-in failures, found in the plumbing run: the frontend
image was built with VITE_API_URL=http://localhost:3001, pointing the browser
STRAIGHT AT THE MONOLITH and bypassing nginx entirely. The monolith does not
serve /api/v1/security/*, so GET /methods 404'd, the SPA saw no `social`, the
Google button never rendered, and the test died on
waiting for getByRole('button', { name: /sign in with google/i })
Sign-in could not work in that stack at all — and it made the nginx route added
alongside the security container dead code.
Same-origin ('' → relative URLs) sends every call through this container's nginx,
which path-routes /api/v1/security/ → security:3002 and the rest → backend:3001.
That is also the documented contract (CLAUDE.md: "same-origin API base … never
hard-code an absolute API host") and what prod does, so the e2e now exercises the
shape we actually ship instead of one we never deploy.
Also fixes my own bug from the previous commit: the new Security-API assertions
pointed at BACKEND_URL (the monolith) and so failed in ~11ms with a 404. They now
use SECURITY_URL, defaulting to the app origin — the same path the browser takes,
which additionally proves the nginx routing.
Co-Authored-By: Claude <claude-opus-4-8> <noreply@anthropic.com>
Claude-Session-Id: cf830721-b1ef-4fe0-a024-035ad280dcf7
Contributor
Automated code review (gate-code-review)Credit balance is too low Report-only — this check never blocks merge. |
Next gap, exposed once the Security API itself started passing: the boundary
model keeps the browser on the app origin — the security service returns
SAME-ORIGIN redirects (/source/oauth/login/google/, /application/o/authorize/),
never an absolute IdP host. Prod routes those via the fuzefront-authentik-idp
Ingress; the e2e nginx routed NONE of them, so every such redirect 404'd and no
browser sign-in could finish. The UI specs showed it as
waiting for navigation to "http://authentik-server:9000/**"
which by design never happens any more.
Mirrors the prod Ingress path list (/application /if /source /flows /ws /-
/outpost.goauthentik.io /api/v3 /static/dist /static/authentik) → authentik-server:9000.
Two placement details that matter:
- The block sits ABOVE the static-asset regexes. nginx evaluates regex locations
in order, so Authentik's /static/dist/*.js would otherwise be swallowed by
`~* \.(js|css|...)$` and try_files'd into a 404.
- /api/v3 must outrank the /api/ catch-all or flow-executor calls hit FuzeFront's
backend and fail with PROVIDER_UNAVAILABLE.
Host is passed through unchanged and the proto pinned: Authentik ignores
X-Forwarded-Prefix and derives callback_url from Host + X-Forwarded-Proto.
Upgrade/Connection are deliberately NOT forwarded — prod's nginx.conf confines
WebSocket upgrades to /socket.io/ to prevent H2C smuggling, and Authentik's /ws
is admin live-updates that no e2e flow needs. Matching that posture rather than
widening it.
Verified: nginx -t (upstreams host-mapped) → syntax is ok, test is successful.
Co-Authored-By: Claude <claude-opus-4-8> <noreply@anthropic.com>
Claude-Session-Id: cf830721-b1ef-4fe0-a024-035ad280dcf7
Contributor
Automated code review (gate-code-review)Credit balance is too low Report-only — this check never blocks merge. |
| add_header Referrer-Policy "strict-origin-when-cross-origin" always; | ||
| proxy_pass http://security:3002; | ||
| proxy_http_version 1.1; | ||
| proxy_set_header Host $host; |
| add_header Referrer-Policy "strict-origin-when-cross-origin" always; | ||
| proxy_pass http://backend:3001; | ||
| proxy_http_version 1.1; | ||
| proxy_set_header Host $host; |
…nin-coverage # Conflicts: # deploy/e2e/nginx.e2e.conf # frontend/tests/oidc-plumbing.e2e.spec.ts
Contributor
Automated code review (gate-code-review)Credit balance is too low Report-only — this check never blocks merge. |
izzywdev
added a commit
that referenced
this pull request
Jul 16, 2026
…rity flows (#276) The MFA stages (TOTP/WebAuthn/SMS) were defined but never BOUND to any flow — the stages-mfa.yaml comment claimed a user-settings binding that did not exist, so none of them were reachable. Binds TOTP/WebAuthn/SMS setup into default-user-settings-flow (device enrollment + the Google link/unlink surface) and fuzefront-mfa-validate into default-authentication-flow with not_configured_action=skip (enforce-if-enrolled, optional). Idempotent, state: present. Verified: helm template → EXIT 0, blueprint present in the authentik-blueprints ConfigMap; YAML parses with Authentik's !Find tags. Note: the CI-autofix branch proposed for this PR was rejected — it added a bcrypt login shim to the monolith, bypassing Authentik and testing a fake Security API. The real cause (the e2e stack never ran the security service) is fixed in #279/#280. Co-Authored-By: Claude <claude-opus-4-8> <noreply@anthropic.com> Claude-Session-Id: cf830721-b1ef-4fe0-a024-035ad280dcf7
This was referenced Jul 16, 2026
izzywdev
added a commit
that referenced
this pull request
Jul 16, 2026
Phone-based 2FA behind the provider-agnostic surface: enroll a phone factor, verify enrollment, challenge at login, remove factor. OTP generation/expiry is delegated to Twilio Verify rather than rolled locally, fronted by services/sms-service so the security-service never talks to the vendor directly. Provider-neutral in the API: no vendor name appears in any response. Config via env only (TWILIO_ACCOUNT_SID / TWILIO_AUTH_TOKEN / TWILIO_VERIFY_SERVICE_SID / SMS_SERVICE_URL). NOTE: the CI-autofix branch proposed here was rejected — it added a bcrypt auth shim to the monolith, which would have greened the sign-in check against a fake Security API that does not exist in prod. The real cause (the e2e stack never ran the security service) is fixed in #279/#280. Co-Authored-By: Claude <claude-opus-4-8> <noreply@anthropic.com> Claude-Session-Id: cf830721-b1ef-4fe0-a024-035ad280dcf7
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Piece B. Sign-in has had no honest CI coverage since the Security API cutover — this adds it.
Why
e2e.ymlcould never provide it.github/workflows/e2e.yml("Playwright sign-in flow") is broken by construction, not by a stale assertion:But the SPA signs in via
POST /api/v1/security/session, brokered through Authentik — a bcrypt row in the platform DB is not a credential Authentik will accept, and the monolith doesn't even serve that route. No test edit can fix that job; the stack is wrong. That's why it's been red on master since the cutover, and red on PRs that touch only markdown.Why here instead
The plumbing workflow already has everything required — full compose stack, Authentik provisioned, a real Authentik user — and, with the security service now in that stack (#279), it's the only place real sign-in can be exercised. Running it here beats duplicating ~300 lines of Authentik provisioning into the other workflow.
What this adds
auth-simple: credentials fromE2E_USER_EMAIL/E2E_USER_PASSWORD(defaults unchanged), so it can target the account that exists in the provider.WorkspaceProvisioningGateonly renders the shell once a personal org exists. A real login provisions it asynchronously — but Permit is a no-op and Kafka is absent in CI, so the gate would spin and the spec would fail for reasons unrelated to auth. Seeding by email is the trick:syncUserToDatabasematches on email and mints its own uuid, so the row is adopted by the login rather than duplicated. Nopassword_hash— the credential lives in Authentik.A pass here means password sign-in genuinely works end to end: browser → Security API → Authentik flow-executor → code exchange → session → dashboard.
Verified
YAML PARSE OK; steps resolve in order (seed → verify → plumbing → sign-in).organization_memberships,provisioning_state,joined_at,permissions) — my first cut used a non-existentmembershipstable and would have failed; corrected before pushing.Stacking
Carries #279 (security service in the compose stack) and #277 (specs repointed at
/api/v1/security/*) — both are prerequisites. Merge order: #279 → #277 → this, or merge this last.Follow-ups (deliberately not bundled)
e2e.yml's sign-in job should be retired once this is green — it cannot be repaired in place.access_deniedOIDC failure is a separate real bug.Playwright sign-in flow/OIDC plumbingare not required checks (onlygate-*are) — which is precisely why the auth e2e could stay red for days. Worth making sign-in required once green.hold+ noauto-merge— master is deploy-on-push.Co-Authored-By: Claude