Skip to content

Instantly share code, notes, and snippets.

@codingfreak
Last active August 22, 2025 08:56
Show Gist options
  • Select an option

  • Save codingfreak/9db97a4db83a718b72c2e8a4f233a268 to your computer and use it in GitHub Desktop.

Select an option

Save codingfreak/9db97a4db83a718b72c2e8a4f233a268 to your computer and use it in GitHub Desktop.
Create federated Azure DevOps service connection
$ErrorActionPreference = 'Stop'
function Invoke-AzureDevOpsRestApi {
# Parameter help description
[CmdletBinding()]
param (
[string]
$OrganizationName = '',
[string]
$ProjectName = '',
[string]
$RelativeUrl,
[string]
$ContentType = '',
[string]
[ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')]
$Method,
[object]
$Body = $null,
[string]
[ValidateSet('VisualStudio', 'AzureDevOps')]
$ApiType = 'AzureDevOps',
[Hashtable]
$AdditionalParameters
)
process {
$api = $ApiType -eq 'VisualStudio' ? 'https://app.vssps.visualstudio.com' : 'https://dev.azure.com'
$baseUrl = $api
$apiVersion = '7.1'
$uri = "$baseUrl/$($OrganizationName.Length -eq 0 ? '' : "$($OrganizationName)/")$($ProjectName.Length -eq 0 ? '' : "$($ProjectName)/")_apis/$($RelativeUrl)?api-version=$apiVersion"
if ($null -ne $AdditionalParameters -and $AdditionalParameters.Count -gt 0) {
foreach ($key in $AdditionalParameters.Keys) {
$uri += "&$($key)=$($AdditionalParameters[$key])"
}
}
$accessToken = Get-AccessToken -ResourceUrl 499b84ac-1321-427f-aa17-267ca6975798
$headers = @{
Accept = "application/json"
Authorization = "Bearer $accessToken"
}
if ($Body.Length -gt 0) {
$response = Invoke-RestMethod -Method $Method -Uri $uri -Headers $headers -Body $Body -ContentType $ContentType
} else {
$response = Invoke-RestMethod -Method $Method -Uri $uri -Headers $headers
}
return $response.value ?? $response
}
}
function Get-AccessToken {
[CmdLetBinding()]
param (
[string]
$TenantId = '',
[string]
$ResourceUrl = '',
[switch]
$AsSecureString
)
if ($TenantId.Length -eq 0) {
$TenantId = (Get-AzContext).Tenant.Id
}
if ($ResourceUrl.Length -gt 0) {
$token = Get-AzAccessToken -ResourceUrl $ResourceUrl -WarningAction SilentlyContinue -AsSecureString
} else {
$token = Get-AzAccessToken -WarningAction SilentlyContinue -AsSecureString
}
if (!$accessToken) {
}
if ($AsSecureString.IsPresent) {
# return the secure token as it is
return $token.Token
}
$accessToken = ConvertFrom-SecureString -SecureString $token.Token -AsPlainText
return $accessToken
}
function Get-ProjectId {
[CmdletBinding()]
param (
[string]
$OrganizationName,
[string]
$ProjectName
)
process {
$result = Invoke-AzureDevOpsRestApi -OrganizationName $OrganizationName `
-Method GET `
-RelativeUrl 'projects'
foreach ($project in $result) {
if ($project.name -eq $ProjectName) {
return $project.id
}
}
return ''
}
}
function New-DefaultDevOpsServiceConnection {
[CmdletBinding()]
param (
[string]
$OrganizationName,
[string]
$ProjectName,
[string]
$ProjectId,
[string]
$ServiceConnectionName,
[string]
$ServicePrincipalId,
[string]
$ServicePrincipalSecret
)
process {
$ctx = Get-AzContext
$jsonBody = @"
{
"name": "$ServiceConnectionName",
"data": {
"creationMode": "Manual",
"environment": "AzureCloud",
"scopeLevel": "Subscription",
"subscriptionName": "$($ctx.Subscription.Name)",
"subscriptionId": "$($ctx.Subscription.Id)"
},
"type": "AzureRM",
"serviceEndpointProjectReferences": [
{
"name": "$ServiceConnectionName",
"projectReference": {
"id": "$ProjectId",
"name": "$ProjectName"
}
}
],
"url": "https://management.azure.com/",
"authorization": {
"parameters": {
"servicePrincipalKey": "$ServicePrincipalSecret",
"tenantId": "$($ctx.Tenant.Id)",
"servicePrincipalId": "$ServicePrincipalId",
"authenticationType": "spnKey"
},
"scheme": "ServicePrincipal"
},
"isReady": true
}
"@
return Invoke-AzureDevOpsRestApi `
-OrganizationName $OrganizationName `
-ProjectName $ProjectName `
-ContentType 'application/json' `
-Method 'POST' `
-RelativeUrl 'serviceendpoint/endpoints' `
-Body $jsonBody
}
}
function New-FederatedCredential {
param (
[string]
$OrganizationName,
[string]
$ProjectName,
[string]
$ServiceConnectionName,
[string]
$ServicePrincipalId,
[string]
$ServicePrincipalName
)
# Build the project name for the federated credential
$builtFederatedCredentialName = "$($OrganizationName.ToLowerInvariant())-$($ProjectName.ToLowerInvariant())"
# Get the name from the naming convention
$federatedCredentialName = "devops-$builtFederatedCredentialName"
Write-Host "Creating federated credential '$federatedCredentialName' for service principal '$ServicePrincipalName' to service connection '$ServiceConnectionName'"
# Retrieve the app registration using the Application ID
$AppRegistration = Get-AzADApplication -ApplicationId $ServicePrincipalId
# Get the Object ID of the app registration
$ApplicationObjectId = $AppRegistration.Id
# Build the subject identifier for the federated credential
$subjectIdentifier = "sc://$OrganizationName/$ProjectName/$ServiceConnectionName"
# Check if the federated credential already exists
$federatedCredential = Get-AzADAppFederatedCredential -ApplicationObjectId $ApplicationObjectId
# check if the name of the retrieved federated credential matches the naming convention of the new federated credential
if ($federatedCredential.Name -eq $federatedCredentialName) {
Write-Host "Federated credential '$federatedCredentialName' already exists for service principal '$ServicePrincipalName' to service connection '$ServiceConnectionName'."
return
}
# Get the authenticated user's memberId
$userProfile = Invoke-AzureDevOpsRestApi -Method GET `
-RelativeUrl 'profile/profiles/me' `
-ApiType VisualStudio
$memberId = $userProfile.id
# Make the API call to get the list of accounts with memberId
$response = Invoke-AzureDevOpsRestApi -Method GET `
-RelativeUrl 'accounts' `
-ApiType VisualStudio `
-AdditionalParameters @{ 'memberId' = $memberId }
# Filter the accounts where AccountName matches the organization name
$accounts = $response
$account = $accounts | Where-Object { $_.AccountName -eq $OrganizationName }
$orgId = $account.AccountId
Write-Host "DevOps Organization ID: $orgId"
New-AzADAppFederatedCredential `
-ApplicationObjectId $ApplicationObjectId `
-Audience 'api://AzureADTokenExchange' `
-Issuer "https://vstoken.dev.azure.com/$orgId" `
-Name $federatedCredentialName `
-Subject $subjectIdentifier | Out-Null
Write-Host "Federated credential '$federatedCredentialName' created successfully."
}
function Convert-ServiceConnectionToFederated {
param (
[string]
$OrganizationName,
[string]
$ProjectName,
[string]
$ServiceConnectionName
)
# Make the REST API call to get service connections
$response = Invoke-AzureDevOpsRestApi `
-OrganizationName $OrganizationName `
-ProjectName $ProjectName `
-Method 'GET' `
-RelativeUrl 'serviceendpoint/endpoints' `
-AdditionalParameters @{
'authSchemes' = 'ServicePrincipal'
'type' = 'azurerm'
'includeFailed' = 'false'
'includeDetails' = 'true'
}
$serviceEndpoints = $response
if (!$serviceEndpoints -or ($serviceEndpoints.Count -eq 0)) {
# NOTE: This endpoint is never returning any service connection which was converted
# to automatic. This means we cannot figure out if a service connection already exists.
Write-Host "No service connections found or service connection already initialized correctly."
return
}
# Filter the service connections
$serviceEndpoint = $serviceEndpoints | Where-Object {
$_.name -eq $ServiceConnectionName -and
$_.authorization.scheme -eq 'ServicePrincipal' -and
$_.data.creationMode -eq 'Manual' -and
!($_.isShared -and $_.serviceEndpointProjectReferences[0].projectReference.name -ne $ProjectName)
} | Select-Object -First 1
$serviceEndpoint = ($serviceEndpoints | Where-Object { $_.name -eq $ServiceConnectionName })
if (!$serviceEndpoint) {
Write-Host "No service connection found with name '$ServiceConnectionName' that can be converted."
return
}
Write-Host "Converting service connection '$($serviceEndpoint.name)' to federated authentication..."
$serviceEndpoint.authorization.scheme = "WorkloadIdentityFederation"
$serviceEndpoint.data.PSObject.Properties.Remove('revertSchemeDeadline')
$serviceEndpoint.authorization.parameters.PSObject.Properties.Remove('authenticationType')
# Convert back to JSON string
$serviceEndpointRequest = $serviceEndpoint | ConvertTo-Json -Depth 99 -Compress
# Make the REST API call to convert the service connection
$result = Invoke-AzureDevOpsRestApi `
-OrganizationName $OrganizationName `
-ProjectName $ProjectName `
-Method "PUT" `
-RelativeUrl "serviceendpoint/endpoints/$($serviceEndpoint.id)" `
-Body $serviceEndpointRequest `
-ContentType "application/json" `
-AdditionalParameters @{
'operation' = 'ConvertAuthenticationScheme'
}
if (!$result) {
throw "Failed to convert service connection '$($serviceEndpoint.name)' to Workload Identity Federated connection."
}
Write-Host "Service connection '$($serviceEndpoint.name)' has been converted to Workload Identity Federated connection."
}
# FILL IN THOSE VALUES!
$orgName = 'TODO'
$projectName = 'TODO'
$serviceConnectionName = 'TODO'
$spId = 'TODO'
$spName = 'TODO'
$spSecret = 'TODO'
# Resolve project id
$projectId = Get-ProjectId -OrganizationName $orgName `
-ProjectName $projectName
Write-Host "Project ID for $projectName is $projectId."
# Create classic service connection
New-DefaultDevOpsServiceConnection -OrganizationName $orgName `
-ProjectName $projectName `
-ProjectId $projectId `
-ServiceConnectionName $serviceConnectionName `
-ServicePrincipalId $spId `
-ServicePrincipalSecret $spSecret
Write-Host "Service connection created in draft mode."
# Upgrade to federated.
New-FederatedCredential `
-OrganizationName $orgName `
-ProjectName $projectName `
-ServicePrincipalId $spId `
-ServicePrincipalName $spName `
-ServiceConnectionName $serviceConnectionName
Write-Host "Giving Azure time to digest 😒..."
Start-Sleep -Seconds 10
Convert-ServiceConnectionToFederated -OrganizationName $orgName `
-ProjectName $projectName `
-ServiceConnectionName $serviceConnectionName
Write-Host "Done"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment