Forum Discussion
401 Unauthorized when calling Power BI Admin Add User API with Service Principal (Fabric Administrat
Hello everyone,
I’m trying to add a Service Principal as Contributor to all Power BI workspaces using the Admin REST API, but I consistently receive HTTP 401 Unauthorized, even though all required permissions appear to be correctly configured.
Scenario
Azure Automation Runbook
Authentication via client_credentials (OAuth 2.0 v2 endpoint)
Caller is a Service Principal
API used:
POST https://api.powerbi.com/v1.0/myorg/admin/groups/{workspaceId}/usersWhat the script does (high level)
Acquires a Power BI access token using client credentials
Calls Admin – GetGroupsAsAdmin successfully
Enumerates all workspaces (non-personal, active)
Attempts to add a Service Principal (principalType = App) as Contributor to each workspace
All POST calls fail with 401 Unauthorized
Error returned
HTTP 401 UnauthorizedThis happens for every workspace, even though enumeration works correctly.
What is working
Token acquisition succeeds
/admin/groups returns all workspaces
/groups returns some workspaces after execution
JWT token validation confirms:
appid matches Client ID
oid matches Service Principal Object ID
aud = https://analysis.windows.net/powerbi/api
Tenant & Security Configuration (Verified)
✅ Fabric Administrator role
The App Registration / Service Principal is assigned to the Fabric Administrator role in Entra ID
✅ Tenant settings (Fabric Admin portal)
Both settings are Enabled and scoped to security groups that contain the Service Principal:Service principals can access read-only admin APIs
Service principals can access admin APIs used for updates
Verified via:
GET https://api.fabric.microsoft.com/v1/admin/tenantsettingsImportant observation
Even though the Service Principal:
Is Fabric Administrator
Can call Admin read APIs
Has update admin APIs enabled
The Admin Add User API still returns 401 Unauthorized.
Question
Is there a recent change or limitation where:
Service Principals can no longer call
POST /admin/groups/{id}/users,
even when assigned Fabric Administrator?Or is there an additional hidden requirement (license, workspace ownership, tenant flag, or unsupported scenario)?
Any clarification from the Power BI / Fabric team would be greatly appreciated.
Thank you.
Hi AbdelmonemKaabi ,
To clarify, a Service Principal does not need to be a workspace Admin to use:
POST /admin/groups/{workspaceId}/usersThis API works with apponly authentication and is designed for tenant level automation scenarios like yours.
Since Admin read APIs are functioning and your script runs successfully locally, the 401 error indicates the identity or token used during the Azure Automation run is not being authorized.
Please check that the Service Principal used in Automation:
1. Is the same Client ID assigned the Fabric Administrator role & Is part of the tenant setting security group
2. Receives a token with Tenant.ReadWrite.All
3. Uses the Application (Client) ID, not the Object ID, as the identifier
There are no known Fabric side restrictions preventing this method.
Regards,
Yugandhar
15 Replies
- lbendlin
Super User
what's the scp of your acquired token?
- AbdelmonemKaabiFrequent Visitor
I currently have only Microsoft Graph permissions: Application permissions (InformationProtectionPolicy.Read.All and User.Read.All) and Delegated permission (User.Read). I then added the Power BI application permission (Tenant.ReadWrite.All), and after that, the script no longer works. The token does contain roles.
My goal is to create a PowerShell script that backs up all Power BI workspaces.
Currently, the script only works if I manually add the service principal (SP) to each workspace. In our company, we have more than 60 workspaces, and sometimes new workspaces are created without informing me. As a result, some backups are missing.To solve this, I created a script that retrieves all workspaces automatically. However, when the script tries to add the service principal as a member to a workspace, it fails with a 401 Unauthorized error. The script is running from an Azure Automation Runbook.
- AbdelmonemKaabiFrequent Visitor
I currently have only Microsoft Graph permissions: Application permissions (InformationProtectionPolicy.Read.All and User.Read.All) and Delegated permission (User.Read). I then added the Power BI application permission (Tenant.ReadWrite.All), and after that, the script no longer works. The token does contain roles.
My goal is to create a PowerShell script that backs up all Power BI workspaces.
Currently, the script only works if I manually add the service principal (SP) to each workspace. In our company, we have more than 60 workspaces, and sometimes new workspaces are created without informing me. As a result, some backups are missing.To solve this, I created a script that retrieves all workspaces automatically. However, when the script tries to add the service principal as a member to a workspace, it fails with a 401 Unauthorized error. The script is running from an Azure Automation Runbook.
- AbdelmonemKaabiFrequent Visitor
I currently have only Microsoft Graph permissions: Application permissions (InformationProtectionPolicy.Read.All and User.Read.All) and Delegated permission (User.Read). I then added the Power BI application permission (Tenant.ReadWrite.All), and after that, the script no longer works. The token does contain roles.
My goal is to create a PowerShell script that backs up all Power BI workspaces.
Currently, the script only works if I manually add the service principal (SP) to each workspace. In our company, we have more than 60 workspaces, and sometimes new workspaces are created without informing me. As a result, some backups are missing.To solve this, I created a script that retrieves all workspaces automatically. However, when the script tries to add the service principal as a member to a workspace, it fails with a 401 Unauthorized error. The script is running from an Azure Automation Runbook.
- AbdelmonemKaabiFrequent Visitor
So this is my script :
# 0) Read Automation variables
$tenantId = Get-AutomationVariable -Name "PBITenantId"
$clientId = Get-AutomationVariable -Name "PBIClientId"
$clientSecret = Get-AutomationVariable -Name "PBIClientSecret"
$spObjectId = "*******************" # from Enterprise applications > Object ID
if ([string]::IsNullOrWhiteSpace($tenantId) -or
[string]::IsNullOrWhiteSpace($clientId) -or
[string]::IsNullOrWhiteSpace($clientSecret) -or
[string]::IsNullOrWhiteSpace($spObjectId)) {
throw "Missing Automation variables. Ensure PBITenantId, PBIClientId1, PBIClientSecret1, PBIServicePrincipalObjectId exist."
}
# 1) Acquire Power BI token (v2.0; resource: analysis.windows.net)
$tokenEndpoint = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"
$scopePbi = "https://analysis.windows.net/powerbi/api/.default"
Write-Output "Requesting Power BI token..."
$pbiToken = Invoke-RestMethod -Method Post -Uri $tokenEndpoint -Body @{
grant_type = "client_credentials"
client_id = $clientId
client_secret = $clientSecret
scope = $scopePbi
} -ContentType "application/x-www-form-urlencoded"
$accessToken = $pbiToken.access_token
$pbiHeaders = @{ Authorization = "Bearer $accessToken" }
Write-Output ("Token OK. ExpiresIn={0}s; Length={1}" -f $pbiToken.expires_in, $accessToken.Length)
# 2) Enumerate ALL workspaces via Admin GET (paged) (caller must be Fabric Admin or SP allowed)
# Doc: Admin - GetGroupsAsAdmin
# https://learn.microsoft.com/en-us/rest/api/power-bi/admin/groups-get-groups-as-admin
$all = @(); $top=500; $skip=0
do {
$listUri = "https://api.powerbi.com/v1.0/myorg/admin/groups?$top=$top&$skip=$skip"
$r = Invoke-WebRequest -Method Get -Uri $listUri -Headers $pbiHeaders
$j = $r.Content | ConvertFrom-Json
$batch = $j.value
if ($batch) { $all += $batch }
$skip += $top
} while ($batch -and $batch.Count -eq $top)
# Filter out personal workspaces and deleted ones
$workspaces = $all | Where-Object {
($_.type -ne "PersonalGroup") -and
($_.name -notlike "PersonalWorkspace*") -and
($_.state -eq $null -or $_.state -eq "Active")
}
Write-Output ("Total groups: {0} | Candidate workspaces: {1}" -f $all.Count, $workspaces.Count)
if (-not $workspaces) { Write-Output "No eligible workspaces found. Exiting."; return }
# 3) Helper: Admin Add User (Service Principal) to workspace
function Add-App-To-WorkspaceAsAdmin {
param(
[Parameter(Mandatory=$true)][string]$WorkspaceId,
[Parameter(Mandatory=$true)][ValidateSet('Admin','Member','Contributor','Viewer')]$AccessRight
)
$uri = "https://api.powerbi.com/v1.0/myorg/admin/groups/$WorkspaceId/users"
$payload = [ordered]@{
identifier = $spObjectId # **Service Principal Object ID** (Enterprise Applications -> Object ID)
principalType = "App"
groupUserAccessRight = $AccessRight
} | ConvertTo-Json
try {
Invoke-RestMethod -Method Post -Uri $uri -Headers $pbiHeaders -ContentType "application/json" -Body $payload -ErrorAction Stop
return @{ ok = $true }
} catch {
$ex = $_.Exception
$status = $null; $details = $null; $www = $null
if ($ex.Response) {
try { $status = [int]$ex.Response.StatusCode } catch {}
try { $www = $ex.Response.Headers["WWW-Authenticate"] } catch {}
try { $details= (New-Object IO.StreamReader($ex.Response.GetResponseStream())).ReadToEnd() } catch {}
}
return @{ ok = $false; status = $status; www = $www; details = $details; message = $ex.Message }
}
}
# 4) Grant SP as Contributor across all candidates (respect ~200 req/hour)
$role = "Contributor"
$success=0; $fail=0; $i=0; $cap=190
foreach ($g in $workspaces) {
$i++
$wid=$g.id; $wname=$g.name
Write-Output ("[{0}/{1}] Granting {2} to SP ({3}) on {4} ({5})..." -f $i, $workspaces.Count, $role, $spObjectId, $wname, $wid)
$res = Add-App-To-WorkspaceAsAdmin -WorkspaceId $wid -AccessRight $role
if ($res.ok) {
Write-Output " - Granted."
$success++
} else {
Write-Warning (" - Failed. HTTP={0}" -f $res.status)
if ($res.www) { Write-Warning (" WWW-Authenticate: {0}" -f $res.www) }
if ($res.details){ Write-Warning (" Body: {0}" -f $res.details) }
if ($res.status -eq 401) {
Write-Warning " Hint: Ensure the caller SP is Fabric Administrator and the 'admin APIs used for updates' toggle is enabled for its security group."
}
$fail++
}
# be gentle with rate limits (~200/hour)
Start-Sleep -Seconds 1
if ($i -ge $cap) { Write-Warning "Hourly cap reached ($cap). Re-run to continue."; break }
}
Write-Output ("Summary: Granted on {0}; Failed on {1}." -f $success, $fail)
# 5) Verify /groups now lists workspaces for the SP (may take a moment to propagate)
$verify = Invoke-WebRequest -Method Get -Uri "https://api.powerbi.com/v1.0/myorg/groups?$top=50" -Headers $pbiHeaders -ErrorAction SilentlyContinueif ($verify) {
$vj = $verify.Content | ConvertFrom-Json
$cnt = if ($vj.value) { $vj.value.Count } else { 0 }
Write-Output ("Verification: /groups returns {0} workspace(s)." -f $cnt)
if ($cnt -gt 0) { $vj.value | Select-Object -First 10 id, name | ForEach-Object { Write-Output (" - {0} | {1}" -f $_.id, $_.name) } }
} else {
Write-Output "Verification call skipped (transient error or propagation delay)."
}
# === Get token (Power BI resource) ===
$tenantId = Get-AutomationVariable -Name "PBITenantId"
$clientId = Get-AutomationVariable -Name "PBIClientId1"
$clientSecret = Get-AutomationVariable -Name "PBIClientSecret1"
$tokenResp = Invoke-RestMethod -Method Post -Uri "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" -Body @{
grant_type = "client_credentials"
client_id = $clientId
client_secret = $clientSecret
scope = "https://analysis.windows.net/powerbi/api/.default"
} -ContentType "application/x-www-form-urlencoded"
$accessToken = $tokenResp.access_token
# === Decode / verify claims ===
$parts = $accessToken -split '\.'
$payloadB64 = $parts[1] + '==='
$payloadJson = [System.Text.Encoding]::UTF8.GetString(
[Convert]::FromBase64String($payloadB64.Substring(0, $payloadB64.Length - ($payloadB64.Length % 4)))
)
$claims = $payloadJson | ConvertFrom-Json
"appid: $($claims.appid)"
"oid: $($claims.oid)"
"aud: $($claims.aud)"
# Optional: compare to stored values for a clear OK/FAIL
$spObjectId = "**************************"
if ($claims.appid -eq $clientId) { "OK: appid matches clientId" } else { "MISMATCH: appid != clientId" }
if ($claims.oid -eq $spObjectId) { "OK: oid matches SP Object ID" } else { "MISMATCH: oid != SP Object ID" }
if ($claims.aud -eq 'https://analysis.windows.net/powerbi/api') { "OK: aud is Power BI" } else { "MISMATCH: unexpected aud" }
$fabricToken = Invoke-RestMethod -Method Post -Uri "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" -Body @{
grant_type = "client_credentials"
client_id = $clientId
client_secret = $clientSecret
scope = "https://api.fabric.microsoft.com/.default"
} -ContentType "application/x-www-form-urlencoded"
$fabHeaders = @{ Authorization = "Bearer $($fabricToken.access_token)" }
# List tenant settings and print only the two admin-API switches
$ts = Invoke-RestMethod -Method Get -Uri "https://api.fabric.microsoft.com/v1/admin/tenantsettings" -Headers $fabHeaders
$namesToCheck = @(
"Service principals can access read-only admin APIs",
"Service principals can access admin APIs used for updates"
)
$ts.value | Where-Object { $namesToCheck -contains $_.title } |
ForEach-Object {
"Setting: $($_.title) Enabled: $($_.enabled)"
if ($_.enabledSecurityGroups) {
" Allowed groups:"; $_.enabledSecurityGroups | ForEach-Object { " - $($_.name) ($($_.graphId))" }
}
} I currently have only Microsoft Graph permissions: Application permissions (InformationProtectionPolicy.Read.All and User.Read.All) and Delegated permission (User.Read). I then added the Power BI application permission (Tenant.ReadWrite.All), and after that, the script no longer works. The token does contain roles.
- V-yubandi-msft
Community Support
Hi AbdelmonemKaabi ,
Thanks for sharing more details. The 401 Unauthorized error isn’t caused by the script or token acquisition, but by the way Power BI Admin APIs handle service principal authentication. Service principals can access admin read APIs like GetGroupsAsAdmin, which is why listing workspaces works as expected.
However, update operations such as
POST /admin/groups/{id}/usersaren’t supported with app only authentication and require a delegated user identity instead of a service principal token. This is why you see the 401 error, even though the service principal is a Fabric Administrator and the Admin API tenant settings are enabled.
To add the service principal to workspaces, use a delegated admin accoun. Once added, the service principal can automate tasks such as backups in those workspaces.
FYI:
For further details, please refer to the attached Microsoft documentation.
Admin - Groups GetGroupsAsAdmin - REST API (Power BI Power BI REST APIs) | Microsoft Learn
Regards,
Yugandhar. - V-yubandi-msft
Community Support
Hi AbdelmonemKaabi ,
Could you please let us know whether your issue has been resolved, or if you are still facing any issues. If you need any additional details or clarification, please feel free to let us know.
- AbdelmonemKaabiFrequent Visitor
Hello V-yubandi-msft lbendlin
Hello @V-yubandi-msft @lbendlin
Thank you for the clarification.
I understand that service principals can successfully call admin read APIs such as GetGroupsAsAdmin, which is working correctly in my case.
However, my issue is specifically related to the following endpoint:
POST /admin/groups/{workspaceId}/users
In my scenario:
• The Service Principal is assigned the Fabric Administrator role in Entra ID
Tenant settings allow:
Service principals to access read-only admin APIs
Service principals to access admin APIs used for updates
Despite all of the above, every attempt to call the Add User admin endpoint returns:
HTTP 401 Unauthorized
My question is not about listing workspaces — that works correctly.
What I am trying to clarify is:
Is POST /admin/groups/{id}/users officially unsupported with app-only authentication, even when the service principal is a Fabric Administrator and tenant update APIs are enabled?
- V-yubandi-msft
Community Support
Hi AbdelmonemKaabi ,
Could you please let us know whether your issue has been resolved, or if you are still facing any issues. If you need any additional details or clarification, please feel free to let us know.
- AbdelmonemKaabiFrequent Visitor
Hello V-yubandi-msft lbendlin
Thank you for the clarification.
I understand that service principals can successfully call admin read APIs such as GetGroupsAsAdmin, which is working correctly in my case.
However, my issue is specifically related to the following endpoint:
POST /admin/groups/{workspaceId}/users
In my scenario:
• The Service Principal is assigned the Fabric Administrator role in Entra ID
Tenant settings allow:
Service principals to access read-only admin APIs
Service principals to access admin APIs used for updates
Despite all of the above, every attempt to call the Add User admin endpoint returns:
HTTP 401 Unauthorized
My question is not about listing workspaces — that works correctly.
What I am trying to clarify is:
Is POST /admin/groups/{id}/users officially unsupported with app-only authentication, even when the service principal is a Fabric Administrator and tenant update APIs are enabled?
- V-yubandi-msft
Community Support
Thank you for clarifying. The POST /admin/groups/{id}/users endpoint isn’t always reliable with app only authentication, even if the Service Principal is a Fabric Administrator and both relevant tenant settings for admin APIs are enabled. Read admin APIs, like GetGroupsAsAdmin, tend to work consistently, but write operations such as adding users can still return a 401 error in some tenants.
It’s worth double checking your script when using principalType = "App", make sure you’re using the Application (Client) ID as the identifier, not the Service Principal Object ID. Using the Object ID can also cause a 401 error.
If you’re already using the Client ID and still getting 401s, it’s likely a limitation or behavior of this endpoint with app only tokens, rather than a configuration issue on your end.
Thanks.
- V-yubandi-msft
Community Support
Hi AbdelmonemKaabi ,
We haven’t received a response from your end yet. Please let us know whether the issue has been resolved or if you’re still facing any difficulties. Feel free to reach out if you need further assistance.
Thank you.
- AbdelmonemKaabiFrequent Visitor
Hello V-yubandi-msft
Thank you for following up. Unfortunately, the issue has not yet been resolved.
I am still receiving a 401 Unauthorized error when attempting to automatically add the service principal (SP) to all workspaces via Azure Automation. Based on the documentation and forum discussions, it appears that the service principal must already be assigned as Admin in a workspace before it can add users programmatically.
As a temporary workaround, I am running the script manually from my local machine instead of Azure Automation.
My goal is to fully automate this process so that each night:
A script runs in Azure Automation
It scans all workspaces in the tenant
Adds the service principal if it is missing
This is necessary because I have:
A nightly script that retrieves all events and refresh history (which requires the SP to be Contributor or Admin).
Another script that exports and downloads reports (which also requires the SP to have sufficient permissions).
At this point, the blocker remains the 401 error when attempting to automatically assign the SP to workspaces.
Please let me know if there is an alternative supported approach to handle this scenario.
- V-yubandi-msft
Community Support
Hi AbdelmonemKaabi ,
To clarify, a Service Principal does not need to be a workspace Admin to use:
POST /admin/groups/{workspaceId}/usersThis API works with apponly authentication and is designed for tenant level automation scenarios like yours.
Since Admin read APIs are functioning and your script runs successfully locally, the 401 error indicates the identity or token used during the Azure Automation run is not being authorized.
Please check that the Service Principal used in Automation:
1. Is the same Client ID assigned the Fabric Administrator role & Is part of the tenant setting security group
2. Receives a token with Tenant.ReadWrite.All
3. Uses the Application (Client) ID, not the Object ID, as the identifier
There are no known Fabric side restrictions preventing this method.
Regards,
Yugandhar
- V-yubandi-msft
Community Support
Hi AbdelmonemKaabi ,
We haven’t received a response from your end yet. Please let us know whether the issue has been resolved or if you’re still facing any difficulties. Feel free to reach out if you need further assistance.
Thank you.