Skip to content
IT // Adventures

Microsoft Entra ID API-driven Provisioning

Automating user provisioning in Entra ID with Microsoft's new 'inbound provisioning' API.

10 min read

Note: This article was originally published for evoila under API-driven Provisioning with Microsoft Entra and Azure Logic Apps .

Last August, Microsoft announced a new feature, “API-driven provisioning”, for its “Entra ID” identity management solution1. This API enables automated provisioning of user accounts, in Entra and Active Directory, from any trusted data source such as an HCM software. API-driven provisioning is based on the SCIM standard2 and is intended to be used in combination with Entra lifecycle workflows3.

The provisioning data flow, as outlined in Figure 1, includes:

  • An “identity directory” containing data about internal and external employees. Usually an HR/HCM system.
  • An automation tool such as PowerShell, Azure runbooks or Logic Apps to load data from the “identity directory”, create a SCIM document and send it to the API endpoint.
  • The Entra “provisioning service” processes the SCIM document and creates, updates or removes objects in Microsoft Entra or Active Directory.
  • If an on-premises Active Directory is in use, a locally installed “provisioning agent”.

Figure 1: Microsoft Entra API-driven provisioning data flow Figure 1: Microsoft Entra API-driven provisioning data flow

In this article, we describe a simple implementation of API-driven provisioning with Logic Apps. It is meant as an example, or starting point, for your own implementation and is loosely based on Microsoft’s “QuickStart with Azure Logic App”.4

For Workday HCM and SuccessFactors, Microsoft offers its own integration that you don’t have to implement yourself:

Prerequisites

Licensing

API-driven provisioning requires Microsoft Entra ID P1 (formerly “Azure Active Directory P1”) licenses.

Azure resources

For our example, we need a few Azure resources:

  • A resource group “rg-HcmProvisioning” as a logical container for our resources.
  • An Azure Storage account “sthcmprovisioning<NNNN>” with a storage table5 “employeedemo” for our sample data set.
  • A Logic App “logic-HcmProvisioning” for our workflow.

To create the resources you can use the Azure CLI, PowerShell or the portal. Creating the Logic App with PowerShell or the CLI requires a “workflow definition”6, which you can find in the appendix below.

We use a PowerShell script to create the resources:

$ProjectName = "HcmProvisioning"
$Location = "westeurope" # Azure region for our resources
$RandomId = Get-Random -Minimum -1000 -Maximum 9999 # We'll use this to make our storage account name globally unique
$StorageName = "st" + $ProjectName.ToLower() + $RandomId
$StorageTableName = "employeedemo"
$ResourceGroupName = "rg-$ProjectName"
$LogicAppName = "logic-$ProjectName"
​
# Create a resource group for our Azure resources
New-AzResourceGroup -Location $Location -Name $ResourceGroupName
​
# Create the storage account...
$StorageAccount = New-AzStorageAccount -ResourceGroupName $ResourceGroupName -Name $StorageName -Location $Location -SkuName "Standard_LRS"
# ... and our employee table
$StorageContext = $StorageAccount.Context
New-AzStorageTable -Name $StorageTableName -Context $StorageContext
​
# Create a Logic App, based on our "empty" workflow definition
$LogicApp = New-AzLogicApp -ResourceGroupName $ResourceGroupName -Name $LogicAppName -Location $Location -DefinitionFilePath "./workflow-definition.json"

HCM data source

For the example, we use the Azure storage table “employeedemo” that we just created in the Azure Storage account. We download Microsoft’s sample data set from GitHub and import it into the table with Azure Storage Explorer.

After the import, the table content looks like this (reduced to the relevant columns):

Company CostCenter Department Devision FirstName FullName LastName
Woodgrove CC3035 Sales Electronics Talya Talya Fleeta Fleeta
Woodgrove CC3035 Sales Electronics Miriam Miriam Dunware Dunware
Woodgrove CC3035 Product Engineering Logistics Ginnie Ginnie Fadiman Fadiman
Fabrikam CC5081 Sales Sales Genevra Genevra Melony Melony
Woodgrove CC3035 Manufacturing Pharma Nyssa Nyssa Roscoe Roscoe

