Unable to fetch OAuth2.0 Token from Azure-DevOps , using Python

Question:

I am trying to use OAuth2 to access the Azure DevopsAPI, to query work-items.
But I am unable to get the access tokene.

I am using Python and Flask. My approach is based on these resources:

Relevant libraries:

from requests_oauthlib import OAuth2Session
from flask import Flask, request, redirect, session, url_for

Parameters:

client_id = "..."
client_secret = "..."
authorization_base_url = "https://app.vssps.visualstudio.com/oauth2/authorize"
token_url = "https://app.vssps.visualstudio.com/oauth2/token"
callback_url = "..."

Step 1: User Authorization. (works fine)

@app.route("/")
def demo():
    azure = OAuth2Session(client_id)
    authorization_url, state = azure.authorization_url(authorization_base_url)

    session['oauth_state'] = state
    authorization_url += "&scope=" + authorized_scopes + "&redirect_uri=" + callback_url
    print(authorization_url)
    return redirect(authorization_url)

Step 2: Retrieving an access token (generates an error)

@app.route("/callback", methods=["GET"])
def callback():

    fetch_body = "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" 
                 "&client_assertion=" + client_secret + 
                 "&grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" 
                 "&assertion=" + request.args["code"] + 
                 "&redirect_uri=" + callback_url

    azure = OAuth2Session(client_id, state=session['oauth_state'])

    token = azure.fetch_token(token_url=token_url, client_secret=client_secret,
                               body=fetch_body,
                               authorization_response=request.url)
    azure.request()

    session['oauth_token'] = token

    return redirect(url_for('.profile'))

The application-registration and adhoc-SSL-certification are working fine (using it just temporary).

When I use the client_assertion in Postman, I get a correct response from Azure:
Postman Request

But when I execute the code, this error is thrown:

oauthlib.oauth2.rfc6749.errors.MissingTokenError: (missing_token) Missing access token parameter.

Which only lets me know, that no token was received.

There is one issue in the generated request body, where the grant_type is added twice:

grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer
grant_type=authorization_code

The first value is expected by Azure, but the second one is generated automatically by the library.

Now when I specify the grant_type in the fetch_token call, like this:

token = azure.fetch_token(token_url=token_url, client_secret=client_secret,
                           body=fetch_body, grant_type="urn:ietf:params:oauth:grant-type:jwt-bearer",
                           authorization_response=request.url)

I get this error

TypeError: prepare_token_request() got multiple values for argument 'grant_type'

And the actual request to Azure is not even sent.

I see in the web_application.py that is used by oauth2_session.py, that grant_type =’authorization_code’ is set fixed, so I guess this library is generally incompatible with Azure.

Is that the case?
If so, what would be the simplest way to connect to Azure-OAuth with Python (Flask)?

I would be very grateful for any advice and help that point me in the right direction.

Asked By: Ben Baker

||

Answers:

I just found the azure.devops library that solves my problem.

Ressources

from azure.devops.connection import Connection
from azure.devops.v5_1.work_item_tracking import Wiql
from msrest.authentication import BasicAuthentication
import pprint

# Fill in with your personal access token and org URL
personal_access_token = '... PAT'
organization_url = 'https://dev.azure.com/....'

# Create a connection to the org
credentials = BasicAuthentication('', personal_access_token)
connection = Connection(base_url=organization_url, creds=credentials)

# Get a client (the "core" client provides access to projects, teams, etc)
core_client = connection.clients.get_core_client()

wit_client = connection.clients.get_work_item_tracking_client()

query = "SELECT [System.Id], [System.WorkItemType], [System.Title], [System.AssignedTo], [System.State]," 
        "[System.Tags] FROM workitems WHERE [System.TeamProject] = 'Test'"
wiql = Wiql(query=query)
query_results = wit_client.query_by_wiql(wiql).work_items

for item in query_results:
    work_item = wit_client.get_work_item(item.id)
    pprint.pprint(work_item.fields['System.Title'])
Answered By: Ben Baker