Skip to content

feat(identity): Authentik per-portal brands + per-domain OIDC redirects (FF-EPIC-11 S4) - #531

Open
claude[bot] wants to merge 2 commits into
masterfrom
claude/portal-authentik-brands
Open

feat(identity): Authentik per-portal brands + per-domain OIDC redirects (FF-EPIC-11 S4)#531
claude[bot] wants to merge 2 commits into
masterfrom
claude/portal-authentik-brands

Conversation

@claude

@claude claude Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

📋 Description

Implements FF-EPIC-11 S4 — the final story of EPIC-11 (tenant-scoped identity): automatic per-domain OIDC redirect-URI registration and per-portal Authentik login branding at portal-provisioning time. This PR completes EPIC-11.

Builds on #504 (backend/src/services/portalProvisioning.ts), #506/#513/#517 (portal-scoped identity).

🔄 Type of Change

  • ✨ New feature (non-breaking change which adds functionality)
  • 🧪 Test addition or improvement

Discovery + reuse (no duplicate paths)

  • backend/src/authentik/authentik-admin.ts — reused verbatim (buildHeaders, findAcrossPages, getAuthentikAdminToken, getAuthentikBaseUrl, AUTHENTIK_TIMEOUT_MS) for both the redirect step and the new brand module. No parallel Admin-API-client path was written.
  • backend/src/custom-domains/authentikRedirect.ts — this ALREADY implements per-domain OIDC redirect-URI registration (createAuthentikRedirectRegistrar()RedirectUriRegistrar.register/deregister), built for FF-EPIC-16 custom domains and driven from CustomHostnameService.project() once a custom domain goes active. Reused directly, unmodifiedportalProvisioning.ts's new authentik_redirect_register step calls the SAME registrar.register(domain) for every row in portal_domains, so the platform subdomain (registered at provisioning time, this PR) and a later custom domain (registered when it activates, pre-existing code) share one registration path and one dedupe/idempotency contract (register() is a no-op if the exact URL is already present). No parallel redirect-URI path was written.
  • deploy/helm/fuzefront/authentik/blueprints/brand-fuseseam.yaml / blueprints-mendys/brand-mendys.yaml — used as the precedent for the per-portal brand shape (authentik_brands.brand, identifiers: { domain }, default: false for anything but the platform brand).
  • backend/src/services/portalProvisioning.ts — the resumable, advisory-locked pipeline this PR adds two steps to.

What was added

AC1 — per-domain OIDC redirect URI registration at provisioning

New authentik_redirect_register INFRA step (blocking, same fail-loud contract as every step before it). For every row in portal_domains for the portal, calls the reused RedirectUriRegistrar.register(domain). Idempotent (the underlying registrar dedupes by exact URL — a no-op on retry). No-op (recorded done, no Authentik call) while fuzefront.platform.multi-tenant-portals is OFF, same convention ensureRootPortal/default_domain_create already use — no new flag was invented.

AC2 — per-portal Authentik brand

Two parts, per the task's "NOT cluster operations" scoping:

  1. backend/src/authentik/portalBrand.ts — the automatic path. createAuthentikBrandRegistrar() upserts an authentik_brands.brand via POST/PATCH /api/v3/core/brands/, keyed by domain, themed from the portal's branding.accent/name/logo/favicon. Wired into provisioning as authentik_brand_register, a best-effort step (like owner_invite) — cosmetic, so its failure never blocks/regresses the portal, unlike the redirect step.
  2. deploy/helm/fuzefront/authentik/blueprint-templates/brand-portal-template.yaml — a documented, NON-LIVE blueprint template following brand-mendys.yaml's shape, for the rare hand-applied/GitOps case. Deliberately placed outside authentik/blueprints/ — that directory is .Files.Glob'd into a ConfigMap that Authentik's worker auto-applies on pod start (see templates/authentik-blueprints.yaml), so a template with literal {{PLACEHOLDER}} tokens sitting there would get auto-applied as a broken real blueprint on the next deploy. The file's header documents this explicitly. No cluster/Helm/kubectl operation was performed — this is a chart-source-only addition; if a portal ever genuinely needs a hand-rendered blueprint, that's a FuzeInfra/GitOps action, flagged below.

AC3 — multi-domain correctness

The redirect step iterates every portal_domains row, not just the primary — each domain gets its own independently-registered, exact-match redirect URI (matching_mode: strict), so a subdomain plus a later FF-EPIC-16 custom domain never collide or mismatch. Covered by a new unit test plus new direct coverage of the reused authentikRedirect.ts registrar (no tests existed for it before this PR).

AC4 — fail loud on registration failure

The redirect step is IN the blocking INFRA_STEPS set: a failure records the step failed, halts the pipeline before the completion checkpoint, and the portal never transitions past provisioning — so a portal that failed OIDC-redirect registration is never left looking "ready" while login is silently broken. (The brand step is explicitly the opposite — cosmetic-only, so it stays best-effort like owner_invite.)

Feature flag

