feat(identity): Authentik per-portal brands + per-domain OIDC redirects (FF-EPIC-11 S4) - #531
Open
claude[bot] wants to merge 2 commits into
Open
feat(identity): Authentik per-portal brands + per-domain OIDC redirects (FF-EPIC-11 S4)#531claude[bot] wants to merge 2 commits into
claude[bot] wants to merge 2 commits into
Conversation
…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
Contributor
Automated code review (gate-code-review)Credit balance is too low Report-only — this check never blocks merge. |
Contributor
Automated code review (gate-code-review)Credit balance is too low Report-only — this check never blocks merge. |
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.
📋 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
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 fromCustomHostnameService.project()once a custom domain goesactive. Reused directly, unmodified —portalProvisioning.ts's newauthentik_redirect_registerstep calls the SAMEregistrar.register(domain)for every row inportal_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: falsefor 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_registerINFRA step (blocking, same fail-loud contract as every step before it). For every row inportal_domainsfor the portal, calls the reusedRedirectUriRegistrar.register(domain). Idempotent (the underlying registrar dedupes by exact URL — a no-op on retry). No-op (recordeddone, no Authentik call) whilefuzefront.platform.multi-tenant-portalsis OFF, same conventionensureRootPortal/default_domain_createalready use — no new flag was invented.AC2 — per-portal Authentik brand
Two parts, per the task's "NOT cluster operations" scoping:
backend/src/authentik/portalBrand.ts— the automatic path.createAuthentikBrandRegistrar()upserts anauthentik_brands.brandviaPOST/PATCH /api/v3/core/brands/, keyed bydomain, themed from the portal'sbranding.accent/name/logo/favicon. Wired into provisioning asauthentik_brand_register, a best-effort step (likeowner_invite) — cosmetic, so its failure never blocks/regresses the portal, unlike the redirect step.deploy/helm/fuzefront/authentik/blueprint-templates/brand-portal-template.yaml— a documented, NON-LIVE blueprint template followingbrand-mendys.yaml's shape, for the rare hand-applied/GitOps case. Deliberately placed outsideauthentik/blueprints/— that directory is.Files.Glob'd into a ConfigMap that Authentik's worker auto-applies on pod start (seetemplates/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_domainsrow, 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 reusedauthentikRedirect.tsregistrar (no tests existed for it before this PR).AC4 — fail loud on registration failure
The redirect step is IN the blocking
INFRA_STEPSset: a failure records the stepfailed, halts the pipeline before the completion checkpoint, and the portal never transitions pastprovisioning— 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 likeowner_invite.)Feature flag
No new flag — this reuses
fuzefront.platform.multi-tenant-portals(the existing EPIC-09 master switch), read viaisMultiTenantPortalsEnabled()per call. Both the ON and OFF paths are unit-tested (tests/portal-provisioning.test.ts, "flag-off leaves provisioning unchanged").🧪 Testing
New/extended test files:
backend/tests/portal-authentik-brand.test.ts— new, unit tests forportalBrand.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 reusedauthentikRedirect.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_registerblocking,authentik_brand_registerbest-effort), new injectableredirectUris/brandRegistrardeps (default to the real Admin-API-backed registrars, degrading to a no-op whenAUTHENTIK_ADMIN_TOKENis 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— extendsportal_provisioning_step_enumwith the two new step names (non-transactionalALTER 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 (
@claudedelegation), not this repo's backend code.📋 Checklist
@fuzefront/custom-hostname-clienttsc errors (identical to master)^#[0-9a-fA-F]{3,8}$accent validator only)Out of scope (not done by this PR)
test-engineerterritory).Generated by Claude Code