> For the complete documentation index, see [llms.txt](https://docs.openiam.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.openiam.com/connector-configuration/microsoft/14-psgraph.md).

# Microsoft Graph PowerShell connector

This page describes how to install and configure Microsoft Graph PowerShell connector in OpenIAM.

Microsoft PS Graph OpenIAM connector is an agent that allows managing Entra ID identities from OpenIAM by running Microsoft Graph PowerShell module. More on this module can be found in the respective [Microsoft Graph PowerShell documentation](https://learn.microsoft.com/en-us/powershell/microsoftgraph/?view=graph-powershell-1.0).

{% hint style="info" %}
Entra ID is previously known as Azure AD.
{% endhint %}

The connector can:

* create and modify users;
* manage licenses — assign them, remove them, and disable individual service plans within a license;
* reset passwords, suspend and resume users, revoke sessions, and remove MFA methods;
* invite guest users;
* create and modify groups — both Security and Microsoft 365 — including members, owners and group nesting;
* delete users and groups;
* synchronize users and groups from Entra ID into OpenIAM.

The connector's business logic is open source and is stored in the `Connector.ps1` PowerShell file, separately from the technical implementation. It can be extended with custom handler scripts, or adjusted to work with other entities using any other cmdlet from the [Microsoft Graph PowerShell documentation](https://learn.microsoft.com/en-us/powershell/microsoftgraph/?view=graph-powershell-1.0).

| Operation               | Users                                                                                                   | Groups                         |
| ----------------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------ |
| SAVE (create or update) | ✅                                                                                                       | ✅                              |
| DELETE                  | ✅                                                                                                       | ✅                              |
| SUSPEND / RESUME        | ✅ — Disables or enables the account; SUSPEND also revokes sessions                                      | ❌ — a group cannot be disabled |
| RESET\_PASSWORD         | ✅                                                                                                       | ❌ — a group has no password    |
| LOGIN (password check)  | ❌ — use SSO                                                                                             | ❌                              |
| SEARCH                  | ✅                                                                                                       | ✅                              |
| Test connection         | ✅ — Authenticates with the tenant id, application id and certificate thumbprint from the managed system |                                |

A DELETE of an object that no longer exists reports success. A transient Graph failure, such as throttling or a service error, always fails the request and is never interpreted as the object being absent.

## Use cases

* **Provisioning joiners into Entra ID** — creating the account, setting its attributes, assigning a licence and putting the user in the groups their role entitles them to, in one save.
* **Keeping licences under control** — assigning and revoking licences from OpenIAM, and switching off individual service plans inside a licence rather than granting the whole package.
* **Group lifecycle** — creating security and Microsoft 365 groups, maintaining their members, owners and nesting, and deleting them when they are no longer needed.
* **Leavers** — suspending the account, revoking its sessions and removing its MFA methods, then deleting it when the retention period ends.
* **Inviting guests** and managing them through the same managed system as employees.
* **Reconciling Entra ID into OpenIAM** — reading users, groups with their memberships, and application role assignments back in.

## Versions and compatibility

The connector drives the **Microsoft Graph PowerShell module**, so what it supports is what that module supports. It does not pin a module version: the cmdlets it calls are resolved from whatever is installed on the connector host, which means a module update can change behaviour without any change to the connector. Microsoft's [Microsoft Graph PowerShell documentation](https://learn.microsoft.com/en-us/powershell/microsoftgraph/?view=graph-powershell-1.0) is the reference for the cmdlets themselves.

## Prerequisites

The operating system, `.NET Framework` and PowerShell requirements are the same for every .NET/PowerShell connector and are listed once in [Installing PowerShell connectors](/connector-configuration/microsoft/1-powershellconnectorinstallation.md#basic-requirements). Beyond those, this connector needs:

* The **Microsoft Graph PowerShell module** on the connector host.
* An **internet connection** from the connector host to the Entra tenant.
* An **application registered in the tenant**, with a certificate and the permissions listed under [Application authentication](#application-authentication-entra-id).

### Installing the Microsoft Graph PowerShell module

Before installing a module it is possible to check if it is already installed. You can open PowerShell console and run:

```powershell
Get-Module Microsoft.Graph -ListAvailable
```

An empty result means the module is not installed. If it reports a module instead, skip this step.

Install it for all users, because the connector runs as a service account rather than as you:

```powershell
Install-Module Microsoft.Graph -Scope AllUsers
```

Run the first command again to confirm. The output names the version, which will differ from this one:

```
    Directory: C:\Program Files\WindowsPowerShell\Modules

ModuleType Version Name            ExportedCommands
---------- ------- ----            ----------------
Manifest   2.14.0  Microsoft.Graph
```

If the install fails with a NuGet provider error like this one, the session is not using TLS 1.2:

{% code overflow="wrap" %}

```
WARNING: Unable to download from URI 'https://go.microsoft.com/fwlink/?LinkID=627338&clcid=0x409' to ''.
WARNING: Unable to download the list of available providers. Check your internet connection.
Install-Module : NuGet provider is required to interact with NuGet-based repositories. Please ensure
that '2.8.5.201' or newer version of NuGet provider is installed.
```

{% endcode %}

Set it for the session and repeat the installation command:

{% code overflow="wrap" %}

```powershell
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
```

{% endcode %}

## Installing the connector

Every .NET/PowerShell connector installs the same way. See [Installing PowerShell connectors](/connector-configuration/microsoft/1-powershellconnectorinstallation.md) for the installer, running several connectors on one host, and removing or updating an installation.

## Starting, stopping, and restarting

The installer registers a pair of Windows services named after the connector name you supply. See [Installing PowerShell connectors](/connector-configuration/microsoft/1-powershellconnectorinstallation.md#starting-stopping-and-restarting).

## Command-line parameters and properties

The connector takes no command-line parameters. Queue names, concurrency, logging and the RabbitMQ settings live in `Connector.config`, documented for the whole family in [Using PowerShell connectors](/connector-configuration/microsoft/2-powershellconnectorsusage.md#working-with-the-connector-configuration).

One setting there matters for this connector in particular: **`SearchResponseBatchSize`** decides how many objects a search sends back at a time. Left at zero, the connector accumulates the entire result and sends it in one response, which is the wrong shape for a directory of any size.

## Verifying the connector is running

{% stepper %}
{% step %}
Confirm the connector's Windows services are running on the host.
{% endstep %}

{% step %}
Check that the connector's request queue in RabbitMQ has a consumer.
{% endstep %}

{% step %}
Run **Test Connection** on the managed system in the Webconsole. It checks that the tenant id, the application id and the certificate thumbprint are all present and that they authenticate together, so a pass means the Entra side is configured.
{% endstep %}

{% step %}
Run a search to prove the path end to end.
{% endstep %}
{% endstepper %}

## Troubleshooting

Troubleshooting for the whole family, including where the logs are and how to replay a request, is in [Using PowerShell connectors](/connector-configuration/microsoft/2-powershellconnectorsusage.md). What follows is specific to this connector.

Before touching Entra ID, the connector validates the request. The message always names the cause. The common ones are listed below.

| Symptom                                                                               | Likely cause                                                                                                                                                      | Resolution                                                                                    |
| ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| `Identity name ... identity value` missing                                            | The request carries no principal attribute or value                                                                                                               | Fix the managed system principal mapping                                                      |
| `Graph cannot look a user up by` or `cannot look a group up by`                       | An unsupported principal attribute was used                                                                                                                       | Key users on `UserPrincipalName` or `Id`, and groups on `Id`, `DisplayName` or `MailNickname` |
| `A rename writes UserPrincipalName` or `Entra ID assigns and never lets anyone write` | A rename was requested for a system keyed on a non-writable attribute                                                                                             | Key users on `UserPrincipalName`, and groups on `DisplayName` or `MailNickname`               |
| `matched N objects instead of exactly one`                                            | A name lookup matched several objects                                                                                                                             | Resolve the name collision, or key the system on `Id`                                         |
| Several values for a single-valued attribute                                          | Multiple entries were sent for an attribute other than `Groups`, `ServicePrincipals` or `Licensing.Data` for users, or `Groups`, `Members` or `Owners` for groups | Fix the mapping so single-valued attributes carry one value                                   |
| The mapped principal differs from the identity value                                  | The map writes the principal attribute with a different value                                                                                                     | Identity changes travel only through `originalIdentityValue`                                  |
| `is a boolean in the managed system, but the request carries`                         | A flag attribute received a non-boolean value                                                                                                                     | Send `true` or `false`                                                                        |
| `not supported for type`                                                              | The operation and object type combination is outside the supported matrix                                                                                         | See the supported operations table at the top of this page                                    |

### Errors Entra ID returns

The table above is what the connector refuses to send. Once a request reaches Entra ID the message is whatever Graph returned, and the connector's wrapper tells you which operation produced it: `Got error while updating a user` comes from a **modify**. Suspend and resume call Graph directly, so their failures carry no wrapper and appear as the bare Graph message.

| Symptom                                                                                                                                | Likely cause                                                                                             | Resolution                                                                                                                                                                                          |
| -------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Insufficient privileges to complete the operation. Status: 403 (Forbidden)` on a user who provisions correctly in every other respect | The target is a protected user, and the application's directory role does not cover sensitive attributes | Assign the application the **Privileged Authentication Administrator** role, take the user out of the role-assignable group, or leave protected users out of the managed system's scope — see below |

#### Why a protected user refuses an otherwise valid change

Entra ID treats a short list of user properties as **sensitive**, and `accountEnabled` is one of them — Microsoft names the action *Disable or enable users*. Suspending a user writes exactly that property, and so does any modify that carries `AccountEnabled`, which makes both a sensitive action rather than an ordinary attribute write.

That is unremarkable for most users. It stops being unremarkable when the target is:

* a **member or owner of a role-assignable group** — one created with `IsAssignableToRole` set to `true`, which Entra protects so that nobody can escalate privilege through its membership; or
* a user whose role is scoped to a **restricted management administrative unit**; or
* a user who holds a directory role of their own.

On any of those, only **Privileged Authentication Administrator** and **Global Administrator** may perform a sensitive action. **User Administrator may not** — and User Administrator is the role [Permissions the portal cannot grant](#permissions-the-portal-cannot-grant) tells you to assign so the connector can reset passwords. A deployment that followed that step and went no further provisions ordinary users without complaint and fails on this one, which is why the error arrives long after the connector is believed to be working.

The `RoleManagement.ReadWrite.Directory` permission in [Setting API permissions](#setting-api-permissions) does not help here either. It governs managing the *membership* of role-assignable groups; this is a write to a *member's* own sensitive property, which is a directory-role question rather than a Graph-permission one.

Graph does not name the user it objected to, or say why — the underlying code is the generic `Authorization_RequestDenied`. Confirm the cause by looking at the groups the failing user belongs to:

{% code overflow="wrap" %}

```
Get-MgUserMemberOf -UserId <user-principal-name> -All |
  Where-Object { $_.AdditionalProperties['@odata.type'] -eq '#microsoft.graph.group' } |
  ForEach-Object { Get-MgGroup -GroupId $_.Id -Property DisplayName,IsAssignableToRole } |
  Select-Object DisplayName, IsAssignableToRole
```

{% endcode %}

If any group comes back with `IsAssignableToRole` set to `True`, that is the cause. The property is immutable, so the group itself cannot be made ordinary. Three ways out, in the order most deployments want them:

1. **Assign the application the Privileged Authentication Administrator role**, the same way [Permissions the portal cannot grant](#permissions-the-portal-cannot-grant) assigns User Administrator. This is the one to choose when the connector is expected to manage privileged accounts at all.
2. **Take the user out of the role-assignable group.** This works because it stops the account being protected — but it also removes whatever privilege the group conferred, so it is the right answer only when the membership was unintended, not a way to quiet the error.
3. **Leave those accounts out of the managed system's scope** and handle them by hand. Reasonable when the protected population is small and privileged enough that you would rather it never moved automatically.

## Registering the connector in OpenIAM

The connector's request queue name in OpenIAM has to match the queue in the connector's own `Connector.config` exactly. Getting the connection parameters out of OpenIAM and into the installer is covered in [Installing PowerShell connectors](/connector-configuration/microsoft/1-powershellconnectorinstallation.md#getting-connection-parameters-with-openiam).

## Application authentication (Entra ID)

The Microsoft Graph PowerShell module can run interactively as a signed-in user, or app-only as a standalone application. The connector is the second: it has no signed-in user, so it authenticates as an application registered in your Entra tenant, holding a certificate rather than a password. Older Entra modules accepted a username and password; this one does not. The setup below is a one-time cost, and the result is that no account password is stored anywhere in the connector.

### The certificate

First, you need to generate a self-signed certificate - this certificate will be used for application registration. Run below commands in PowerShell, but set your own certificate name that makes sense for your environment. Also, in the example below a 10-year certificate with a key length of 2048 was created, an exportable one. You may adjust settings if needed.

{% code overflow="wrap" %}

```powershell
$certname = "OIAM_Test1"
New-SelfSignedCertificate -Subject "CN=$certname" -CertStoreLocation "Cert:\LocalMachine\My" -KeyExportPolicy Exportable -KeySpec Signature -KeyLength 2048 -KeyAlgorithm RSA -HashAlgorithm SHA256 -NotAfter (Get-Date).AddYears(10)
```

{% endcode %}

The command prints the certificate it created. The thumbprint it prints is the value the managed system will need as its password:

{% code overflow="wrap" %}

```
   PSParentPath: Microsoft.PowerShell.Security\Certificate::LocalMachine\My

Thumbprint                               Subject
----------                               -------
F94D89E659338D90E58830721CA0F9FAEE6ECA45 CN=OIAM_Test1
```

{% endcode %}

### Running the registration script

When certificate is created, go to official Microsoft documentation page to take a registration script to run [Use app-only authentication with the Microsoft Graph PowerShell SDK](https://learn.microsoft.com/en-us/powershell/microsoftgraph/app-only?view=graph-powershell-1.0).

Follow Microsoft's guidelines and save the script for executing. To prepare for executing the script you will need to export the certificate (not the public key, just a certificate) that you've just created on the previous step. To do this you need to go to the certificate console, select the certificate and press *Export* like shown below.

<figure><img src="https://2965242200-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F0uHWHRgcad6P03ehqgmX%2Fuploads%2Fgit-blob-44eab0bd3855b3e567c6fd9c016d57c4bdf2c65c%2F14-psgraph-06-exporting.png?alt=media" alt="The Certificates snap-in for the local computer, with a certificate selected and the All Tasks submenu open on Export"><figcaption></figcaption></figure>

<figure><img src="https://2965242200-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F0uHWHRgcad6P03ehqgmX%2Fuploads%2Fgit-blob-3f19df011ea07c4a8ad1fa0c77016f98c5f1c7f9%2F14-psgraph-06-exporting2.png?alt=media" alt="The Certificate Export Wizard on the Export Private Key page, with No, do not export the private key selected"><figcaption></figcaption></figure>

You will need the certificate path for executing a script. Run the script that was saved on a previous step. To do this you can open PowerShell console and run the command

{% code overflow="wrap" %}

```
.\[SCRIPT_NAME] -AppName [NAME_YOU_GIVE_AN_APPLICATION] -CertPath [PATH_TO_EXPORTED_CERTIFICATE]
```

{% endcode %}

The script opens a browser window for authentication; you need permission to register applications in the tenant. On success it prints the new application's id and the URL that grants it admin consent:

{% code overflow="wrap" %}

```
Certificate loaded
App registration created with app ID <application-client-id>
Service principal created
Success

Please go to the following URL in your browser to provide admin consent
https://login.microsoftonline.com/<tenant-id>/adminconsent?client_id=<application-client-id>
```

{% endcode %}

If the registration script crashes the console instead of running:

```
Process is terminated due to StackOverflowException.
```

that is the same TLS 1.2 problem. Set the protocol for the session before running the script:

{% code overflow="wrap" %}

```powershell
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
```

{% endcode %}

### Granting admin consent

Log into the Azure portal and open **App registrations**.

Here you will be able to find the application that you have just created on the Step-3. In this case it has a name *Graph\_Documentation*. You can go to the application page and select *API Permissions*.

<figure><img src="https://2965242200-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F0uHWHRgcad6P03ehqgmX%2Fuploads%2Fgit-blob-9d8e42bb3a271cad260d2f3d0d789e1ab44dec1f%2F14-psgraph-10-api-permissions.png?alt=media" alt="The Configured permissions table for the application, listing Group.Read.All and User.Read.All with a status of not granted"><figcaption></figcaption></figure>

As it can be seen from the above image, your application is not granted consent by default, so to make it operational you need to press on *Grant admin consent for openiamdemo*. Once you do it, your application (and connector) would be able to perform operations listed in API permissions.

### Setting API permissions

By default (as you see on above screenshot) you get read permissions. If you need a connector only for reading data, for example loading objects to OpenIAM without following modification, that permission looks good. However, to modify objects you need to add the application permissions for the features you intend to use.

| Feature                                                                       | Application permission                               |
| ----------------------------------------------------------------------------- | ---------------------------------------------------- |
| User provisioning and synchronization                                         | `User.ReadWrite.All`                                 |
| Group provisioning and synchronization, including members, owners and nesting | `Group.ReadWrite.All`                                |
| Guest invitations                                                             | `User.Invite.All` (included in `User.ReadWrite.All`) |
| Role-assignable groups (`IsAssignableToRole`)                                 | `RoleManagement.ReadWrite.Directory`                 |

{% hint style="info" %}
A connector that was deployed before group support was configured typically carries only **User.ReadWrite.All**. Add **Group.ReadWrite.All** and grant admin consent again before you provision groups.
{% endhint %}

### Permissions the portal cannot grant

Not all permissions can be set through the *API permissions* page. There is no way to grant an application permission to reset user passwords from that page — by default only applications running in interactive mode can do that. Instead, assign your application the *User Administrator* directory role. You can do this from the Entra admin center or with PowerShell.

You will need the application (client) ID for either method. It is on the application's **Overview** page, alongside the tenant id — see [Collecting the managed system values](#collecting-the-managed-system-values).

**From the Entra admin center**

{% stepper %}
{% step %}
Go to **Roles and administrators**.
{% endstep %}

{% step %}
Open the **User Administrator** role.
{% endstep %}

{% step %}
Select **Add assignment**.
{% endstep %}

{% step %}
Search for your application by name, select its service principal, and confirm.
{% endstep %}
{% endstepper %}

**With PowerShell**

{% code overflow="wrap" %}

```
Connect-MgGraph -Scopes RoleManagement.ReadWrite.Directory, Application.Read.All

$principal = Get-MgServicePrincipal -Filter "AppId eq '<application-client-id>'"
$role = Get-MgDirectoryRole -Filter "DisplayName eq 'User Administrator'"

New-MgDirectoryRoleMemberByRef -DirectoryRoleId $role.Id `
  -OdataId "https://graph.microsoft.com/v1.0/directoryObjects/$($principal.Id)"
```

{% endcode %}

{% hint style="info" %}
**Get-MgDirectoryRole** returns only roles that are already activated in the tenant. If it returns nothing, activate the role first from its template with **Get-MgDirectoryRoleTemplate** and **New-MgDirectoryRole**, then repeat the assignment.
{% endhint %}

Once the role is assigned, the connector can reset user passwords.

### Collecting the managed system values

The last step needed for completing a preparation is to collect some data for inputting into OpenIAM Managed System configuration page.

You will need the following data:

* Application (client) ID. It goes to the *Login ID* property on the OpenIAM configuration page.
* Directory (tenant) ID. It should be inserted into the *Host URL* property field.

<figure><img src="https://2965242200-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F0uHWHRgcad6P03ehqgmX%2Fuploads%2Fgit-blob-e1722c723ef2a332fae47ebd1b6c74edd055834e%2F14-psgraph-13-IDs.png?alt=media" alt="The Essentials panel on the application registration Overview page, showing the Application client ID and the Directory tenant ID"><figcaption></figcaption></figure>

* The certificate **thumbprint**, from the certificate generated in [The certificate](#the-certificate). Read it in **MMC** → **Certificate snap-in** → **Local Computer certificates** → **Personal** → your certificate. This goes in the *Password* property

<figure><img src="https://2965242200-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F0uHWHRgcad6P03ehqgmX%2Fuploads%2Fgit-blob-cf833cc3c6dfaf82984d43627ebaee5d00aef022%2F14-psgraph-14-thumbprint.png?alt=media" alt="The Details tab of the certificate properties dialog with the Thumbprint field selected and its value shown in the panel below"><figcaption></figcaption></figure>

## Configuring the managed system

Follow the standard steps in [Configure a managed system](/administration-guide/lifecycle/configure-managed-system.md). Three fields carry Entra-specific values, collected in the previous section:

| Field        | Value                                         |
| ------------ | --------------------------------------------- |
| **Host URL** | The **Directory (tenant) ID**                 |
| **Login ID** | The application's **Application (client) ID** |
| **Password** | The **thumbprint** of the certificate         |

### How the connector addresses objects

Every request carries the object's identity, taken from the managed system's *principal attribute*. The connector can resolve the following.

| Object | Principal attribute can be          | Required to create              | Required to rename              |
| ------ | ----------------------------------- | ------------------------------- | ------------------------------- |
| User   | `UserPrincipalName`, `Id`           | `UserPrincipalName`             | `UserPrincipalName`             |
| Group  | `Id`, `DisplayName`, `MailNickname` | `DisplayName` or `MailNickname` | `DisplayName` or `MailNickname` |

* Entra ID assigns the directory object `Id` when the object is created, so a managed system keyed on `Id` can look up, update and delete objects, but can never create or rename them.
* `DisplayName` and `MailNickname` are **not unique** in Entra ID. A group lookup by name that matches more than one group is refused, and nothing is modified or deleted until the collision is resolved.
* Guest-invitation systems, where `InvitationScope` is set to `true`, address the invitee by e-mail and are exempt from these rules. See *Guest users* below.
* Any other principal attribute, `Mail` for example, is refused, because Graph cannot look objects up by it.

### Custom handlers

A managed system attribute whose value is a script filename replaces the built-in operation with that script. The file must live in the connector's installation folder.

| Attribute                              | Replaces                                                         |
| -------------------------------------- | ---------------------------------------------------------------- |
| `ADD_HANDLER` and `MODIFY_HANDLER`     | The create and update halves of SAVE, for users and groups alike |
| `DELETE_HANDLER`                       | DELETE                                                           |
| `PASSWORD_HANDLER`                     | RESET\_PASSWORD and LOGIN                                        |
| `SUSPEND_HANDLER` and `RESUME_HANDLER` | SUSPEND and RESUME                                               |
| `SEARCH_HANDLER`                       | SEARCH                                                           |

Requests routed to a handler skip the attribute map validations that assume the built-in logic, such as the single-valued list and the principal match. The identity presence check still applies.

### Simulation mode

* Set the managed system attribute `isSimulation` to `true` to skip SAVE, DELETE, RESET\_PASSWORD, SUSPEND and RESUME, each with a warning. SEARCH still runs. Use this to dry-run policy changes.
* Set `saveSimulation` to `true`, either as a managed system attribute or in `Connector.config`, to capture each incoming request as a JSON file in the connector's `Simulation` folder, for offline replay and support.

## Attribute mapping and policy map

Attributes reach Entra ID through the policy map on the managed system, as they do for every connector — see [Configure a managed system](/administration-guide/lifecycle/configure-managed-system.md). Two things about this connector shape how you write that map.

**Every value carries an operation code**, and the connector acts on the code rather than only on the value. The codes are the same four everywhere:

| Code        | Meaning                                                                             |
| ----------- | ----------------------------------------------------------------------------------- |
| `ADD`       | Set the value, or add it to a multi-valued attribute                                |
| `REPLACE`   | Set the value, replacing what is there. For most attributes it behaves as `ADD`     |
| `DELETE`    | Remove the value — clear the attribute, leave the group, revoke the licence or role |
| `NO_CHANGE` | Skip the attribute entirely                                                         |

This is why one map covers both directions: a group membership is joined and left through the same `Groups` attribute, and a licence is assigned and revoked through the same `Licensing.Data`.

**Most attributes are single-valued, and a few are not.** Sending several values for a single-valued attribute is refused before anything reaches Entra ID. The multi-valued ones are `Groups`, `ServicePrincipals` and `Licensing.Data` for users, and `Groups`, `Members` and `Owners` for groups.

The two sections below cover what each object type accepts.

## Provisioning users

### Creating and updating users

SAVE looks the user up by the identity value. If the user is not found it is created; if the user is found it is updated.

The policy map may contain **any property of the Graph user object**, and the attribute name must match the Graph parameter name — `DisplayName`, `GivenName`, `Surname`, `JobTitle`, `Department`, `UsageLocation`, `AccountEnabled`, `Mail`, `MobilePhone`, `OnPremisesImmutableId`, and so on.

Value operations behave as follows: `ADD` and `REPLACE` set the value, `DELETE` clears it (on update only), and `NO_CHANGE` is ignored. Boolean attributes take `true` or `false`.

Entra ID requires the following attributes to **create** a user. An update needs only the attributes you are changing.

| Attribute                  | Note                      |
| -------------------------- | ------------------------- |
| `UserPrincipalName`        | The identity value itself |
| `DisplayName`              |                           |
| `MailNickname`             |                           |
| `AccountEnabled`           | `true` or `false`         |
| `PasswordProfile.Password` | Initial password          |

### Passwords

* `PasswordProfile.Password` sets the password, and `PasswordProfile.ForceChangePasswordNextSignIn` takes `true` or `false`. `PasswordProfile.ForceChangePasswordNextSignInWithMfa` forces the user to pass MFA before changing the password.
* **RESET\_PASSWORD** applies the mapped `PasswordProfile.*` attributes. `PasswordProfile.Password` with `ADD` or `REPLACE` is required, otherwise the reset is refused.
* On a SAVE **update**, the password is applied only when `ForceChangePasswordOnSave` is mapped as `true`. Otherwise `PasswordProfile.Password` is ignored, so ordinary attribute updates never rotate passwords.

### Licenses

License assignment is controlled by the `Licensing.Data` attribute. It is multi-valued: supply one JSON object per value, each with its own operation, where `ADD` and `REPLACE` assign the license and `DELETE` removes it.

{% code overflow="wrap" %}

```json
{ "LicenseName": "ENTERPRISEPACK", "DisabledPlans": [ "YAMMER_ENTERPRISE" ] }
```

{% endcode %}

* `LicenseName` is the SKU part number, such as `ENTERPRISEPACK`, or the SKU id as a GUID.
* `DisabledPlans` is optional and holds the service-plan names or ids to switch off inside the license.
* The user's `UsageLocation` must be set before a license can be assigned.

To find the license names available in your organization, go to the connector machine, or any other machine where the Microsoft Graph PowerShell module is available, and run the following.

```
Connect-MgGraph -Scope User.ReadWrite.All, Directory.ReadWrite.All
Get-MgSubscribedSku -All | Select-Object SkuPartNumber
```

The output lists one SKU part number per line:

```
SkuPartNumber
-------------
WINDOWS_STORE
ENTERPRISEPACK
FLOW_FREE
POWER_BI_STANDARD
TEAMS_EXPLORATORY
STANDARDPACK
```

A single license package consists of a set of nested services. To grant access only to particular services inside a package, you first need the names of the service plans it contains. For the `POWER_BI_STANDARD` package, for example, run the following.

{% code overflow="wrap" %}

```
$availableLicenses = Get-MgSubscribedSku -All
$curLicEntity = $availableLicenses | Where-Object SkuPartNumber -eq 'POWER_BI_STANDARD'
$curLicEntity.ServicePlans
```

{% endcode %}

{% code overflow="wrap" %}

```
AppliesTo ProvisioningStatus ServicePlanId                        ServicePlanName
--------- ------------------ -------------                        ---------------
User      Success            c948ea65-2053-4a5a-8a62-9eaaaf11b522 PURVIEW_DISCOVERY
Company   Success            113feb6c-3fe4-4440-bddc-54d774bf0318 EXCHANGE_S_FOUNDATION
User      Success            2049e525-b859-401b-b2a0-e0a31c4b1fe4 BI_AZURE_P0
```

{% endcode %}

To assign `POWER_BI_STANDARD` while excluding the `PURVIEW_DISCOVERY` plan, and at the same time revoke `FLOW_FREE` entirely, set `Licensing.Data` as multi-valued with two items.

{% code overflow="wrap" %}

```
{"LicenseName":"POWER_BI_STANDARD","DisabledPlans":["PURVIEW_DISCOVERY"]}  => with operation code 'Add'
{"LicenseName":"FLOW_FREE","DisabledPlans":[]} => with operation code 'Delete'
```

{% endcode %}

Licenses and plans can be given either by name or by identifier. The two examples below are equivalent.

```
[{"LicenseName":"POWER_BI_STANDARD","DisabledPlans":["BI_AZURE_P0"]}]
```

{% code overflow="wrap" %}

```
[{"LicenseName":"a403ebcc-fae0-4ca2-8c8c-7a907fd6c235","DisabledPlans":["2049e525-b859-401b-b2a0-e0a31c4b1fe4"]}]
```

{% endcode %}

### Group memberships and application roles

| Attribute           | One value per entry                                                                                | Operations                                                                |
| ------------------- | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| `Groups`            | The group object **Id** (GUID)                                                                     | `ADD` and `REPLACE` join the group, `DELETE` leaves it, `NO_CHANGE` skips |
| `ServicePrincipals` | JSON: `{ "Id": "<service principal object id>", "Roles": ["<app role id>"] }`, or `"Roles": "All"` | `ADD` and `REPLACE` assign, `DELETE` revokes                              |

Both attributes are idempotent: adding a membership that already exists, or removing one that is absent, is a no-op.

The connector works with the Service Principal objects of the application, as shown below.

<figure><img src="https://2965242200-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F0uHWHRgcad6P03ehqgmX%2Fuploads%2Fgit-blob-83636398a971870489c97489239a638767ee1470%2F14-psgraph-17-revoking-roles.png?alt=media" alt="Entra portal showing the Service Principal object of an application"><figcaption></figcaption></figure>

The accepted JSON forms for `ServicePrincipals` are as follows.

{% code overflow="wrap" %}

```
{"id": "Service principal ID", "roles": []}            - assign or revoke the empty role of the service principal
{"id": "Guid of service principal", "roles": ["all"]}  - assign or revoke all roles of the service principal
{"id": "Guid of service principal", "roles": ["Role ID"]} - assign or revoke particular roles
```

{% endcode %}

### Manager

`Manager` takes the manager's `UserPrincipalName` or `Id`. `ADD` and `REPLACE` set it, `DELETE` removes it.

A manager that cannot be found or assigned produces a **warning** and the save still succeeds — the manager link is treated as supplementary.

### Sessions and MFA cleanup

Each of the attributes below takes `true` with `ADD` or `REPLACE`.

| Attribute                                | Effect                                             |
| ---------------------------------------- | -------------------------------------------------- |
| `RevokeSignInSession`                    | Revokes all refresh tokens                         |
| `RemoveAllMicrosoftAuthenticatorMethods` | Deletes every Microsoft Authenticator registration |
| `RemoveAllPhoneAuthenticationMethods`    | Deletes every phone sign-in method                 |

### Suspending, resuming and deleting a user

* SUSPEND sets `AccountEnabled` to `false` and revokes sessions. RESUME sets `AccountEnabled` to `true`. Both fail when the user cannot be found.
* DELETE removes the user. A user that is already absent reports success.

### Renaming a user

A rename arrives as a SAVE whose `originalIdentityValue` differs from the new identity value. The connector finds the user by the **original** `UserPrincipalName`, applies every other change first, and writes the new `UserPrincipalName` **last**, so a partially applied save never strands the user under an unreachable name.

If the original value no longer resolves, the request is refused. A user that already carries the new value is never silently adopted.

### Guest users

Set the managed system attribute `InvitationScope` to `true`. SAVE then sends a guest **invitation** instead of creating a user: the identity value is the invitee's e-mail address, and a guest who has already been invited is not invited twice.

| Attribute               | Description                                                                                                                         | Type    |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `UserPrincipalName`     | E-mail address of the user to invite. Required.                                                                                     | String  |
| `InviteRedirectUrl`     | URL the invited user is redirected to after accepting the invitation, for example `https://myapplications.microsoft.com`. Required. | String  |
| `SendInvitationMessage` | Whether to send the invitation e-mail.                                                                                              | Boolean |

DELETE, SUSPEND and RESUME work on a guest account exactly as they do on a regular user. RESET\_PASSWORD is not available for invited guests.

{% hint style="info" %}
After the invitation is sent, the user receives their own `UserPrincipalName`. For further management of guest users, a user synchronization process is required to retrieve the correct `UserPrincipalName`. Guest users are then managed in the same way as standard Entra ID users.
{% endhint %}

## Provisioning groups

### Creating and updating groups

SAVE looks the group up by the identity value. If the group is not found it is created; if the group is found it is updated. The map may contain **any property of the Graph group object** — `DisplayName`, `Description`, `MailNickname`, `MailEnabled`, `SecurityEnabled`, `GroupTypes`, `Visibility`, `IsAssignableToRole`, and so on.

The connector creates two kinds of groups.

| Kind                | How to request it             | Defaults applied at create when unmapped    |
| ------------------- | ----------------------------- | ------------------------------------------- |
| Security group      | Map nothing special           | `SecurityEnabled=true`, `MailEnabled=false` |
| Microsoft 365 group | Set `GroupTypes` to `Unified` | `SecurityEnabled=false`, `MailEnabled=true` |

* `MailNickname` is required by Entra ID for every group. When it is not mapped, the connector derives it from the `DisplayName`, keeping only letters, digits and the `._-` characters. Every applied default is written to the connector log.
* A minimal security-group map needs nothing beyond the identity value, normally `DisplayName`. The defaults cover the rest.
* `GroupTypes` cannot be changed after creation, so there is no conversion between security and Microsoft 365 groups. `Visibility` applies to Microsoft 365 groups only.

{% hint style="info" %}
Distribution lists and mail-enabled security groups cannot be created through Microsoft Graph. A map that sets **MailEnabled=true** without **GroupTypes=Unified** is refused before anything is created. Manage those groups through Exchange.
{% endhint %}

### Members, owners and nesting

| Attribute | One value per entry                                         | Direction                              |
| --------- | ----------------------------------------------------------- | -------------------------------------- |
| `Members` | The member's object **Id**, or a user's `UserPrincipalName` | Who is IN this group                   |
| `Owners`  | The owner's object **Id**, or a user's `UserPrincipalName`  | Who OWNS this group                    |
| `Groups`  | The parent group's object **Id**                            | Which groups THIS group is nested into |

For each value, `ADD` and `REPLACE` add, `DELETE` removes, and `NO_CHANGE` skips. All three attributes are idempotent.

Graph refuses to remove the **last owner** of a Microsoft 365 group, and that failure is reported as-is. A group created by the connector has no owner until `Owners` assigns one.

### Renaming and deleting a group

* A rename works exactly like the user rename described above: it is routed by `originalIdentityValue`, applied last, and refused when the original name no longer resolves. Only systems keyed on `DisplayName` or `MailNickname` can rename a group. Note that changing `MailNickname` does **not** change the SMTP address already provisioned for a Microsoft 365 group.
* DELETE removes the group by its resolved object id. A group that is already absent reports success, and an ambiguous name match is refused rather than deleting either group.

### What the connector refuses for groups, and why

| Refusal                                         | Reason                                                                      |
| ----------------------------------------------- | --------------------------------------------------------------------------- |
| Create when the system is keyed on `Id`         | Entra ID assigns the object id, so it does not exist before the group does  |
| `MailEnabled=true` without `GroupTypes=Unified` | Graph cannot create distribution or mail-enabled security groups            |
| A name lookup matching several groups           | Names are not unique, and the connector never guesses which group was meant |
| SUSPEND, RESUME or RESET\_PASSWORD              | A group cannot be disabled and has no password                              |

## Reconciliation and synchronization

How synchronization and reconciliation work in general is covered in the Administration guide — see [*Synchronize and import data*](/administration-guide/lifecycle/synchronize-and-import-data.md) and [*Configure reconciliation*](/administration-guide/lifecycle/configure-reconciliation.md). This section covers only what is specific to this connector.

The managed system attribute `SEARCH_QUERY` holds a read-only PowerShell query. The connector validates it, rejecting any modifying command, and streams the results back in batches whose size is set by `SearchResponseBatchSize` in the connector configuration.

| Goal                          | Example query                                                                                     |
| ----------------------------- | ------------------------------------------------------------------------------------------------- |
| Users                         | `Get-MgUser -All -Property Id,DisplayName,UserPrincipalName,Mail,AccountEnabled,AssignedLicenses` |
| Groups                        | `Get-MgGroup -All -Property Id,DisplayName,MailNickname,Description,GroupTypes`                   |
| Groups with member lists      | `Get-GroupsWithMembers -All -Property Id,DisplayName -ReturnUserMemberProperty UserPrincipalName` |
| Applications with assignments | `Get-ApplicationsWithMembers -All`                                                                |
| Users with manager data       | `Get-UserExtended -All -Property Id,UserPrincipalName`                                            |

* The list of synchronized attributes must include the principal attribute — `UserPrincipalName` for users, or `Id` for id-keyed groups — and the query's `-Property` must select it, because it becomes the identity value of every returned record.
* You will only get the attributes named in `-Property`. Be explicit about everything you need, otherwise Microsoft Graph skips them for optimization purposes.
* The managed system's object type decides whether results are returned as users or as groups, so the query must match it.
* Extra attributes are filled on request. For **users** these are `Groups`, holding membership ids, and `AuthenticationMethods`. For **groups** it is `Groups`, holding parent-group ids.

{% hint style="info" %}
Requesting **AuthenticationMethods** is a heavy operation. Use it only when it is truly necessary, and avoid including it in the default query and attribute configuration on the OpenIAM side.
{% endhint %}

### Synchronizing groups with members and owners

To synchronize all groups or a specific group with their members and owners, use the **custom** `Get-GroupsWithMembers` cmdlet. It can return one or more of the following attributes.

* `UserMembers` — all user members of the group.
* `GroupMembers` — all group members of the group.
* `DeviceMembers` — all device members of the group.
* `OwnerUserMembers` — all user owners of the group.

To return all groups with members and owners, use the following query.

{% code overflow="wrap" %}

```
Get-GroupsWithMembers -All [-Property] [-Filter] [-ReturnUserMemberProperty] [-ReturnOwnerProperty] 
```

{% endcode %}

| Query                      | Description                                                                                                                                                                                                                                                                                                                                                                                                   |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `All`                      | Required to retrieve all groups.                                                                                                                                                                                                                                                                                                                                                                              |
| `Property`                 | Specifies a set of group properties, such as `Id`, `Description`, `DisplayName`, `MailNickname`.                                                                                                                                                                                                                                                                                                              |
| `Filter`                   | Specifies a filter for retrieving groups, for example: "`MailEnabled` equals `false`".                                                                                                                                                                                                                                                                                                                        |
| `ReturnUserMemberProperty` | <p>Specifies the attribute included in the <code>UserMembers</code> attribute in the synchronization result. It can be:</p><ul><li><strong><code>Id</code></strong> – <code>UserMembers</code> will contain the IDs of users (default).</li><li><strong><code>UserPrincipalName</code></strong> – <code>UserMembers</code> will contain the <code>UserPrincipalName</code> of users.</li></ul>                |
| `ReturnOwnerProperty`      | <p>Specifies the attribute included in the <code>OwnerUserMembers</code> attribute in the synchronization result. It can be:</p><ul><li><strong><code>Id</code></strong> – <code>OwnerUserMembers</code> will contain the IDs of users (default).</li><li><strong><code>UserPrincipalName</code></strong> – <code>OwnerUserMembers</code> will contain the <code>UserPrincipalName</code> of users.</li></ul> |

Use the following query to return a specific group with its members and owners.

{% code overflow="wrap" %}

```
Get-GroupsWithMembers -GroupId [-Property] [-ReturnUserMemberProperty] [-ReturnOwnerProperty] 
```

{% endcode %}

| Query                      | Description                                                                                                                                                                                                                                                                                                                                                                                                   |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GroupId`                  | Specifies the group ID and is required to retrieve a specific group.                                                                                                                                                                                                                                                                                                                                          |
| `Property`                 | Specifies a set of group properties, such as `Id`, `Description`, `DisplayName`, `MailNickname`.                                                                                                                                                                                                                                                                                                              |
| `ReturnUserMemberProperty` | <p>Specifies the attribute included in the <code>UserMembers</code> attribute in the synchronization result. It can be:</p><ul><li><strong><code>Id</code></strong> – <code>UserMembers</code> will contain the IDs of users (default).</li><li><strong><code>UserPrincipalName</code></strong> – <code>UserMembers</code> will contain the <code>UserPrincipalName</code> of users.</li></ul>                |
| `ReturnOwnerProperty`      | <p>Specifies the attribute included in the <code>OwnerUserMembers</code> attribute in the synchronization result. It can be:</p><ul><li><strong><code>Id</code></strong> – <code>OwnerUserMembers</code> will contain the IDs of users (default).</li><li><strong><code>UserPrincipalName</code></strong> – <code>OwnerUserMembers</code> will contain the <code>UserPrincipalName</code> of users.</li></ul> |

### Synchronizing applications

To synchronize all applications or a specific application, use the `Get-MgServicePrincipal` cmdlet.

{% code overflow="wrap" %}

```
Get-MgServicePrincipal -All -Property Id, AppId, DisplayName, Description | Select-Object Id, AppId, Description

Get-MgServicePrincipal -ServicePrincipalId ff26af16-a8e1-45a2-9598-a78428646a3e -Property Id, AppId, DisplayName, Description | Select-Object Id, AppId, DisplayName, Description 
```

{% endcode %}

To include members and owners, use the custom `Get-ApplicationsWithMembers` cmdlet. It can return one or more of the following attributes.

* `UserMembers` — all user members of the application.
* `GroupMembers` — all group members of the application.
* `ServiceMembers` — all ServicePrincipal members of the application.
* `OwnerUserMembers` — all user owners of the application.
* `OwnerServiceMembers` — all ServicePrincipal owners of the application.

To return all applications with members and owners, use the following query.

{% code overflow="wrap" %}

```
Get-ApplicationsWithMembers -All [-Property] [-Filter] [-ReturnUserMemberProperty] [-ReturnOwnerProperty] 
```

{% endcode %}

| Query                      | Description                                                                                                                                                                                                                                                                                                                                                                                           |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `All`                      | Required to retrieve all applications.                                                                                                                                                                                                                                                                                                                                                                |
| `Property`                 | Should contain a set of group properties, such as `Id`, `AppId`, `DisplayName`, `Description`.                                                                                                                                                                                                                                                                                                        |
| `Filter`                   | Filter used to retrieve groups, for example: `"tags/any(t:t eq 'WindowsAzureActiveDirectoryIntegratedApp')"`.                                                                                                                                                                                                                                                                                         |
| `ReturnUserMemberProperty` | <p>Attribute to be included in the <code>UserMembers</code> attribute in the synchronization result. It can be:</p><ul><li><strong><code>Id</code></strong> – <code>UserMembers</code> will contain the IDs of users (default).</li><li><strong><code>UserPrincipalName</code></strong> – <code>UserMembers</code> will contain the <code>UserPrincipalName</code> of users.</li></ul>                |
| `ReturnOwnerProperty`      | <p>Attribute to be included in the <code>OwnerUserMembers</code> attribute in the synchronization result. It can be:</p><ul><li><strong><code>Id</code></strong> – <code>OwnerUserMembers</code> will contain the IDs of users (default).</li><li><strong><code>UserPrincipalName</code></strong> – <code>OwnerUserMembers</code> will contain the <code>UserPrincipalName</code> of users.</li></ul> |

To return a specific application with its members and owners, use the following query structure.

{% code overflow="wrap" %}

```
Get-ApplicationsWithMembers -Id [-Property] [-ReturnUserMemberProperty] [-ReturnOwnerProperty] 
```

{% endcode %}

| Query                      | Description                                                                                                                                                                                                                                                                                                                                                                                           |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Id`                       | Specific `ServicePrincipalId`, required for retrieving a specific application.                                                                                                                                                                                                                                                                                                                        |
| `Property`                 | Should contain a set of group properties, such as `Id`, `AppId`, `DisplayName`, `Description`.                                                                                                                                                                                                                                                                                                        |
| `ReturnUserMemberProperty` | <p>Attribute to be included in the <code>UserMembers</code> attribute in the synchronization result. It can be:</p><ul><li><strong><code>Id</code></strong> – <code>UserMembers</code> will contain the IDs of users (default).</li><li><strong><code>UserPrincipalName</code></strong> – <code>UserMembers</code> will contain the <code>UserPrincipalName</code> of users.</li></ul>                |
| `ReturnOwnerProperty`      | <p>Attribute to be included in the <code>OwnerUserMembers</code> attribute in the synchronization result. It can be:</p><ul><li><strong><code>Id</code></strong> – <code>OwnerUserMembers</code> will contain the IDs of users (default).</li><li><strong><code>UserPrincipalName</code></strong> – <code>OwnerUserMembers</code> will contain the <code>UserPrincipalName</code> of users.</li></ul> |

### Synchronizing guest users

Filter guest users by `UserType = Guest`. The same properties are used as for standard Entra ID users.

{% code overflow="wrap" %}

```
Get-MgUser -Filter "UserType eq 'Guest'" -Property Id, Mail, UserPrincipalName, Name, DisplayName 

Get-MgUser -Filter "UserType eq 'Guest' and Mail eq 'test.user@openiam.com'" -Property Id, Mail, UserPrincipalName, Name, DisplayName 
```

{% endcode %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.openiam.com/connector-configuration/microsoft/14-psgraph.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
