Skip to main content
cancel
Showing results for 
Search instead for 
Did you mean: 

Get Fabric Certified for FREE during Fabric Data Days. Don't miss your chance! Request now

Reply
0_0
Frequent Visitor

Automated Deployment w/ Service Principal in DevOps

Hello,

 

Given that it has been sometime since the public preview of the service principal integration with ADO, I'm attempting deployment via a SPN and have run across an issue, this is more of a post where I'm seeking confirmation that it's either with the script or the SPN credentials that were setup, feel free to point out any areas of redundancy or documentation though I haven't seen much documentation apart for the Git Repo. For the connection name it was for the configured connection which I'm not entirely sure what it meant by that. For the credentials for testing purposes it was all hardcoded into the script(no encryption): I have a feeling that its with my SPN credentials but wanted a 2nd opinion before expending more time, this is the response returned to me when I ran the DevOps Pipeline:

Entering function: SetFabricHeaders
Entering function: ConvertSecureStringToPlainText
Entering function: GetWorkspaceByName
Entering function: GetConnectionByName
Entering function: GetErrorResponse
Starting try block
Setting Fabric headers
Converting service principal secret to secure string
Creating PSCredential object
Authenticating with Azure using service principal
Retrieving access token
Error in GetSecureTokenForServicePrincipal: ClientSecretCredential authentication failed:
Error in SetFabricHeaders: ClientSecretCredential authentication failed:
Returning from function
Script failed. Error: ClientSecretCredential authentication failed:

 

# This sample script calls the Fabric API to programmatically connect to and update the workspace with content in Git.

# For documentation, please see:
# https://learn.microsoft.com/en-us/rest/api/fabric/core/git/connect
# https://learn.microsoft.com/en-us/rest/api/fabric/core/git/initialize-connection
# https://learn.microsoft.com/en-us/rest/api/fabric/core/git/update-from-git
# https://learn.microsoft.com/en-us/rest/api/fabric/core/long-running-operations/get-operation-state

# Instructions:
# 1. Install PowerShell (https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell)
# 2. Install Azure PowerShell Az module (https://learn.microsoft.com/en-us/powershell/azure/install-azure-powershell)
# 3. Run PowerShell as an administrator
# 4. Fill in the parameters below
# 5. Change PowerShell directory to where this script is saved
# 6. > ./GitIntegration-ConnectAndUpdateFromGit.ps1

# Parameters - fill these in before running the script!
# =====================================================

$workspaceName = "workspace"      # The name of the workspace

$shouldDisconnect = $false               # Determines whether the workspace should be disconnected before connecting to a new Azure DevOps connection details.

# AzureDevOps details
$azureDevOpsDetails = @{
    gitProviderType = "AzureDevOps"
    organizationName = "org"
    projectName = "project"
    repositoryName = "project"
    branchName = "branch"
    directoryName = "directory"
}


# Relevant for Configured Connection authentication
$connectionName = "connectionName" # Replace with the connection display name that stores the gitProvider credentials (Required for GitHub. For ADO it is required only when using ConfiguredConnection)

$gitProviderDetails = $azureDevOpsDetails

$principalType = "ServicePrincipal"

# Relevant for ServicePrincipal
$clientId = "clientId"                   #The application (client) ID of the service principal
$tenantId = "tenantId"                   #The directory (tenant) ID of the service principal
$servicePrincipalSecret = "spSecret"  #The secret value of the service principal

# End Parameters =======================================

$global:baseUrl = "https://api.fabric.microsoft.com/v1" # Replace with environment-specific base URL. For example: "https://api.fabric.microsoft.com/v1"
$global:resourceUrl = "https://api.fabric.microsoft.com/v1"

$global:fabricHeaders = @{}

Write-Host "Entering function: SetFabricHeaders"
function SetFabricHeaders() {
    try {
        if ($principalType -eq "ServicePrincipal") {
            $secureFabricToken = GetSecureTokenForServicePrincipal
            if (-not $secureFabricToken) {
                throw "Failed to retrieve secure token for service principal."
            }

            # Convert SecureString to plain text
            $fabricToken = ConvertSecureStringToPlainText($secureFabricToken)

            if (-not $fabricToken) {
                throw "Failed to convert secure token to plain text."
            }

            $global:fabricHeaders = @{
                'Content-Type' = "application/json"
                'Authorization' = "Bearer $fabricToken"
            }

            Write-Host "Fabric headers successfully set."
        } else {
            throw "Invalid principal type. Please choose either 'UserPrincipal' or 'ServicePrincipal'."
        }
    } catch {
        Write-Host "Error in SetFabricHeaders: $($_.Exception.Message)" -ForegroundColor Red
        throw $_
    }
}

function GetSecureTokenForServicePrincipal() {
    try {
        Write-Host "Converting service principal secret to secure string"
        $secureServicePrincipalSecret = ConvertTo-SecureString -String $servicePrincipalSecret -AsPlainText -Force

        Write-Host "Creating PSCredential object"
        $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $clientId, $secureServicePrincipalSecret

        Write-Host "Authenticating with Azure using service principal"
        Connect-AzAccount -ServicePrincipal -TenantId $tenantId -Credential $credential -ErrorAction Stop | Out-Null

        Write-Host "Retrieving access token"
        $secureFabricToken = (Get-AzAccessToken -ResourceUrl $global:resourceUrl).Token

        if (-not $secureFabricToken) {
            throw "Access token retrieval failed."
        }

        Write-Host "Access token successfully retrieved."
        return $secureFabricToken
    } catch {
        Write-Host "Error in GetSecureTokenForServicePrincipal: $($_.Exception.Message)" -ForegroundColor Red
        throw $_
    }
}

Write-Host "Entering function: ConvertSecureStringToPlainText"
function ConvertSecureStringToPlainText($secureString) {
    $ssPtr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureString)
    Write-Host "Starting try block"
    try {
        $plainText = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($ssPtr)
    } finally {
        [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ssPtr)
    }
    Write-Host "String: $plainText"
    return $plainText
}

Write-Host "Entering function: GetWorkspaceByName"
function GetWorkspaceByName($workspaceName) {
    # Get workspaces    
    $getWorkspacesUrl = "$global:baseUrl/workspaces"
    Write-Host "Invoking REST Method for GetWorkspaceByName"
    $workspaces = (Invoke-RestMethod -Headers $global:fabricHeaders -Uri $getWorkspacesUrl -Method GET).value

    # Try to find the workspace by display name
    $workspace = $workspaces | Where-Object {$_.DisplayName -eq $workspaceName}

    Write-Host "Returning from function"
    return $workspace
}

Write-Host "Entering function: GetConnectionByName"
function GetConnectionByName($connectionName) {
    # Get workspaces    
    $getConnectionsUrl = "$global:baseUrl/connections"
    Write-Host "Invoking REST Method for GetConnectionByName"
    $connections = (Invoke-RestMethod -Headers $global:fabricHeaders -Uri $getConnectionsUrl -Method GET).value

    # Try to find the connection by display name
    $connection = $connections | Where-Object {$_.DisplayName -eq $connectionName}

Write-Host "Returning from function"
    return $connection
}

Write-Host "Entering function: GetErrorResponse"
function GetErrorResponse($exception) {
    # Relevant only for PowerShell Core
    $errorResponse = $_.ErrorDetails.Message
 
    if(!$errorResponse) {
        # This is needed to support Windows PowerShell
        if (!$exception.Response) {
            Write-Host "Returning from function"
            return $exception.Message
        }
        $result = $exception.Response.GetResponseStream()
        $reader = New-Object System.IO.StreamReader($result)
        $reader.BaseStream.Position = 0
        $reader.DiscardBufferedData()
        $errorResponse = $reader.ReadToEnd();
    }
 
    Write-Host "Returning from function"
    return $errorResponse
}

