question

AlecSanchez-0169 avatar image
0 Votes"
AlecSanchez-0169 asked JamesTran-MSFT commented

MSAL SPA and Python Flask on behalf of "Invalid Audience" Response

Hello!

Thank you for taking the time to read this post.

I'm currently developing a full stack application, and an attempting to use an on behalf of flow to call some azure graph APIs. I'm currently able to retrieve a token on behalf of the user, but am receiving a response stating the token has an invalid audience when attempting to call a graph API using the on behalf of token. The audience for the on behalf of token is for azure APIs (https://management.azure.com), not graph APIs (confirmed on https://jwt.ms).

Can someone please provide any assistance on properly calling an azure graph API on behalf of an SPA client, using Python/Flask?


  • Frontend: Javascript/React

  • Backend: Python/Flask

The idea is....

  1. User logs into frontend using @azure/msal-react. This currently works.

  2. User invokes an API call to the Python/Flask application to return information from a mysql database (on-premise stuff). This currently works, and we are able to protect our APIs using azure's tokens.

  3. User invokes an API call to the Python/Flask application, which requests a token on behalf of the user, to call an Azure Graph API, in this case User.ReadBasic.All. This does not work at this time, we are getting a response from Flask stating 'message': 'Access token validation failure. Invalid audience.'


Some additional notes regarding our current setup.

  • The React SPA, and Python Flask Backend are each configured as their own azure application.

  • The frontend application was added as a known application for the backend in Azure Scopes.

  • In the manifest for both applications, accessTokenAcceptedVersion has been changed to 2

  • We do not have "Access tokens" or "ID tokens" checked in authentication for the frontend or backend

  • Below is the function called each time the user invokes an API call to the backend

export const getToken = async (graphToken = false) => {

 const pca = new PublicClientApplication(msalConfig)
 const accounts = pca.getAllAccounts()
 const account = accounts[0]

 //msalLoginScope.scope = api://<client-id-of-python-flask-application>/.default
 //graphConfig.graphmeEndpoint = https://graph.microsoft.com/.default
 //If true, use the above 'graphmeEndpoint' scope to return an access token for all available scopes the user may have
 //Otherwise return a token for the backend's application

 const resp = await pca.acquireTokenSilent({
     scopes: graphToken ? graphConfig.graphMeEndpoint : msalLoginScope.scopes,
     account,
 })
 console.log("Returning access token")

 return resp.accessToken

}

  • This is the function for the backend API on behalf of


       current_access_token = request.headers.get("Authorization", None)
    
    
         if current_access_token is None:
             print("Could not find access token in header")
             raise AuthError({"code": "invalid_header","description":"Unable to parse authorization"" token."}, 401)
    
    
         #SCOPE = "https://management.azure.com/user_impersonation"
         #acquire token on behalf of the user that called this API
         arm_resource_access_token = AuthenticationHelper.get_confidential_client().acquire_token_on_behalf_of(
             user_assertion=current_access_token.split(' ')[1],
             scopes=[current_app.config.get("SCOPE")]
         )
    
         if "error" in arm_resource_access_token:
             raise AuthError({"code": arm_resource_access_token.get("error"),"description":""+arm_resource_access_token.get("error_description")+""}, 404)
    
         headers = {'Authorization': arm_resource_access_token['token_type'] + ' ' + arm_resource_access_token['access_token']}
    
         #MSAL_API_USER_READ_BASIC_ALL = "https://graph.microsoft.com/User.ReadBasic.All"
         subscriptions_list = req.get(current_app.config.get("MSAL_API_USER_READ_BASIC_ALL"), headers=headers).json()
    
         return jsonify(subscriptions_list) <-- This returns the message stating invalid audience.
    

Full response
<class 'dict'>
{'error': {'code': 'InvalidAuthenticationToken', 'message': 'Access token validation failure. Invalid audience.', 'innerError': {'date': '2021-08-16T22:38:08', 'request-id': 'a client id', 'client-request-id': 'a client id'}}}
<Response 325 bytes [200 OK]>






























azure-ad-graphazure-ad-msal
· 4
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

Update 1

The token being sent to the backend (and used to return an ObO token) is obtained using the scope of the backend API URi, which returns as an audience of "<backend-client-id>".
The backend is able to retrieve an ObO token with an audience of "https://management.azure.com", but we are trying to call a graph API of "https://graph.microsoft.com/User.ReadBasic.All", and the Azure response is stating we have an incorrect audience.

So this got me thinking, and I've also taken a look at other posts extremely similar to this issue.... I believe there is something incorrectly configured with the scopes, possibly with both the scope of the token the client sends to the backend, and the scope used to retrieve the ObO token. More to follow...

0 Votes 0 ·

Update 2

Okay so, I believe I'm making some progress.....
The client is retrieving and sending a token for the backend (Python/Flask) application.
In this scenario we want the backend to call the API "https://graph.microsoft.com/User.ReadBasic.All" on behalf of the client.
The backend uses the token (this token should be for the backend application, not the graph API we want to call) with the scope of the intended API call, in this case a graph API, therefore I've set the scope to "https://graph.microsoft.com/.default".

We successfully retrieve a graph token....

  • Has the audience of "aud": "https://graph.microsoft.com"

  • Has scopes of all potential graph APIs the token could be used for

  • Version is 1 "ver": "1.0",

We no longer get a response stating "Invalid Audience", the response now states "Invalid Version", I'm assuming we should be sending a version 2.0 token, but am unsure how to fix this in my particular situation, once again, more to follow.



0 Votes 0 ·

Update 3

Not much progress, but I've done some additional research.

  1. The client is still sending a token to the backend with a scope of the backend API URi

  2. The backend takes this token, with any scope (the scope should contain the azure API), and acquires a token on behalf of the SPA client <-- I've tried using a scope of the graph API, and also just the backend client ID, both are returning tokens with versions of 1.0, according to the azure AD documentation (https://docs.microsoft.com/en-us/azure/active-directory/develop/reference-app-manifest#manifest-reference) the version is determined by the manifest with the key "accessTokenAcceptedVersion", I currently have this set to 2, but am still receiving version 1.0 tokens.

  3. The backend then sends this token in the header to the request of the graph API



0 Votes 0 ·

Correction, the accessTokenAcceptedVersion dictates the version accepted by the API.

0 Votes 0 ·

1 Answer

AlecSanchez-0169 avatar image
0 Votes"
AlecSanchez-0169 answered JamesTran-MSFT commented

I've finally got something working.

  1. Regarding the original "Invalid Audience" response, the client should be sending the backend (or middle tier API) a token FOR THAT APPLICATION.

  2. The scope in my backend function to request the ObO token, is the scope required by the downstream API. This is returning a version 1.0 token, my current understanding is single tenant applications return a 1.0 token, from here I am able to call v1.0 APIs referenced here https://docs.microsoft.com/en-us/graph/api/overview?view=graph-rest-1.0

If you're looking to recreate these helper functions, please take a look at this github repo, there is a folder for both the frontend (django in the github case) and backend (python/flask).

https://github.com/Azure-Samples/ms-identity-python-on-behalf-of



· 1
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

@AlecSanchez-0169
Thank you for following up on this and I'm glad that you were able to resolve your issue!

0 Votes 0 ·