Skip to content

Okta setup for Floh

Step-by-step guide for using an Okta Developer Edition org with Floh: OIDC sign-in for the admin UI and inbound SCIM 2.0 provisioning from Okta into Floh.

Integration Direction Floh surface Okta surface
OIDC login Okta authenticates users /api/auth/* OIDC Web Application
Inbound SCIM Okta pushes users and groups /scim/v2 App with SCIM 2.0 provisioning
Outbound (Floh → Okta) Floh pushes users to Okta okta connector Okta Management API + API token

These are two separate app integrations in Okta. OIDC handles who can log in. Authorization comes from Floh permission scopes on the access token (scope, falling back to scp) — not from Okta groups. SCIM creates and updates Floh user rows and user_group membership; it does not grant Floh permissions.

For Floh behavior and API details, see Inbound SCIM. To push users from Floh to Okta (create / update / deactivate), see Okta outbound user lifecycle and the okta connector (not the generic scim connector — Okta does not host /scim/v2 on the org URL).


1. Create an Okta Developer account

  1. Sign up at developer.okta.com (free Developer Edition org).
  2. Note your org URL: https://<org-name>.okta.com (also shown on the Admin Console home page).
  3. Sign in to the Admin Console (https://<org-name>-admin.okta.com or Admin from the developer dashboard).

You will use this org for both OIDC and SCIM. Production customer orgs follow the same steps with stricter change control.


2. OIDC application (Floh login)

Floh requires OIDC at startup: OIDC_ISSUER, OIDC_CLIENT_ID, and at least one channel audience (FLOH_CONSOLE_AUDIENCE, FLOH_PORTAL_AUDIENCE, or FLOH_MCP_AUDIENCE). Those channel audiences are also the only values accepted as the access-token aud. Confidential client secrets and redirect URIs belong to the BFFs (env/console.env, env/portal.env). See also Configuring OIDC in the developer guide.

2.1 Create the OIDC app

  1. ApplicationsApplicationsCreate App Integration.
  2. Sign-in method: OIDC - OpenID Connect.
  3. Application type: Web Application.
  4. Name: e.g. Floh (local dev).
  5. Grant types: enable Authorization Code (and Refresh Token if you use long-lived API access from the same client).
  6. Sign-in redirect URIs (must match Floh exactly):
  7. HTTPS console BFF: https://localhost:7072/bff/callback
  8. HTTPS portal BFF: https://localhost:7073/bff/callback
  9. Sign-out redirect URIs (optional): your Floh web origin, e.g. https://localhost:7072.
  10. Controlled access: for dev, Allow everyone in your organization to access is simplest.
  11. Save and open the app’s General tab. Copy Client ID and Client secret.

2.2 Authorization Server (issuer)

Floh’s default Okta issuer is the org’s default custom authorization server:

https://<org-name>.okta.com/oauth2/default

Confirm via SecurityAPIAuthorization ServersdefaultMetadata URI (issuer).

2.3 Authorization scopes

Floh derives every caller's permissions from the access token's scope claim — see Authorization. The former OIDC_ROLE_* group-to-role sync has been removed, so Okta groups no longer grant Floh permissions on their own.

To authorize users through Okta directly, the authorization server must issue Floh's permission scopes (workflow:read, approval:decide, ...) in the access token:

  1. SecurityAPIAuthorization ServersdefaultScopes → add each Floh permission you intend to grant.
  2. Access Policies → add a rule that grants those scopes to the Floh client for the users or groups that should hold them.
  3. Sign in as a test user and confirm the access token carries the expected Floh permissions on scope (space-delimited string) or scp (string or string array). Floh reads scope first and falls back to scp.

A groups claim is still useful for populating Floh user_group membership (used by approval routing), but it does not affect authorization.

2.4 Floh .env (OIDC)

OIDC_ISSUER=https://<org-name>.okta.com/oauth2/default
OIDC_CLIENT_ID=<client-id-from-okta>
# Channel audiences: the API accepts these, and only these, as the token `aud`,
# and at least one is required at startup. Each value must equal the audience
# Okta actually mints for that channel's tokens -- see the note below.
FLOH_CONSOLE_AUDIENCE=<audience-okta-mints-for-console-tokens>
FLOH_PORTAL_AUDIENCE=<audience-okta-mints-for-portal-tokens>
# FLOH_RESOURCE_ID names the API resource for operator tooling. It is not
# accepted as `aud`. OIDC_AUDIENCE is a deprecated alias of it.
FLOH_RESOURCE_ID=http://floh.api
OIDC_SCOPE=openid profile email groups

Put the confidential-client secret and redirect URI in env/console.env (OIDC_CLIENT_SECRET, OIDC_REDIRECT_URI=https://localhost:7072/bff/callback), not in the API .env.

Okta cannot currently complete this login

The API accepts a token only when its aud is one of the channel audiences, and the relying party has to request that audience for the IdP to stamp it. Both checked-in BFF gateway configs (docker/bff/console.json, docker/bff/portal.json) request only openid profile email offline_access and expose no configurable resource or API scope. With Authifi the resource binding is established server-side by setup-authifi-oidc-clients; Okta has no equivalent, so no combination of Okta-side configuration currently yields a token this API accepts. Aligning the authorization server's Audience with FLOH_*_AUDIENCE is necessary but not sufficient.

Tracked in #1189. Treat the rest of this guide as the provider-side half of a configuration that needs that issue resolved before it can work end to end.

The audience is a two-sided contract. Setting FLOH_*_AUDIENCE only changes which aud values Floh's verifier accepts — it does not change what Okta puts in the token. Okta stamps the aud from the Audience setting on the authorization server serving the request, so that value and the matching FLOH_*_AUDIENCE must be identical. Floh's own Authifi-based deployments use per-channel identifiers (http://console.floh.api, http://portal.floh.api, http://mcp.floh.api).

Restart the Floh server after changing OIDC settings. Log in via the web UI; on first login Floh upserts the user. Permissions come from the access token's scope claim (or scp when scope is absent) on every request — there is no first-login role sync.


3. SCIM bearer token on Floh (inbound)

Inbound SCIM calls Floh with:

Authorization: Bearer <scim-token>

Tokens are stored hashed in scim_client; Floh never stores the raw token after creation. There is not yet a first-class admin API in the UI for issuing SCIM tokens—create a row in the database for dev/test.

3.1 Generate a token and insert a client

From the repo root (Postgres running, migrations applied):

node scripts/create-scim-client.mjs --name "Okta SCIM"

This inserts an active scim_client row and prints the raw scim_… bearer token once. To print SQL only (no database connection), add --sql-only.

Legacy manual SQL (equivalent):

pnpm --filter @floh/server exec node --input-type=module -e "
import { createHash, randomBytes, randomUUID } from 'node:crypto';

const rawToken = 'scim_' + randomBytes(32).toString('hex');
const tokenHash = createHash('sha256').update(rawToken).digest('hex');
const tokenPrefix = rawToken.slice(0, 16);
const id = randomUUID();

console.log('Save this bearer token now (shown once):');
console.log(rawToken);
console.log();
console.log('Run in psql:');
console.log(\`INSERT INTO scim_client (id, name, token_prefix, token_hash, status)
VALUES ('\${id}', 'Okta SCIM', '\${tokenPrefix}', '\${tokenHash}', 'active');\`);
"

Apply the SQL manually if you used --sql-only or the legacy snippet (defaults match local Docker Compose):

docker compose -f docker/docker-compose.yml exec -T postgres \
  psql -U floh -d floh -c "<paste INSERT here>"

Use the printed scim_… value as the Bearer token in Okta (next section). To revoke access, set status = 'inactive' on that scim_client row.

3.2 Verify Floh SCIM is reachable

Base URL (no trailing slash on the host path; Okta appends resource paths):

https://<floh-host>/scim/v2

Local dev example:

https://localhost:7070/scim/v2

Okta’s cloud must reach your Floh host. For local development, expose the API with an HTTPS tunnel (e.g. ngrok, Cloudflare Tunnel) and use that public URL in Okta. Plain localhost only works if Okta runs provisioning from your machine (it does not in a real dev org).

Quick check (replace host and token):

curl -sS "https://<floh-host>/scim/v2/ServiceProviderConfig" \
  -H "Authorization: Bearer <scim-token>" | jq .

Expect 200 with patch.supported: true and bulk.supported: false. 401 means the token does not match any active scim_client row.


4. Okta SCIM application (provision into Floh)

Create a second app integration dedicated to provisioning. It does not replace the OIDC Web app from section 2.

4.1 Create the app

  1. ApplicationsApplicationsCreate App Integration.
  2. Sign-in method: OIDC - OpenID Connect (Okta’s SCIM wizard is tied to an app integration; SSO to Floh via this app is optional if you already use section 2).
  3. Application type: Web Application (or SPA only if you do not need a client secret for anything else—SCIM provisioning uses the bearer token to Floh, not this client secret).
  4. Name: e.g. Floh SCIM Provisioning.
  5. Redirect URIs: not required for SCIM-only testing; you can use a placeholder or the same Floh callback if you enable SSO on this app later.
  6. Save.

Alternatively, some teams use App IntegrationCreate New AppSAML 2.0 or SWA with SCIM enabled; the SCIM tab settings below are what matter, not the SSO protocol.

4.2 Enable SCIM provisioning

  1. Open the new app → Provisioning tab → Configure.
  2. Integration:
  3. SCIM connector or SCIM 2.0 (wording varies by Okta UI version).
  4. SCIM connector base URL: https://<floh-host>/scim/v2
  5. Unique identifier field for users: userName (Floh keys provisioning on email-style userName).
  6. Supported provisioning actions: enable at minimum:
    • Import new users and profile updates (Push New Users / Push Profile Updates)
    • Push Profile Updates
    • Deactivate users
  7. Disable Import password / password sync (Floh does not support SCIM password changes).
  8. Authentication:
  9. Mode: HTTP Header / Bearer token / OAuth Bearer Token (label varies).
  10. Token: paste the raw scim_… token from section 3.1.
  11. Test Connector (or Test API Credentials): should succeed against ServiceProviderConfig or a test GET.

4.3 Provisioning assignments

  1. ProvisioningTo AppEdit → enable:
  2. Create Users
  3. Update User Attributes
  4. Deactivate Users
  5. Assignments → assign users or groups that should be provisioned into Floh.

4.4 Attribute mapping (Users)

Under ProvisioningTo AppUser attribute mappings, align with Floh’s identity policy:

Okta / SCIM attribute Floh usage Notes
userName Primary email / login key Required; use the user’s work email.
emails[type eq "work"].value user.email Should match userName unless you have an approved custom mapping.
displayName Display name Optional but recommended.
externalId scim_user_identity.external_id Okta user id; not copied to user.sub.
active user.active false deactivates; does not hard-delete.

Do not map password fields. Floh ignores SCIM password operations.

4.5 Group push (optional)

After user provisioning works:

  1. ProvisioningTo App → enable Push Groups (or Sync groups).
  2. Push Groups tab → select Okta groups to push.
  3. Floh maps SCIM Groups to user_group and memberships—not to Floh RBAC roles or entitlements (see Groups policy).

Use Okta groups for Floh approval routing (user_group membership) via SCIM group push or the OIDC groups claim (section 2). Neither path grants Floh permissions; those come from access-token scopes (section 2.3).

4.6 Run provisioning

  • ProvisioningRun provisioning (or wait for the next sync cycle).
  • In Okta, open the user → ProvisioningView Logs for per-request success/failure.

In Floh, provisioned users appear as unconfirmed until they complete an OIDC login with the same email (userName). SCIM does not set iss / sub; first Okta login binds the real identity.


5. End-to-end test checklist

  • [ ] OIDC: sign in to Floh web UI with an Okta user; the access token carries the expected Floh permission scopes.
  • [ ] SCIM: assign a new user to the SCIM app in Okta; user row appears in Floh with confirmed = false until OIDC login.
  • [ ] SCIM: change displayName in Okta; Floh user updates on sync.
  • [ ] SCIM: deactivate user in Okta; Floh user.active becomes false; iss / sub unchanged.
  • [ ] SCIM (optional): push a group; Floh user_group and memberships update.
  • [ ] Audit: SCIM writes emit scim.user.* / scim.group.* actions (no bearer token in metadata).

6. Troubleshooting

Symptom Likely cause What to do
Okta Test Connector fails with connection error Floh not reachable from Okta (localhost, firewall) Use an HTTPS tunnel to your dev API; confirm URL is …/scim/v2 without a double /v2/v2.
401 / Invalid SCIM bearer token Wrong token, inactive client, or typo Re-run section 3.1; confirm status = 'active'; use Authorization: Bearer scim_… with no extra quotes.
400 invalidFilter Unsupported filter Floh supports equality filters only (userName eq "…", etc.); see Filters.
User created in Floh but cannot log in SCIM user not confirmed User must sign in via OIDC (section 2) with the same email as userName.
User has no admin permissions SCIM does not grant authorization Grant the Floh permission scopes to the user in the Okta authorization server's access policy; SCIM group membership does not confer them.
409 uniqueness Email or externalId conflict Another SCIM client or user already owns the identity; see integration tests / identity policy.
Okta provisioning log shows 415 or parse errors Content-Type Floh accepts application/scim+json (Okta’s default).
Profile update does not deactivate Wrong PATCH op Floh supports replace on active; unsupported ops return 400 without changing the user.

Logs: check Floh server logs for Unexpected SCIM route failure on 5xx. Do not log bearer tokens.

CSRF: SCIM routes skip CSRF when the request uses Authorization: Bearer … only; Okta does not need Floh CSRF cookies.


7. Production notes

  • Use a stable public Floh URL and TLS for the SCIM base URL.
  • Issue a dedicated scim_client per Okta org (or environment); rotate by creating a new token, updating Okta, then deactivating the old client.
  • Keep OIDC and SCIM apps in sync on email / userName conventions across environments.
  • Document Okta change windows; full reconciliations can update many users and groups at once.