Write-Host "Starting try block"
try {
    Write-Host "Setting Fabric headers"
    SetFabricHeaders
    Write-Host "Fabric headers set."

    Write-Host "Getting workspace by name: $workspaceName"
    $workspace = GetWorkspaceByName $workspaceName 
    Write-Host "Workspace lookup result: $($workspace | ConvertTo-Json -Depth 3)"

    if (!$workspace) {
        Write-Host "A workspace with the requested name was not found." -ForegroundColor Red
        Write-Host "Returning from function Set Fabric Headers."
        return
    }
    Write-Host "Connecting the workspace '$workspaceName' to Git."
    $connectUrl = "$global:baseUrl/workspaces/$($workspace.Id)/git/connect"
    Write-Host "Connect URL: $connectUrl"

    $connectToGitBody = @{}
    if ($gitProviderDetails.gitProviderType -eq "AzureDevOps") {
        Write-Host "Getting connection by name: $connectionName"
        $connection = GetConnectionByName $connectionName
        Write-Host "Connection lookup result: $($connection | ConvertTo-Json -Depth 3)"

        if (-not $connection) {
            Write-Host "A connection with the requested name was not found." -ForegroundColor Red
            return
        }

        $connectToGitBodyObject = @{
            gitProviderDetails = $gitProviderDetails
            myGitCredentials = @{
                source = "ConfiguredConnection"
                connectionId = $connection.id
            }
        }

        $connectToGitBody = $connectToGitBodyObject | ConvertTo-Json -Depth 3
        Write-Host "Connect to Git body: $connectToGitBody"

        try {
            Invoke-RestMethod -Headers $global:fabricHeaders -Uri $connectUrl -Method POST -Body $connectToGitBody
            Write-Host "Workspace '$workspaceName' connected to Git." -ForegroundColor Green
        } catch {
            Write-Host "Failed to connect workspace to Git: $($_.Exception.Message)" -ForegroundColor Red
            return
        }
    }

    Write-Host "Connect to Git body: $connectToGitBody"
    Invoke-RestMethod -Headers $global:fabricHeaders -Uri $connectUrl -Method POST -Body $connectToGitBody
    Write-Host "Workspace '$workspaceName' connected to Git." -ForegroundColor Green

    Write-Host "Initializing Git connection for workspace '$workspaceName'."
    $initializeConnectionUrl = "$global:baseUrl/workspaces/$($workspace.Id)/git/initializeConnection"
    Write-Host "Initialize Connection URL: $initializeConnectionUrl"
    $initializeConnectionResponse = Invoke-RestMethod -Headers $global:fabricHeaders -Uri $initializeConnectionUrl -Method POST
    Write-Host "Initialize Connection Response: $($initializeConnectionResponse | ConvertTo-Json -Depth 3)"

    if ($initializeConnectionResponse.RequiredAction -eq "UpdateFromGit") {
        Write-Host "Updating the workspace '$workspaceName' from Git."
        $updateFromGitUrl = "$global:baseUrl/workspaces/$($workspace.Id)/git/updateFromGit"
        $updateFromGitBody = @{ 
            remoteCommitHash = $initializeConnectionResponse.RemoteCommitHash
            workspaceHead = $initializeConnectionResponse.WorkspaceHead
        } | ConvertTo-Json -Depth 3

        Write-Host "Update from Git body: $updateFromGitBody"
        $updateFromGitResponse = Invoke-WebRequest -Headers $global:fabricHeaders -Uri $updateFromGitUrl -Method POST -Body $updateFromGitBody
        Write-Host "Update from Git response headers: $($updateFromGitResponse.Headers | ConvertTo-Json)"

        $operationId = $updateFromGitResponse.Headers['x-ms-operation-id']
        $retryAfter = $updateFromGitResponse.Headers['Retry-After']
        Write-Host "Operation ID: $operationId, Retry-After: $retryAfter"

        $getOperationState = "$global:baseUrl/operations/$operationId"
        do {
            Write-Host "Polling operation state from: $getOperationState"
            $operationState = Invoke-RestMethod -Headers $global:fabricHeaders -Uri $getOperationState -Method GET
            Write-Host "Operation status: $($operationState.Status)"

            if ($operationState.Status -in @("NotStarted", "Running")) {
                Write-Host "Sleeping for $retryAfter seconds"
                Start-Sleep -Seconds $retryAfter
            }
        } while ($operationState.Status -in @("NotStarted", "Running"))
    } else {
        Write-Host "Expected RequiredAction to be UpdateFromGit but found $($initializeConnectionResponse.RequiredAction)" -ForegroundColor Red
    }

    if ($operationState.Status -eq "Failed") {
        Write-Host "Update failed. Error: $($operationState.Error | ConvertTo-Json -Depth 3)" -ForegroundColor Red
    } else {
        Write-Host "Workspace '$workspaceName' successfully updated from Git." -ForegroundColor Green
    }
} catch {
    $errorResponse = GetErrorResponse($_.Exception)
    Write-Host "Script failed. Error: $errorResponse" -ForegroundColor Red
}

 

