3

We are using Azure AD to authenticate and get the refreshed access token every 30 mins. We invoke below method which acquires security token and add it to request header.

var userObjectId = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
var authContext = new AuthenticationContext(Authority, new NaiveSessionCache(userObjectId));
var credential = new ClientCredential(ConfigurationManager.AppSettings["ida:ClientId"],
ConfigurationManager.AppSettings["ida:ClientSecret"]);

    try
    {
    var authenticationResult = authContext.AcquireTokenSilent(ConfigurationManager.AppSettings["WebAPIBaseAddress"], credential, new UserIdentifier(userObjectId, UserIdentifierType.UniqueId));
    //set cookie for azure oauth refresh token - on successful login
    var httpCookie = HttpContext.Current.Response.Cookies["RefreshToken"];
    if (httpCookie != null)
        httpCookie.Value = authenticationResult.RefreshToken;

    request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken);
    }
    catch
    {
    //Get access token using Refresh Token 
    var authenticationResult = authContext.AcquireTokenByRefreshToken(httpCookie.Value, credential, ConfigurationManager.AppSettings["WebAPIBaseAddress"]);
    }

In above method, we have used AcquireTokenSilent method which gives us access token. Since access token lasts only for certain period of time. After its expiry, we call AcquireTokenByRefreshToken to get refresh token.

The above code works well, however we are getting below exception randomly:

Microsoft.IdentityModel.Clients.ActiveDirectory.AdalSilentTokenAcquisitionException: Failed to acquire token silently. Call method AcquireToken 
   at Microsoft.IdentityModel.Clients.ActiveDirectory.AcquireTokenSilentHandler.SendTokenRequestAsync() 
   at Microsoft.IdentityModel.Clients.ActiveDirectory.AcquireTokenHandlerBase.<RunAsync>d__0.MoveNext()
ErrorCode: failed_to_acquire_token_silently

What could be the reason of such inconsistent behaviour? The same code is working on few environments (Stage/Dev) but its throwing error randomly on Production.

Please suggest.

Arturo Martinez
  • 3,737
  • 1
  • 22
  • 35
  • How did you configure the authentication ? Did you have a look at this post http://stackoverflow.com/questions/34888661/azure-active-directory-mvc-application-best-practices-to-store-the-access-toke ? – Thomas Jan 21 '16 at 08:57
  • What is the version are you using for Microsoft.IdentityModel.Clients.ActiveDirectory? – juvchan Jan 21 '16 at 08:59
  • @Thomas: Thanks. We have done the authentication in preetymuch the same fashion in startup.cs as mentioned in the link shared by you. – Sushant Sonarghare Jan 21 '16 at 09:15
  • @juvchan: The specific version of Microsoft.IdentityModel.clients.ActiveDirectory we are using is 2.16 (2.16.20422.1202) – Sushant Sonarghare Jan 21 '16 at 09:21
  • Is there any specific reason to stick with this old version? I recommend you to upgrade to the latest stable version (2.20.301151232) which just released less than 1 week ago. Works fine for me. I believe it also does not contain breaking change which break your code. Avoid using alpha release. – juvchan Jan 21 '16 at 09:24
  • Thanks @juvchan. But, how is it working with other environments for which the AD is configured under the same subscription with the same code base? – Sushant Sonarghare Jan 21 '16 at 09:30
  • @SushantSonarghare, when you mention "inconsistent", do you mean the error only occurs in your Production environment and only occurs randomly? – juvchan Jan 21 '16 at 09:32
  • Yes @juvchan. This is happening only on Production and that too randomly. – Sushant Sonarghare Jan 21 '16 at 09:36
  • Based on experience, this could due to configurations because the same codebase works fine on other environments... There is too little info to help you determine the root cause at this point – juvchan Jan 21 '16 at 09:39
  • 1
    @SushantSonarghare, when you try to get the token silently, it will get the token from the TokenCache. So if the token is not valid anymore you will get this error. what you can do is catching AdalException and force the user to reauthentificate. – Thomas Jan 21 '16 at 10:04
  • 3
    Side note, you should never need to call AcquireTokenByRefreshToken. If you call AcquireTokenSilent, ADAL will automatically select the best refresh token from the cache - and it will save the new refresh token transparently. Let me stress this: assuming that you are persisting your cache, there should be no scenario whatsoever in which you must manipulate the refresh token directly. – vibronet Jan 21 '16 at 18:55
  • Thank you everyone for your responses. Unfortunately, we are getting the exception in other environments too. – Sushant Sonarghare Jan 22 '16 at 10:31
  • Try enabling logging to get better diagnostics. See http://stackoverflow.com/q/27364887/18044 – MvdD Jan 23 '16 at 20:49
  • As a side / side note, adding RefreshToken as a cookie, provides the user-agent the opportunity to use it. – Brent Schmaltz Jan 28 '16 at 07:12

2 Answers2

2

We were able to resolve this. It seems to be a small mistake in the code itself. When the AccessToken expires, it throws an exception and it tries to fetch a new one using AcquireTokenByRefreshToken in the catch block. Here we were not setting the newly received refresh token back in the Cookie. We need to add below statement in the catch block also, so that it would get the Refresh token, which can then be passed back to generate a new Access Token.

httpCookie.Value = authenticationResult.RefreshToken;
  • 1
    It's not how OAuth should be used! Storing any token in cookies is highly insecure (refer to http://security.stackexchange.com/questions/100600/where-to-store-access-and-refresh-tokens-on-asp-net-client-web-app-calling-a-r ). Besides it will lead you to strange auth flows. Please read again answer here or my other answer ( http://stackoverflow.com/questions/25267831/asp-net-web-api-and-openid-connect-how-to-get-access-token-from-authorization-c/34730754#34730754 ) to see how to handle refresh tokens. Basically saying they should be transparent for you (acquire silent does it automatically). – andrew.fox Apr 14 '16 at 06:38
  • you cannot do this in catch method. Here the issue is authenticationResult is not picking data and throws error at that point. how can authenticationResult.RefreshToken has value? – Kurkula Mar 03 '17 at 20:01
1

First of all, before using AcquireTokenSilent you must invoke AcquireTokenByAuthorizationCodeAsync.

var context = new AuthenticationContext(authorityUri);
var credential = new ClientCredential(clientId, clientSecretKey);

await context.AcquireTokenByAuthorizationCodeAsync(authorizationCode, new Uri(redirectUri), credential);

AcquireTokenByAuthorizationCodeAsync stores access token and refresh token in TokenCache.DefaultShared (for user uniqueId received from auth procedure).

Assuming you do that, access tokens and refresh tokens do expire. If that happens, you must catch AdalSilentTokenAcquisitionException exception:

try
{
    // currentUser = new UserIdentifier() for: ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier")
    AuthenticationResult authResult = await context.AcquireTokenSilentAsync(resourceUri, credential, currentUser);

    return authResult.AccessToken;
}
catch (AdalSilentTokenAcquisitionException)
{
    return null;
}

Invoke this method before every request to the resource. It doesn't cost much, ideally nothing, or oauth API hit with refreshToken.

But when AdalSilentTokenAcquisitionException is thrown (or it's a first call). You must call procedure that performs full access code retrieval from oauth API. What procedure? It depends on the type of auth process you're using.

With full owin auth it can be:

  • redirect to authority uri with {"response_type", "code" }
  • or invoking HttpContext.GetOwinContext().Authentication.Challenge(OpenIdConnectAuthenticationDefaults.AuthenticationType); (return null from controller's action as the Challenge() method alters HTTP response to force redirection to auth server). End processing of current request (with returning null). Auth server will invoke your authorization method (AuthorizationCodeReceived event from UseOpenIdConnectAuthentication's Notifications) with new authorization code. Later, redirect back to the origin page that needs the token.

So, you may get AdalSilentTokenAcquisitionException because cache is expired and refreshToken is expired. You have to reauthenticate again (it's transparent, no login page required).

andrew.fox
  • 7,435
  • 5
  • 52
  • 75
  • Thanks for the detailed answer & sorry for the late reply, but we are doing the steps suggested by you. Can you please elaborate more on the Full Code retrieval section of your answer? var authContext = new AuthenticationContext(Authority, new NaiveSessionCache(userObjectId)); authContext.AcquireTokenByAuthorizationCode(code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, GraphResourceId); – Sushant Sonarghare Mar 21 '16 at 07:07
  • @SushantSonarghare - What kind of authentication process are you using? a) Owin .net library like here: https://github.com/Azure-Samples/active-directory-dotnet-webapp-webapi-multitenant-openidconnect/blob/master/TodoListWebApp/App_Start/Startup.Auth.cs#L34 ; or b) ADAL library https://powerbi.microsoft.com/en-us/documentation/powerbi-developer-authenticate-to-power-bi-service/ ? – andrew.fox Mar 29 '16 at 18:26