Forum Discussion

AbdelmonemKaabi's avatar
AbdelmonemKaabi
Frequent Visitor
7 months ago
Solved

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}/users

    What 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

    1. All POST calls fail with 401 Unauthorized

      Error returned

       

       
      HTTP 401 Unauthorized

       

       

      This 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/tenantsettings
               

              Important 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.

  • V-yubandi-msft's avatar
    V-yubandi-msft
    6 months ago

    Hi AbdelmonemKaabi ,

    To clarify, a Service Principal does not need to be a workspace Admin to use: 

    POST /admin/groups/{workspaceId}/users

    This 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

    • AbdelmonemKaabi's avatar
      AbdelmonemKaabi
      Frequent 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.

    • AbdelmonemKaabi's avatar
      AbdelmonemKaabi
      Frequent 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.

    • AbdelmonemKaabi's avatar
      AbdelmonemKaabi
      Frequent 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.

  • 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.

  • 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}/users

    aren’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.

  • 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.

    • AbdelmonemKaabi's avatar
      AbdelmonemKaabi
      Frequent 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?

  • 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.

    • AbdelmonemKaabi's avatar
      AbdelmonemKaabi
      Frequent 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?

  • 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.

  • 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.

    • AbdelmonemKaabi's avatar
      AbdelmonemKaabi
      Frequent 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's avatar
        V-yubandi-msft
        Icon for Community Support rankCommunity Support

        Hi AbdelmonemKaabi ,

        To clarify, a Service Principal does not need to be a workspace Admin to use: 

        POST /admin/groups/{workspaceId}/users

        This 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

  • 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.