FIL
FIL

Reputation: 1218

Should TokenCache be used for offline acces with MSAL?

I am implementing authentication using MSAL and I need some guidance for handling refresh tokens.

My Angular Web App is authenticating with my ASP.NET Web API using MSAL. Web API requires some scopes for accessing Microsoft Graph, so it uses "On Behalf Of" OAuth 2.0 flow to get an access token for calling MS Graph. This part is done and works.

The problem is that MS Graph will be called after some time by my .NET daemon app (using OBO flow) when access token will expire.

What I need is to get refresh token by my Web API and cache it (e.g. in SQL database) so it can be read by daemon app and used to obtain a valid access token.

I suppose that the TokenCache for the confidential client application is the right way to do this but I'm not sure how to get a valid access token by daemon app.

Here is the code of my daemon app I want to use to get access token from AAD:

var userAssertion = new UserAssertion(
    <accessToken>,
    "urn:ietf:params:oauth:grant-type:jwt-bearer");
var authority = authEndpoint.TrimEnd('/') + "/" + <tenant> + "/";
var clientCredencial = new ClientCredential(<clientSecret>);
var authClient = new ConfidentialClientApplication(<clientId>, authority, <redirectUri>, 
    clientCredencial, <userTokenCache>, null);

try
{
    var authResult =
        await authClient.AcquireTokenOnBehalfOfAsync(<scopes>, userAssertion, authority);
    activeAccessToken = authResult.AccessToken;
}
catch (MsalException ex)
{
    throw;
}

Should I provide <userTokenCache> to get the refresh token form cache? If yes, UserAssertion requires an <accessToken> to be provided, but I don't know what value should be used.

Or should I make a token request on my own and get the refresh token from the response since it is not supported by MSAL? Then I could store the refresh token in the database and use it as <accessToken> with null as <userTokenCache> in daemon app.

I thought it is possible to get the refresh token using MSAL, but I found it is not.

Update

I forgot to say that all of my apps use the same Application ID (this is due to the limitations of the AADv2 endpoint, although I just found that it was removed from the docs at Nov 2nd 2018).

Why not client credentials flow?

Communication with MS Graph could be performed in Web API (using OBO flow) but the task may be delayed by the user, e.g. send mail after 8 hours (Web API will store tasks in the database). The solution for this case is an app (daemon) that runs on schedule, gets tasks from the database and performs calls to MS Graph. I prefer not to give admin consent to any of my apps because it is very important to get consent from the user. If the consent is revoked, call to MS Graph should not be performed. That is why the daemon app should use the refresh token to get access token from AAD for accessing MS Graph (using OBO flow).

I hope it is clear now. Perhaps I should not do it this way. Any suggestion would be appreciated.

Upvotes: 2

Views: 2983

Answers (1)

Jean-Marc Prieur
Jean-Marc Prieur

Reputation: 1651

MSAL does handle the refresh token itself, you just need to handle the cache serialization. - the userTokenCache is used by the OBO call, and you use the refresh token by calling AcquireTokenSilentAsycn first (that's what refreshes tokens) - the applicationTokenCache is used by the client credentials flow (AcquireTokenForApplication).

I'd advise you to have a look at the following sample which illustrates OBO: https://github.com/Azure-Samples/active-directory-dotnet-native-aspnetcore-v2, in particular TodoListService/Extensions/TokenAcquisition.cs#L275-L294

the code is :

var accounts = await application.GetAccountsAsync();
try
{
 AuthenticationResult result = null;
 var allAccounts = await application.GetAccountsAsync();
 IAccount account = await application.GetAccountAsync(accountIdentifier);
 result = await application.AcquireTokenSilentAsync(scopes.Except(scopesRequestedByMsalNet), account);
 return result.AccessToken;
}
catch (MsalUiRequiredException ex)
{
 ...

Now the cache is itself initialized from the bearer token that is sent by your client to your Web API. See

TodoListService/Extensions/TokenAcquisition.cs#L305-L336

private void AddAccountToCacheFromJwt(IEnumerable<string> scopes, JwtSecurityToken jwtToken, AuthenticationProperties properties, ClaimsPrincipal principal, HttpContext httpContext)
{
 try
 {
  UserAssertion userAssertion;
  IEnumerable<string> requestedScopes;
  if (jwtToken != null)
  {
   userAssertion = new UserAssertion(jwtToken.RawData, "urn:ietf:params:oauth:grant-type:jwt-bearer");
   requestedScopes = scopes ?? jwtToken.Audiences.Select(a => $"{a}/.default");
  }
  else
  {
   throw new ArgumentOutOfRangeException("tokenValidationContext.SecurityToken should be a JWT Token");
   }

   var application = CreateApplication(httpContext, principal, properties, null);

   // Synchronous call to make sure that the cache is filled-in before the controller tries to get access tokens
   AuthenticationResult result = application.AcquireTokenOnBehalfOfAsync(scopes.Except(scopesRequestedByMsalNet), userAssertion).GetAwaiter().GetResult();
  }
  catch (MsalUiRequiredException ex)
  {
   ...

Upvotes: 1

Related Questions