A more secure alternative to PAT tokens for accessing Azure DevOps Programmatically

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.

 1function Get-WebClient {
 2    [CmdletBinding()]
 3    param
 4    (
 5        $pat
 6    )
 7
 8    $webclient = new-object System.Net.WebClient
 9    $webclient.Encoding = [System.Text.Encoding]::UTF8
10    $encodedPat = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes(":$pat"))
11    $webclient.Headers.Add("Authorization", "Basic $encodedPat")
12    return $webclient
13}

which is used thus

1$wc = Get-WebClient -pat "a-pat-string"
2$result= $wc.DownloadString("https://dev.azure.com/MyOrg/MyProject/_apis/build/builds") | ConvertFrom-Json
3$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

 1function Get-WebClient {
 2    [CmdletBinding()]
 3    param
 4    (
 5        $ClientID ,
 6        $Secret   ,
 7        $TenantID 
 8    )
 9
10    # This is a static value
11    $AdoAppClientID = "499b84ac-1321-427f-aa17-267ca6975798/.default";
12
13    $loginUrl = "https://login.microsoftonline.com/$tenantId/oauth2/token"
14    $body = @{
15        grant_type    = "client_credentials"
16        client_id     = $ClientID
17        client_secret = $Secret 
18        resource      = $AdoAppClientID
19    }
20    $token = Invoke-RestMethod -Uri $loginUrl -Method POST -Body $body
21
22    
23    $webclient = new-object System.Net.WebClient
24    $webclient.Encoding = [System.Text.Encoding]::UTF8
25    $webclient.Headers.Add("Authorization", "Bearer $($token.access_token)")
26    return $webclient
27}
28
29$wc = Get-WebClient -ClientID "a string" -Secret "a secret" -TenantID "a tenant id"
30$result= $wc.DownloadString("https://dev.azure.com/MyOrg/MyProject/_apis/build/builds") | ConvertFrom-Json
31$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?