Microsoft Entra API-driven provisioning App

To enable “API-driven provisioning” in an Entra tenant, an “Enterprise Application” has to be added from the gallery. Sign in to the Microsoft Entra Admin Center and navigate to Identity > Applications > Enterprise applications.

Figure 2: Add an enterprise application Figure 2: Add an enterprise application

Select “New application”, enter “API-driven” in the search box and then select the application “API-driven provisioning to Microsoft Entra ID”. Keep the default settings and click “Create”.

Figure 3: Search for the API-driven provisioning app Figure 3: Search for the API-driven provisioning app

Open the newly created app and switch to the “Provisioning” menu (Fig. 4). Click “Get started” in the “Provision User Accounts” tile, set the “Provisioning Mode” to “Automatic” and save the settings.

Figure 4: Enable provisioning Figure 4: Enable provisioning

After the initial configuration has been created, you can see additional options in the left menu under “Provisioning” (Fig. 5):

  • Mappings control the mapping of data fields between the data source and Microsoft Entra. The default settings are sufficient for our demo.
  • Under Settings, a valid email address for notifications should be entered. You can also set a threshold here to prevent the accidental deletion of many user accounts.

Figure 5: Provisioning settings Figure 5: Provisioning settings

Logic App Managed Identity

The Azure Logic App will use a “service principal” to authenticate to Microsoft Entra. To do so, we enable the “system-assigned managed identity”7 in our Logic App. Then we use PowerShell to grant the managed identity the necessary MS Graph permissions.

Open the Logic App resource in the Azure portal and navigate to “Identity” (Fig. 6). Change the status to “On” and click “Save” to create a managed identity. The managed identity gets the same name as the Logic App (“logic-HcmProvisioning”) in Entra.

Figure 6: Enable the managed identity Figure 6: Enable the managed identity

In the next step, we use a PowerShell script and the MS Graph API to give the service principal the necessary permissions8:

  • SynchronizationData-User.Upload allows the Logic App to upload data to the Identity Synchronization Service.
  • AuditLog.Read.All allows the Logic App to read log entries in the audit log.
Install-Module -Name "Microsoft.Graph" -Scope AllUsers
​
Connect-MgGraph -Scopes "Application.Read.All","AppRoleAssignment.ReadWrite.All,RoleManagement.ReadWrite.Directory"
$GraphApp = Get-MgServicePrincipal -Filter "AppId eq '00000003-0000-0000-c000-000000000000'"
​
# Find our Logic App's managed service principal
$ManagedSp = Get-MgServicePrincipal -Filter "DisplayName eq '$LogicAppName'" # the service principal has the same name as the logic app we created earlier
​
# Search for app role permission "SynchronizationData-User.Upload" and assign it to our service principal
$PermissionName = "SynchronizationData-User.Upload"
$AppRole = $GraphApp.AppRoles | Where-Object {$_.Value -eq $PermissionName -and $_.AllowedMemberTypes -contains "Application"}
New-MgServicePrincipalAppRoleAssignment -PrincipalId $ManagedSp.Id -ServicePrincipalId $ManagedSp.Id -ResourceId $GraphApp.Id -AppRoleId $AppRole.Id
​
# Search for app role permission "AuditLog.Read.All" and assign it to our service principal
$PermissionName = "AuditLog.Read.All"
$AppRole = $GraphApp.AppRoles | Where-Object {$_.Value -eq $PermissionName -and $_.AllowedMemberTypes -contains "Application"}
New-MgServicePrincipalAppRoleAssignment -PrincipalId $ManagedSp.Id -ServicePrincipalId $ManagedSp.Id -ResourceId $GraphApp.Id -AppRoleId $AppRole.Id

Additionally, we allow the Logic App to read our data source (Azure Storage Table):

New-AzRoleAssignment -PrincipalId $ManagedSp.Id -RoleDefinitionName "Storage Table Data Reader" -ResourceName $StorageName -ResourceType "Microsoft.Storage/storageAccounts" -ResourceGroupName $ResourceGroupName 

You can check the MS Graph permissions of the service principal in Entra by selecting the app “logic-HcmProvisioning” under “Enterprise Application” (remove the filter “Application type == Enterprise application” if necessary):

Figure 7: Service principal permissions Figure 7: Service principal permissions

You can see the “Storage Table Data Reader” permission in the Azure portal in the “Storage account” resource under “Access Control (IAM) > Role Assignments”:

Figure 8: Service principal roles Figure 8: Service principal roles

Configure the Logic App workflow

Open the Logic App Designer

Now that we have met and configured all the prerequisites, we can finally start developing our Logic App workflow. Navigate to the Logic App in the Azure portal and open the “Logic App Designer”. Currently, the Logic App consists of only two steps:

  • A trigger “Recurrence 0500am every day”, which starts the workflow every day at 05:00 UTC+1
  • and an action “Compose ProvisioningAPI Settings”, which creates a JSON object that we need later.

Define the API properties

Edit the “Compose ProvisioningAPI Settings” action and replace the “Uri” entry with your own “Provisioning API Endpoint”. You can find this information in the “API-drive provisioning to Microsoft Entra ID” enterprise application, under Provisioning > Overview > View technical information.

The action should now look roughly like this:

{
  "Audience": "https://graph.microsoft.com",
  "Uri": "https://graph.microsoft.com/beta/servicePrincipals/abdefghi-1234-5678-jklm-nopqrstuvwxy/synchronization/jobs/API2AAD.b649b126ed68488484d86210336bb33f.21803b3e-5c34-4571-b37a-bc1ce9ec3556/bulkUpload"
}

Read the data source

Add another action “Get entities (V2)” of the type “Azure Table Storage”. Enter a name for the connection object (e.g. “conn-sthcmprovisioning”), select “Logic Apps Managed Identity” as the authentication type and then click “Create.”

Under Parameters

  • for Storage Account Name, select “enter custom value” and enter the name of your Azure Storage account (e.g. “sthcmprovisioning1234”)
  • for Table, select “employeedemo” in the drop-down.

To make the workflow easier to read, click the title of the action and change the name to “Get employee data from storage table”.

Create SCIM objects

We have to create a SCIM/JSON object for each entry in the data source. To do that,

  • create a “For each” action of the type “Control”,
  • select “Get entities result / List of Entities” as the parameter,
  • change the name of the “For each” action to “For each employee”.

Then create another action inside the “For each employee” block:

  • This time we need a “Compose” action of the type “Data Operations”.
  • Change the name to “Compose employee SCIM”.
  • Copy the JSON code below into the “Inputs” field. The code contains a SCIM schema that maps the fields of our data source to Microsoft Entra (e.g. FullName => displayName). It also sets the active status based on the field WorkerStatus in our data source.
{
  "bulkId": "@{guid()}",
  "data": {
    "schemas": [
      "urn:ietf:params:scim:schemas:core:2.0:User",
      "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
    ],
    "userName": "@{items('For_each_employee')?['UserID']}",
    "displayName": "@{items('For_each_employee')?['FullName']}",
    "externalId": "@{items('For_each_employee')?['WorkerID']}",
    "name": {
      "familyName": "@{items('For_each_employee')?['LastName']}",
      "givenName": "@{items('For_each_employee')?['FirstName']}"
    },
    "active": @{if(equals(items('For_each_employee')?['WorkerStatus'],'Active'),true,false)}
  },
  "method": "POST",
  "path": "/Users"
}

The action should now look like this:

Figure 9: Create a SCIM object

Figure 9: Create a SCIM object

Combine SCIM objects

Now we have to combine the individual SCIM objects into a single bulk SCIM document that contains the data of all employees

To do so, create a new “Join” action of the type “Data Operations”,

  • click the “From” parameter and select insert expression
  • paste outputs('Compose_employee_SCIM') into the text field and click “Update”.
  • Use a comma (“,”) for the “Join With” parameter.
  • Change the name of the action to “Join employee SCIM”.

Then create another “Compose” action. Here we use a combination of JSON syntax and Logic App expressions to create our SCIM document.

  • Name the action “Compose SCIM bulk payload”
  • and copy the following JSON code into the Inputs field:
{
  "schemas": [
    "urn:ietf:params:scim:api:messages:2.0:BulkRequest"
  ],
  "Operations": @{json(concat('[',body('Join_employee_SCIM'),']'))},
  "failOnErrors": null
}

Send the SCIM document to the API

In the last step of our workflow, we send our SCIM document to the API endpoint.

Add a new “HTTP” action and

  • use the expression outputs('Compose_ProvisioningAPI_Settings')?['Uri'] as the URI,
  • select POST as the method,
  • in the “Headers” section, add a new entry Content-Type with the value application/scim+json,
  • use the “Compose SCIM bulk payload” action as the body,
  • select “Managed Identity” as the authentication type
  • and add the expression outputs('Compose_ProvisioningAPI_Settings')?['Audience'] in the Audience field.

Change the name of the action to “POST SCIM payload to API”.

You can find the complete workflow definition in the appendix of this article.

Provisioning logs

We are now ready to run the Logic App workflow. Click “Run” and wait until the workflow has sent the SCIM document to the API endpoint. As the result of the “POST SCIM payload to API” action you should see “HTTP Status 202 (Accepted)”. This means the API has received the request and will start processing it shortly.

In the meantime, navigate to the API-driven inbound provisioning application in Microsoft Entra. Click “Provisioning” and then open the “Provisioning Logs” to display details about the provisioning operations, as shown in Figure 10:

Figure 10: Provisioning logs Figure 10: Provisioning logs

Next steps

Now that you have seen our sample implementation, you may be wondering how to use your own HR data and connect your own “identity directory”…

Thanks to the flexibility of Azure Logic Apps and the large number of available connectors, you are not limited to using Azure Tables as a data source. Here are a few alternative scenarios for data sources that you could connect in your own environment:

The example shown here can serve as a basis for further Entra lifecycle workflows9 (“joiner-mover-leaver” processes).

Sources

  1. Microsoft Tech Community: Introducing a New Flexible Way of Bringing Identities from Any Source into Microsoft Entra ID!
  2. System for Cross-domain Identity Management: SCIM 2, the open API for managing identities is now complete and published under the IETF.
  3. Microsoft Entra ID Governance: What are lifecycle workflows?
  4. API-driven inbound provisioning tutorials: Quickstart with Azure Logic App
  5. Azure Storage: What is Azure Table storage?
  6. Logic Apps: Workflow definition language
  7. Managed identities for Azure resources: What are managed identities for Azure resources?
  8. Permissions for API-driven provisioning: Grant access to the inbound provisioning API
  9. Microsoft Entra ID Governance: Understanding lifecycle workflows

Appendix

Workflow definition (empty)

To create an Azure Logic App with PowerShell or the Azure CLI, a “workflow definition” has to be specified. You can use the following “empty” workflow definition to create a simple workflow with one trigger and one action:

{
  "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
  "actions": {
    "Compose_ProvisioningAPI_Settings": {
      "inputs": {
        "Audience": "https://graph.microsoft.com",
        "Uri": "<Your API-driven inbound provisioning URI>"
      },
      "runAfter": {},
      "type": "Compose"
    }
  },
  "contentVersion": "1.0.0.0",
  "outputs": {},
  "parameters": {
    "$connections": {
      "defaultValue": {},
      "type": "Object"
    }
  },
  "triggers": {
    "Recurrence_0500am_every_day": {
      "evaluatedRecurrence": {
        "frequency": "Day",
        "interval": 1,
        "schedule": {
          "hours": ["5"]
        },
        "timeZone": "W. Europe Standard Time"
      },
      "recurrence": {
        "frequency": "Day",
        "interval": 1,
        "schedule": {
          "hours": ["5"]
        },
        "timeZone": "W. Europe Standard Time"
      },
      "type": "Recurrence"
    }
  }
}

Workflow definition (complete)

You can use the following (anonymized) workflow definition as a reference for your own workflow:

{
  "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
  "actions": {
    "Compose_ProvisioningAPI_Settings": {
      "inputs": {
        "Audience": "https://graph.microsoft.com",
        "Uri": "https://graph.microsoft.com/beta/servicePrincipals/abdefghi-1234-5678-jklm-nopqrstuvwxy/synchronization/jobs/API2AAD.b649b126ed68488484d86210336bb33f.21803b3e-5c34-4571-b37a-bc1ce9ec3556/bulkUpload"
      },
      "runAfter": {},
      "type": "Compose"
    },
    "Compose_SCIM_bulk_payload": {
      "inputs": {
        "Operations": "@json(\r\n  concat('[',body('Join_employee_SCIM'),']')\r\n)",
        "failOnErrors": null,
        "schemas": ["urn:ietf:params:scim:api:messages:2.0:BulkRequest"]
      },
      "runAfter": {
        "Join_employee_SCIM": ["Succeeded"]
      },
      "type": "Compose"
    },
    "For_each_employee": {
      "actions": {
        "Compose_employee_SCIM": {
          "inputs": {
            "bulkId": "@{guid()}",
            "data": {
              "active": "@if(equals(items('For_each_employee')?['WorkerStatus'],'Active'),true,false)",
              "displayName": "@{items('For_each_employee')?['FullName']}",
              "externalId": "@{items('For_each_employee')?['WorkerID']}",
              "name": {
                "familyName": "@{items('For_each_employee')?['LastName']}",
                "givenName": "@{items('For_each_employee')?['FirstName']}"
              },
              "schemas": [
                "urn:ietf:params:scim:schemas:core:2.0:User",
                "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
              ],
              "userName": "@{items('For_each_employee')?['UserID']}"
            },
            "method": "POST",
            "path": "/Users"
          },
          "type": "Compose"
        }
      },
      "foreach": "@body('Get_employee_data_from_storage_table')?['value']",
      "runAfter": {
        "Get_employee_data_from_storage_table": ["Succeeded"]
      },
      "type": "Foreach"
    },
    "Get_employee_data_from_storage_table": {
      "inputs": {
        "host": {
          "connection": {
            "name": "@parameters('$connections')['azuretables']['connectionId']"
          }
        },
        "method": "get",
        "path": "/v2/storageAccounts/@{encodeURIComponent(encodeURIComponent('sthcmprovisioning1234'))}/tables/@{encodeURIComponent('employeedemo')}/entities"
      },
      "runAfter": {
        "Compose_ProvisioningAPI_Settings": ["Succeeded"]
      },
      "type": "ApiConnection"
    },
    "Join_employee_SCIM": {
      "inputs": {
        "from": "@outputs('Compose_employee_SCIM')",
        "joinWith": ","
      },
      "runAfter": {
        "For_each_employee": ["Succeeded"]
      },
      "type": "Join"
    },
    "POST_SCIM_payload_to_API": {
      "inputs": {
        "authentication": {
          "audience": "@{outputs('Compose_ProvisioningAPI_Settings')?['Audience']}",
          "type": "ManagedServiceIdentity"
        },
        "body": "@outputs('Compose_SCIM_bulk_payload')",
        "headers": {
          "Content-Type": "application/scim+json"
        },
        "method": "POST",
        "uri": "@outputs('Compose_ProvisioningAPI_Settings')?['Uri']"
      },
      "runAfter": {
        "Compose_SCIM_bulk_payload": ["Succeeded"]
      },
      "type": "Http"
    }
  },
  "contentVersion": "1.0.0.0",
  "outputs": {},
  "parameters": {
    "$connections": {
      "defaultValue": {},
      "type": "Object"
    }
  },
  "triggers": {
    "Recurrence_0500am_every_day": {
      "evaluatedRecurrence": {
        "frequency": "Day",
        "interval": 1,
        "schedule": {
          "hours": ["5"]
        },
        "timeZone": "W. Europe Standard Time"
      },
      "recurrence": {
        "frequency": "Day",
        "interval": 1,
        "schedule": {
          "hours": ["5"]
        },
        "timeZone": "W. Europe Standard Time"
      },
      "type": "Recurrence"
    }
  }
}

In the graphical editor, the workflow should look like this:

Figure 11: Complete Logic App workflow Figure 11: Complete Logic App workflow