Sitemap

List All Personal Access Tokens (PATs) expiration dates in Azure DevOps for all users.

9 min readMar 24, 2025

Introduction

In this blog post, I will walk you through setting up an Azure DevOps pipeline using a YAML file to retrieve a list of all PATs, including their names, expiration dates, and the users who created them.

In Azure DevOps, managing and monitoring Personal Access Tokens (PATs) is crucial for security and access control.

Personal Access Tokens (PATs) in Azure DevOps expire based on the duration set when they are created. While expiration is a security measure, it can cause significant issues if not properly managed. Here’s why:

🔴 Consequences of PAT Expiration in Azure DevOps

1️⃣ CI/CD Pipeline Failures

  • Many teams use PATs for authentication in Azure DevOps pipelines to access repositories, artifacts, or external services.
  • If a PAT expires, automated builds, deployments, and scheduled jobs can fail, causing delays.

2️⃣ Git Authentication Issues

  • Developers and automated scripts often use PATs for Git operations (cloning, pushing, or pulling repositories).
  • When a PAT expires, users might suddenly lose access, leading to errors like:
remote: HTTP 401 Unauthorized
fatal: Authentication failed
  • This can disrupt development workflows and block critical code commits.

3️⃣ Service Integration Breakdowns

  • PATs are often used for integration between Azure DevOps and third-party tools (e.g., Jenkins, Terraform, external monitoring tools).
  • An expired token can break these integrations, leading to unmonitored services, failed deployments, or missing notifications.

4️⃣ Access Denied for Automation Scripts

  • Many teams use PowerShell, Python, or other scripts that rely on PATs for automating DevOps tasks (e.g., managing work items, running queries).
  • When a PAT expires, these scripts fail unexpectedly, causing operational headaches.

5️⃣ Production Downtime Risks

  • If an expired PAT is used in production-critical processes, it can lead to system outages, failed updates, or untracked incidents.

Prerequisites

Before running the YAML pipeline, we must ensure the following prerequisites:

  1. Azure DevOps access with appropriate permissions to retrieve PATs.

To access and list PATs using the Azure DevOps REST API, you need to be assigned one of the following Azure DevOps security roles:

Press enter or click to view image in full size

🔹 By default, only Organization Owners and Administrators can access PATs.

2. Azure Key Vault configured to store the PAT securely.

In my example, I have created a Key Vault named: kvdevops1

Press enter or click to view image in full size

3. A service connection to access Azure Key Vault and Azure DevOps APIs.

Before I will describe the technical steps to give access for an Azure DevOps service connection to a Key Vault, I would like to explain a tricky issue.

To allow a service connection access in Azure DevOps to retrieve information for all the PATs in the DevOps organization, we need………….. A PAT!

So, we will store an active PAT in Azure Key Vault and use it in an API request to get an Authorization header.

  • Be aware that if the pipeline will suddenly stop working, probably the PAT was expired!

To allow access to Azure Key Vault for an Azure DevOps service connection, we need to add Key Vault Administrator permissions to the Key Vault and stores the PAT that allows retrieving all exiting PATs information in our Azure DevOps’s organization.

az role assignment create - assignee <your-user-id> - role "Key Vault Administrator" - scope "/subscriptions/xxxxxxxxxxxxxxxxxxxxxxxxxx/resourceGroups/mydevops/providers/Microsoft.KeyVault/vaults/kvdevops1"
Press enter or click to view image in full size

4. Storing the PAT in Azure Key Vault

  • To store the PAT in Azure Key vault, just navigate to the Azure Key Vault we have created in step 2, named kvdevops1.
  • Now navigate to Secrets and click on Generate/Import.
  • At the name area, type DevOpsPAT
  • At the secret value field, fill in one of your active PATs from Azure DevOps. Click create.
Press enter or click to view image in full size
  • Verify that you can see the DevOpsPAT under the secrets field.
Press enter or click to view image in full size

5. A configured YAML pipeline in your Azure DevOps repository.

At the next paragraph, we will review the Yaml file and its different components.

trigger: none

variables:

- name: keyvname
value: "kvdevops1"
- name: ADOName
value: tzahikolber

jobs:
- job: CheckPATExpiry
steps:

- task: AzureKeyVault@2
displayName: Get PAT from KeyVault
inputs:
azureSubscription: DemoSA
keyVaultName: "$(keyvname)"
secretsFilter: 'DevOpsPAT'

- task: AzurePowerShell@5
displayName: Get PAT expiriation date
inputs:
azureSubscription: DemoSA
ScriptType: 'InlineScript'
Inline: |
$PAT = "$(DevOpsPAT)"
# Create the Authorization header with the provided PAT
$AzureDevOpsAuthenicationHeader = @{Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$($PAT)")) }

# Azure DevOps API URL for fetching users
$UriUsers = "https://vssps.dev.azure.com/$(ADOName)/_apis/graph/users?api-version=6.1-preview.1"

# Fetch all users
$UsersResult = Invoke-RestMethod -Uri $UriUsers -Method Get -Headers $AzureDevOpsAuthenicationHeader

# Iterate through users
foreach ($user in $UsersResult.value)
{
# Fetch the PAT details for each user
$UriUserPAT = "https://vssps.dev.azure.com/$(ADOName)/_apis/tokenadmin/personalaccesstokens/$($user.descriptor)?api-version=6.1-preview.1"
$UserPATResult = Invoke-RestMethod -Uri $UriUserPAT -Method Get -Headers $AzureDevOpsAuthenicationHeader

# Iterate through each user's PATs
foreach ($up in $UserPATResult.value)
{
# Check if the PAT scope is 'app_token', otherwise show the specific scope
if ($up.scope -eq 'app_token') {
$accessToken = 'Full access'
} else {
$accessToken = $up.scope.Replace(" ", "`r`n")
}

# Output the user and associated PAT information
Write-Host "User: $($user.displayName)"
Write-Host "Scope: $accessToken"
Write-Host "Valid To: $($up.validTo)"
Write-Host "PATNAME: $($up.displayName)"
Write-Host "---------------------------------"

# Optionally, you could export this data to a CSV for further use
$output = [PSCustomObject]@{
UserDisplayName = $user.displayName
Scope = $accessToken
ValidTo = $up.validTo
PATNAME = $up.displayName
}

}
}

azurePowerShellVersion: 'LatestVersion'



Breakdown of the Pipeline

  • The Yaml above is the simple version of the listing the PATs into the pipeline’s console.
    Later on, I will demonstrate a small add-on that also creates a CSV and saves it to one of the repo’s folders.

Before breaking the pipeline into tasks, we can configure few variables in order to make the tasks easier to read and save some work in case we are changing one of the components in the Yaml like the Key Vault name or Azure DevOps organization name.

1. Fetching the PAT from Azure Key Vault

  • The AzureKeyVault@2 task securely retrieves the stored PAT from Azure Key Vault, that we created on step 4.

2. Authenticating with Azure DevOps API

  • The script generates an authentication header using the PAT:
$AzureDevOpsAuthenicationHeader = @{Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$($PAT)")) }
  • The API call retrieves all users from the Azure DevOps organization.
$UsersResult = Invoke-RestMethod -Uri $UriUsers -Method Get -Headers $AzureDevOpsAuthenicationHeader

3. Fetching PATs for Each User

  • The script iterates through each user and fetches their associated PATs.
$UserPATResult = Invoke-RestMethod -Uri $UriUserPAT -Method Get -Headers $AzureDevOpsAuthenicationHeader
  • It extracts and displays relevant details such as name, expiration date, and scope.
Write-Host "User: $($user.displayName)"
Write-Host "Scope: $accessToken"
Write-Host "Valid To: $($up.validTo)"
Write-Host "PATNAME: $($up.displayName)"
Write-Host "---------------------------------"

4. Output and Logging

  • The script prints the details to the pipeline logs on screen with all the parameters you need to know about all the PATs status.

Running the Pipeline

When you run the pipeline, the first part as I mentioned, is to retrieve the PAT from the Key Vault.

  • In case you configured the secret, and the Azure DevOps Service account has the right permissions as described on step 3, the Get PAT from KeyVault step will pass:
Press enter or click to view image in full size
  • In case that the Azure DevOps service account wasn’t assigned with the Key Vault Administrator permissions, we will get the next error at the Get PAT from KeyVault step:
Press enter or click to view image in full size
  • In case that the secret wasn’t configured, we will get the next error at the Get PAT from KeyVault step:
  • After the Get PAT from KeyVault step passed successfully, we will get the list of all the PATs, expiration dates and the users that owns the PATs:
Press enter or click to view image in full size

Exporting the PAT’s details to a CSV in DevOps Repo

In case you would like to export the PAT’s details to a CSV file in a folder at the DevOps repo, use the next pipeline, that saves the output to a file called UserPATInfo.csv at folder called Alerts.

trigger: none

variables:
# - template: ../conf/shared.ref.conf.yml

- name: keyvname
value: "kvdevops1"
- name: ADOName
value: tzahikolber
- name: ProjectName
value: Demo
- name: FolderPath
value: Alerts

jobs:
- job: CheckPATExpiry
#pool: "myPoolName"
steps:
- checkout: self
persistCredentials: true
- task: AzureKeyVault@2
displayName: Get PAT from KeyVault
inputs:
azureSubscription: DemoSA
keyVaultName: "$(keyvname)"
secretsFilter: 'DevOpsPAT'

- task: AzurePowerShell@5
displayName: Get PAT expiriation date
inputs:
azureSubscription: DemoSA
ScriptType: 'InlineScript'
Inline: |
#Import-Module Az
# Get the current date
$currentDate = Get-Date
$PAT = "$(DevOpsPAT)"
# Create the Authorization header with the provided PAT
$AzureDevOpsAuthenicationHeader = @{Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$($PAT)")) }

# Azure DevOps API URL for fetching users
$UriUsers = "https://vssps.dev.azure.com/$(ADOName)/_apis/graph/users?api-version=6.1-preview.1"

# Fetch all users
$UsersResult = Invoke-RestMethod -Uri $UriUsers -Method Get -Headers $AzureDevOpsAuthenicationHeader

# Iterate through users
foreach ($user in $UsersResult.value)
{
# Fetch the PAT details for each user
$UriUserPAT = "https://vssps.dev.azure.com/$(ADOName)/_apis/tokenadmin/personalaccesstokens/$($user.descriptor)?api-version=6.1-preview.1"
$UserPATResult = Invoke-RestMethod -Uri $UriUserPAT -Method Get -Headers $AzureDevOpsAuthenicationHeader

# Iterate through each user's PATs
foreach ($up in $UserPATResult.value)
{
# Check if the PAT scope is 'app_token', otherwise show the specific scope
if ($up.scope -eq 'app_token') {
$accessToken = 'Full access'
} else {
$accessToken = $up.scope.Replace(" ", "`r`n")
}

# Output the user and associated PAT information
Write-Host "User: $($user.displayName)"
Write-Host "Scope: $accessToken"
Write-Host "Valid To: $($up.validTo)"
Write-Host "PATNAME: $($up.displayName)"
Write-Host "---------------------------------"

# Optionally, you could export this data to a CSV for further use
$output = [PSCustomObject]@{
UserDisplayName = $user.displayName
Scope = $accessToken
ValidTo = $up.validTo
PATNAME = $up.displayName
}

# Define the file path inside the repository
$repoFolder = "$(System.DefaultWorkingDirectory)\$(FolderPath)"
$csvFilePath = "$repoFolder\UserPATInfo.csv"

# Ensure the folder exists
if (!(Test-Path -Path $repoFolder)) {
New-Item -ItemType Directory -Path $repoFolder -Force | Out-Null
}

# Initialize an empty CSV file (if not exists)
if (!(Test-Path -Path $csvFilePath)) {
New-Item -Path $csvFilePath -ItemType File | Out-Null
}

# Append the result to a CSV file
$output | Export-Csv -Path $csvFilePath -NoTypeInformation -Append

Write-Host "CSV file saved at: $csvFilePath"
}
}
azurePowerShellVersion: 'LatestVersion'

- script: |
git config --global user.name "tzahik77"
git config --global user.email "tzahik77@msft.com"
git checkout -b master
git add .
git commit -m "Updating json $csvFilePath"
#git push origin HEAD:master
git push origin HEAD:$(Build.SourceBranchName)
displayName: Commit CSV to Repository
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
  • After pipeline completed running, we will be able to see the csv file named UserPATInfo.csv under the Alerts folder:
Press enter or click to view image in full size

🔧 Best Practices to Avoid PAT Expiration Issues

Use Azure DevOps Managed Identities or OAuth instead of PATs where possible.
Use Service Connections with Managed Identity for pipelines instead of hardcoding PATs.
Regularly Rotate PATs and track expiration dates using Azure DevOps security policies.
Store PATs Securely in Azure Key Vault or a secrets manager instead of embedding them in scripts.
Enable Alerts for PAT Expiry so teams get notified before expiration.

Conclusion

This YAML pipeline provides a streamlined method to audit PATs in Azure DevOps.
By running this pipeline regularly, you can enhance security by monitoring expiring tokens and identifying unnecessary or overly permissive PATs.

Stay proactive in managing access controls and ensure that expired or unused tokens are revoked in a timely manner.

--

--

Tzahi Kolber
Tzahi Kolber

Written by Tzahi Kolber

During the last 18 years, I was working as a Senior PFE within Exchange area at Microsoft. Now I’m Senior Consult as Azure IAAS, DevOps & Automations.

No responses yet