No new flag — this reuses fuzefront.platform.multi-tenant-portals (the existing EPIC-09 master switch), read via isMultiTenantPortalsEnabled() per call. Both the ON and OFF paths are unit-tested (tests/portal-provisioning.test.ts, "flag-off leaves provisioning unchanged").

🧪 Testing

cd backend && npm install
PERMIT_API_KEY=ci-no-real-permit-calls npm run test:coverage -- --runInBand --testPathIgnorePatterns="permit-integration|billing-"
# Test Suites: 30 passed, 30 total / Tests: 458 passed, 458 total

PERMIT_API_KEY=ci-no-real-permit-calls npm run test:integration
# Test Suites: 11 passed, 11 total / Tests: 204 passed, 204 total

npx tsc --noEmit
# Only the pre-existing (unmodified by this PR) @fuzefront/custom-hostname-client
# module-not-found errors in src/custom-domains/{customHostnameService,factory}.ts,
# identical to clean master.

npm run lint
# 0 errors, 7 pre-existing warnings unrelated to this change.

New/extended test files:

  • backend/tests/portal-authentik-brand.test.ts — new, unit tests for portalBrand.ts (create, idempotent update, no-accent, malformed-accent rejection, no-token → null).
  • backend/tests/authentik-redirect-registrar.test.ts — new, first-ever direct coverage of the reused authentikRedirect.ts (multi-domain correctness, idempotent no-op, preserves pre-existing entries).
  • backend/tests/portal-provisioning.test.ts — extended: happy-path now asserts the redirect/brand calls; new describe blocks for AC4 (fail-loud + resume), AC3 (multi-domain), flag-off (both new steps no-op, portal still completes), and AC2 (brand failure is non-blocking).

🔧 Implementation Details

Backend Changes:

  • backend/src/services/portalProvisioning.ts — two new steps (authentik_redirect_register blocking, authentik_brand_register best-effort), new injectable redirectUris/brandRegistrar deps (default to the real Admin-API-backed registrars, degrading to a no-op when AUTHENTIK_ADMIN_TOKEN is unset, same contract as the existing custom-domains registrar).
  • backend/src/authentik/portalBrand.ts — new, per-portal brand upsert.
  • backend/src/migrations/021_portal_provisioning_authentik_steps.ts — extends portal_provisioning_step_enum with the two new step names (non-transactional ALTER TYPE ... ADD VALUE, same pattern as migration 016).

Infra (documentation only, no cluster op performed):

  • deploy/helm/fuzefront/authentik/blueprint-templates/brand-portal-template.yaml — new, non-live reference template.

🚩 FuzeInfra delegation

None required for this PR — no Helm/ConfigMap/cluster change was made; the new blueprint file is deliberately outside the chart's live-glob path (see AC2 above) and nothing here touches a deployed resource. If a future story wants the template file actually rendered/applied for a specific dedicated-instance portal, that render-and-apply step is FuzeInfra/GitOps territory (@claude delegation), not this repo's backend code.

📋 Checklist

  • Self-review completed
  • Both flag states (ON/OFF) tested
  • New unit tests added; existing provisioning + scope-guard tests still green
  • No new lint errors; only pre-existing @fuzefront/custom-hostname-client tsc errors (identical to master)
  • No new HTTP routes added (rate-limiting requirement N/A)
  • No polynomial/ReDoS-prone regex added (bounded ^#[0-9a-fA-F]{3,8}$ accent validator only)
  • Package-lock/coverage churn reverted before push

Out of scope (not done by this PR)

  • Infra/cluster ops — applying any blueprint, deploying/restarting Authentik — is FuzeInfra/GitOps, not performed here.
  • UI — no frontend work; branding/theming here is server-side Authentik-brand config only.
  • Independent acceptance/contract test suite — this PR's tests are backend unit/integration tests, not the independent QA suite (test-engineer territory).

Generated by Claude Code

…ts (FF-EPIC-11 S4)

Adds two resumable provisioning steps (authentik_redirect_register,
authentik_brand_register) to the portal provisioning pipeline: the redirect
step blocks/fails-loud (login must never be silently broken), the brand step
is best-effort/cosmetic. Reuses the existing authentik-admin.ts client and
custom-domains/authentikRedirect.ts registrar rather than duplicating them,
and adds a new authentik/portalBrand.ts Admin-API-backed brand registrar plus
a documented (non-live) blueprint template following brand-mendys.yaml.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0127R9Zcw92tmBkxUUuLEB79
@claude
claude Bot requested a review from izzywdev as a code owner August 3, 2026 18:20
@claude claude Bot added the auto-merge Enable squash auto-merge once CI passes label Aug 3, 2026
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Automated code review (gate-code-review)

Credit balance is too low

Report-only — this check never blocks merge.

@github-actions
github-actions Bot enabled auto-merge (squash) August 3, 2026 18:22
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Automated code review (gate-code-review)

Credit balance is too low

Report-only — this check never blocks merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto-merge Enable squash auto-merge once CI passes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant