Skip to content

fix(e2e): run the security-service in the compose stack + route /api/v1/security - #279

Merged
izzywdev merged 3 commits into
masterfrom
claude/e2e-stack-security-service
Jul 16, 2026
Merged

fix(e2e): run the security-service in the compose stack + route /api/v1/security#279
izzywdev merged 3 commits into
masterfrom
claude/e2e-stack-security-service

Conversation

@izzywdev

Copy link
Copy Markdown
Owner

The gap

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, /api/v1/app-registry and 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.

The security service — which now serves all SPA auth — has had zero e2e coverage since the cutover. That is why E2E (sign-in) has been red on master since then, and red on every PR since, including #278 which touches only a shell script and markdown. A docs-and-script PR cannot break sign-in — that's the tell. It was never flake, and "pre-existing" was never a reason to wave it off.

Prod was never affected: the k8s Ingress routes /api/v1/securityfuzefront-security ahead of nginx, which is why real sign-in works.

The fix

  • Adds the security service to docker-compose.e2e.yml — with Authentik + DB env, because password sign-in is brokered server-side through the flow-executor, so Authentik is required even for the plain email/password path (not just social).
  • Routes /api/v1/security/security:3002 in deploy/e2e/nginx.e2e.conf, above the /api/ catch-all — mirroring the prod Ingress.
  • frontend now depends_on it: nginx resolves upstreams at startup and refuses to boot on an unresolvable one (confirmed while testing: host not found in upstream "security").

Verified (actually run, not asserted)

docker compose -f docker-compose.e2e.yml config   → PARSE OK; services: …, security
nginx -t (upstreams host-mapped)                  → syntax is ok, test is successful

Scope

Foundation only. Not included: .github/workflows/e2e.yml (the sign-in workflow hand-starts the monolith with no Authentik — separate piece), the oidc-plumbing spec base URLs, and frontend/nginx.conf parity. hold + no auto-merge — master is deploy-on-push.

Co-Authored-By: Claude

…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
@izzywdev izzywdev added the hold label Jul 16, 2026
@github-actions
github-actions Bot enabled auto-merge (squash) July 16, 2026 12:28
@github-actions

Copy link
Copy Markdown
Contributor

Automated code review (gate-code-review)

Credit balance is too low

Report-only — this check never blocks merge.

Comment thread deploy/e2e/nginx.e2e.conf
Comment thread deploy/e2e/nginx.e2e.conf
Comment thread deploy/e2e/nginx.e2e.conf
Comment thread deploy/e2e/nginx.e2e.conf
Comment thread deploy/e2e/nginx.e2e.conf
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
@github-actions

Copy link
Copy Markdown
Contributor

Automated code review (gate-code-review)

Credit balance is too low

Report-only — this check never blocks merge.

@izzywdev

Copy link
Copy Markdown
Owner Author

Self-review catch — pushed b6a763a. The first cut of this PR would have added the container but still left sign-in broken.

I had set the security service's AUTHENTIK_REDIRECT_URI to http://localhost:4173/api/v1/security/social/callback. That URI is not in the OIDC provider's redirect_uris, and Authentik matches them matching_mode: strict — so it would have rejected the authorize call and password login would have failed anyway.

The reason it matters: password sign-in is a full authorize→code→exchange, not just a flow-executor call. services/authentikPassword.ts drives the flow-executor, then GETs the authorize URL with the session cookies and lifts code out of the 302 Location. So the redirect_uri must be registered, but never has to be browser-reachable — the server follows the redirect itself.

Fixed by reusing the shared, already-registered /api/auth/oidc/callback, which is exactly what prod's security service does (authentik.oidc.redirectUrihttps://app.fuzefront.com/api/auth/oidc/callback). Registered set lives in authentik/blueprints/provider-oidc.yaml.

Re-verified: docker compose -f docker-compose.e2e.yml config → PARSE OK.

(The failing open-pr check here is the repo-wide GitHub Actions is not permitted to create or approve pull requests issue — see #278 — not a defect in this PR.)

🤖 Addressed by Claude Code

…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
@github-actions

Copy link
Copy Markdown
Contributor

Automated code review (gate-code-review)

Credit balance is too low

Report-only — this check never blocks merge.

@izzywdev

Copy link
Copy Markdown
Owner Author

Regression I introduced here — fixed in d164f91. Adding the security container broke the OIDC-plumbing stack:

dependency failed to start: container fuzefront-e2e-backend-1 exited (1)

I checked it against a control (the same plumbing workflow on claude/authn-google-direct, before this container existed): there the stack came up fine and the test failed on its own merits. So this crash was mine, not pre-existing.

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.

Fix: serialise — security now waits for backend to be healthy, so the chain is already applied and security's run is a harmless no-op. Chain: postgres → backend → security → frontend. Verified docker compose config parses (no cycle).


Worth flagging separately, because it's the next real problem rather than this PR's: that control run shows plumbing's genuine failure is

GET /?error=oidc_error&message=access_denied
backend | ❌ Failed to initialize OIDC client: connect ECONNREFUSED 172.18.0.5:9000

i.e. the OIDC flow itself returns access_denied. So this PR unblocks the stack, but plumbing will likely still fail on that — a real auth bug, tracked next, not something to hide behind "pre-existing".

🤖 Addressed by Claude Code

@izzywdev izzywdev removed the hold label Jul 16, 2026
@izzywdev
izzywdev merged commit b29d669 into master Jul 16, 2026
47 of 49 checks passed
@izzywdev
izzywdev deleted the claude/e2e-stack-security-service branch July 16, 2026 14:02
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
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
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.

2 participants