Microsoft Entra ID Connector
The Entra ID connector imports users, groups, and group memberships
from a Microsoft Entra (formerly Azure AD) tenant into IdentityMesh
through the Microsoft Graph API. It runs as a standard IdentityMesh
plug-in DLL — drop it into the engine’s Connectors/ folder, give it
a tenant ID and an app-only client secret, and the engine handles
scheduling, watermarking, and projection into the mesh.
This runbook covers phase 1: import only. Write-back (creating users, modifying group membership, disabling accounts) is on the roadmap as phase 2 and is called out explicitly at the end of this document.
At a glance
| Property | Value |
|---|---|
| Connector name | EntraId |
| Direction | Import (one-way, source → mesh) in phase 1 |
| Object types | User, Group, GroupMember |
| Authentication | App-only — ClientSecretCredential against Graph |
| Watermark style | Per-object-type Graph delta cursors (@odata.deltaLink) |
| Delta lifetime | ~30 days; full re-sync if the connector idles longer |
| Throttling behaviour | Honours Retry-After on HTTP 429 / 503, up to 5 retries |
| Required permissions | User.Read.All, Group.Read.All, GroupMember.Read.All (Graph application permissions) |
If you operate both an on-premises Active Directory and Entra, run this connector alongside the bundled Active Directory connector — they project into the same mesh objects, and the merge behaviour is governed by the join rules configured in the Admin UI.
Prerequisites
You will need:
-
An Entra tenant where you can create app registrations. The tenant ID is the directory GUID visible at the top of the Entra portal.
-
A Global Administrator (or someone with the targeted directory role to grant Microsoft Graph application permissions). Permission grants are tenant-wide, so this step is usually performed by a directory admin and not the connector operator.
-
An IdentityMesh deployment with:
- The
EntraId.dllconnector DLL in the engine’sConnectors/folder (the installer drops it there automatically once installer wiring lands; manual install is described below). - Read access to the
IM_Secretstable (or to your Key Vault / Vault backend if you’ve migrated secrets persecrets-keyvault.md/secrets-vault.md).
- The
-
Outbound HTTPS to
graph.microsoft.comfrom whichever host runs the import — the central sync engine, or a relay agent if you have routed this connector through one. No inbound firewall rules are needed.
Step 1 — Register an Entra application
-
Sign in to https://entra.microsoft.com as a Global Administrator.
-
Navigate to Identity → Applications → App registrations → New registration.
-
Use the following values:
Field Value Name IdentityMesh-EntraConnectorSupported account types Single tenant Redirect URI (leave blank — app-only auth) -
After creation, note the Application (client) ID and the Directory (tenant) ID from the Overview blade. You will need both later.
Step 2 — Grant Microsoft Graph application permissions
On the new app’s API permissions blade:
-
Click Add a permission → Microsoft Graph → Application permissions.
-
Add the following permissions:
Permission Why it’s needed User.Read.AllRead users via the /users/deltaendpointGroup.Read.AllRead groups via the /groups/deltaendpointGroupMember.Read.AllEnumerate group members via /groups/{id}/members -
Click Grant admin consent for <your tenant> to authorise the permissions. The Status column should change to “Granted for <tenant>” with a green check mark.
These are read-only permissions. The phase-1 connector cannot create, modify, or delete anything in your tenant even if it tried to — nothing in the DLL calls a write endpoint, and Graph would reject the call regardless because no write permission has been granted.
Step 3 — Create a client secret
On the Certificates & secrets blade:
- Click New client secret.
- Description:
IdentityMesh import — rotated YYYY-MM-DD. - Expiry: choose 12 months to align with the rotation cadence in
secret-rotation.md. Shorter is fine; longer is discouraged. - Copy the secret VALUE immediately — Entra only displays it once. The Secret ID is not the secret; you want the column labelled Value.
Calendar the rotation now. The Entra portal will silently let the
secret expire and the connector will start emitting 401 Unauthorized
errors at the time the secret crosses its expiry. Treat the rotation
the same way as any other Tier 1 secret in your environment.
Step 4 — Provision the secret in IdentityMesh
The connector reads its client secret through IdentityMesh’s secret
store. Use a stable secret reference name like
secret://entra/<tenant-name>/clientsecret.
The shipped helper script does this for you:
cd <connectors-source>\IdentityMesh.Connectors.Entra
.\sample.ps1 `
-ConnectionString "Server=.;Database=IdentityMesh;Trusted_Connection=True;" `
-TenantId "00000000-0000-0000-0000-000000000000" `
-ClientId "11111111-1111-1111-1111-111111111111" `
-ClientSecret "<paste-the-Value-from-step-3>" `
-SecretRef "secret://entra/contoso/clientsecret"
The script DPAPI-encrypts the secret with the same scheme described
in secrets-and-dpapi.md, upserts it into
IM_Secrets, and prints a connector configuration block ready for
the next step.
If you have migrated to Azure Key Vault
(secrets-keyvault.md) or HashiCorp Vault
(secrets-vault.md) instead of DPAPI, store the
secret in your vault under the same reference name. The
AuthMaterializer resolves the reference at run time — no connector
code changes are required.
Step 5 — Configure the connector
Create the connector through the Admin UI or by POSTing to
/api/connectors. The configuration is two JSON documents.
ConfigJson
{
"TenantId": "00000000-0000-0000-0000-000000000000",
"GroupSelectionFilter": null,
"PageSize": 999,
"ImportUsers": true,
"ImportGroups": true,
"ImportMemberships": true,
"ExternalIdAttribute": "id"
}
| Field | Default | Notes |
|---|---|---|
TenantId | (req) | The directory GUID from Step 1 |
GroupSelectionFilter | null | Optional Graph OData filter — see “Scoping the group set” below |
PageSize | 999 | Graph caps users at 999. Reduce if you see request-timeout pressure |
ImportUsers | true | Set to false to skip the user delta query entirely |
ImportGroups | true | Set to false to skip the group delta query entirely |
ImportMemberships | true | Set to false for very large tenants — see “Scaling notes” |
ExternalIdAttribute | "id" | id (object GUID) is recommended. userPrincipalName is allowed but UPN can change |
AuthJson
{
"Mode": "ClientSecret",
"ClientId": "11111111-1111-1111-1111-111111111111",
"ClientSecret": "secret://entra/contoso/clientsecret"
}
The ClientSecret field is a secret reference, not the plaintext
secret. The Auth Materializer resolves it at run time and only the
resulting bearer token is held in memory for the lifetime of one
import.
What gets synced
User attributes
| Graph property | Mesh attribute | Notes |
|---|---|---|
id | ExternalId | Default external ID; immutable GUID |
displayName | displayName | |
userPrincipalName | userPrincipalName | Sign-in name; can change |
mail | mail | May be null for accounts without a mailbox |
givenName | givenName | |
surname | surname | |
jobTitle | jobTitle | |
department | department | |
companyName | companyName | |
officeLocation | officeLocation | |
mobilePhone | mobilePhone | |
businessPhones | businessPhones | Array → comma-joined string in the mesh |
accountEnabled | accountEnabled | Boolean |
userType | userType | Member or Guest |
usageLocation | usageLocation | Two-letter country code |
Manager hierarchy ($expand=manager), profile photos, mailbox-only
properties, and custom security attributes are not imported in
phase 1 — see “What’s deferred to phase 2”.
Group attributes
| Graph property | Mesh attribute | Notes |
|---|---|---|
id | ExternalId | |
displayName | displayName | |
mailNickname | mailNickname | |
mail | mail | |
description | description | |
groupTypes | groupTypes | Array → comma-joined; Unified for M365 groups |
securityEnabled | securityEnabled | |
mailEnabled | mailEnabled | |
visibility | visibility | Public / Private / HiddenMembership |
Group members
Each (group, member) edge is emitted as a separate GroupMember
object with a composite external ID <groupId>:<memberId>. The
attributes carry:
groupId— the Entra group object GUIDmemberId— the user, group, or service principal object GUIDmemberType—UserorGroup. Entra supports nested groups, so expect group-in-group rows when your tenant uses them.
Scoping the group set
A typical Entra tenant has many more groups than IdentityMesh needs to
manage — distribution lists, dynamic security groups, license-bearing
groups, and so on. Use GroupSelectionFilter to narrow the set.
Examples:
{ "GroupSelectionFilter": "startswith(displayName,'IM-')" }
Only groups whose display name begins with IM-. A common convention
is to prefix groups that should flow through IdentityMesh.
{ "GroupSelectionFilter": "securityEnabled eq true and mailEnabled eq false" }
Only pure security groups (skip distribution lists and Microsoft 365 groups).
{ "GroupSelectionFilter": "groupTypes/any(c:c eq 'Unified')" }
Only Microsoft 365 (Unified) groups.
The filter is a Graph OData filter — see the Graph $filter docs for the full grammar.
Watermark behaviour
Graph delta queries return an @odata.deltaLink URL when pagination
exhausts. IdentityMesh stores that URL verbatim as the connector
watermark and passes it back on the next run; Graph then returns only
the rows that have changed since.
The connector encodes one delta link per object type into a tiny JSON wrapper so a single watermark string covers everything:
{ "u": "<users delta link>", "g": "<groups delta link>" }
Memberships do not have their own delta cursor in phase 1 — they are recomputed from the current group set on every run.
Delta link expiry
Graph delta links are valid for approximately 30 days of idle time (Microsoft does not publish an exact value, and the practical ceiling depends on tenant load). If a delta link expires, the next request returns HTTP 410 (Gone) and the connector surfaces the error.
Operator action when a delta link expires:
- Note the expiry in the run history.
- Trigger a one-off full sync for the connector through the
Admin UI (“Run now” with mode = Full). This bypasses the stored
watermark and walks the entire
/users/deltaand/groups/deltafeeds end to end, then stores fresh delta links. - Resume normal scheduled delta sync.
If your operational schedule guarantees runs at least every 7 days, delta-link expiry should never be observed.
Throttling and retries
Graph enforces per-application rate limits. Bulk delta queries on large tenants can occasionally trip them.
The connector handles Retry-After automatically:
- On HTTP 429 (Too Many Requests) or 503 (Service Unavailable), the
connector sleeps for the duration in the
Retry-Afterresponse header (or 5 s × attempt if the header is absent). - It retries up to 5 times before surfacing the error to the engine.
If you see Retry-After log entries during normal operation, lower
PageSize from 999 to 250 — fewer rows per request reduces the
chance of long-running queries that count against the per-resource
budget.
For very large tenants, consider:
- Splitting users and groups across two connectors (one with
ImportUsers=true, ImportGroups=falseand one inverse). - Disabling membership reads on the user/group connector and adding a third connector dedicated to memberships.
Security model
- App-only auth — no user impersonation, no consent prompts. The connector signs in as itself, so audit logs in Entra attribute the reads to the registered app, not to a human.
- Outbound only — the connector calls
https://graph.microsoft.com. No inbound paths. - Read-only in phase 1 — no Graph write permission is ever requested.
- Secret in transit — the client secret never leaves the IdentityMesh secret store unencrypted; a bearer token resolved from it is held only in memory for the duration of one import.
- Logging — bearer tokens, secret values, and refresh tokens are never written to logs. Delta links are truncated in informational log entries (full values still go to the watermark store, which is protected the same way connector configs are).
What’s deferred to phase 2
The following are not in phase 1 and will be added in a follow-up phase:
- Export / write-back — creating users, updating attributes,
managing group membership, and disabling accounts. Phase 1 calls to
ExportAsyncthrowNotSupportedExceptionwith an explanatory message, so the engine surfaces the limitation cleanly rather than silently dropping changes. - Manager hierarchy —
$expand=manageradds an extra cost per user. Phase 2 will add it as an opt-in flag. - Profile photos — large binary blobs are out of scope for the generic attribute store.
- Custom security attributes — Entra’s tenant-scoped custom attribute schema differs from the standard property bag and needs its own projection rules.
- Mailbox properties —
aboutMe,responsibilities, etc. live on the user but are populated by Exchange Online and are inconsistent across mailbox-less accounts. - Batched membership reads — phase 1 issues one
/groups/{id}/memberscall per group (N+1). Phase 2 will batch up to 20 of these into a single/$batchrequest, materially reducing latency on large tenants.
Troubleshooting
401 Unauthorized on every page
- The client secret has expired or been rotated outside IdentityMesh. Generate a new secret and update the secret store entry.
- The app’s admin consent has been revoked. Re-grant on the API permissions blade.
403 Forbidden on /users/delta or /groups/delta
- The required Graph application permission is missing or not granted. Re-check the API permissions blade and the Status column.
410 Gone
- The stored delta link has expired. Trigger a Full-mode run; see “Delta link expiry” above.
Slow imports
- Check the connector log for
Retry-Afterwarnings — Graph is throttling. LowerPageSizeor split the connector. - Disable
ImportMembershipsif you don’t yet need group-membership data in the mesh. Group-member reads dominate runtime on tenants with thousands of groups.
High row count on every delta run
- The user delta endpoint emits a row whenever any tracked
property changes — including transient changes like
lastSignInDateTime. The connector requests a tight$selectset to limit this, but property changes outside that set still count. This is expected Graph behaviour and not a bug.
Cross-references
secret-rotation.md— rotating the Entra client secret on a 12-month cadence.secrets-and-dpapi.md— the default DPAPI-backed secret store used by the helper script.secrets-keyvault.md— pointing the connector at Azure Key Vault instead of DPAPI.secrets-vault.md— pointing the connector at HashiCorp Vault instead of DPAPI.relay-agent.md— running this connector on a relay agent in a remote network instead of the central engine.