Enterprise identity providers
FerroEHR does not manage users. There is no user table, no user API, and no plan to add one: identity administration is delegated to your identity provider (IdP), and the CDR consumes standard OIDC bearer tokens. This page records that posture and walks through connecting the two enterprise IdPs we are asked about most — Microsoft Entra ID (Azure AD) and AD FS — plus the answer for plain-LDAP directories.
- The posture: users live in the IdP
- How the CDR consumes an IdP
- Microsoft Entra ID (Azure AD)
- AD FS (on-premises Active Directory)
- “We only have LDAP”
- Multi-tenant deployments
The posture: users live in the IdP
A clinical data repository is the wrong place to store credentials. A user store would make the CDR an authentication product — password lifecycle, lockout policy, MFA, recovery flows, and the largest new attack surface the product could grow — duplicating what a dedicated IdP already does under your existing governance. So the split is deliberate and permanent:
- The IdP owns identities: accounts, passwords, MFA, group/role membership, lifecycle (joiners/movers/leavers), and session policy.
- The CDR owns authorization: it validates the token, mines roles from its claims, and enforces RBAC/ABAC and per-EHR access control on every request.
The HTTP Basic user list in ferroehr.toml is a bootstrap/dev convenience,
not a user store — production deployments authenticate with OIDC bearer
tokens.
Note
The admin console follows the same rule: it signs in against the same IdP and has no user-management screens. To create, disable, or re-role a user, use your IdP’s own administration surface.
How the CDR consumes an IdP
Two configuration groups do all the work:
- Token validation (
[auth.oidc]): the server discovers the JWKS from the issuer’s.well-known/openid-configurationand validates each bearer token’s signature,iss, and (when configured)aud— see the OIDC settings table. - Role mining (
[authz.rbac]):FERROEHR__AUTHZ__RBAC__ROLE_CLAIMS(default["realm_access.roles","scope"]— the Keycloak shape) names the JWT claim paths whose values become the caller’s roles for the role layer.
Everything below is just those two groups pointed at a different issuer.
Microsoft Entra ID (Azure AD)
Entra ID exposes a standards-compliant OIDC issuer per tenant.
-
Register an application (Entra admin center → App registrations). Note the Directory (tenant) ID and the Application (client) ID.
-
Define app roles (App registration → App roles): create roles named after the CDR roles you use (for example
USER,CLINICAL,ADMIN) and assign users/groups to them (Enterprise applications → your app → Users and groups). Entra puts assigned app roles in the token’srolesclaim. -
Expose an audience: either use the client ID as the audience or add an Application ID URI (for example
api://ferroehr). -
Point the CDR at the tenant issuer and mine the
rolesclaim:export FERROEHR__AUTH__OIDC__ISSUER=https://login.microsoftonline.com/<tenant-id>/v2.0 export FERROEHR__AUTH__OIDC__AUDIENCES=api://ferroehr export FERROEHR__AUTHZ__RBAC__ROLE_CLAIMS='["roles"]' -
Verify: request a token for the app (any OAuth2 client credential or auth-code flow) and call the API. A valid token without a required role must get
403; no token,401.
Tip
Group-based deployments can emit the
groupsclaim instead and list it inROLE_CLAIMS— but group claims arrive as object IDs unless you configure group names, so app roles usually read better in policy.
AD FS (on-premises Active Directory)
AD FS 2016+ speaks OIDC. This is also the supported path for on-premises Active Directory in general: front AD with AD FS (or another OIDC-capable broker) rather than pointing anything at LDAP.
-
Create an Application Group (AD FS Management → Application Groups → Web API template, or Server application + Web API for interactive clients). The Web API’s identifier becomes the token audience.
-
Issue role claims: on the Web API’s Issuance Transform Rules, add a rule mapping AD group membership to the
roleclaim (template: Send Group Membership as a Claim), one rule per CDR role. -
Point the CDR at the AD FS issuer and mine the
roleclaim:export FERROEHR__AUTH__OIDC__ISSUER=https://adfs.example.com/adfs export FERROEHR__AUTH__OIDC__AUDIENCES=ferroehr-api export FERROEHR__AUTHZ__RBAC__ROLE_CLAIMS='["role"]'Discovery works out of the box (
https://adfs.example.com/adfs/.well-known/openid-configuration). -
Verify exactly as above:
401without a token,403with a token that lacks the required role.
Note
AD FS emits a single string for one role and an array for several; the role-mining layer accepts both shapes on any configured claim path.
“We only have LDAP”
The CDR does not speak LDAP, by design — LDAP bind would put password handling back inside the CDR. Front the directory with an OIDC-capable broker and connect that instead:
- Active Directory → AD FS (above) or Entra ID (if synced).
- Generic LDAP → Keycloak with LDAP user federation (the Keycloak example then applies verbatim), or any other OIDC provider that can federate LDAP.
The broker owns the LDAP bind; the CDR sees only signed tokens.
Multi-tenant deployments
Tenancy is also credential-derived: the tenant is read from a JWT claim per request (see multi-tenancy), so a multi-tenant IdP setup simply issues the tenant claim alongside the roles. No client — including the admin console — chooses a tenant; the credential does.