Obtain Azure DevOps Personal Access Token programmatically

A colleague approached me with a question about automating the creation of Personal Access Token (PAT) for Azure DevOps. As usual, I tried taking the easy route of doing a quick search online, however all I could find were some convoluted/incomplete examples, and lots and lots of misunderstandingс. Which comes as a surprise, as the task is actually quite simple, once you figure out what it entails. So here’s how you can obtain PAT via few simple API calls (all done via PowerShell).

First, you need to have an Azure AD application, and have the user_impersonation scope for Azure DevOps added to it. In other words, go to the Azure AD blade, create a new app registration or use an existing one. Go to API permissions > Add a permission > select Azure DevOps > select user_impersonation under Delegate permissions > confirm. Not much you can do wrong here, as this is the only permissions you can select for said API anyway. Depending on which users you plan to generate PATs for, you might as well consider granting consent for the entire directory, but that’s optional.

Required permissions for obtaining personal access tokenOnce permissions have been added/consent granted, you need to obtain an access token. Use the exact same methods you’d use for any other Azure AD integrated application. The scope is the only thing you need to modify, and for it, use the value of “499b84ac-1321-427f-aa17-267ca6975798/.default“. This basically translates to “all scopes for Azure AD DevOps API”. Then, request the token.

UPDATE: Since I’ve received multiple queries on the “get token” part, here’s a link to the Graph API documentation. Do note that the method outlined here only works for delegate permissions. Also, here’s a sample way to obtain a token, that leverages the MSAL methods. Again, a sample, multiple other methods are available, use the one that best fits your needs.

Add-Type -Path "C:\Program Files\WindowsPowerShell\Modules\MSAL\Microsoft.Identity.Client.dll"

$app2 = [Microsoft.Identity.Client.PublicClientApplicationBuilder]::Create("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx").WithRedirectUri("http://localhost/xxxx").WithBroker().Build()
$Scopes = New-Object System.Collections.Generic.List[string]
$Scope = "499b84ac-1321-427f-aa17-267ca6975798/.default" # Means "All scopes for the Azure DevOps API resource"
$Scopes.Add($Scope)

$token = $app2.AcquireTokenInteractive($Scopes).ExecuteAsync().Result

$authHeader1 = @{
'Authorization'="Bearer $($token.AccessToken)"
}

Once you have a valid token, you can issue queries against the Azure AD DevOps API, which is again very similar to what you are used to with other Azure AD integrated apps and the Graph API. For example, the query below will return all currently configured PAT tokens for the user:

$uri = "https://vssps.dev.azure.com/vasil0445/_apis/tokens/pats?api-version=7.1-preview.1"
$res = Invoke-WebRequest -Method GET -Uri $uri -Debug -Verbose -Headers $authHeader1
($res.Content | ConvertFrom-Json).PatTokens

displayName : new
validTo : 2022-03-13T15:40:35.1266667Z
scope : vso.work
targetAccounts : {0ced1f28-dfb7-4073-90a7-5f000c7c1550}
validFrom : 2022-02-11T15:40:49.5133333Z
authorizationId : 4721a58e-73b2-41a6-8225-50c022e6fd7d
token :

The one thing that you need to modify in the above request is the organization name. If you don’t want to hardcode it, here’s how to obtain a list of ADO orgs for the current user. First, run a GET request against the /profile/profiles/me endpoint to get the GUID of the user, then pass it in a GET request against the /accounts endpoint and fetch the AccountName value(s) out of the reply:

#Get userId from /profiles
$uri = "https://app.vssps.visualstudio.com/_apis/profile/profiles/me?api-version=7.1-preview.1"
$res = Invoke-WebRequest -Method GET -Uri $uri -Debug -Verbose -Headers $authHeader1
$memberid = ($res.Content | ConvertFrom-Json).Id

#Call /accounts to get the org(s)
$uri = "https://app.vssps.visualstudio.com/_apis/accounts?memberId=$memberId?api-version=7.1-preview.1"
$res = Invoke-WebRequest -Method GET -Uri $uri -Debug -Verbose -Headers $authHeader1
$org = ($res.Content | ConvertFrom-Json).AccountName

Of course, you can have multiple organizations, so handle things as necessary. Or just hardcode it.

Anyway, enough off topic. Here’s how to generate a new PAT for the current user. In a nutshell, you need to run a POST request against the /tokens/pats endpoint and provide a JSON payload with few properties. Those include the displayName, validity (validTo), permissions (scope) and whether the PAT should be valid for the current org, or all orgs the user has access to (allOrgs). Do make sure to use appropriate values here, especially for the scope parameter, copy/paste the below example at your own peril 🙂

#Create a new PAT
$uri = "https://vssps.dev.azure.com/vasil0445/_apis/tokens/pats?api-version=7.1-preview.1"
$body = @{
displayName = "new_token"
scope = "app_token"
validTo = "2022-12-01T23:46:23.319Z"
allOrgs= "false"
}

$res = Invoke-WebRequest -Method POST -Uri $uri -Debug -Verbose -Headers $authHeader1 –ContentType 'application/json' -Body ($body | ConvertTo-Json)
($res.Content | ConvertFrom-Json).patToken.token

