Authentication
IdentityMesh’s Admin API supports two caller-authentication schemes:
- Windows Negotiate (Kerberos / NTLM over SPNEGO). The default. Works with domain-joined hosts and AD-issued credentials. No configuration required.
- JWT Bearer (OIDC / Entra ID). Opt-in. Enables cloud IdP
scenarios — users signed in via Entra, tokens minted by the
Microsoft identity platform, presented as
Authorization: Bearer <jwt>.
Both can be active simultaneously. A "SmartAuth" policy scheme in
the pipeline inspects every request: if it carries a Bearer
token, the JwtBearer handler validates it; otherwise Negotiate
runs as before. Browsers continue to do SPNEGO even when the
JWT path is enabled.
When to enable JWT
- Your operators sign in through Entra ID or another OIDC provider and you don’t want them to hop onto a Windows session to reach the admin portal.
- Machine-to-machine callers (CI pipelines, automation) can acquire tokens via client credentials or managed identity, but can’t do Kerberos.
- You want a single auth story across Admin API + future surfaces.
If your deployment is entirely on an AD-joined LAN, the default Negotiate-only config is fine — skip this document.
Configuration
Set three keys under Authentication:Jwt in appsettings.json (or
the equivalent environment variables — ASP.NET Core’s default binding
uses Authentication__Jwt__… on Linux/containers):
"Authentication": {
"Jwt": {
"Enabled": true,
"Authority": "https://login.microsoftonline.com/<tenant-id>/v2.0",
"Audience": "<api-app-id-or-uri>"
}
}
Enabled— gate.falsekeeps the old Negotiate-only pipeline. Defaults tofalse.Authority— OIDC issuer URL. For Entra v2, that’shttps://login.microsoftonline.com/<tenantId>/v2.0. The handler downloads.well-known/openid-configurationfrom this URL at startup to pick up signing keys.Audience— the value the token’saudclaim must match. Either the API’s app-registration Application (client) ID, or the App ID URI (api://<guid>) — whichever your client apps request.
The service fails to start if Enabled=true and either
Authority or Audience is missing — prefer a loud startup error
over a silent fallback.
Entra ID setup (recommended path)
One app registration, two purposes: identifies the API (audience), and defines the app roles that map to IdentityMesh permissions.
1. Create the app registration
Entra portal → App registrations → New registration.
- Name: IdentityMesh Admin API
- Supported account types: single tenant (typical) or multi-tenant per your deployment policy
- Redirect URI: leave blank (the admin API doesn’t own a sign-in UI — client apps handle it)
After creation, grab:
- Application (client) ID → use as
Authentication:Jwt:Audience - Directory (tenant) ID → authority URL is
https://login.microsoftonline.com/<tenantId>/v2.0
2. Expose an API (optional but recommended)
App registration → Expose an API:
- Application ID URI:
api://<client-id>(use the default). If you set theAudienceto this URI rather than the raw client ID, tokens requestapi://<client-id>/.defaultas scope.
3. Define app roles
App registration → App roles → Create three:
| Display name | Value | Allowed member types |
|---|---|---|
| IdentityMesh Admin | IdentityMesh.Admin | Users/Groups (and optionally Applications for M2M) |
| IdentityMesh Operator | IdentityMesh.Operator | Users/Groups |
| IdentityMesh Viewer | IdentityMesh.Viewer | Users/Groups |
Entra emits the assigned app-role values into the token’s roles
claim. Our JwtBearer configuration sets
RoleClaimType = "roles", so ClaimsPrincipal.IsInRole("IdentityMesh.Admin")
lights up for callers who have that assignment.
4. Wire the role names into IdentityMesh config
Map the role values to IdentityMesh’s built-in role names in
IdentityMesh:Roles:
"IdentityMesh": {
"Roles": {
"Admin": "IdentityMesh.Admin",
"Operator": "IdentityMesh.Operator",
"Viewer": "IdentityMesh.Viewer"
}
}
RoleResolverService checks these values against
ClaimsPrincipal.IsInRole(...) — no code change is needed, same
resolver that handled Windows groups handles Entra app roles.
If you run a mixed deployment (some users on Windows groups, some on
Entra app roles), rename the Entra app roles to match your AD
group names — e.g. set both to Corp\IdentityMesh-Admins.
IsInRole does a string-equals over every role claim the principal
carries, so one config value can satisfy both populations.
5. Assign users / groups
Entra portal → Enterprise applications → IdentityMesh Admin API →
Users and groups → Add user/group → pick the principal and the
role. Without an assignment the user authenticates successfully but
falls through to the Viewer default (which is what the existing
resolver does for unmapped Windows users).
Verification
End-to-end with a real token
# Acquire a token for yourself via Azure CLI (requires az login first)
$token = az account get-access-token `
--resource <audience-value> `
--query accessToken -o tsv
# Hit a gated endpoint
curl -H "Authorization: Bearer $token" https://<api-host>/api/license
A successful 200 + JSON body means: token validated, role resolved, permission check passed.
Smoke-check the scheme selector
With JWT enabled, the same host should still accept Negotiate — a
request from a domain-joined browser (no Authorization header) hits
the Negotiate branch, a request with a Bearer token hits JwtBearer.
The 401 WWW-Authenticate header on an anonymous call surfaces
whichever scheme the challenge scheme picked; confirms multi-scheme
wiring didn’t regress SPNEGO.
Operational notes
- Clock skew: JWT validation tolerates 5 minutes of skew by
default. Keep host clocks within that — an NTP-drifted host will
reject otherwise-valid tokens with a
SecurityTokenNotYetValiderror. - Metadata caching: The handler refreshes the OIDC metadata document every 24 h by default. Key rotations at the IdP are picked up automatically; a forced restart shortens that to zero.
- Audit trail: the actor-context resolver populates
IM_ObjectAudit.ActorUpnfrom theupn/preferred_usernameclaim andActorSidfrom the Entraoidclaim for JWT callers. That way the audit query “who touched connector X” works across both caller types without operators needing to know which scheme was used. - HTTPS metadata: JwtBearer downloads the OIDC metadata over HTTPS by default. In Development the check is relaxed so you can point at a local mock; in Production it’s enforced.
- Rate limiting: The
sensitivepolicy partitions byUser.Identity.Name, which for JWT callers is thepreferred_usernameclaim. A caller burning through the limit under one scheme can’t evade the cap by switching schemes in the same session, because the partition key is the same string.
Notes
- Group-GUID mapping. Entra’s
groupsclaim carries Object IDs (GUIDs). The matcher does an exact-string compare against the configured role values, so the cleanest mapping is via Entra app roles (which carry stable string names). Group-name translation via Graph lookup is on the roadmap. - Multi-tenant acceptance. Single-authority validation is the default. Multi-tenant acceptance for SaaS-style deployments is available on request — contact support.
Related
secrets-and-dpapi.md— token secrets (client credentials) should live in a secret store, notappsettings.json.