Auth Configuration¶
Floh signs every user in through Authifi. This document covers the Authifi objects Floh depends on (OIDC clients, resource servers, roles, and groups) and the Floh settings that must agree with them.
- Deployment Guide covers the host, the deploy secrets and variables, and the failure reference.
- Configuration Map explains how configuration layers resolve and which one wins.
- Roles & Entitlements covers the authorization model those Authifi objects implement.
Contents¶
- Key terms
- Quickstart
- Channel audiences
- Console BFF token slots
- Authifi client registration
- Upgrading past LSA-9939 (legacy authorization removal)
- Migrating channel resource identifiers to
https - Platform access rollout
- The Floh roles catalog
- Console Authifi sync
Key terms¶
- Channel. One of Floh's three ways in: the admin console, the user portal, and the MCP server that AI tools connect to. Each channel has its own Authifi client and resource server.
- OIDC client. The Authifi registration a channel signs users in with:
floh-clientfor the console,floh-portal-clientfor the portal, andfloh-mcp-clientfor the MCP server. - Resource server and audience. A resource server is Authifi's record of an
API that accepts tokens. Its identifier, called the audience, looks like a URL
(for example
https://console.floh.api) but is only a name; nothing is served there. Each token names the audience it was issued for, and the Floh API accepts only tokens issued for one of the three channel audiences. - BFF (backend for frontend). The small server in front of the console and the portal. It handles sign-in and keeps tokens on the server, so the browser never holds them.
- Token slot. A token the console BFF holds for each signed-in user. The console has two: one for the Floh API, and one for Authifi's admin API, which the Authifi sync and Floh roles pages use.
- Step-up. A fresh MFA prompt Floh requires before a sensitive change, such as applying the Authifi sync.
Quickstart¶
Most deployments set up Authifi once, from the console. These steps are the short version; each links to the full description.
On a new tenant deployed with the GitHub Actions workflow, follow the
first-time bring-up checklist
instead of steps 1 and 2, then continue at step 3. The workflow also requires
the portal client's secret, PORTAL_OIDC_CLIENT_SECRET, and that client does
not exist until the checklist creates it.
- Create the console's Authifi objects. In Authifi, create the
Floh Console APIresource server and thefloh-clientOIDC client linked to it. The console signs in through these two, so the sync cannot create them. See Before you run the sync. - Deploy Floh with the
floh-clientsecret asOIDC_CLIENT_SECRETand the Authifi admin API address asAUTHIFI_ADMIN_TARGET. See Floh deployment. - Sign in to the console as an Authifi tenant admin or super admin who can complete MFA.
- Preview the sync. Go to Admin > Authifi sync and run Preview. It lists the changes it would make and writes nothing.
- Apply. Confirm and complete the MFA prompt. Apply creates what is missing in Authifi, such as the portal and MCP resource servers and Floh's roles and groups, then saves the role list to Floh.
- If the portal or MCP client is missing, Apply creates it and stops. Rotate
the new client's secret in Authifi, deploy it as
PORTAL_OIDC_CLIENT_SECRETorMCP_OIDC_CLIENT_SECRET, then return to step 4. - If saving the role list is denied, the page offers to add you to the
floh-console:admingroup. Accept, choose Sign in again, then run Preview and Apply once more to save the list. - Verify. Run Preview again. It should report "No changes needed — Authifi already matches the desired state.", or "Preview ready: 0 changes" followed by a count of reports.
- Reports are differences the sync shows but never changes, such as an
extra Floh role or group in Authifi, or an edited namespace description.
They don't block setup, and Apply won't clear them. Fix them in Authifi,
or remove extras with the CLI's
sync-authifi-rbac -- --apply --prune. - "Authifi already matches; update Floh catalog cache." means the role list still needs saving. Apply once more.
- Any remaining change means a step is unfinished; see Running the sync.
- Test sign-in by role. In Admin > Floh roles, on the Members tab,
add test users to different groups, for example
floh-console:requestor,floh-console:approver, andfloh-portal:requestor. Sign in to the console and the portal as each user and check that each sees only what the role allows. A user picks up a group change at their next sign-in or session refresh.
Channel audiences¶
Each channel's tokens are issued for that channel's audience (see Key terms). This section covers which audiences the API accepts and where their values come from.
The API verifies a token's aud against an allow-list built from
FLOH_CONSOLE_AUDIENCE,
FLOH_PORTAL_AUDIENCE, and FLOH_MCP_AUDIENCE. An unset or blank value
defaults to https://console.floh.api, https://portal.floh.api, or
https://mcp.floh.api, and the API logs which ones it defaulted outside
development and test. Startup fails when two channels resolve to the same
audience or when one equals OIDC_ISSUER. The API publishes the resolved
values as channelAudiences on GET /api/auth/config, which is how the console
Authifi sync knows what identifier to give a resource server it creates.
FLOH_RESOURCE_ID (and its deprecated alias OIDC_AUDIENCE)
is resolved into config but has no production consumer — its real job is
the scripts/sync-authifi-rbac.mjs operator tool and unrelated legacy
configuration paths. OIDC client setup ignores and preserves the pair. It is
not a startup requirement.
Console BFF token slots¶
The console BFF holds two tokens for each signed-in user (see Key terms): one for the Floh API, and one for Authifi's admin API. This section covers how both are configured and why the Authifi one needs token exchange.
For the console BFF, NODE_CONFIG owns the bffProxy array and the named
bff.tokenSlots. Every slot must include scopes as a JSON string array —
BFF 3.8.1 rejects a missing or non-array scopes field at startup. Both slots
declare "scopes": [] with "scopeMatch": "contains", which together mean the
slot asserts nothing about the issued token and leaves each resource server as
the authority. Both parts are required: scopeMatch defaults to "contains"
only for a primary slot and to "exact" for every other acquisition mode, and
under "exact" an empty array rejects any token that carries any scope at all —
surfacing as resource_token_rejected with reason: "extra_scope". Neither
token here carries a fixed scope set, because the Floh API token carries the
caller's Floh permissions and the exchanged Authifi token carries the caller's
Authifi permissions, both derived per user.
Do not copy auth.authorizationParams.scope into a slot's scopes. Those are
authorization request scopes; openid and offline_access are consumed by the
OIDC and refresh machinery and never appear in an issued access token, so a slot
asserting them rejects every login with reason: "missing_scope". The login
scopes stay where they take effect — auth.authorizationParams.scope in
docker/bff/console.json (openid, profile, email, offline_access) —
while both slots assert "scopes": [] with "scopeMatch": "contains". Do not reintroduce standalone
BFF_PROXY_TARGET or BFF_PROXY_PATH entries for the console service: node-config's
custom-environment-variable precedence would override the array shape with
scalar values and silently corrupt the proxy contract. AUTHIFI_ADMIN_TARGET
is the remaining GitHub var for the tenant-pinned Authifi admin HTTP API. The
admin token slot resource is OIDC_ISSUER (Authifi API RSID). jwksUri is
OIDC_JWKS_URI from ci.env (the issuer's discovery jwks_uri). Deploy
fetches {OIDC_ISSUER}/.well-known/openid-configuration and fails if
jwks_uri disagrees. The workflow writes OIDC_JWKS_URI into ~/floh/.env
and AUTHIFI_ADMIN_TARGET into ~/floh/env/console.env.
The admin slot uses "acquisition": "exchange", which obtains its token by RFC
8693 token exchange against the login session's access token rather than by the
refresh grant. The refresh grant cannot serve this slot at all: the authorization
server pins resource indicators onto the authorization code at /authorize, and
a refresh token can only ever request a resource already on that list, so asking
it for the Authifi admin resource fails with invalid_target regardless of
configuration.
This places a prerequisite on the OIDC client registration in each environment:
the client named by OIDC_CLIENT_ID must have
urn:ietf:params:oauth:grant-type:token-exchange among its registered grant
types. pnpm run setup-authifi-oidc-clients registers it when it creates the
console client, and refuses to proceed against a pre-existing console client
that lacks it rather than reconciling URIs and reporting success over a client
that cannot serve /authifi. A client registered by an earlier version of that
tooling needs the grant added in Authifi, or deleting so setup can recreate it.
Without it the exchange fails as unauthorized_client and every request
through the /authifi mount returns 401, while ordinary login continues to work
— the failure is confined to that mount and never ends the session. Check the
gateway log for token_exchange_failed, which records the slot name and the
provider's error code.
Authifi client registration¶
Before client registration, reconcile the Authifi authorization catalog for all three channels. The deploy workflow never runs this command automatically:
pnpm run sync-authifi-rbac -- --dry-run
pnpm run sync-authifi-rbac -- --apply
pnpm run sync-authifi-rbac -- --dry-run
The command rebuilds @floh/shared before planning so a leftover gitignored
packages/shared/dist cannot apply an older catalog. It loads repo .env when it exists, with direct shell values taking
precedence. Paste the admin JWT into gitignored .authifi-admin-token (not
.env).
Do not use the Floh console or the portal while running these commands. The
admin token belongs to the same Authifi SSO session as both apps, and each one
ends that session at the IdP on its ordinary logout and on a re-login after
the API has rejected its token — signing in from the session-expired overlay
always does, because both apps deliberately route that Sign In through
/bff/logout (RP-initiated logout) so Authifi re-prompts instead of replaying
a token the API already refused. RP-initiated logout is single-logout: it takes
the copied admin token down with it, and the next command fails with
HTTP 401 (UnauthorizedError; revoked_token) well before the token's exp.
A revocation during --apply can interrupt the reconciliation partway, after
earlier mutations have already landed. Re-running is safe, since the sync
plans against live state and only writes what is still missing, but avoid the
interruption rather than recover from it: keep both apps closed for the
duration, and on a revoked token take a fresh copy and re-run immediately
rather than waiting.
This applies to every command here, including the read-only dry runs, and it is
separate from the MFA requirement that gates --apply alone, described below.
Cross-app session coupling is tracked in
LSA-10232. The CLI derives AUTHIFI_BASE_URL, numeric AUTHIFI_TENANT_ID, and
AUTHIFI_ADMIN_RESOURCE from that token when they are unset. Set all three
FLOH_*_AUDIENCE identifiers in .env or the shell. OIDC client setup uses
the same token file (SERVICES_AUTH_URL / AUTH_CLI_ACCESS_TOKEN remain
deprecated aliases of the derived base URL and in-memory token copy).
sync-authifi-rbac needs nothing beyond that admin token. It resolves the three
known OIDC clients and their current resource-server bindings from Authifi's own
tenant-scoped admin API (GET /clients and the resource-server listing with its
clients relation included), so there is no separately-authenticated external
CLI to install or log in to for this command. Client registration
(setup-authifi-oidc-clients) now uses the same Authifi admin API directly.
On a tenant where none of floh-client, floh-portal-client, or
floh-mcp-client exists yet, the command plans the RBAC catalog without
client assignments and says so — that is the bootstrap run, and it is what lets
client registration (which requires the channel resource servers) happen at all.
Rerun the same command after registration to verify the bindings that
setup-authifi-oidc-clients created against desired state and repair managed
channel links that are missing or wrong; on a clean tenant it reports no
changes. A tenant
carrying only some of the three, a duplicate of any one of them, or a matching
client row with no usable positive integer id is neither state: the run fails
closed naming the counts, before any mutation.
--apply additionally requires the token file to hold an MFA / AAL2
session. Authifi gates group writes behind step-up authentication and answers a
password-only token with 401 and
WWW-Authenticate: … error="insufficient_user_authentication", acr_values="mod-mf".
The command inspects the token before contacting Authifi and refuses to apply
when the token carries no MFA, when its auth_time is older than the max_age
Authifi enforces on those writes, or when its exp / step-up window has fewer
than 30 seconds left before mutation, so a partial apply is not started.
Re-authenticate with MFA, paste the fresh token, and run the apply promptly;
resource-server, permission, and access-role writes are unaffected, so an
interrupted run resumes on the next apply.
STEP_UP_ACR_ALIASES controls which JWT acr values satisfy that
acr_values=mod-mf challenge. The sync parser accepts only a JSON object whose
keys are safe non-empty challenge strings and whose values are non-empty arrays
of safe ACR strings. Invalid JSON, empty keys, empty arrays, unsafe characters,
or prototype-pollution keys fail closed before client construction or network
I/O.
That preflight is intentionally limited to readable JWTs. If the admin token
file is opaque rather than JWT-shaped, the CLI cannot inspect
acr, auth_time, or exp locally; Authifi's server-side authorization response
remains authoritative for whether the token satisfies the step-up requirement.
The first dry-run must show the expected console, portal, and MCP changes and
perform no writes. Apply requires confirmation. The final dry-run should report
No changes. For removal of stale Floh-owned roles, groups, relationships, or
managed permissions, first preview with --dry-run --prune, then run
--apply --prune; prune requires an additional confirmation and never deletes
resource servers, client assignments, or unrelated tenant objects. Do not use
--yes for production maintenance unless an approved non-interactive procedure
has captured and reviewed the exact plan.
Duplicate access-role names on different resource servers are safe, but prune
is still authoritative for Floh-owned leftovers. The sync planner and apply
path qualify each managed role by both resource-server identifier and role
name. A leftover floh-* role on another resource server remains a prune
target even when a desired role with the same name exists elsewhere, while the
concrete desired same-named live role is preserved because prune only unlinks a
group when that exact linked role id is scheduled for deletion.
On Authifi HTTP failures, CLI output is permanently redacted down to the
allowlisted top-level WWW-Authenticate fields error, acr_values, and
numeric max_age. The command never prints the admin token, any HTTP response
body, error_description, arbitrary challenge parameters, excluded quoted
challenge text, or URL userinfo/query secrets from credential-bearing Authifi
base URLs.
Two OIDC clients back a deploy, and both must exist in the Authifi tenant before the first login attempt:
| Client | Callback | Notes |
|---|---|---|
floh-client |
https://<DEPLOY_DOMAIN>/bff/callback |
Console. The console BFF is the relying party. |
floh-portal-client |
https://<DEPLOY_PORTAL_DOMAIN>/bff/callback |
Portal. The portal BFF is the relying party. |
Both callbacks are /bff/callback, not /api/auth/callback. The portal gateway
derives its callback from AUTH_BASE_URL and its bff.pathPrefix, so it cannot be
overridden through PORTAL_OIDC_REDIRECT_URI.
The same command also registers floh-mcp-client, a confidential client with no
browser origins. It backs the MCP server rather than a deployed web tier, so it
is not required for login and its minted secret belongs in env/mcp.env, not in
a deploy secret. See MCP Server Setup.
A client that already exists is never re-created, so setup checks the live one
instead: if floh-mcp-client is missing the authorization_code or
refresh_token grant, or authenticates with anything other than
client_secret_post, the run stops before its first write and names the field
to fix. Correct it in the Authifi admin UI, or delete the client and re-run.
The API also accepts floh-mcp-agent, the public client that will replace
floh-mcp-client for agent sign-in by device approval
(LSA-11274). Setup does not
register it yet, and until it exists in Authifi no token can reach that path,
so nothing changes on upgrade. Its tokens are accepted only as Authifi NHE
delegation tokens on the MCP channel, and agents are refused on step-up routes;
see Agent delegation tokens.
Rolling back is a code revert with no data or configuration step.
Each managed client must be authorized for its channel resource server:
floh-client uses FLOH_CONSOLE_AUDIENCE, floh-portal-client uses
FLOH_PORTAL_AUDIENCE, and floh-mcp-client uses FLOH_MCP_AUDIENCE. The
console BFF requests FLOH_CONSOLE_AUDIENCE on authorize; the portal BFF
requests FLOH_PORTAL_AUDIENCE. The three channel identifiers must be pairwise
distinct, and none may equal the AUTHIFI_ADMIN_RESOURCE derived from the
admin token. Setup removes each managed client's sibling-channel assignments
before binding each client only to its own channel server. It does not remove
unrelated client relations. In particular, pre-existing transitional links
remain untouched until LSA-10182.
Setup ignores and preserves FLOH_RESOURCE_ID and its deprecated
OIDC_AUDIENCE alias even when both exist with conflicting values.
sync-authifi-rbac still validates that pair and may fail until the operator
corrects the values or removes the stale assignments.
pnpm run setup-authifi-oidc-clients reconciles all of this idempotently.
Existing-client reconciliation is additive: every registered callback and
post-logout URI is preserved — including a deployed origin still needed on a
tenant shared with local development — except a callback whose parsed
pathname is exactly /api/auth/callback or /api/auth/callback/, the
API-hosted callback the BFF migration retired. The origins you pass are
appended if missing, not substituted for what is already registered. Always
preview first:
pnpm run setup-authifi-oidc-clients -- --tenant <tenant-slug> --yes --dry-run \
--identity-provider <your-authifi-identity-provider> \
--console-origin https://floh.authilize.com --console-origin https://localhost:7072 \
--portal-origin https://myfloh.authilize.com --portal-origin https://localhost:7073
--identity-provider is repeatable and is required whenever the plan includes
creating the console, portal, or MCP client — the run aborts before the dry run
prints anything without it. AUTHIFI_IDENTITY_PROVIDERS=nih,google in the
environment is equivalent.
--vault does not exempt you. It is additive rather than exclusive:
computePlan receives no vault flag and queues create-console whenever the
console client is absent, so on a genuinely new tenant even
setup-authifi-oidc-clients -- --vault --dry-run aborts on the same guard.
Supply a provider on the first run against a tenant, whatever else the run is
for.
The dry run prints required additions separately from known-deprecated
callback removals — the exact /api/auth/callback[/] classification is the
only automatic removal this tool performs — and closes with a count of each.
Read that output before dropping --dry-run. See
the portal guide for the full flag reference.
The OIDC dry-run does not preview numeric client-resource bindings before the
clients exist, but it does resolve the tenant, validate all three resource
servers, and perform the same final reread verification the apply path uses.
Setup requires the target tenant to be stated in TENANT (or --tenant for a
one-off) and fails closed when neither is set, then persists the derived tenant
fields before any Authifi mutation. Run the RBAC sync first, then verify in Authifi that each client is
linked to its matching resource server. For catalog
rollback, restore the prior catalog revision, preview with
pnpm run sync-authifi-rbac -- --dry-run --prune, then apply with
--apply --prune. A normal dry-run/apply only reports extras; deletions and
unlinks of catalog removals require prune. Relationship removals are planned
before object deletion.
Rollback of the channel-only setup behavior is code-only: restore the prior setup version to restore its former prerequisite and relationship behavior. No database or Authifi data migration runs in LSA-10181, and rollback neither recovers nor mutates client secrets.
Rolling back past LSA-10194 restores the old authoritative-replacement
policy for existing-client URI reconciliation. That older code treats every
registered URI you do not pass as absent and deletes it, so it cannot restore
a callback this version already removed as known-deprecated — code rollback
undoes the policy, not a past deletion. Before running the older script
against a client this version has reconciled, re-add via --console-origin /
--portal-origin (or CONSOLE_ORIGINS / PORTAL_ORIGINS) any origin that
must still resolve, and preview with --dry-run first.
This rollout does not add runtime Authifi grant rechecks on delayed steps (LSA-9940).
Upgrading past LSA-9939 (legacy authorization removal)¶
The 20260901130000_drop_legacy_authorization_rbac migration drops the
api_token, permission_override, role_permission, user_role,
permission, and role tables. Two things must be true before you deploy
it, or the release will fail closed:
- A default approver group must exist. Approval fallback and default
escalation used to scan
user_rolefor admins; they now resolvesettings.system.defaultApproverGroupIdonly. Migration20260901125000_backfill_default_approver_groupruns immediately before the drop. If the setting already names auser_groupwith at least one live member, it is a no-op. Otherwise it copies every live user (deleted = falseandactive = true) who currently has the legacy DBadminrole — excluding the reserved system user — into a group named Default Approvers (reused if that name already exists) and writes the setting. If that live-admin set is empty, or the legacy tables are already gone and the setting is still missing, migrate throws; it will not invent approvers. After deploy you can change the group in Settings → System, but you cannot clear it:PUT /api/admin/settingsrejects an explicitnull, and the group routes already refuse to delete the selected group or remove its last member. The drop'sup()still refuses any DDL unless the setting is a UUID naming auser_groupwith at least one non-deleted, active member. - Authifi must already be issuing scopes. Every request's permissions
come from the access token's
scopeclaim after this release. Re-runpnpm run sync-authifi-rbac -- --dry-runand then--applyso the catalog's roles — including the new console-onlysystem:administer— exist in Authifi, and confirm your admin users hold them. A user whose token lackssystem:administerloses admin-only console behavior (permanent user delete, project-authority bypass,scope=alllistings, template/scheduler/email-log administration).
Also remove ALLOW_PERMISSION_OVERRIDES and any OIDC_ROLE_* variables from
your environment; the server no longer reads them. floh_-prefixed API tokens
are rejected with 401, so migrate any script or MCP client still using one to
an OIDC refresh token (see MCP Setup).
Migrating channel resource identifiers to https¶
FLOH_CONSOLE_AUDIENCE, FLOH_PORTAL_AUDIENCE, and FLOH_MCP_AUDIENCE now
default to the https scheme, per RFC 9728,
which requires a resource identifier to be an https URL — a hard requirement
for the MCP channel, whose authorization spec builds discovery on that document.
Each value must match its Authifi resource-server identifier byte for byte:
acceptedAudiences is an exact-match union of exactly these three values, so a
scheme mismatch means no token for that channel can ever validate.
Three consequences when you change an identifier in Authifi:
- There is no overlap window. The allow-list has one slot per channel and
holds no alternates, so the tenant rename and the
config/public/ci.envupdate must land in the same maintenance window. Whichever side moves first, that channel is down until the other follows. - Active sessions must re-authenticate. Access tokens already issued carry
the old
audand fail verification immediately; they are not refreshed in place. Expect console and portal users to log in again. - Update your operator
.envbefore the next sync run.sync-authifi-rbacprovisions against whatever identifier is configured. If the tenant is onhttpsand your.envstill sayshttp, the next run plans a duplicate resource server on the stale identifier, together with the whole permission and role tree. Confirm withpnpm run sync-authifi-rbac -- --dry-run— a correctly aligned run plans no resource-server creates.
Rollback. Reverting the application config alone is not sufficient once the tenant has been renamed; the two must be reverted together, or the channel stays broken in the opposite direction. There is no migration or image component to roll back — the change is config-only on the Floh side.
Platform access rollout¶
The console labels this section Floh roles, with Roles and Members
tabs headed Floh roles and Floh members. The configuration keeps the
platform-access name — PLATFORM_ACCESS_GROUP_IDS and the
/platform-access/... routes are unchanged — so expect the variable you set
and the heading an administrator reports to differ.
As of LSA-10171, the normal way to run this rollout once the API and console
are already deployed is the console Authifi sync page,
not the CLI. A signed-in Authifi tenant or super admin previews and applies the
same desired state the CLI applies, then the browser persists the resulting
group-id map through a normal Floh API call — no GitHub repository variable or
redeploy required. The CLI (sync-authifi-rbac, --emit-group-ids) and the
PLATFORM_ACCESS_GROUP_IDS environment variable described in the rest of this
section are not removed: they remain the supported path for first-time
bring-up (before any administrator can sign in), headless/CI-driven
maintenance, break-glass recovery when the console is unreachable, and
environment-level rollback. Authifi itself stays authoritative either way — the
persisted map is only a non-secret id cache the console reads to address
groups it already has live authorization for.
This rollout assumes the console BFF's Authifi admin proxy is configured only by
committed NODE_CONFIG plus OIDC_ISSUER (admin slot resource / Authifi API
RSID), OIDC_JWKS_URI (discovery jwks_uri), and deploy-time
AUTHIFI_ADMIN_TARGET. AUTHIFI_ADMIN_TARGET
must stay the fixed tenant-scoped
<authifi-base>/auth/admin/tenants/<positive-tenant-id> URL for the deploy
target. Never derive it from browser input or request parameters.
For first-time bring-up (before any administrator can sign in to run the console flow) or headless/CI-driven maintenance, apply in this order:
- apply Authifi desired state
- capture the group id map (below) and set it on the API
- deploy BFF image/config
- deploy the API and console images
Once the API and console are live, an Authifi tenant or super admin normally does steps 1–2 from the Console Authifi sync page instead — see below.
The Floh roles catalog¶
This section describes the CLI/environment-variable capture path. It is the fallback for headless operation, break-glass recovery, and any environment without a signed-in Authifi admin available to run the console flow; the normal path once the console is deployed is Console Authifi sync.
The console renders one row per canonical role. Floh owns those names — they are the same list the sync CLI applies as desired state — but Authifi assigns each group a numeric id at creation time, and the console needs the id to read the group's members. That map is deploy-time configuration rather than a runtime lookup: the console has no credential that can enumerate groups it is not itself a member of, so a runtime catalog read would show each administrator a different, silently incomplete list.
Capture the map after the sync has created the groups:
It prints a single PLATFORM_ACCESS_GROUP_IDS=… line, or exits non-zero
naming the groups it could not find — it never emits a partial map, because a
partial map is exactly what produces a console screen that looks complete and
is not. Store the value as the PLATFORM_ACCESS_GROUP_IDS repository variable;
the deploy appends it to the API's .env when set.
An absent map never blocks startup. LSA-10117 retired the
PLATFORM_ACCESS_ENABLED gate, so there is no off position to fall back to and
a rolling deploy can carry an API that has the map alongside one that does not.
Outside development and test the API logs a [config] warning naming the
variable and the console renders an empty catalog, which is the same
fail-closed outcome the browser already produces for a partial map.
An invalid map — malformed JSON, an unknown group name, a non-positive id, two groups sharing an id, or a missing group — fails startup in every environment, including development. Silently dropping a bad entry would degrade the screen in the same invisible way the old runtime lookup did.
Every subsequent sync-authifi-rbac run compares the configured map against
live Authifi and exits non-zero if they disagree, naming the groups and both
ids — but only when the deployed value is present in that run's environment.
The repository variable reaches the API's .env on the deploy host, not the
shell you run the sync from, so export it first or the run passes without
checking anything:
PLATFORM_ACCESS_GROUP_IDS="$(env -u GITHUB_TOKEN gh variable get PLATFORM_ACCESS_GROUP_IDS)" \
pnpm run sync-authifi-rbac
A run that resolves every managed group but finds no configured value says so on stderr rather than reporting a clean comparison. It still exits 0, because the run that first creates the groups legitimately has no map to compare yet.
Recover from reported drift by re-running --emit-group-ids, updating the
repository variable, and then re-running the Deploy workflow — the drift
means the deployed console is addressing groups that no longer exist or now
belong to different roles.
# `--emit-group-ids` prints a whole `PLATFORM_ACCESS_GROUP_IDS=…` line, and
# pnpm's reporter writes its own lines to stdout ahead of it. Select the emitted
# line, then strip the key — storing the raw capture would put the reporter
# banner and the variable's own name inside the value, which the sync's JSON
# parser then reports as an unusable map.
# Chained: pasted lines do not stop on failure without `set -e`, and every way
# this can fail is silent. An expired token or an unreachable Authifi leaves
# `emitted` empty, and an empty map is not rejected downstream — the API now
# boots with an empty catalog and only warns — so an unchained sequence would
# store nothing and deploy it, turning a drift recovery into a total loss of
# the roles screen.
emitted="$(pnpm run sync-authifi-rbac -- --emit-group-ids | grep -m1 '^PLATFORM_ACCESS_GROUP_IDS=')" \
&& [ -n "${emitted#PLATFORM_ACCESS_GROUP_IDS=}" ] \
&& env -u GITHUB_TOKEN gh variable set PLATFORM_ACCESS_GROUP_IDS \
--body "${emitted#PLATFORM_ACCESS_GROUP_IDS=}" \
&& env -u GITHUB_TOKEN gh workflow run deploy.yml
Restarting the API is not sufficient and is the failure mode worth naming:
the variable is read only by the Deploy workflow, which re-copies the public
overlay from the repository and appends PLATFORM_ACCESS_GROUP_IDS to
~/floh/public/ci.env on the host. Nothing on the host reads the repository
variable, so a restart re-reads the same ci.env and reloads the very map the
drift check just rejected — leaving the console pointed at the stale groups
while the operator believes the recovery succeeded. Deploying is what
propagates the new value; if a full deploy is not acceptable, edit
~/floh/public/ci.env on the host directly and restart, accepting that the
next deploy overwrites it from the repository variable.
There is no runtime flag to flip. The surface is visible to any caller holding
both system:administer and user:read, so access is granted and revoked
entirely through the predefined Authifi groups — see
Floh roles terminology and authority,
which also covers the refresh/login-based removal semantics. The gate that
previously guarded this rollout
(PLATFORM_ACCESS_ENABLED) was retired in
LSA-10117 once
LSA-9824 shipped — effective
scopes now provably equal requested ∩ client/resource allowed ∩ user-granted on
both the authorization-code exchange and refresh. No client-credential fallback.
A stale PLATFORM_ACCESS_ENABLED left in an overlay is inert and does not fail
startup.
To take the surface away from an operator, remove their system:administer or
user:read grant, or revert the console image. Because there is no
environment-level off switch, the Authifi desired state in step 1 is what
decides who sees these screens.
Console Authifi sync¶
The normal, post-deployment way to apply Authifi desired state and capture the group-id map is the console itself, not the CLI above.
Before you run the sync¶
The sync creates almost everything, but it cannot start until someone can sign in to the console and the console can read Authifi. That needs the following, created once by hand in Authifi or with the CLI (see item 5).
| # | Prerequisite | Details |
|---|---|---|
| 1 | Authifi resource server Floh Console API, identified by FLOH_CONSOLE_AUDIENCE |
Console resource server |
| 2 | Authifi client floh-client linked to Floh Console API, with the console callback, response type code, token exchange, and an identity provider |
Console OIDC client |
| 3 | An Authifi tenant admin or super admin who can complete MFA | Admin account |
| 4 | API and console on the same release, with OIDC_CLIENT_SECRET and AUTHIFI_ADMIN_TARGET set (the GitHub Actions deploy also needs PORTAL_OIDC_CLIENT_SECRET) |
Floh deployment |
The sync creates or repairs the rest; see What the sync creates.
Console resource server¶
A resource server named Floh Console API whose identifier equals the API's
FLOH_CONSOLE_AUDIENCE (default https://console.floh.api).
Console OIDC client¶
A web client with client ID floh-client that has:
- the callback
<console-origin>/bff/callback; - the response type
code, which the console BFF requests at sign-in; - the grant types
authorization_code,refresh_token, andurn:ietf:params:oauth:grant-type:token-exchange. Without token exchange, sign-in works but every Authifi request from the console returns 401 (see Variables); - at least one identity provider. The sync copies these to the portal and MCP clients;
- a link to
Floh Console API.
Set the token endpoint auth method to client_secret_post as well. Sign-in
does not depend on it, but the sync plans a repair on the first run when it
differs. scripts/authifi/floh-client.json
is the full reference definition.
Admin account¶
An account that is an Authifi tenant admin or super admin and can complete MFA through the client's identity provider. Apply requires step-up (item 6).
Floh deployment¶
- The API and console deployed from the same release.
OIDC_CLIENT_SECRETset to thefloh-clientsecret, andAUTHIFI_ADMIN_TARGETset (Variables).- For the GitHub Actions deploy,
PORTAL_OIDC_CLIENT_SECRETas well. The workflow refuses to run without it, so on a new tenant follow the first-time bring-up checklist, which creates every client, and use the sync afterward.
What the sync creates¶
Everything else is created by the sync: the portal and MCP clients, their
resource servers, the floh namespace, and the Floh permissions, roles, and
groups. Once sign-in works, it also repairs drift on floh-client itself
(item 4). Saving the group map needs Floh system:administer; when the
account lacks it, the page offers to grant it (item 8).
Running the sync¶
- Sign in to the console as an Authifi tenant or super admin. Any
authenticated Floh user can open the page — the nav entry ("Authifi sync")
is permission-unconstrained (no permission predicate at all), and the
route
/authifi-syncis guarded only byauthGuard(authentication, not authorization). The page body itself is labeled "Authifi admins only," and every Authifi mutation is authorized by Authifi against the caller's own delegated token, so a non-admin's actions are rejected by Authifi, not by Floh. - Preview. The page first runs an OIDC-client reconciliation preflight,
then, when the three known clients are already usable, reads live Authifi
RBAC state and reports the planned dependency-ordered actions (resource
servers, permissions, access roles, groups, and the
flohnamespace grant) plus any "reports" — drift Authifi holds that the console will not apply automatically (for example, a description changed on an already-managed object). Preview performs no writes, and does not prompt for reauthentication. - Preview issues one Authifi read per entity list plus one per parent row
for each relation, which on a populated tenant is a few dozen sequential
requests. The page reports them as
read N of Mwith the stage being read, so a slow tenant is distinguishable from a hung page. The count of expected reads grows as each list read reveals how many per-row reads follow, soMrising mid-run is normal. Collapsing these into a handful of relation-bearing reads is tracked as LSA-10216. - Apply, after an explicit confirmation dialog naming the change count. Apply requires step-up reauthentication before its first write: the console requests it up front, and declining (closing the popup, a blocked popup, or a timeout) leaves both Authifi and Floh untouched. Nothing is written until the verification succeeds, so a declined prompt is a no-op rather than a partial run. Mutations run sequentially in the previewed order. If any mutation fails, the run stops immediately at that action — later actions never run and no group-id map is persisted. Earlier mutations in that run may already be live in Authifi; re-run Preview to see and finish the remaining drift. This is a stop, not a rollback: nothing in Floh reverts an Authifi object a partial run already created or updated. Because the planner is idempotent, the next Preview/Apply reconciles the rest of the desired state on top of whatever landed — there is nothing to manually undo in Authifi.
- OIDC-client setup before RBAC sync.
floh-clientremains a hard prerequisite. If it is missing or ambiguous, Preview and Apply fail closed with setup guidance rather than guessing.- If
floh-portal-clientorfloh-mcp-clientis missing, Preview shows a client setup only plan. Apply creates the missing clients from explicit desired payloads, copies the console client's projected direct provider relation (GET /authifi/clients/{id}/providerswithid/nameonly), verifies the result, and then stops before any RBAC write or Floh map persistence. - That successful setup-only state intentionally does not surface a plaintext secret. Rotate the newly created client secrets in Authifi, deploy them into Floh config, then run Preview again. The next preview proceeds into normal RBAC sync against the newly deployed clients.
- The rotation reminder persists for the rest of the browser session, including after a run that failed partway. Once a client exists, no later preview can rediscover that its secret was minted and discarded — the planner reports no create — so that banner is the only remaining record. Treat it as an open task, not a status line; reloading the page clears it.
- When all three clients already exist, the preflight may still find additive URI, provider-link, canonical resource-server-link, or namespace drift. That repair pass is still client reconciliation only: Preview enumerates the exact PATCH/PUT actions, Apply performs only those actions and verifies them, then re-previews automatically so the RBAC plan is computed against the state those writes just produced. You still approve that second Apply. Repair-only completion does not require secret rotation; a run that created clients stops instead, because Floh does not keep the secrets it minted and they must be rotated and deployed first.
- Namespace ownership. Floh's desired state places all three clients in
the
flohnamespace. A client Authifi reports in another namespace — or in none — produces onemove client … into namespace "floh"action that writes only thenamespaceIdfield and leaves the client's configuration untouched. The console has no way to express an empty namespace, so a sync cannot detach a client from a namespace it already belongs to. - On a tenant whose
flohnamespace does not exist yet, ownership is deferred rather than skipped silently: Preview lists it under Deferred to a later run, the RBAC phase of the same sync creates the namespace, and the following Preview offers the move. - The console resource server is a prerequisite; the sync creates the other
two. The API publishes the identifier it accepts for each channel as
channelAudiencesonGET /api/auth/config. The values come fromFLOH_CONSOLE_AUDIENCE,FLOH_PORTAL_AUDIENCE, andFLOH_MCP_AUDIENCE, which default tohttps://console.floh.api,https://portal.floh.api, andhttps://mcp.floh.api. The console finds each channel's resource server by its canonical Floh name —Floh Console API,Floh Portal API, andFloh MCP API— and checks it against the published identifier. Floh Console APImust exist with the published console identifier, andfloh-clientmust be linked to it. The console signs in through that resource server, so it cannot create it.- When
Floh Portal APIorFloh MCP APIis missing, Preview lists the link from that channel's client under Deferred to a later run, and the RBAC phase of the same sync creates the resource server with the published identifier and links the client. A run that also creates clients stops for secret rotation first (item 4); the next Preview creates the resource servers. - Preview fails closed, with zero writes, when a canonical name exists more
than once, when a server under a canonical name has an identifier other
than the published one, or when a differently named resource server holds
a published identifier. The message names both values. Fix the identifier
in Authifi, or set the matching
FLOH_*_AUDIENCEon the API and the BFFs, then run Preview again. The console never renames or re-identifies a resource server. - Each Preview re-reads the API's public config. It blocks without writing
when that read fails or the API does not publish
channelAudiences, which happens when the API is older than the console. - Additional bindings on those servers are ignored, including pre-existing legacy transitional links that ordinary setup leaves untouched until LSA-10182. Do not rename these resource servers in Authifi — the name is how the console tells the channels apart.
On a tenant without Floh Console API or floh-client, create the two by
hand as listed in Before you run the sync, or
run the CLI
Authifi client registration flow once
(sync-authifi-rbac -- --apply, then setup-authifi-oidc-clients, as in
the bring-up checklist, steps 7–9a); after
that the console is the normal path. Use the CLI flow for a GitHub Actions
deploy, which also needs the portal client's secret before it runs.
- Step-up (AAL2). Two places enforce it, and both use the existing same-origin step-up popup with no separate configuration:
- Before Apply's first write, requested by the page itself (item 3). Enforcement here is console-side: the Authifi mutations travel through the BFF gateway, whose route allowlist carries no assurance condition. Adding one is tracked as LSA-10218.
PUT /api/auth/platform-access-group-idsenforces it server-side, so the Floh-side map cannot be written from a session without recent MFA regardless of what the browser did. An authenticated caller lacking it receives an RFC 9470401challenge (MFA_OR_AAL_2_REQUIRED) and the interceptor completes the challenge and retries the save.- If an individual Authifi mutation returns
401 MFA_OR_AAL_2_REQUIREDmid-run, the same interceptor handles the challenge and retries that one mutation. - A tenant whose identity provider cannot satisfy
mod-mfcannot complete Apply from the console. Use the CLI Authifi client registration flow for that environment instead — the same requirement already applies to connector and PAM administration in this console, so it is not new capability. - Known limitation: a deployment that sets
STEP_UP_POPUP_ENABLED=falsealso cannot complete Apply. The page requests the popup challenge directly rather than through the interceptor, so it does not fall back to the full-page login that flag selects, and the run stops at reauthentication without writing anything. No shipped configuration sets the flag today; honouring it is tracked as LSA-10230. Use the CLI flow on such a deployment until that lands. - Post-apply proof and persistence. After every mutation in a run
succeeds (or Apply found nothing to change), the browser re-reads Authifi
live, proves an exact canonical
flohnamespace group map, and only then callsPUT /api/auth/platform-access-group-ids— which requires Flohsystem:administerplus step-up, and writes the map plus one audit row in a single transaction. The persisted map is a cache/index for the console's ownGET /api/auth/config, never an input back into what gets applied to Authifi: every Preview re-derives desired state from live Authifi, not from the cache. - Persistence denied (403). A caller who can sync Authifi but lacks
system:administersees the sync complete against Authifi while saving the map to Floh is denied. The page offers a separate, explicitly confirmed action to grant the caller's own subject membership in thefloh-console:admingroup — the same group covered under Tenant-admin recovery for administrator self-removal below. Granting it does not save the map: the current session's Floh permissions are unchanged until the operator signs in again, and signing in navigates the whole page away, so nothing the page had computed survives. The page therefore offers Sign in again and no in-page retry. After signing back in, run Preview and Apply once more — Authifi is already in sync by then, so that run takes the cache-only path in item 9 below: zero Authifi mutations, a fresh live re-verification, and the save. 8b. Two operators at once. Floh does not serialize concurrent operators, and deliberately so. If someone else changes the same tenant between your Preview and Apply — including while you are verifying your identity — the failure is safe rather than silent: a create is refused by Authifi and nothing is overwritten, and a client-configuration patch is refused by Floh itself, which re-reads the client immediately before writing and stops the run if it moved. Both cases name the conflict in the error and ask you to run Preview again, which re-plans from Authifi's current state. - Cache-only update. When Authifi already matches desired state (Preview reports zero changes) but the persisted map is absent or does not match what the live namespace actually holds, Apply is still meaningful: it performs zero Authifi mutations, re-verifies live state, and persists the freshly proven map — the fresh-database / stale-cache recovery path, with no separate command needed.
- Verify convergence. After a successful Apply, click Run preview
again (the button relabels itself once a preview has already run in this
session) — a second Preview against caught-up Authifi reports no
actionable changes. If the caller holds both
system:administeranduser:read, the success state also links directly to Floh roles.
The console makes no server-side Authifi call and introduces no new Authifi
credential: every request goes through the existing tenant-pinned
/authifi BFF proxy using the signed-in caller's own exchanged token. See
Roles & Entitlements — Console Authifi sync
for the security model, the exact BFF allowlist, and the residual blast radius.
Rollback, precisely. There is no single "undo" command; roll back the pieces that actually moved:
- Image/config. Revert the console and API images (and their compose
NODE_CONFIG) the same way as any other deploy rollback — see Deploying. This affects only the sync UI and the persistence route; it does not touch anything already written to Authifi or to the persisted map. - The environment map. Restore a known-good
PLATFORM_ACCESS_GROUP_IDSrepository variable and redeploy to force the API back onto the env-fallback path documented under The Floh roles catalog. This does not delete the persisted setting — see the next bullet — it only changes whatGET /api/auth/configserves while the stored value is absent or itself invalid. - The persisted
platform-access-group-idssetting. There is no delete endpoint. Remove or replace it only through an authorized operational means: either run a fresh, successful console Apply as a caller holdingsystem:administer(which overwrites it with a freshly proven map), or callPUT /api/auth/platform-access-group-idsdirectly as that caller with a corrected map. Do not edit thesystem_settingrow by hand outside a reviewed operational procedure — the route's transaction is what keeps the setting and its audit row consistent. - Authifi desired-state writes are not transactionally rolled back. A stopped or interrupted Apply leaves any already-applied Authifi mutations in place, by design (see step 3 above). Recovery is a subsequent Preview/Apply — including from the CLI, which shares the same planner and desired state — never a manual revert of individual Authifi objects.
Verify the proxy allowlist before checking anything that needs a session. The
gateway matches allowlist routes against the request's full original URL, so the
committed routes carry the /authifi mount prefix; a route missing that prefix
matches nothing and the gateway denies the whole mount with 404 — before the
session check runs, and identically for every method and path, which is easy to
misread as "the route does not exist upstream". An unauthenticated request
separates the two denials with no credentials or session:
# Expect 401 (allowlist matched, session required). 404 means the allowlist
# routes lost their /authifi mount prefix.
curl -s -o /dev/null -w '%{http_code}\n' \
"https://<console-domain>/authifi/groups/1/members"
# Expect 404 — a non-allowlisted sibling stays denied.
curl -s -o /dev/null -w '%{http_code}\n' \
"https://<console-domain>/authifi/roles"
On a host-runtime dev stack the console BFF serves HTTPS on 7074 directly, so
target it with curl -sk https://localhost:7074/authifi/groups/1/members.
As of LSA-10196 the shared /authifi allowlist is 21 exact method+prefix
entries spanning access roles, clients, groups, identity providers, namespaces,
resource servers, and UMRS roles/grants — not the three group-only routes this
section described before console Authifi sync existed. The Floh roles/Members screens above
still use only the GET/PUT/DELETE subset under /authifi/groups/…/users;
the remaining entries exist for the console Authifi sync feature. For the exact
table, the residual DELETE/GET-subtree overmatches, and the mitigating Authifi
contract, see
Roles & Entitlements — Console Authifi sync.
The Floh roles screens still read their group catalog (which named groups
exist) from Floh's own configuration rather than listing from Authifi — see
the role catalog below — even though GET
/authifi/groups (the list endpoint) is now reachable through the proxy for the
sync feature.
Live non-production verification matrix:
- Floh console admin succeeds for assigned-namespace membership reads and writes.
- Ordinary non-admin access fails.
- Outside-namespace operations, privileged-group operations, and wrong-tenant operations fail.
- Group CRUD and role CRUD fail.
- Ordinary console login and Floh
/apiWebSocket traffic remain healthy. - forged browser Authorization is stripped before proxying, and CSRF-less mutation fails.
- Authifi audit attributes the mutation to the human administrator without Floh minting or logging credentials.
Removal semantics:
- Access removal becomes authoritative on the next token refresh/login, not immediately.
- Worst-case exposure is exactly
3600 s, the validated access-token lifetime of the Authifi admin resource server identified byAUTHIFI_ADMIN_RESOURCE. The sync CLI resolves this value from the live resource-server list and fails closed if it exceeds one hour.RESOURCE_SERVER_CONFIG.accessToken.lifetimeinscripts/sync-authifi-rbac.mjsowns only the Floh channel resource servers (console, portal, MCP) and must not be used for the admin proxy removal window.
Tenant-admin recovery for administrator self-removal¶
- Sign in as an Authifi tenant administrator, or use another console admin that still retains namespace access.
- Restore the affected administrator's membership in the predefined
floh-console:admingroup through the supported Authifi recovery path. - Re-authenticate in Floh so the refreshed session picks up the restored grant.
Rollback paths:
- Restore the prior console and console BFF images and
NODE_CONFIG. - have an Authifi tenant administrator remove the namespace grant through Authifi's administrative surface. The Floh sync CLI intentionally exposes no grant-revocation method.
Back-channel logout¶
The deployed console and portal BFFs accept OIDC back-channel logout at
/bff/backChannelLogout (enabled through NODE_CONFIG in
docker/docker-compose.deploy.yml), which lets Authifi end a Floh browser
session directly. Authifi only sends it to a client that has registered the
URI, and the Console Authifi sync registers it:
- After a deploy, run Preview from the deployed console. On a client that
has no back-channel URI yet, the plan includes a config patch for
floh-clientandfloh-portal-client, whose detail mentions back-channel logout. Apply it. - A sync run from
localhost, or from anyhttp:origin, never registers the URI: Authifi cannot reach that origin, and a client holds only one URI, so a local run must not replace the deployed one. - If a client already has a different back-channel URI, the sync leaves it and shows a note naming both values. Correct it in Authifi if that origin should not be receiving logout notifications.
To verify, sign in to the Floh console and to the Authifi admin console in the same browser, sign out of the Authifi admin console, then reload the Floh console: it should show the signed-out state without any request failing first. Administrator termination and the concurrent-session limit trigger the same behavior once Authifi sends logout tokens for them (LSA-10334).
To roll back, remove "auth":{"backchannelLogout":true} from the two BFF
NODE_CONFIG values and redeploy. Optionally clear the registered URI in
Authifi — an empty value disables back-channel logout for the client.