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 import, which is unchanged since phase 1, and the group write-back that shipped in phase 2: the connector creates Security and Microsoft 365 groups, and converges their members and owners, through Microsoft Graph. User write-back (creating users, updating attributes, disabling accounts) is still on the roadmap and is called out explicitly at the end of this document.


At a glance

PropertyValue
Connector nameEntraId
DirectionImport users, groups and memberships; export groups (new in this release)
Object typesUser, Group (membership imports as the group’s member attribute); GroupMember on export only
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 permissionsImport: User.Read.All, Group.Read.All, GroupMember.Read.All. Group export: Group.ReadWrite.All, GroupMember.ReadWrite.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 Entra ID connector DLL in the host’s Connectors/ folder. Both the Sync Engine MSI and the Relay Agent MSI install it; nothing to copy (see relay-agent.md).
    • 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
    Group.ReadWrite.AllGroup export only — create groups, set owners, delete groups
    GroupMember.ReadWrite.AllGroup export only — add and remove group 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.

If you only import, grant the three read permissions and stop there. With those alone the connector cannot create, modify, or delete anything in your tenant — Graph rejects any write because no write permission has been granted. Add the two ReadWrite permissions only when you enable group write-back (see “Group write-back” below), and grant admin consent again after adding them.

User-PasswordProfile.ReadWrite.All is not part of this connector. It is needed only if you use the IdentityMesh Vault to rotate Entra user passwords through the same app registration; see vault-operations.md.

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.

Store it with the secretscli tool that ships with the Sync Engine, run on the engine host:

secretscli set secret://entra/contoso/clientsecret "<paste-the-Value-from-step-3>"

The tool DPAPI-encrypts the secret with the scheme described in secrets-and-dpapi.md and stores it in IM_Secrets. Confirm it with secretscli exists <ref>.

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. IdentityMesh resolves the reference at run time — the connector configuration is the same either way.


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",
  "ObjectTypes": [
    { "name": "User",  "attributes": ["id", "userPrincipalName", "displayName", "mail", "department", "jobTitle", "accountEnabled", "onPremisesSyncEnabled", "onPremisesSecurityIdentifier"] },
    { "name": "Group", "attributes": ["id", "displayName", "mailNickname", "groupTypes", "securityEnabled", "onPremisesSyncEnabled"] }
  ]
}
FieldDefaultNotes
TenantId(req)The directory GUID from Step 1
ObjectTypesabsentOptional per-type attribute lists — see “Choosing attributes per type” below. When present it decides which types import, and ImportUsers / ImportGroups are ignored
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 “Throttling and retries”
ExternalIdAttribute"id"id (object GUID) is recommended. userPrincipalName is allowed but UPN can change

Choosing attributes per type

Without ObjectTypes, the connector selects a built-in property list for users and another for groups, which is what every connector saved before this key existed still does. With it, each entry names a type (User or Group, nothing else is accepted) and the Graph properties to select; an empty list means the built-in one, never an attribute-less import. A type that is absent from the array is not imported at all.

The Admin Portal’s Entra form edits this array with a Discover button that lists Graph v1.0’s properties for each type. Discovery reads Graph’s public schema, so it works before the connector has credentials. The properties the connector itself needs are always loaded whatever the list says and are locked in the editor: id, the anchor attribute and the hybrid provenance trio (onPremisesSyncEnabled, onPremisesSamAccountName, onPremisesSecurityIdentifier) on users; id, groupTypes, securityEnabled, mailEnabled and onPremisesSamAccountName on groups. The effective list becomes the $select on the type’s delta query, which is one request for the whole type, so a property the tenant rejects fails that type’s import rather than one attribute; the connector logs the $select it sent when that happens.

Changing a type’s list invalidates that type’s delta link: the connector keeps a fingerprint of the selected properties in the watermark and, when it changes, runs a full read of that type on the next import rather than resuming a delta that would carry the old projection. The form says so when you change the list. If a relay agent older than this release ran the connector while the list was being changed, run one full import after upgrading the agent.

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. IdentityMesh 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 still deferred”.

Group attributes

Graph propertyMesh attributeNotes
idExternalId
displayNamedisplayName
mailNicknamemailNickname
mailmail
descriptiondescription
groupTypesgroupTypesArray → comma-joined; Unified for M365 groups
securityEnabledsecurityEnabled
mailEnabledmailEnabled
visibilityvisibilityPublic / Private / HiddenMembership
onPremisesSyncEnabledonPremisesSyncEnabledNew in this release. true for a group synced from on-premises AD, so the portal can show it as AD-managed
onPremisesSecurityIdentifieronPremisesSecurityIdentifierNew in this release. The on-premises SID (S-1-5-...) when the group is synced from AD

Group members

Membership is imported on the group, as a member attribute holding the members’ object GUIDs joined with ; — the same shape as an Active Directory group’s member. Users and nested groups are both listed. A group with no members has no member attribute at all.

Membership is deliberately not imported as objects of its own. A standalone membership row can never become a mesh relationship: the join engine only joins an object to a mesh object of the same type, and an inbound flow holds one value per attribute. Edges come from a multi-valued reference attribute on the group, so that is what the connector imports. (GroupMember still exists as an export type — it is how membership adds and removes are written back to Graph.)

To turn the list into mesh membership you need three things:

  1. The member / Group schema row: reference, multi-valued. The installer’s database step creates it on every install and upgrade, so an upgraded system already has it. A database updated outside the installer needs the database step run (see Running the database step yourself) before the services start.
  2. An inbound attribute flow on this connector for the Group type: member → member. Inbound flows are rule-driven; without the rule the attribute never reaches the mesh.
  3. Join rules for User and Group that anchor on id. The engine matches each member GUID against the mesh anchor, so users anchored on userPrincipalName never resolve.

The engine then writes one member relationship from the group to each resolved member, prunes the ones no longer listed on the next import, and skips members with no mesh object yet (they resolve on a later run). Vault scopes and self-service groups count these edges as membership.


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 have no cursor of their own. Graph’s group delta reports a membership change as the group itself reappearing in the feed, so the connector re-reads the full member list of every group the delta returns and the group’s member attribute always carries the whole list.

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


Group write-back

Group write-back is new in this release; treat the first rollout as a pilot on a scoped group set.

The exporter handles the Group object type: it creates groups and converges their members and owners through Microsoft Graph. A User change in an export batch is not thrown as an error; it comes back as a failed confirmation with an explanatory message, so the engine records the limitation against that queue item instead of confirming a change it never made or aborting the batch.

What the exporter does for a group:

A group export that wrote nothing is never confirmed, so a transient Graph failure is retried rather than marked done. The Graph client is keyed on its credential and shared across the batch.

Additional application permissions are required beyond the read-only set in Step 2: Group.ReadWrite.All and GroupMember.ReadWrite.All. Grant admin consent again after adding them.

The two self-service projection templates (Self-Service Group Export (AD) and Self-Service Group Export (Entra ID)) are applied from the connector’s Projection → Apply template page and are the intended way to drive this exporter; see self-service-groups.md.


What’s still deferred

The following are not implemented 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