Microsoft-authentication-library-for-dotnet: The ID for the cosmos cache entry is a random string instead of the account identifier when using AquireTokenOnBehalfOf [Bug]

Created on 24 Feb 2021  路  13Comments  路  Source: AzureAD/microsoft-authentication-library-for-dotnet

Logs and Network traces
Without logs or traces, it is unlikely that the team can investigate your issue. Capturing logs and network traces is described at https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/logging

Which Version of MSAL are you using ?
Microsoft.Identity.Client 4.25.0

Platform
.Net Framework 4.7.2

What authentication flow has the issue?

  • Desktop / Mobile

    • [ ] Interactive

    • [ ] Integrated Windows Auth

    • [ ] Username Password

    • [ ] Device code flow (browserless)

  • Web App

    • [ ] Authorization code

    • [x] OBO

  • Daemon App

    • [ ] Service to Service calls

Other? - please describe;

This is when adding msal to an existing app. Msal is not in production on the app currently.

Repro

var your = (code) => here;

Expected behavior
AcquireTokenSilent successfully retrieves token from the cache.

Actual behavior
When using AquireTokenByAuthorisationCode the id of the cosmos cache entry is the account identifier (objectId.tenantId). Then when I call AcquireTokenSilent the token is successfully retrieved from the cache.

However when using AquireTokenOnBehalfOf the id of the cosmos cache entry is (what looks like) a random string e.g. "JwCRE_PxyK4t2A76iuST6W_jewmNE-1epMH8yAXFPMg" or "DpUHeYQg5xT52Tdk62t_shU5m1Lm2BIVkbhQ6EtzucE". The rest of the cache entry looks correct. Then when I call AcquireTokenSilent, the retrieval fails with "No Refresh Token found in the cache", which makes sense since it is trying to use the account identifier to retrieve the token from the cache but that account identifier doesnt exist.

Possible Solution

Additional context/ Logs / Screenshots
Add any other context about the problem here, such as logs and screebshots.

answered question

All 13 comments

@SirElTomato
When using OBO, the token cache key needs to be a hash of the token received by the web API doing OBO. Indeed, the same user could use several devices to access the same API, but some of these devices could be in conditions where conditional access policies apply differently (for instance there could be conditional access policies obliging devices to be on the corporate network, and one device could be on the corporate network and another in a cafe. The one in the cafe should not be able to enable the web API to access the downstream API)

@jmprieur
Thanks for your reply. I'm not sure I understand why that is relevant to my problem though. I'm using an MS caching library (Microsoft.Extensions.Caching.Cosmos) to handle saving and retrieving the tokens and whilst it saves the tokens, it can cannot retrieve them for use later on. Please suggest how I can proceed?

Thanks

