Skip to content

security-service: multi-tenant IdP broker (resolve tenant per request, drive the right Authentik) #434

Description

@izzywdev

security-service is the single front door for identity across all tenants, and Authentik is an implementation detail behind it. That contract is already stated in backend/security/src/providers/authentik/config.ts"no vendor name leaks past this boundary into the API surface" — but the implementation is single-tenant. This is the work that makes it true for more than one tenant.

Blocks the MendysRobotics silo shipped in #428: the instance, its provider and its database all exist and are correct, but no MendysRobotics login can succeed until this lands.

Current state

Authentik configuration is read straight from process.env at ~12 call sites across 7 files, with a FuzeFront default hard-coded at each:

File Reads
services/oidc.ts:52,86 ISSUER_URL, CLIENT_ID, BASE_URL
services/authentikPassword.ts:137,158,640 BASE_URL, ENROLLMENT_FLOW_SLUG, ADMIN_TOKEN
services/machine-identity.ts:35,53,151 BASE_URL, ISSUER_URL, CLIENT_ID
providers/authentik/accountApi.ts:27,34 BASE_URL, ADMIN_TOKEN
providers/authentik/AuthentikIdentityProvider.ts:718 ISSUER_URL
routes/auth.ts:370,453 ISSUER_URL, ENROLLMENT_FLOW_SLUG
routes/invitations.ts:81 ISSUER_URL

Defaults such as 'http://localhost:9000/application/o/fuzefront/' and 'fuzefront-enrollment' are literals in the code. providers/authentik/config.ts is the natural seam — it already centralises the browser-facing paths — but every function there is a zero-arg env reader.

Scope

  1. Tenant registry. Host → { issuerUrl, baseUrl, clientId, clientSecret, adminToken, enrollmentFlowSlug, appBaseUrl, googleBrokered }. Two entries to start: app.fuzefront.comauthentik-server:9000; live./marketplace.mendysrobotics.comauthentik-mendys-server:9000. Chart-driven (a values list rendered to a ConfigMap or JSON env), not a code constant.
  2. Per-request resolution. Middleware resolves the tenant from the Host header and puts it on the request. Fail closed — an unrecognised host must not silently fall back to FuzeFront, or a misrouted request would authenticate against the wrong directory. That is the whole boundary.
  3. Thread it through. Replace every process.env.AUTHENTIK_* read above with the resolved tenant. The awkward ones are machine-identity.ts, authentikPassword.ts and accountApi.ts, which are also called from non-request contexts (provisioning scripts, seed jobs) and so cannot simply read a request-scoped value — they need an explicit tenant argument.
  4. Sessions must be tenant-scoped. security-service issues its own JWT/session. A session minted for one tenant must not validate on another — include the tenant in the token and check it, and scope the cookie to the tenant domain. Getting this wrong silently rejoins the two directories that feat(authentik): MendysRobotics identity silo — separate Authentik instance + platform OIDC client #428 exists to separate.
  5. Login mechanisms for the Mendys tenant (decided):
    • Password + enrollment → server-side flow driver (authentikPassword.ts, Authentik's /api/v3 flow-executor over ClusterIP). The browser never touches Authentik, and no Authentik native paths are routed on the Mendys hosts — unlike app.fuzefront.com, which does route them.
    • Social → the existing server-brokered path (/api/v1/security/social/google/callback, googleOidc.ts). Must be per-tenant and must never fall back to Authentik's /source/oauth/*. Note securityService.googleBrokered defaults to "false" in the chart today; the Mendys tenant needs "true". Per-tenant Google client credentials are a design question — decide whether tenants share one Google client or each brings its own.
  6. Chart wiring. Per-tenant env/config replacing the flat AUTHENTIK_* block in templates/security.yaml, plus routing /api/v1/security/* and /api/auth/* from the Mendys hosts to security-service.

Out of scope

Anything in the MendysRobotics repo (tracked at izzywdev/MendysRobotics#253), and retiring the FuzeFront-side mendys-datasets provider (staged separately so datasets sign-in is not interrupted).

Acceptance criteria

  • Unknown Host fails closed with no FuzeFront fallback, covered by a test
  • No process.env.AUTHENTIK_* read remains outside the tenant registry
  • A session minted for tenant A is rejected by tenant B, covered by a test
  • FuzeFront login is provably unchanged (existing Playwright sign-in flow still green)
  • A MendysRobotics password login and enrollment succeed end to end against authentik_mendys, with no Authentik hostname reaching the browser
  • Brokered Google works for the Mendys tenant without touching /source/oauth/*

Risk

This refactors the live authentication path for FuzeFront itself, which has a history of subtle auth outages (probe timeouts evicting the IdP, DNS stalls inside login flows, gravatar lookups in request handling). The FuzeFront tenant's behaviour should be provably identical before the second tenant is enabled — the existing Playwright sign-in flow is the gate.

Related: #428 (the silo), #433 (corrected exposure docs), izzywdev/FuzeInfra#421 (the database).

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions