Background

When working with Azure DevOps, you may need to access the REST API if you wish to perform scripted tasks such as creating work items, or generating reports. Historically, you had to use a Personal Access Token (PAT) to do this.

If you look in my repo of useful Azure DevOps PowerShell scripts you will find all the scripts make use of a function that creates an authenticated WebClient object using a passed in PAT token.

function Get-WebClient {
    [CmdletBinding()]
    param
    (
        $pat
    )

    $webclient = new-object System.Net.WebClient
    $webclient.Encoding = [System.Text.Encoding]::UTF8
    $encodedPat = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes(":$pat"))
    $webclient.Headers.Add("Authorization", "Basic $encodedPat")
    return $webclient
}

which is used thus

$wc = Get-WebClient -pat "a-pat-string"
$result= $wc.DownloadString("https://dev.azure.com/MyOrg/MyProject/_apis/build/builds") | ConvertFrom-Json
$result.value

The problem with this approach is that the PAT tokens have to be managed. They expire after a period of time, so have to be regenerated and also, as they are in effect passwords, need to be stored securely.

A better approach

A newly available and better approach is to use an Azure AD App Service Principle to authenticate to Azure DevOps. This addresses the issues with PAT tokens, as the App Service Principles do not expire and are defined securely in Azure AD.

The basic setup is as follows

  1. Create a new Azure AD App
  2. Add the new App Service Principle to the Azure DevOps organisation as a user
  3. Grant the App Service Principle the required permissions in Azure DevOps
  4. Use the App Service Principle for programmatic authenticate to Azure DevOps e.g to the API

The sample script hence becomes

function Get-WebClient {
    [CmdletBinding()]
    param
    (
        $ClientID ,
        $Secret   ,
        $TenantID 
    )

    # This is a static value
    $AdoAppClientID = "499b84ac-1321-427f-aa17-267ca6975798/.default";

    $loginUrl = "https://login.microsoftonline.com/$tenantId/oauth2/token"
    $body = @{
        grant_type    = "client_credentials"
        client_id     = $ClientID
        client_secret = $Secret 
        resource      = $AdoAppClientID
    }
    $token = Invoke-RestMethod -Uri $loginUrl -Method POST -Body $body

    
    $webclient = new-object System.Net.WebClient
    $webclient.Encoding = [System.Text.Encoding]::UTF8
    $webclient.Headers.Add("Authorization", "Bearer $($token.access_token)")
    return $webclient
}

$wc = Get-WebClient -ClientID "a string" -Secret "a secret" -TenantID "a tenant id"
$result= $wc.DownloadString("https://dev.azure.com/MyOrg/MyProject/_apis/build/builds") | ConvertFrom-Json
$result.value

Now, the observant amongst you will have noticed in this sample the Get-WebClient function still takes a secret, which is less than optimal. So, in most use-case it is recommended that the token is retrieved using a certificate, rather than a secret, but the process is basically the same. See the worked Microsoft example for details.

The one potential downside of this approach is that the App Service Principle may require a paid for Azure DevOps license, but this is not always the case, it depends on the API calls you will be making.

Broadly speaking, calls to get Work Item details can be done with free stakeholder licenses, but others to get build or code details will probably require a basic license. However, remember you do get 5 free basic licenses, so there is a good chance you have one spare, or at worst they are only $6 a month.

So is this something that may make your programmatic access to Azure DevOps easier and more secure?