Skip to content

Microsoft Entra ID setup for Floh

Step-by-step guide for using Microsoft Entra ID (Azure AD) with Floh: OIDC sign-in for the web UI and inbound SCIM 2.0 provisioning from Entra into Floh.

Integration Direction Floh surface Entra surface
OIDC login Entra authenticates users /api/auth/* App registration (Web)
Inbound SCIM Entra pushes users and groups /scim/v2 Enterprise application provisioning
Outbound (Floh → Entra) Floh pushes users to Entra entra-id connector Microsoft Graph API

These are two separate Entra integrations. OIDC handles who can log in. Authorization comes from Floh permission scopes on the access token (scope, falling back to scp) — not from Entra groups in the ID token. 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 Entra (create / update / disable), see the entra-id connector — that is a separate integration from inbound SCIM.


1. Prerequisites

  1. An Entra ID tenant with permission to create app registrations and enterprise applications.
  2. Floh running with Postgres and migrations applied.
  3. A public HTTPS URL for Floh's API when Entra cloud runs provisioning (plain localhost is not reachable from Microsoft). Use an HTTPS tunnel for local dev (ngrok, Cloudflare Tunnel, etc.).
  4. OIDC and SCIM can share the same Entra tenant but use different apps (sections 2 and 4).

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.

2.1 Create the app registration

  1. Microsoft Entra admin centerIdentityApplicationsApp registrationsNew registration.
  2. Name: e.g. Floh (OIDC login).
  3. Supported account types: Accounts in this organizational directory only (single tenant) unless you need multi-tenant.
  4. Redirect URI: Webhttps://<console-origin>/bff/callback (for local HTTPS dev: https://localhost:7072/bff/callback). Register a second confidential client for the portal at https://<portal-origin>/bff/callback (https://localhost:7073/bff/callback locally).
  5. Register and note the Application (client) ID and Directory (tenant) ID.

2.2 Client secret

  1. Open the app → Certificates & secretsNew client secret.
  2. Copy the secret value immediately (shown once).

2.3 Groups claim (optional)

Floh derives permissions from the access token's scope claim, falling back to scp when scope is absent — see Authorization. The former OIDC_ROLE_* group-to-role sync has been removed, so an Entra groups claim no longer grants Floh permissions. It is still useful for populating Floh user_group membership used by approval routing.

  1. IdentityGroups → create groups, e.g. floh-admins, floh-requestors. Note each group's Object ID (Entra emits object IDs in the claim by default — not display names).
  2. Assign test users to those groups.
  3. App registration → Token configurationAdd groups claim → select Security groups (or All groups for dev). Emit the claim on the ID token (not only the access token).
  4. Confirm a test user's ID token includes groups as an array of object IDs after sign-in.

Entra-specific notes:

  • Do not add a bare groups value to OIDC_SCOPE. Entra's v2.0 endpoint treats unknown scopes as resource scopes and returns invalid_scope. Built-in OIDC scopes are openid, email, profile, and offline_access; group membership comes from Token configuration, not from a scope string.
  • Microsoft's UserInfo endpoint cannot be customized with additional claims, so Floh reads groups from the ID token (and from userinfo when an IdP does emit it there).
  • With the default Entra groups claim the values are group object IDs, not display names. To emit display names instead, configure a supported group-name claim format in Token configuration.

Important: neither SCIM-pushed groups (section 4) nor the OIDC groups claim grants Floh permissions. Authorization comes exclusively from the access token's scope claim (Entra v2 typically emits the same values on scp), which the Entra app registration must be configured to issue.

2.4 Floh .env (OIDC)

OIDC_ISSUER=https://login.microsoftonline.com/<tenant-id>/v2.0
OIDC_CLIENT_ID=<application-client-id>
# 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
# Entra actually mints for that channel's tokens -- see the note below.
FLOH_CONSOLE_AUDIENCE=<audience-entra-mints-for-console-tokens>
FLOH_PORTAL_AUDIENCE=<audience-entra-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

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.

Entra 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; Entra has no equivalent, so no combination of Entra-side configuration currently yields a token this API accepts. Setting the Application ID URI and FLOH_*_AUDIENCE to matching values 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 Entra puts in the token. Entra stamps the aud from the API the token was requested for, which is the Application ID URI under Expose an API (typically api://<client-id>), 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. On first login, Floh upserts the user. Permissions are read from the access token's scope claim (or scp when scope is absent) on every request, so the app registration must be configured to issue Floh's permission scopes.


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.

3.1 Generate a token and insert a client

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

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

This inserts an active scim_client row and prints the raw scim_… bearer token once.

To print SQL only (no database connection):

node scripts/create-scim-client.mjs --name "Entra SCIM" --sql-only

To revoke access later, set status = 'inactive' on that scim_client row.

3.2 Verify Floh SCIM is reachable

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

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

Hosted deploy (DEPLOY_DOMAIN, e.g. floh.authilize.com): use https://floh.authilize.com/scim/v2. Caddy proxies /scim/v2/* to the API server (see Deployment guide); do not use /api/scim/v2.

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. Entra enterprise application (SCIM provisioning into Floh)

Create a second Entra integration dedicated to directory push. It does not replace the OIDC app registration from section 2.

4.1 Create the enterprise application

  1. IdentityApplicationsEnterprise applicationsNew application.
  2. Create your own application (non-gallery).
  3. Name: e.g. Floh SCIM Provisioning.
  4. Select Integrate any other application you don't find in the gallery (Non-gallery).
  5. Create.

4.2 Enable automatic provisioning

  1. Open the enterprise app → ProvisioningGet started.
  2. Provisioning Mode: Automatic.
  3. Admin Credentials:
  4. Tenant URL: https://<floh-host>/scim/v2
  5. Secret Token: paste the raw scim_… token from section 3.1.
  6. Test Connection — should succeed against Floh's SCIM endpoint.
  7. Save.

4.3 Provisioning scope

  1. ProvisioningSettings → under Mappings, open Provision Microsoft Entra ID Users.
  2. Ensure Enabled is set to Yes for:
  3. Create
  4. Update
  5. Deactivate (Floh maps this to active: false; no hard delete)
  6. AssignmentsUsers and groups → assign users or groups that should sync into Floh.

Start with users only. Enable group provisioning after user sync is validated (section 4.5).

4.4 Attribute mappings (Users)

Under ProvisioningMappingsProvision Microsoft Entra ID UsersAttribute mappings, align with Floh's identity policy:

Entra / SCIM attribute Floh usage Notes
userName Primary email / login key Required; use the user's work email (UPN).
emails[type eq "work"].value user.email Should match userName.
displayName Display name Optional but recommended.
externalId scim_user_identity.external_id Map from Entra objectId; not copied to user.sub.
active user.active false deactivates; does not hard-delete.

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

Default Entra mappings often work with minimal changes — verify userName resolves to the user's mail/UPN and externalId maps to objectId.

4.5 Group push (optional — pilot users first)

After user provisioning works:

  1. ProvisioningMappingsProvision Microsoft Entra ID Groups → set Enabled to Yes.
  2. Assign Entra security groups to the enterprise app under Users and groups.
  3. Floh maps SCIM Groups to user_group and memberships — not to Floh RBAC roles or entitlements (see Groups policy).

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

Known limitation: Floh supports group member add and replace via SCIM PATCH; member remove is not fully supported. If Entra logs show failures when users leave a pushed group, check Floh server logs and plan a follow-up handler extension.

4.6 Run provisioning

  1. ProvisioningProvision on demand (single user) or wait for the scheduled cycle (~40 minutes by default).
  2. ProvisioningView provisioning logs for per-request success/failure.

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


5. Authorization: SCIM groups vs access-token scopes

Mechanism What it controls in Floh When to use
SCIM group push user_group membership (workflows, approvals referencing groups) Directory sync from Entra
OIDC groups claim Same user_group membership when Floh maps ID-token groups Approval routing without SCIM group push
Access-token scopes Floh permissions (system:administer, approval:decide, …) Who can administer Floh, approve, etc.

Recommended pilot path:

  1. Enable SCIM user provisioning only.
  2. Validate OIDC login confirms SCIM-preprovisioned users.
  3. Configure the Entra app registration to issue Floh permission scopes on the access token (section 2.3).
  4. Enable SCIM group push if workflows need Floh user_group membership synced from Entra.

Many deployments use the same Entra security groups for directory sync and for IdP access-policy assignment, but they are configured on different Entra apps (OIDC app registration vs SCIM enterprise application). Group membership never substitutes for access-token scopes.


6. End-to-end test checklist

  • [ ] OIDC: sign in to Floh web UI with an Entra user; the access token carries the expected Floh permission scopes (scope or scp).
  • [ ] SCIM: assign a new user to the SCIM enterprise app; user row appears in Floh (unconfirmed until OIDC login).
  • [ ] SCIM: change displayName in Entra; Floh user updates on next provisioning cycle.
  • [ ] SCIM: disable user in Entra; 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).

7. Troubleshooting

Symptom Likely cause What to do
Test Connection fails Floh not reachable from Microsoft (localhost, firewall) Use an HTTPS tunnel to your dev API; confirm URL is …/scim/v2 without a double /v2/v2.
401 / invalid bearer token Wrong token, inactive client, or typo Re-run node scripts/create-scim-client.mjs; confirm status = 'active'.
400 invalidFilter Unsupported filter Floh supports equality filters only; 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 and the OIDC groups claim do not grant authorization Grant Floh permission scopes on the Entra app registration / access policy so they appear on the access token scope or scp claim.
invalid_scope on login OIDC_SCOPE includes bare groups Use openid profile email only; configure groups via Token configuration (section 2.3).
409 uniqueness Email or externalId conflict Another SCIM client or user already owns the identity; see identity policy.
Group member remove fails PATCH remove not supported Known gap; member add/replace work. Track remediation if Entra logs show failures on group leave.
Provisioning stuck Assignment missing Confirm user/group is assigned to the SCIM enterprise app, not only the OIDC app.

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; Entra does not need Floh CSRF cookies.


8. Production notes

  • Use a stable public Floh URL and TLS for the SCIM base URL.
  • Issue a dedicated scim_client per Entra tenant (or environment); rotate by creating a new token, updating Entra, then deactivating the old client.
  • Keep OIDC and SCIM apps aligned on email / userName conventions across environments.
  • The entra-id connector is for Floh → Entra outbound provisioning via Graph API, not for Entra → Floh directory import. Use inbound SCIM for the latter.