girfpsqnpqeci3vxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Of course, make sure to capture the PAT string, as it won’t be shown anywhere else. For the sake of completeness, here’s how to get the details on specific PAT:

#Get a specific PAT
$uri = "https://vssps.dev.azure.com/vasil0445/_apis/tokens/pats?authorizationId=4721a58e-73b2-41a6-8225-50c022e6fd7d&api-version=7.1-preview.1"
$res = Invoke-WebRequest -Method GET -Uri $uri -Debug -Verbose -Headers $authHeader1
($res.Content | ConvertFrom-Json).PatToken

displayName : new
validTo : 2022-03-13T15:40:35.1266667Z
scope : vso.work
targetAccounts : {0ced1f28-dfb7-4073-90a7-5f000c7c1550}
validFrom : 2022-02-11T15:40:49.5133333Z
authorizationId : 4721a58e-73b2-41a6-8225-50c022e6fd7d
token :

Do note that the token value will be empty, so again, make sure to capture it upon creation. For more details on working with PATs, refer to the official documentation.

9 thoughts on “Obtain Azure DevOps Personal Access Token programmatically

  1. Carsten says:

    #Requires -Module MSAL.PS
    $clientId = “872cd9fa-d31f-45e0-9eab-6e460a02d1f1” # Visual Studio
    $adoResourceId = “499b84ac-1321-427f-aa17-267ca6975798” # Azure DevOps app ID
    $msalToken = Get-MsalToken -Scopes “$adoResourceId/.default” -ClientId $clientId -ErrorAction Stop

    $Headers = @{
    “Content-Type” = “application/json”
    Authorization = “Bearer $($msalToken.accessToken)”
    }

    $profiles=Invoke-RestMethod -Uri ‘https://app.vssps.visualstudio.com/_apis/profile/profiles/me?api-version=6.0’ -Method get -ContentType “application/json” -Headers $Headers
    $orgs=Invoke-RestMethod -Uri (‘https://app.vssps.visualstudio.com/_apis/accounts?memberId={0}&api-version=6.0′ -f $profiles.publicAlias) -Method get -ContentType “application/json” -Headers $Headers
    $orgs | Select-Object -First 1

    $PatAPIVersion=’api-version=7.0-preview.1′
    $PatAPIUrl=’_apis/Tokens/Pats’

    # list PAT
    $pattokens=Invoke-RestMethod -Uri ($org.accountUri+$PatAPIUrl+’?’+$PatAPIVersion) -Method get -ContentType “application/json” -Headers $Headers
    $pattokens.patTokens

    Reply
  2. Joshua Tresario says:

    Hey, I am trying to apply this, and think I have covered every step, because I am getting a 203 response? Do you know what the issue could be for getting such a response? I assume it is permissions, but is it though?

    Reply
  3. yashali says:

    Hi,
    I have been at this for an embarrassingly long time. But never got it to work. Could you please help?

    Here’s what I have been trying to do:
    1. Created an app under AAD. Added the user_impersonation API permission to it. Created a secret and noted down the App ID
    2. Been trying to use this python code to generate a new PAT:

    import msal
    import requests

    config = {
    “authority”: “https://login.microsoftonline.com/tenant-id”,
    “client_id”: “client-app-id”,
    “scope”: [ “client-app-id/.default”], # also tried with “499b84ac-1321-427f-aa17-267ca6975798/.default”
    “secret”: “client-secret”,
    “endpoint”: “https://vssps.dev.azure.com/my-org/_apis/tokens/pats?api-version=7.1-preview.1”
    }

    app = msal.ConfidentialClientApplication(
    config[“client_id”], authority=config[“authority”],
    client_credential=config[“secret”])

    result = app.acquire_token_for_client(scopes=config[“scope”])
    print(result[‘access_token’])

    pats=requests.post(config[“endpoint”],
    headers={‘Authorization’: ‘Bearer ‘ + result[‘access_token’]},
    data={“displayName”:”new_token1″,”scope”:”vso.packaging_write”,
    “validTo”:”2022-12-01T23:46:23.319Z”,”allOrgs”:”false”})
    print(pats.json())

    Reply
    1. Vasil Michev says:

      You cannot use application permissions/client credentials flow. Only delegate permissions are supported afaik.

      Reply
      1. yashali says:

        The Azure DevOps : user_impersonation is a Delegated Type API permission added to the SP/App registration (client-id). Would it still not work ?

        Reply
        1. Vasil Michev says:

          It should work, but the token you’re obtaining in the example above is in the application context – that’s not supported afaik.

  4. foobar says:

    The scope is the only thing you need to modify, and for it, use the value of “499b84ac-1321-427f-aa17-267ca6975798/.default“. This basically translates to “all scopes for Azure AD DevOps API”.

    >>>>>>>>>>>>>>>>>> HERE it reply says only an admin for the org can do that <<<<<<<<<<<<<<<<<<<

    Then, request the token.

    Reply
    1. Vasil Michev says:

      That depends on the org settings. By default the user_impersonation scope does NOT require admin consent. The examples above assume that you have the necessary permissions.

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.