Runbooks / Connector Entra

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

PropertyValue
Connector nameEntraId
DirectionImport (one-way, source → mesh) in phase 1
Object typesUser, Group, GroupMember
AuthenticationApp-only — ClientSecretCredential against Graph
Watermark stylePer-object-type Graph delta cursors (@odata.deltaLink)
Delta lifetime~30 days; full re-sync if the connector idles longer
Throttling behaviourHonours Retry-After on HTTP 429 / 503, up to 5 retries
Required permissionsUser.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:

  1. An Entra tenant where you can create app registrations. The tenant ID is the directory GUID visible at the top of the Entra portal.

  2. 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.

  3. An IdentityMesh deployment with:

    • The EntraId.dll connector DLL in the engine’s Connectors/ folder (the installer drops it there automatically once installer wiring lands; manual install is described below).
    • Read access to the IM_Secrets table (or to your Key Vault / Vault backend if you’ve migrated secrets per secrets-keyvault.md / secrets-vault.md).
  4. Outbound HTTPS to graph.microsoft.com from 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

  1. Sign in to https://entra.microsoft.com as a Global Administrator.

  2. Navigate to Identity → Applications → App registrations → New registration.

  3. Use the following values:

    FieldValue
    NameIdentityMesh-EntraConnector
    Supported account typesSingle tenant
    Redirect URI(leave blank — app-only auth)
  4. 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:

  1. Click Add a permission → Microsoft Graph → Application permissions.

  2. Add the following permissions:

    PermissionWhy it’s needed
    User.Read.AllRead users via the /users/delta endpoint
    Group.Read.AllRead groups via the /groups/delta endpoint
    GroupMember.Read.AllEnumerate group members via /groups/{id}/members
  3. 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:

  1. Click New client secret.
  2. Description: IdentityMesh import — rotated YYYY-MM-DD.
  3. Expiry: choose 12 months to align with the rotation cadence in secret-rotation.md. Shorter is fine; longer is discouraged.
  4. 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"
}
FieldDefaultNotes
TenantId(req)The directory GUID from Step 1
GroupSelectionFilternullOptional Graph OData filter — see “Scoping the group set” below
PageSize999Graph caps users at 999. Reduce if you see request-timeout pressure
ImportUserstrueSet to false to skip the user delta query entirely
ImportGroupstrueSet to false to skip the group delta query entirely
ImportMembershipstrueSet 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 propertyMesh attributeNotes
idExternalIdDefault external ID; immutable GUID
displayNamedisplayName
userPrincipalNameuserPrincipalNameSign-in name; can change
mailmailMay be null for accounts without a mailbox
givenNamegivenName
surnamesurname
jobTitlejobTitle
departmentdepartment
companyNamecompanyName
officeLocationofficeLocation
mobilePhonemobilePhone
businessPhonesbusinessPhonesArray → comma-joined string in the mesh
accountEnabledaccountEnabledBoolean
userTypeuserTypeMember or Guest
usageLocationusageLocationTwo-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 propertyMesh attributeNotes
idExternalId
displayNamedisplayName
mailNicknamemailNickname
mailmail
descriptiondescription
groupTypesgroupTypesArray → comma-joined; Unified for M365 groups
securityEnabledsecurityEnabled
mailEnabledmailEnabled
visibilityvisibilityPublic / 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:


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.

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:

  1. Note the expiry in the run history.
  2. 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/delta and /groups/delta feeds end to end, then stores fresh delta links.
  3. 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:

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:


Security model


What’s deferred to phase 2

The following are not in phase 1 and will be added in a follow-up phase:


Troubleshooting

401 Unauthorized on every page

403 Forbidden on /users/delta or /groups/delta

410 Gone

Slow imports

High row count on every delta run


Cross-references