Any Assistance is much appreciated!

 

Thanks!

10 REPLIES 10
v-hjannapu
Community Support
Community Support

Hello @0_0  ,
Thank you for reaching out to the Microsoft Fabric Community Forum.
The error you are encountering, ClientSecretCredential authentication failed, suggests an issue with the Service Principal (SPN) credentials or their configuration in your Azure DevOps pipeline. Below are some steps to troubleshoot and resolve the issue, along with relevant documentation:

  • Check the $clientId, $tenantId, and $servicePrincipalSecret are correct and that the SPN has the necessary permissions in Microsoft Entra ID and Fabric. The SPN should be assigned a role (e.g., Contributor or Admin) in the Fabric workspace. Check the setup here: https://learn.microsoft.com/en-us/fabric/data-warehouse/service-principals.
  • Confirm that the SPN has been granted access to the workspace via the Fabric Admin portal and has appropriate roles (e.g: Admin, Member, or Contributor). You can manage access as described here: https://learn.microsoft.com/en-us/fabric/fundamentals/give-access-workspaces.
  • The $connectionName parameter refers to a pre-configured connection in Fabric for Azure DevOps. Ensure this connection exists and is correctly set up in the Fabric portal under workspace settings. For guidance, refer to: Git Integration with Fabric.
    Hardcoding credentials in the script is not recommended for security reasons. Instead, store them in Azure DevOps secure variables or Azure Key Vault and reference them in the pipeline. See: Azure Key Vault in Azure DevOps.

For further assistance, please share additional error details (if available) or confirm if the SPN has the required permissions.

Best Regards,
Harshitha.

Thanks For the Input,

 

Up until the Get Connection By Name function it works perfectly fine, I'm stumped on what it means by ConfiguredConnection, is this a service connection we set up linking it to the Service Principal within DevOps. I'm currently getting this response from the GetConnectionBy Name:

 

Connection lookup result: {
  "value": []
}
Connect to Git body: {
  "gitProviderDetails": {
    "directoryName": "directoryname",
    "repositoryName": "repositoryname",
    "organizationName": "organizationname",
    "projectName": "projectname",
    "gitProviderType": "AzureDevOps",
    "branchName": "prod"
  },
  "myGitCredentials": {
    "source": "ConfiguredConnection",
    "connectionId": "null"
  }
}
Failed to connect workspace to Git: Response status code does not indicate success: 400 (Bad Request).

 

v-hjannapu
Community Support
Community Support

Hello @0_0  ,
Thanks for the update. 

The Connection lookup result: { "value": [] } and 400 Bad Request errors indicate that the $connectionName specified in your script doesn’t match any configured connection in the Fabric workspace, resulting in a null connectionId.

This refers to a Git connection set up in the Fabric workspace (Settings > Git integration) for your Azure DevOps repository, not a DevOps service connection. It links Fabric to your Azure DevOps organization, project, repository, and branch.

 

Steps to Resolve:

  • In the Fabric portal, go to your workspace’s Settings > Git integration and confirm the $connectionName matches an existing connection. If not, set it up with your Azure DevOps details: Git Integration with Fabric.
  • Ensure the Service Principal has Contributor or Admin access to the workspace and Azure DevOps repository.
  • Since SPN support for Azure DevOps updateFromGit is limited , try using a Personal Access Token (PAT) with UserPrincipal authentication.

Please confirm if the connection is set up correctly or share the full 400 error response for further assistance.

Best Regards,
Harshitha.

 

Thanks, I managed to get the connection functional thanks to your suggestion but am unable to connect the workspace from Git:

 

try {
     Invoke-RestMethod -Headers $global:fabricHeaders -Uri $connectUrl -Method POST -Body $connectToGitBody
     Write-Host "Workspace '$workspaceName' connected to Git." -ForegroundColor Green
} catch {
     Write-Host "Failed to connect workspace to Git: $($_.Exception.Message)" -ForegroundColor Red
     return
}

 

Failed to connect workspace to Git: Response status code does not indicate success: 400 (Bad Request). I'm assuming it would be an issue with the permissions of the connections, It has access to the repository with the ability to contribute as well as contributor access in our workspace and is able to call public APIs enabled within the admin portal. 
v-hjannapu
Community Support
Community Support

Hi @0_0,
Thank you for the update. The 400 Bad Request error when connecting the workspace to Git likely stems from an issue with the $connectToGitBody or Service Principal permissions for Git operations(Verify the SPN has Admin access)

check whether the JSON includes a valid connectionId (not null) from the Fabric workspace’s configured connection. Log $connectToGitBody before Invoke-RestMethod to confirm

If the issue still happening, please let me know.

Regards,
Harshitha.

Hi @0_0,
I hope the information provided above assists you in resolving the issue. If you have any additional questions or concerns, please do not hesitate to contact us. We are here to support you and will be happy to help with any further assistance you may need.

Regards,
Harshitha.

Hey @v-hjannapu ,

 

I'm still unable to connect to git, I have a feeling it may be with the portion or the delegated scopes for the SPN itself.

    $connectToGitBody = @{}
    if ($gitProviderDetails.gitProviderType -eq "AzureDevOps") {
        Write-Host "Getting connection by name: $connectionName"
        $connection = GetConnectionByName $connectionName
        Write-Host "Connection lookup result: $($connection | ConvertTo-Json -Depth 3)"

        if (-not $connection) {
            Write-Host "A connection with the requested name was not found." -ForegroundColor Red
            return
        }

        $connectToGitBodyObject = @{
            gitProviderDetails = $gitProviderDetails
            myGitCredentials = @{
                source = "ConfiguredConnection"
                connectionId = $connection.id
            }
        }

        if (-not $workspace.Id) { Write-Host "Workspace ID is null"; return }
        if (-not $connection.id) { Write-Host "Connection ID is null"; return }
        $connectToGitBody = $connectToGitBodyObject | ConvertTo-Json -Depth 3
        Write-Host "Connect to Git body: $connectToGitBody"

        try {
            Invoke-RestMethod -Headers $global:fabricHeaders -Uri $connectUrl -Method POST -Body $connectToGitBody
            Write-Host "Workspace '$workspaceName' connected to Git." -ForegroundColor Green
        } catch {
            Write-Host "Failed to connect workspace to Git. Detailed error: $($_ | ConvertTo-Json -Depth 3)" -ForegroundColor Red
            return
        }
    }

 

Thanks!

v-hjannapu
Community Support
Community Support

Hi @0_0 ,
We regret the inconvenience caused. Please consider raising a Microsoft support ticket for further investigation. You can explain all the troubleshooting steps you have taken to help them better understand the issue.

You can create a Microsoft support ticket with the help of the link below: 
https://learn.microsoft.com/en-us/power-bi/support/create-support-ticket


Hi @0_0,
I hope the information provided above assists you in resolving the issue. If you have any additional questions or concerns, please do not hesitate to contact us. We are here to support you and will be happy to help with any further assistance you may need.

Regards,
Harshitha.

KevinChant
Super User
Super User

I think I see the issue, because I have had the same problem. When using convert-tosecurestring in a PowerShell task you need to wrap the value around quotes like below, even if it works outside of ADO:

ConvertTo-SecureString '${{parameters.azure_client_secret}}' -AsPlainText -Force

 

Helpful resources

Announcements
November Fabric Update Carousel

Fabric Monthly Update - November 2025

Check out the November 2025 Fabric update to learn about new features.

Fabric Data Days Carousel

Fabric Data Days

Advance your Data & AI career with 50 days of live learning, contests, hands-on challenges, study groups & certifications and more!

FabCon Atlanta 2026 carousel

FabCon Atlanta 2026

Join us at FabCon Atlanta, March 16-20, for the ultimate Fabric, Power BI, AI and SQL community-led event. Save $200 with code FABCOMM.