Configure Microsoft Entra roles for multi-tenant SaaS SSO
On this page
- How SSO works in multi-tenant SaaS
- Prerequisites
- CluedIn role values
- Method 1: Configure roles with Microsoft Graph Explorer
- Method 2: Configure all roles with Microsoft Graph PowerShell
- Optional: assign Microsoft Entra groups with the script
- Assign users or groups in Microsoft Entra admin center
- Enable Automatic Role Synchronization in CluedIn
- Verify the configuration
- Roll back app-role changes
- Troubleshooting
- Microsoft references
This article explains how to configure Microsoft Entra application roles for CluedIn multi-tenant (MT) SaaS when using Microsoft Entra ID single sign-on (SSO) with automatic role synchronization.
This article applies to CluedIn multi-tenant SaaS. It does not describe the application-registration flow used by CluedIn PaaS or Private SaaS deployments.
How SSO works in multi-tenant SaaS
CluedIn multi-tenant SaaS uses a CluedIn-managed, multi-tenant Microsoft Entra application registration. The application registration is owned by CluedIn and is not created in the customer tenant.
When a customer enables Microsoft Entra SSO, Microsoft Entra creates or uses a local Enterprise application in the customer tenant. In Microsoft Entra terminology, this Enterprise application is the service principal: the local representation of the CluedIn multi-tenant application in that tenant.
Because the customer does not own the CluedIn application registration, the customer cannot add app roles through App registrations > App roles. Instead, tenant-specific app roles are added to the customer tenant’s CluedIn service principal by using Microsoft Graph.
Microsoft supports this scenario explicitly: app roles that should exist only in one tenant for a multi-tenant application can be added to the appRoles collection of the service principal.
The role flow is:
- Create the CluedIn app roles on the CluedIn Enterprise application (service principal) in the customer tenant.
- Assign users or Microsoft Entra groups to those roles.
- Microsoft Entra emits the assigned role values in the
rolesclaim when the user signs in. - CluedIn reads the role claim and maps the values to CluedIn roles when Automatic Role Synchronization is enabled.
The Value of the Microsoft Entra app role is the value sent in the roles claim. Use the CluedIn role values shown in this article exactly. The display name can be more descriptive, but the value should remain stable.
Prerequisites
Before configuring roles, make sure that:
- Microsoft Entra SSO has already been enabled for the CluedIn organization.
- The CluedIn Enterprise application exists in the customer Microsoft Entra tenant.
- You can access Microsoft Entra admin center > Enterprise applications.
- You have sufficient Microsoft Entra administrative rights. Microsoft specifically documents Privileged Role Administrator for the Graph Explorer role-management procedure. A Global Administrator also has sufficient administrative rights.
- If you use Microsoft Graph Explorer, Graph Explorer can receive the required delegated Microsoft Graph permissions in the tenant.
- Automatic Role Synchronization is enabled in CluedIn before testing role synchronization.
CluedIn role values
The following role values can be used for the standard CluedIn roles.
| Display name | Value | Description |
|---|---|---|
| Data Governance Administrator | DataGovernanceAdministrator | Role responsible for approving changes made by Data Governance users. |
| Data Compliance | DataCompliance | Role responsible for daily operations around data compliance. |
| Data Steward | DataSteward | Role dedicated to cleaning data using Clean and Prepare modules. |
| Data Compliance Administrator | DataComplianceAdministrator | Role responsible for approving changes made by Data Compliance users. |
| Guest | Guest | Guest user with minimal, read-only permissions. |
| User | User | User who can view all modules as read-only. |
| Data Architect | DataArchitect | Role responsible for designing an organization’s enterprise data strategy. |
| Deduplication Reviewer | DeduplicationReviewer | Role responsible for reviewing deduplication project results and approving groupings. |
| Organization User | OrganizationUser | User within an organization who can view all modules as read-only. |
| Data Governance | DataGovernance | Role responsible for monitoring and maintaining data quality. |
| Report Manager | ReportManager | User who can generate reports for compliance matters such as breach, subject request, and retention. |
| Organization Admin | OrganizationAdmin | Administrator within the organization. |
| Deduplication Administrator | DeduplicationAdministrator | Role responsible for creating and maintaining deduplication projects and merging the results back into the system. |
| Data Steward Administrator | DataStewardAdministrator | Role responsible for approving changes made by Data Stewards. |
Method 1: Configure roles with Microsoft Graph Explorer
Graph Explorer is useful for a one-off configuration or for inspecting the service principal before automation.
Find the CluedIn service principal Object ID
- Sign in to the Microsoft Entra admin center.
- Go to Entra ID > Enterprise applications > All applications.
- Open the CluedIn application used for SSO.
- On Overview, copy the Object ID.
Use the Object ID of the Enterprise application, not the Application (client) ID. Microsoft Graph addresses this local service principal by its Object ID.
Grant Graph Explorer permissions
Open Microsoft Graph Explorer, sign in to the customer tenant, and verify the tenant shown in the Graph Explorer header.
Microsoft’s dedicated Enterprise application role-management guidance instructs administrators to use Privileged Role Administrator and consent to:
Application.ReadWrite.AllDirectory.ReadWrite.All
The current Microsoft Graph PATCH /servicePrincipals/{id} reference lists Application.ReadWrite.All as the least-privileged delegated permission and Directory.ReadWrite.All as a higher-privileged alternative. If your organization follows least privilege strictly, Application.ReadWrite.All is sufficient for the service-principal update API; the dedicated Microsoft role-management walkthrough currently requests both permissions.
In Graph Explorer, select your profile avatar and choose Consent to permissions. Search for the permissions and grant consent.
If the Consent to permissions list is empty
An empty permissions list is not expected. Before trying to change the CluedIn Enterprise application, verify Graph Explorer itself is correctly consented in the tenant:
- In Microsoft Entra admin center, go to Enterprise applications.
- Search for Graph explorer.
- Open the Graph Explorer Enterprise application.
- Open Permissions.
- Select Grant admin consent for the tenant if required.
- Sign out of Graph Explorer, sign in again, and verify the correct tenant.
If the permissions list remains empty, use the PowerShell method later in this article. The PowerShell method does not depend on the Graph Explorer permissions UI.
Read the existing app roles
In Graph Explorer, select GET and run:
https://graph.microsoft.com/v1.0/servicePrincipals/<SERVICE-PRINCIPAL-OBJECT-ID>?$select=id,appId,displayName,appRoles
Save the returned appRoles collection before making any changes.
A PATCH of appRoles replaces the collection. Do not send only the new CluedIn roles. Existing roles must also be included or they can be removed from the service principal.
A service principal can contain an existing role such as msiam_access. Preserve it.
Microsoft Graph returns the read-only origin property on app roles. The appRole API documentation states that origin must not be included in POST or PATCH requests, so remove origin from each role object before sending the PATCH.
Add a role
Each role requires a unique GUID in its id property. For example, to add the OrganizationAdmin role, append an object like this to the existing appRoles array:
{
"allowedMemberTypes": [
"User"
],
"description": "Administrator within the organization.",
"displayName": "Organization Admin",
"id": "<NEW-GUID>",
"isEnabled": true,
"value": "OrganizationAdmin"
}
Generate a new GUID for each new role. For example, in PowerShell:
[guid]::NewGuid().Guid
After adding all required roles, change Graph Explorer to PATCH and use:
https://graph.microsoft.com/v1.0/servicePrincipals/<SERVICE-PRINCIPAL-OBJECT-ID>
The request body must contain the complete role collection:
{
"appRoles": [
{
"allowedMemberTypes": ["User"],
"description": "<EXISTING-ROLE-DESCRIPTION>",
"displayName": "<EXISTING-ROLE-DISPLAY-NAME>",
"id": "<EXISTING-ROLE-ID>",
"isEnabled": true,
"value": null
},
{
"allowedMemberTypes": ["User"],
"description": "Administrator within the organization.",
"displayName": "Organization Admin",
"id": "<NEW-GUID>",
"isEnabled": true,
"value": "OrganizationAdmin"
}
]
}
A successful update normally returns 204 No Content.
Run the GET request again and verify that all expected roles are present.
Method 2: Configure all roles with Microsoft Graph PowerShell
For repeatable deployments, PowerShell is safer than manually editing the appRoles collection. The script in this section:
- connects to a specific tenant;
- reads and preserves all existing app roles;
- creates any missing standard CluedIn roles;
- updates the metadata of matching CluedIn roles while preserving their existing role IDs;
- removes the read-only
originproperty from the PATCH payload; - creates a timestamped JSON backup before changing anything;
- supports
-WhatIf; - can export the exact PATCH body for review or use in Graph Explorer;
- verifies the roles after the PATCH;
- optionally assigns Microsoft Entra groups to roles from a CSV file.
Install Microsoft Graph PowerShell
PowerShell 7 is recommended.
Install-Module Microsoft.Graph.Authentication -Scope CurrentUser
The script uses Invoke-MgGraphRequest, so the Authentication module is sufficient for the Graph calls.
PowerShell script
Save the following as Set-CluedInMtSaasAppRoles.ps1.
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
param(
[Parameter(Mandatory = $true)]
[Guid]$TenantId,
[Parameter(Mandatory = $true)]
[Guid]$ServicePrincipalObjectId,
[switch]$UseDeviceCode,
[ValidateNotNullOrEmpty()]
[string]$BackupDirectory = '.',
[string]$ExportPatchPath,
# Optional CSV with columns: GroupObjectId,RoleValue
[string]$GroupRoleMappingCsv
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
function Assert-MicrosoftGraphAuthenticationModule {
if (-not (Get-Module -ListAvailable -Name Microsoft.Graph.Authentication)) {
throw @'
Microsoft.Graph.Authentication is not installed.
Install it with:
Install-Module Microsoft.Graph.Authentication -Scope CurrentUser
'@
}
Import-Module Microsoft.Graph.Authentication -ErrorAction Stop
}
function Connect-CluedInGraph {
param(
[Parameter(Mandatory = $true)]
[Guid]$Tenant,
[switch]$DeviceCode,
[switch]$EnableGroupAssignments
)
$scopes = @('Application.ReadWrite.All')
if ($EnableGroupAssignments) {
$scopes += @(
'AppRoleAssignment.ReadWrite.All',
'Group.Read.All'
)
}
$connectParameters = @{
TenantId = $Tenant.Guid
Scopes = $scopes
ContextScope = 'Process'
NoWelcome = $true
}
if ($DeviceCode) {
$connectParameters['UseDeviceCode'] = $true
}
Connect-MgGraph @connectParameters
$context = Get-MgContext
if (-not $context) {
throw 'Microsoft Graph authentication did not return a context.'
}
if ($context.TenantId -ne $Tenant.Guid) {
throw "Connected to tenant '$($context.TenantId)' instead of '$($Tenant.Guid)'."
}
$missingScopes = @(
$scopes | Where-Object { $context.Scopes -notcontains $_ }
)
if ($missingScopes.Count -gt 0) {
throw "The access token is missing required scopes: $($missingScopes -join ', ')"
}
Write-Host "Connected account : $($context.Account)"
Write-Host "Connected tenant : $($context.TenantId)"
Write-Host "Granted scopes : $($context.Scopes -join ', ')"
}
function Get-CluedInRoleDefinitions {
@(
[pscustomobject]@{
DisplayName = 'Data Governance Administrator'
Value = 'DataGovernanceAdministrator'
Description = 'Role responsible for approving changes made by Data Governance users.'
}
[pscustomobject]@{
DisplayName = 'Data Compliance'
Value = 'DataCompliance'
Description = 'Role responsible for daily operations around data compliance.'
}
[pscustomobject]@{
DisplayName = 'Data Steward'
Value = 'DataSteward'
Description = 'Role dedicated to cleaning data using Clean and Prepare modules.'
}
[pscustomobject]@{
DisplayName = 'Data Compliance Administrator'
Value = 'DataComplianceAdministrator'
Description = 'Role responsible for approving changes made by Data Compliance users.'
}
[pscustomobject]@{
DisplayName = 'Guest'
Value = 'Guest'
Description = 'Guest user with minimal, read-only permissions.'
}
[pscustomobject]@{
DisplayName = 'User'
Value = 'User'
Description = 'User who can view all modules as read-only.'
}
[pscustomobject]@{
DisplayName = 'Data Architect'
Value = 'DataArchitect'
Description = "Role responsible for designing an organization's enterprise data strategy."
}
[pscustomobject]@{
DisplayName = 'Deduplication Reviewer'
Value = 'DeduplicationReviewer'
Description = 'Role responsible for reviewing deduplication project results and approving groupings.'
}
[pscustomobject]@{
DisplayName = 'Organization User'
Value = 'OrganizationUser'
Description = 'User within an organization who can view all modules as read-only.'
}
[pscustomobject]@{
DisplayName = 'Data Governance'
Value = 'DataGovernance'
Description = 'Role responsible for monitoring and maintaining data quality.'
}
[pscustomobject]@{
DisplayName = 'Report Manager'
Value = 'ReportManager'
Description = 'User who can generate reports for compliance matters such as breach, subject request, and retention.'
}
[pscustomobject]@{
DisplayName = 'Organization Admin'
Value = 'OrganizationAdmin'
Description = 'Administrator within the organization.'
}
[pscustomobject]@{
DisplayName = 'Deduplication Administrator'
Value = 'DeduplicationAdministrator'
Description = 'Role responsible for creating and maintaining deduplication projects and merging the results back into the system.'
}
[pscustomobject]@{
DisplayName = 'Data Steward Administrator'
Value = 'DataStewardAdministrator'
Description = 'Role responsible for approving changes made by Data Stewards.'
}
)
}
function Get-ServicePrincipalWithAppRoles {
param(
[Parameter(Mandatory = $true)]
[Guid]$ObjectId
)
$uri = "https://graph.microsoft.com/v1.0/servicePrincipals/$($ObjectId.Guid)?`$select=id,appId,displayName,appRoleAssignmentRequired,appRoles"
$servicePrincipal = Invoke-MgGraphRequest `
-Method GET `
-Uri $uri `
-OutputType PSObject
if (-not $servicePrincipal.id) {
throw "Service principal '$($ObjectId.Guid)' was not found."
}
return $servicePrincipal
}
function ConvertTo-PatchableAppRole {
param(
[Parameter(Mandatory = $true)]
$Role
)
# Do not include origin. Microsoft Graph documents origin as read-only.
[ordered]@{
allowedMemberTypes = @($Role.allowedMemberTypes)
description = [string]$Role.description
displayName = [string]$Role.displayName
id = [string]$Role.id
isEnabled = [bool]$Role.isEnabled
value = $Role.value
}
}
function Merge-CluedInAppRoles {
param(
[Parameter(Mandatory = $true)]
$ExistingRoles,
[Parameter(Mandatory = $true)]
$Definitions
)
$existing = @($ExistingRoles)
$merged = [System.Collections.Generic.List[object]]::new()
$changes = [System.Collections.Generic.List[object]]::new()
$duplicateValues = @(
$existing |
Where-Object { $null -ne $_.value -and -not [string]::IsNullOrWhiteSpace([string]$_.value) } |
Group-Object -Property value |
Where-Object Count -gt 1
)
if ($duplicateValues.Count -gt 0) {
$values = $duplicateValues.Name -join ', '
throw "The service principal contains duplicate app role values: $values"
}
# Start by preserving every existing role, including non-CluedIn roles.
foreach ($role in $existing) {
$merged.Add((ConvertTo-PatchableAppRole -Role $role))
}
foreach ($definition in $Definitions) {
$matchingIndex = -1
for ($i = 0; $i -lt $merged.Count; $i++) {
if ($null -ne $merged[$i].value -and
([string]$merged[$i].value -ceq [string]$definition.Value)) {
$matchingIndex = $i
break
}
}
if ($matchingIndex -ge 0) {
$existingRole = $merged[$matchingIndex]
$changed = $false
if ([string]$existingRole.displayName -cne [string]$definition.DisplayName) {
$existingRole.displayName = $definition.DisplayName
$changed = $true
}
if ([string]$existingRole.description -cne [string]$definition.Description) {
$existingRole.description = $definition.Description
$changed = $true
}
if (-not $existingRole.isEnabled) {
$existingRole.isEnabled = $true
$changed = $true
}
if (@($existingRole.allowedMemberTypes).Count -ne 1 -or
@($existingRole.allowedMemberTypes)[0] -cne 'User') {
$existingRole.allowedMemberTypes = @('User')
$changed = $true
}
$changes.Add([pscustomobject]@{
Action = if ($changed) { 'Update' } else { 'Keep' }
Value = $definition.Value
Id = $existingRole.id
})
}
else {
$newRole = [ordered]@{
allowedMemberTypes = @('User')
description = $definition.Description
displayName = $definition.DisplayName
id = (New-Guid).Guid
isEnabled = $true
value = $definition.Value
}
$merged.Add($newRole)
$changes.Add([pscustomobject]@{
Action = 'Create'
Value = $definition.Value
Id = $newRole.id
})
}
}
[pscustomobject]@{
Roles = @($merged)
Changes = @($changes)
}
}
function Save-AppRoleBackup {
param(
[Parameter(Mandatory = $true)]
$ServicePrincipal,
[Parameter(Mandatory = $true)]
[string]$Directory
)
if (-not (Test-Path -LiteralPath $Directory)) {
New-Item -ItemType Directory -Path $Directory -Force | Out-Null
}
$timestamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd-HHmmss')
$path = Join-Path $Directory "cluedin-appRoles-$timestamp.json"
$backup = [ordered]@{
capturedUtc = (Get-Date).ToUniversalTime().ToString('o')
servicePrincipalId = [string]$ServicePrincipal.id
servicePrincipalAppId = [string]$ServicePrincipal.appId
servicePrincipalName = [string]$ServicePrincipal.displayName
appRoleAssignmentRequired = [bool]$ServicePrincipal.appRoleAssignmentRequired
appRoles = @($ServicePrincipal.appRoles)
}
$backup |
ConvertTo-Json -Depth 20 |
Set-Content -LiteralPath $path -Encoding utf8
return (Resolve-Path -LiteralPath $path).Path
}
function Get-AllGraphCollectionItems {
param(
[Parameter(Mandatory = $true)]
[string]$Uri
)
$items = [System.Collections.Generic.List[object]]::new()
$next = $Uri
while ($next) {
$response = Invoke-MgGraphRequest -Method GET -Uri $next -OutputType PSObject
foreach ($item in @($response.value)) {
$items.Add($item)
}
$nextProperty = $response.PSObject.Properties['@odata.nextLink']
if ($nextProperty) {
$next = [string]$nextProperty.Value
}
else {
$next = $null
}
}
return @($items)
}
function Add-CluedInGroupRoleAssignments {
param(
[Parameter(Mandatory = $true)]
$ServicePrincipal,
[Parameter(Mandatory = $true)]
[string]$CsvPath
)
if (-not (Test-Path -LiteralPath $CsvPath)) {
throw "Group-role mapping CSV '$CsvPath' does not exist."
}
$rows = @(Import-Csv -LiteralPath $CsvPath)
if ($rows.Count -eq 0) {
Write-Host 'The group-role mapping CSV is empty. No assignments to create.'
return
}
$roleByValue = @{}
foreach ($role in @($ServicePrincipal.appRoles)) {
if ($null -ne $role.value -and -not [string]::IsNullOrWhiteSpace([string]$role.value)) {
$roleByValue[[string]$role.value] = $role
}
}
$assignmentUri = "https://graph.microsoft.com/v1.0/servicePrincipals/$($ServicePrincipal.id)/appRoleAssignedTo?`$select=id,principalId,principalDisplayName,principalType,appRoleId"
$existingAssignments = @(Get-AllGraphCollectionItems -Uri $assignmentUri)
foreach ($row in $rows) {
if (-not $row.GroupObjectId -or -not $row.RoleValue) {
throw 'Each CSV row must contain GroupObjectId and RoleValue.'
}
$groupId = [Guid]$row.GroupObjectId
$roleValue = [string]$row.RoleValue
if (-not $roleByValue.ContainsKey($roleValue)) {
throw "Role value '$roleValue' does not exist on the CluedIn service principal."
}
$role = $roleByValue[$roleValue]
$groupUri = "https://graph.microsoft.com/v1.0/groups/$($groupId.Guid)?`$select=id,displayName"
$group = Invoke-MgGraphRequest -Method GET -Uri $groupUri -OutputType PSObject
$alreadyAssigned = @(
$existingAssignments | Where-Object {
([string]$_.principalId -eq [string]$group.id) -and
([string]$_.appRoleId -eq [string]$role.id)
}
).Count -gt 0
if ($alreadyAssigned) {
Write-Host "Already assigned: '$($group.displayName)' -> '$roleValue'"
continue
}
$body = @{
principalId = [string]$group.id
resourceId = [string]$ServicePrincipal.id
appRoleId = [string]$role.id
} | ConvertTo-Json
Invoke-MgGraphRequest `
-Method POST `
-Uri "https://graph.microsoft.com/v1.0/servicePrincipals/$($ServicePrincipal.id)/appRoleAssignedTo" `
-Body $body `
-ContentType 'application/json' | Out-Null
Write-Host "Assigned: '$($group.displayName)' -> '$roleValue'"
}
}
# ----- Main -----
Assert-MicrosoftGraphAuthenticationModule
Connect-CluedInGraph `
-Tenant $TenantId `
-DeviceCode:$UseDeviceCode `
-EnableGroupAssignments:([bool]$GroupRoleMappingCsv)
$servicePrincipal = Get-ServicePrincipalWithAppRoles -ObjectId $ServicePrincipalObjectId
Write-Host "Service principal : $($servicePrincipal.displayName)"
Write-Host "Object ID : $($servicePrincipal.id)"
Write-Host "Application ID : $($servicePrincipal.appId)"
$backupPath = Save-AppRoleBackup `
-ServicePrincipal $servicePrincipal `
-Directory $BackupDirectory
Write-Host "Backup : $backupPath"
$definitions = Get-CluedInRoleDefinitions
$mergeResult = Merge-CluedInAppRoles `
-ExistingRoles @($servicePrincipal.appRoles) `
-Definitions $definitions
$mergeResult.Changes |
Sort-Object Action, Value |
Format-Table Action, Value, Id -AutoSize
$patchBody = @{
appRoles = @($mergeResult.Roles)
} | ConvertTo-Json -Depth 20
if ($ExportPatchPath) {
$patchBody | Set-Content -LiteralPath $ExportPatchPath -Encoding utf8
Write-Host "PATCH body exported: $ExportPatchPath"
}
$patchUri = "https://graph.microsoft.com/v1.0/servicePrincipals/$($ServicePrincipalObjectId.Guid)"
if ($PSCmdlet.ShouldProcess(
"$($servicePrincipal.displayName) [$($servicePrincipal.id)]",
'Replace appRoles with the preserved and merged role collection')) {
Invoke-MgGraphRequest `
-Method PATCH `
-Uri $patchUri `
-Body $patchBody `
-ContentType 'application/json' | Out-Null
$updated = Get-ServicePrincipalWithAppRoles -ObjectId $ServicePrincipalObjectId
$missing = @(
$definitions | Where-Object {
$definition = $_
-not (@($updated.appRoles) | Where-Object {
$null -ne $_.value -and
([string]$_.value -ceq [string]$definition.Value)
})
}
)
if ($missing.Count -gt 0) {
throw "Verification failed. Missing role values: $($missing.Value -join ', ')"
}
Write-Host 'Role verification succeeded.'
@($updated.appRoles) |
Where-Object { $null -ne $_.value } |
Sort-Object value |
Select-Object displayName, value, id, isEnabled |
Format-Table -AutoSize
if ($GroupRoleMappingCsv) {
Add-CluedInGroupRoleAssignments `
-ServicePrincipal $updated `
-CsvPath $GroupRoleMappingCsv
}
}
Run the script safely
First use -WhatIf. This authenticates, reads the current Enterprise application, creates a backup, calculates the merged role collection, and shows the intended changes without sending the PATCH.
.\Set-CluedInMtSaasAppRoles.ps1 `
-TenantId '<CUSTOMER-TENANT-ID>' `
-ServicePrincipalObjectId '<CLUEDIN-ENTERPRISE-APP-OBJECT-ID>' `
-UseDeviceCode `
-BackupDirectory '.\backup' `
-ExportPatchPath '.\cluedin-appRoles-patch.json' `
-WhatIf
Review the output and the exported JSON. Then run again without -WhatIf:
.\Set-CluedInMtSaasAppRoles.ps1 `
-TenantId '<CUSTOMER-TENANT-ID>' `
-ServicePrincipalObjectId '<CLUEDIN-ENTERPRISE-APP-OBJECT-ID>' `
-UseDeviceCode `
-BackupDirectory '.\backup' `
-ExportPatchPath '.\cluedin-appRoles-patch.json'
The -UseDeviceCode option is particularly useful when interactive authentication through Windows Web Account Manager (WAM) fails or cannot open a browser window.
For example, if Connect-MgGraph returns an error similar to:
InteractiveBrowserCredential authentication failed: A window handle must be configured.
run the script with -UseDeviceCode.
Optional: assign Microsoft Entra groups with the script
You can assign groups after the roles have been created. Create a CSV such as cluedin-role-groups.csv:
GroupObjectId,RoleValue
11111111-1111-1111-1111-111111111111,OrganizationAdmin
22222222-2222-2222-2222-222222222222,DataSteward
33333333-3333-3333-3333-333333333333,DataGovernance
Use the Microsoft Entra Object ID of each group.
Then run:
.\Set-CluedInMtSaasAppRoles.ps1 `
-TenantId '<CUSTOMER-TENANT-ID>' `
-ServicePrincipalObjectId '<CLUEDIN-ENTERPRISE-APP-OBJECT-ID>' `
-UseDeviceCode `
-BackupDirectory '.\backup' `
-GroupRoleMappingCsv '.\cluedin-role-groups.csv'
When group assignment is enabled, the script additionally requests:
AppRoleAssignment.ReadWrite.AllGroup.Read.All
Application.ReadWrite.All is already requested for managing the service principal. Microsoft documents AppRoleAssignment.ReadWrite.All together with application read permissions for creating app-role assignments.
If you do not want the script to manage group assignments, omit -GroupRoleMappingCsv and assign users or groups through the Microsoft Entra admin center instead.
Assign users or groups in Microsoft Entra admin center
After the roles exist on the Enterprise application:
- Go to Microsoft Entra admin center > Enterprise applications.
- Open the CluedIn Enterprise application.
- Select Users and groups.
- Select Add user/group.
- Select the user or group.
- Select the CluedIn role.
- Select Assign.
Repeat this for the required role assignments.
Optional: require explicit assignment
The Enterprise application has an Assignment required? setting. When enabled, users generally need to be explicitly assigned to the application before Microsoft Entra allows access.
Do not enable Assignment required? until you have confirmed that all required administrators and user groups have assignments. Enabling it too early can prevent users from signing in.
Enable Automatic Role Synchronization in CluedIn
For Microsoft Entra-managed roles to control CluedIn membership, Automatic Role Synchronization must be enabled for the organization.
After changing a role assignment in Microsoft Entra, the user should sign out of CluedIn and sign in again so that Microsoft Entra issues a new token containing the current roles claim.
Verify the configuration
After configuring the roles and assignments:
- Run the service-principal GET request again and verify that the required app roles are present.
- In Microsoft Entra admin center, confirm the user or group assignment under Enterprise applications > CluedIn > Users and groups.
- Confirm Automatic Role Synchronization is enabled in CluedIn.
- Sign out of CluedIn completely and sign back in.
- In CluedIn, verify that the expected role is assigned to the user.
If the Microsoft Entra assignment is correct but the CluedIn role is not updated, verify the exact app-role Value first. For example, use OrganizationAdmin, not Organization Admin.
Roll back app-role changes
The PowerShell script saves the original service-principal state before changing it. To restore a backup, use the appRoles from the backup file, but remove the read-only origin property before PATCHing.
The following example converts a backup into a valid rollback body:
$backup = Get-Content '.\backup\cluedin-appRoles-YYYYMMDD-HHMMSS.json' -Raw |
ConvertFrom-Json
$roles = foreach ($role in @($backup.appRoles)) {
[ordered]@{
allowedMemberTypes = @($role.allowedMemberTypes)
description = $role.description
displayName = $role.displayName
id = $role.id
isEnabled = $role.isEnabled
value = $role.value
}
}
$body = @{ appRoles = @($roles) } | ConvertTo-Json -Depth 20
Invoke-MgGraphRequest `
-Method PATCH `
-Uri 'https://graph.microsoft.com/v1.0/servicePrincipals/<SERVICE-PRINCIPAL-OBJECT-ID>' `
-Body $body `
-ContentType 'application/json'
Restoring an old app-role collection can remove roles created after the backup. Review the rollback JSON before applying it.
Troubleshooting
Graph Explorer Consent to permissions is empty
Verify that Graph Explorer is signed into the correct customer tenant. Then check the Graph explorer Enterprise application in Microsoft Entra and grant admin consent if required. If the list remains empty, use Microsoft Graph PowerShell with -UseDeviceCode.
Connect-MgGraph reports a WAM window-handle error
Use device-code authentication:
Connect-MgGraph `
-TenantId '<CUSTOMER-TENANT-ID>' `
-Scopes 'Application.ReadWrite.All' `
-UseDeviceCode `
-ContextScope Process `
-NoWelcome
Graph returns 403 Authorization_RequestDenied
Check both parts of authorization:
- the signed-in administrator has a suitable Microsoft Entra directory role;
- the Graph access token contains the required delegated scope.
For Graph Explorer role management, use Privileged Role Administrator or Global Administrator. For the PowerShell role-creation script, verify Application.ReadWrite.All appears in:
Get-MgContext | Format-List Account,TenantId,Scopes
PATCH removes an existing role
The appRoles property is a collection. A PATCH that contains an incomplete collection can remove entries that were omitted. Restore the collection from a backup, then re-run the merge using the full existing collection.
Graph rejects origin
Remove origin from the request body. Microsoft Graph returns it when reading app roles, but documents it as read-only and not valid in POST or PATCH requests.
The role exists but CluedIn does not receive it
Check the following:
- the user or group is actually assigned to that role on the CluedIn Enterprise application;
- the role
valuematches the intended CluedIn role value; - Automatic Role Synchronization is enabled;
- the user signed out and signed back in after the assignment changed.
Microsoft references
- Configure the role claim - Microsoft identity platform
- appRole resource type - Microsoft Graph
- Update servicePrincipal - Microsoft Graph
- Create an enterprise application from a multitenant application
- Work with Graph Explorer
- Prerequisites to use PowerShell or Graph Explorer for Microsoft Entra roles
- Grant an appRoleAssignment for a service principal