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¶
- An Entra ID tenant with permission to create app registrations and enterprise applications.
- Floh running with Postgres and migrations applied.
- A public HTTPS URL for Floh's API when Entra cloud runs provisioning (plain
localhostis not reachable from Microsoft). Use an HTTPS tunnel for local dev (ngrok, Cloudflare Tunnel, etc.). - 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¶
- Microsoft Entra admin center → Identity → Applications → App registrations → New registration.
- Name: e.g.
Floh (OIDC login). - Supported account types: Accounts in this organizational directory only (single tenant) unless you need multi-tenant.
- Redirect URI: Web →
https://<console-origin>/bff/callback(for local HTTPS dev:https://localhost:7072/bff/callback). Register a second confidential client for the portal athttps://<portal-origin>/bff/callback(https://localhost:7073/bff/callbacklocally). - Register and note the Application (client) ID and Directory (tenant) ID.
2.2 Client secret¶
- Open the app → Certificates & secrets → New client secret.
- 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.
- Identity → Groups → 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). - Assign test users to those groups.
- App registration → Token configuration → Add groups claim → select Security groups (or All groups for dev). Emit the claim on the ID token (not only the access token).
- Confirm a test user's ID token includes
groupsas an array of object IDs after sign-in.
Entra-specific notes:
- Do not add a bare
groupsvalue toOIDC_SCOPE. Entra's v2.0 endpoint treats unknown scopes as resource scopes and returnsinvalid_scope. Built-in OIDC scopes areopenid,email,profile, andoffline_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
groupsfrom 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:
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):
This inserts an active scim_client row and prints the raw scim_… bearer token once.
To print SQL only (no database connection):
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):
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¶
- Identity → Applications → Enterprise applications → New application.
- Create your own application (non-gallery).
- Name: e.g.
Floh SCIM Provisioning. - Select Integrate any other application you don't find in the gallery (Non-gallery).
- Create.
4.2 Enable automatic provisioning¶
- Open the enterprise app → Provisioning → Get started.
- Provisioning Mode: Automatic.
- Admin Credentials:
- Tenant URL:
https://<floh-host>/scim/v2 - Secret Token: paste the raw
scim_…token from section 3.1. - Test Connection — should succeed against Floh's SCIM endpoint.
- Save.
4.3 Provisioning scope¶
- Provisioning → Settings → under Mappings, open Provision Microsoft Entra ID Users.
- Ensure Enabled is set to Yes for:
- Create
- Update
- Deactivate (Floh maps this to
active: false; no hard delete) - Assignments → Users 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 Provisioning → Mappings → Provision Microsoft Entra ID Users → Attribute 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:
- Provisioning → Mappings → Provision Microsoft Entra ID Groups → set Enabled to Yes.
- Assign Entra security groups to the enterprise app under Users and groups.
- Floh maps SCIM Groups to
user_groupand 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¶
- Provisioning → Provision on demand (single user) or wait for the scheduled cycle (~40 minutes by default).
- Provisioning → View 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:
- Enable SCIM user provisioning only.
- Validate OIDC login confirms SCIM-preprovisioned users.
- Configure the Entra app registration to issue Floh permission scopes on the access token (section 2.3).
- Enable SCIM group push if workflows need Floh
user_groupmembership 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 (
scopeorscp). - [ ] SCIM: assign a new user to the SCIM enterprise app; user row appears in Floh (unconfirmed until OIDC login).
- [ ] SCIM: change
displayNamein Entra; Floh user updates on next provisioning cycle. - [ ] SCIM: disable user in Entra; Floh
user.activebecomesfalse;iss/subunchanged. - [ ] SCIM (optional): push a group; Floh
user_groupand 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_clientper 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 /
userNameconventions across environments. - The
entra-idconnector is for Floh → Entra outbound provisioning via Graph API, not for Entra → Floh directory import. Use inbound SCIM for the latter.
Related documentation¶
- Inbound SCIM — endpoints, policies, audit events
- Okta setup for Floh — parallel guide for Okta (same Floh surfaces)
- Integration guide — OIDC and API overview
- Developer guide — Configuring OIDC
- Entra ID connector — Floh → Entra outbound (Graph API)
- Entra ID connector setup — app registration for outbound connector