Personal Access Token (PAT) is the most recommended authentication method used in automation for authenticating into Azure DevOps Services (ADO). However, by design PAT is used as an alternate password of ADO users, when being used in automation, the automation actually running as the ADO user who generated the PAT. That might not be an ideal authentication identity for automation in some environments where the operation of the automation could be affected by policies that are not designed for automation like having to re-login every X period of time.
In today’s blog post, I’m going to share with you an alternative authentication method that could be used in your non-GUI-based applications especially one that requires zero human interaction during its operation like scheduled automation.
OAuth 2.0 Authentication
Azure DevOps supports OAuth 2.0 as one of its supported authentication methods, allowing your app to seamlessly access ADO REST APIs with minimal ask for usernames and passwords by using the OAuth 2.0 protocol to authorize your app for a user and generate an access token. The generated access token later is used by your app to call the REST APIs.
OAuth 2.0 is mostly recommended for GUI-based web applications where you can establish an interactive login session to generate an access token to be used in calls to ADO REST APIs. However, non-GUI-based applications could leverage this authentication method as well.
How OAuth 2.0 works in Azure DevOps?
In order to use OAuth 2.0 in your app, first, you need to register your app and get an app ID from Azure DevOps Services. Using that app ID, send your users to Azure DevOps Services to authorize your app to access their organizations. After that, use that authorization to get an access token for that user. When your app calls Azure DevOps Services APIs for that user, use that user’s access token.
OAuth 2.0 for non-GUI based Azure DevOps Automation
Steps to apply OAuth 2.0 for non-GUI automation are quite similar to the GUI-based ones except the authorization of the app will be deferred to a single user identity.
The following illustration demonstrates the full flow of the process of applying OAuth 2.0 for non-GUI based automation:

Component | Description |
---|---|
ADO Org Admin | An administrator of the ADO organization |
ADO App Dev | A developer who manages the development, deployment, and operation of the automation |
visualstudio.com | The ADO services for the token management |
Key Vault | An Azure Key Vault instance that stores credentials used by the automation |
ADO App | Automation that uses Azure DevOps Services |
ADO REST APIs | The Azure DevOps Services REST APIs |
blue flow | The process of pre-deployment of the automation |
purple flow | The process of retrieving an access token to call ADO APIs |
green flow | The process of renewing an expired access token. |
Pre-deployment of the automation
This is a process of preparing all needed resources for a successful OAuth 2.0 authentication setup for the automation.
- The ADO automation developer needs to register the automation with ADO. The developer goes to https://app.vsaex.visualstudio.com/app/register to do the registration. When registering the automation, along with basic information about the automation like the company name, the application name, the developer also needs to specify which authorization scopes the automation needs
- Once the registration is complete, ADO will create an ADO app. This ADO app information will be later used by the automation to establish a successful OAuth 2.0 authentication
- Next, the developer needs to authorize the ADO app to access the ADO organization with the requested authorization scopes. This step could be done by the developer himself/herself. However, some authorization scopes require the ADO Organization admin level so in this case, the developer needs to ask the ADO Organization admin to perform the authorization for the ADO app
- To authorize the ADO app, the authorizer needs to call the following authorization URL:
https://app.vssps.visualstudio.com/oauth2/authorize?client_id={appID}&response_type=Assertion&state={state}&scope={scope}&redirect_uri={callbackURL}
- Once the ADO app gets authorized, the authorizer will receive an authorization code, this code is actually a JWT (JSON Web Token) token, short-lived, and will be expired after 15 minutes
- The authorizer hands over the authorization code to the developer to generate an initial access token
- Within 15 minutes from the time the authorization code is generated, the developer needs to make a
POST
call tohttps://app.vssps.visualstudio.com/oauth2/token
to request an initial access token - Once the access token request is complete, the developer will receive an access token along with a refresh token, which will be used later to renew the access token when it expires. Both the access token and refresh token are JWT tokens. The access token is a short-lived one and will only last for 1 hour whereas the refresh token will be expired after 1 year
- The developer needs to store the initial pair of the access token & refresh token along with the ADO app client secret in a Key Vault for later use by the automation.
Retrieving the access token to call Azure DevOps APIs
To use the access token, just include it as a bearer token in the Authorization
header of the HTTP requests.
- The automation makes a request to retrieve the access token from the Key Vault
- The Key Vault returns the access token to the automation
- The automation makes calls to the ADO REST APIs with the following HTTP header:
Authorization: Bearer {access_token}
.
Renewing the access token
The access token is short-lived and will be expired in 1 hour after generated. The access token expires, the automation could use the refresh token to renew the expired access token.
- The automation makes a
POST
call tohttps://app.vssps.visualstudio.com/oauth2/token
to request a new access token - Once the request is complete, the automation will receive a fresh access token as well as a new refresh token
- The automation needs to store that new pair of the access token and refresh token to the Key Vault for later use.
A tricky thing here is the automation needs to renew the expired access token before the refresh token expires. Therefore, the automation should have a scheduled task to make sure the refresh token will be renewed before it gets expired.
Demonstration code
The following Python code snippet demonstrates how to use OAuth 2.0 to authenticate into ADO REST APIs to get a list of PATs of all users under an ADO organization as well as how to renew an access token when it expires.
In order to run the code snippet, you need to get an initial access token first (as mentioned in step 7 of the pre-deployment of the automation) and store it in a file called token.json
. You could use the following curl
command to make the POST
call:
curl --location --request POST 'https://app.vssps.visualstudio.com/oauth2/token' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer' \ --data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer' \ --data-urlencode 'client_assertion=PUT YOUR ADO APP CLIENT SECRET HERE' \ --data-urlencode 'assertion=PUT YOUR ADO APP AUTHORIZATION CODE HERE' \ --data-urlencode 'redirect_uri=PUT YOUR ADO APP AUTHORIZATION CALLBACK URL HERE'
NOTE: To simplify the demonstration code, instead of storing the access token & refresh token in a Key Vault, this pair of tokens is stored in a file called token.json
.
import logging import json import requests from azure.devops.exceptions import AzureDevOpsServiceError from azure.devops.connection import Connection from msrest.authentication import BasicTokenAuthentication ADO_ORG_URL = 'PUT YOUR ADO ORGANIZATION URL HERE' ADO_APP_CLIENT_SECRET = 'PUT YOUR ADO APP CLIENT SECRET HERE' ADO_APP_REDIRECT_URL = 'PUT YOUR ADO APP AUTHORIZATION CALLBACK URL HERE' def retrieve_oauth_token(): with open('token.json', 'r') as token_file: return json.load(token_file) def save_oauth_token(oauth_token): with open('token.json', 'w') as token_file: json.dump(oauth_token, token_file) def refresh_oauth_token(): oauth_token = retrieve_oauth_token() logging.info('Refreshing the OAuth token...') response = requests.post( url='https://app.vssps.visualstudio.com/oauth2/token', data={ 'client_assertion_type': 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', 'client_assertion': ADO_APP_CLIENT_SECRET, 'grant_type': 'refresh_token', 'assertion': oauth_token['refresh_token'], 'redirect_uri': ADO_APP_REDIRECT_URL }, headers={ 'Content-Type': 'application/x-www-form-urlencoded' } ) logging.info( 'Saving the refreshed OAuth token to token.json...') oauth_token = response.json() save_oauth_token(oauth_token) def list_pat_per_person(connection): graph_client = connection.clients_v6_0.get_graph_client() user_list_response = graph_client.list_users() ado_users = user_list_response.graph_users for ado_user in ado_users: user_tokens = [] token_admin_client = connection.clients_v6_0.get_token_admin_client() while True: token_list = token_admin_client.list_personal_access_tokens( ado_user.descriptor) for token in token_list.value: user_tokens.append(token.display_name) if not token_list.continuation_token: break print(str(ado_user.principal_name) + ": " + str(user_tokens)) def get_ado_connection(): oauth_token = retrieve_oauth_token() return Connection( base_url=ADO_ORG_URL, creds=BasicTokenAuthentication( token=oauth_token ) ) # Initialize an ADO connection ado_connection = get_ado_connection() try: # List PATs of all users under the organization list_pat_per_person( connection=ado_connection ) except AzureDevOpsServiceError as ado_error: # Token is expired, refresh it if 'TF400813' in str(ado_error): refresh_oauth_token() # Reinitialize an ADO connection with the fresh token ado_connection = get_ado_connection() # List PATs of all users under the organization list_pat_per_person( connection=ado_connection )
Some thoughts…
Using the OAuth 2.0 authentication method could be something unnecessary or even overkill for some automation. Having to go through a long list of steps for the automation pre-deployment setup in order to have an initial pair of the access token & refresh token created and then maintain another automation along with the main one to make sure the refresh token wouldn’t be expired. That might be too much for some projects and they would stick with the PAT option for their convenience.
A fun experience that I had when working on building automation accessing ADO REST APIs was one of my project’s ADO organizations had Conditional access policies enabled, forcing everyone in the organization to re-login every X hours. The automation was working like a charm until it suddenly stopped working with the following error message:
VS403463: The conditional access policy defined by your Azure Active Directory administrator has failed.
A weird thing was the automation was mystically working back again after hours of doing nothing. It took us a while to investigate and turned out in order to make the automation working, the ADO user associated with the PAT used by the automation has to be logged in! Clearly, the operation of the automation was being affected by the enabled conditional access policies, which obviously were unacceptable for the automation. No one wanted to stay logged in 24/7 just to keep the automation running. If I was that person, it would be challenging for me to take a day off.
What do you think about the suggested approach? Let me know your thoughts in the comment section below. You can also learn more about OAuth 2.0 support in Azure DevOps here.
Great article, really helped me understand the flow and get authenticated, however, I think you have a small mistake.
The grant_type in the posts are mixed up between the pre deployment and the refresh token flow.
What worked for me was:
Initial access token: grant_type needed to be urn:ietf:params:oauth:grant-type:jwt-bearer
Refresh access token: grant_type needed to be refresh_token
Great article and the final thoughts are valuable to me. Thanks!