@SirElTomato, I understand your question now. Thanks for clarifying.
You don't need to, and shouldn't call AcquireTokenSilent in web APIs (when you've done OBO).
Indeed AcquireTokenOnBehalfOf already does a cache lookup as it has the elements to do it (the incoming token). AcquireTokenSilent does not have these elements.

How can we improve our guidance/doc? Are you aware of somewhere in the wiki/doc where we advise you to call AcquireTokenSilent before AcquireTokenOnBehalfOf ?

@jmprieur
No worries.

I'm still a bit confured, what do I call instead of AcquireTokenSilent?

Let me explain my scenario a bit more.
I have a react native mobile app which signs the user in.
I then send the JWT access token to my custom .net WebAPI.
From the WebAPI I create a ConfidentialClientApplication and configure it to use Cosmos as the UserTokenCache using the Microsoft.Extensions.Caching.Cosmos package.
The WebAPI then calls AcquireTokenOnBehalfOf and passes in the UserAssertion which is created using the JWT from the mobile app.
This creates a cache entry but the ID of that entry is not the account identifier, it is an encrypted string instead.
Then when I want to call Graph API later on behalf of this user, I try to get the tokens using AcquireTokenSilent(scopes, account identifier) but that fails because there is no entry in the cache with the ID equal to that account identifier.

Are you suggesting that I call AcquireTokenSilent before I call AcquireTokenOnBehalfOf in the flow I described above?

Just call AcquireTokenOnBehalfOf @SirElTomato.
Don't call AcquireTokenSilent at all in your web API.

See what Microsoft.Identity.Web, (which you might want to use if you are on ASP.NET Core, btw): https://github.com/AzureAD/microsoft-identity-web/blob/9d9a44abb6c814c9c6d132b46312c94b8ad2ce32/src/Microsoft.Identity.Web/TokenAcquisition.cs#L554, called from https://github.com/AzureAD/microsoft-identity-web/blob/9d9a44abb6c814c9c6d132b46312c94b8ad2ce32/src/Microsoft.Identity.Web/TokenAcquisition.cs#L221. It doesn't call AcquireTokenSilent at all in the case of AcquireTokenOnBehalfOf.

AcquireTokenOnBehalfOf does internally do something already similar to AcquireTokenSilent, but with the right cache keyu (these weird key you've seen).

BTW, we have a breaking change bug which is about throwing when customers use AcquireTokenSilent in the case of OBO flows: https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/1966

@jmprieur Oh okay I see.
Does that mean I need to save the JWT which was originally sent from the mobile app. Then later when I want to call the Graph API, I use that JWT which I have saved to create the UserAssertion to call AcquireTokenOnBehalfOf with?

When I was using AquireTokenByAuthorisationCode, I was saving the IAccount of the user so that I could use the account identifier to call AcquireTokenSilent. But in this case I want to save the original jwt and use that to call AcquireTokenOnBehalfOf whenever I need a token?

I have tried doing what I said above and saved the original jwt from the mobile app in my user document. I then retrieve this jwt from the user document whenever I need to call the graph api.

Thanks for your help, assuming this is the correct approach we can close the issue.

As for the documentation, are you able to point me to where it says that I should save the jwt in order to use it to create a user assertion to retrieve the tokens from the cache? I'll have a read through and try and get back with any suggestions which might have helped me.

Thanks @SirElTomato.
You might want to read https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-web-api-call-api-overview and the following pages.

And to answer your questions above, @SirElTomato

Does that mean I need to save the JWT which was originally sent from the mobile app. Then later when I want to call the Graph API, I use that JWT which I have saved to create the UserAssertion to call AcquireTokenOnBehalfOf with?

Yes. And since the API probably has duration of the request

When I was using AquireTokenByAuthorisationCode, I was saving the IAccount of the user so that I could use the account identifier to call AcquireTokenSilent. But in this case I want to save the original jwt and use that to call AcquireTokenOnBehalfOf whenever I need a token?

Yes. you are building a web API. AcquireTokenByAuthorisationCode is for web apps.

@jmprieur Thanks for your help in resolving this.

In the link you provided it states this, which lead to my confusion:

"The protected web API can also call AcquireTokenSilent later to request tokens for other downstream APIs on behalf of the same user. AcquireTokenSilent refreshes the token when needed."

There are also no examples which don't use the .net core JWT authorisation middleware that I could find. I'm using .net framework and recieve the jwt from the mobile app and store it myself.

@jmprieur I am now having a problem with ConfidentialClientApplication.RemoveAsync
If I pass the IAccount as below I get the following error

            var authResult = await AquireTokenOnBehalfOf(scopes, jwt);

            await ConfidentialClientApplication.RemoveAsync(authResult.Account);

'Response status code does not indicate success: NotFound (404); Substatus: 0; ActivityId: 65f56124-de0e-47d8-8150-0a530559e161; Reason: ({ "Errors": [ "Resource Not Found. Learn more: https://aka.ms/cosmosdb-tsg-not-found" ] });'

In the link you provided it states this, which lead to my confusion:

"The protected web API can also call AcquireTokenSilent later to request tokens for other downstream APIs on behalf of the same user. AcquireTokenSilent refreshes the token when needed."

I've fixed the documentation, @SirElTomato. thanks for the heads-up.

Was this page helpful?
0 / 5 - 0 ratings