Udostępnij za pośrednictwem


Caching and handling expired tokens in Azure Mobile Services managed SDK

A while back my colleague Josh Twist had a great series of posts about A**zu**re **Mo**bile Services (or “Zumo”, as he called). This was when the service was in its infancy, and some things changed, but the content is still excellent. One of the topics was about caching authentication tokens, so that the application wouldn’t need to authenticate the user every time, instead relying on a cached version of the authentication token obtained via one of the calls to the LoginAsync method in the client. And in the following post, he talked about how to handle the case when those tokens were expired using service filters. It is a great post, but the example uses an old (and now unsupported) version of the client SDK for managed code, so I decided to post an updated version of it.

As I mentioned before, the “service filters” which existed in the initial client SDK were replaced by the HttpClient primitives, most notably the HttpMessageHandler class (or its subclass DelegatingHandler, which is more commonly used). Since there was already a good HTTP pipeline with a type which did exactly what the original service filters did, it didn’t make sense to have two, so the service filters were dropped. As a side note, the original client SDK for mobile services didn’t use the HttpClient primitives because they were not released as a portable library; when it did, we changed the mobile services client SDK to use it.

So on to the code. The AuthHandler inherits from the DelegatingHandler class, and when it’s SendAsync method is called, it first sends the message along to the rest of the pipeline, and if it gets an unauthorized response (401), it first tries to log in to the mobile service, then resends the request with the appropriate authentication header set. This handler also calls a delegate that saves the user information, so that the application will have it the next time it’s launched. One last thing which is worth noting is that, since the request can be sent multiple times, my implementation is cloning it, as the request body may not be read more than once.

  1. class AuthHandler : DelegatingHandler
  2. {
  3.     public IMobileServiceClient Client { get; set; }
  4.  
  5.     private Action<MobileServiceUser> saveUserDelegate;
  6.  
  7.     public AuthHandler(Action<MobileServiceUser> saveUserDelegate)
  8.     {
  9.         this.saveUserDelegate = saveUserDelegate;
  10.     }
  11.  
  12.     protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
  13.     {
  14.         if (this.Client == null)
  15.         {
  16.             throw new InvalidOperationException("Make sure to set the 'Client' property in this handler before using it.");
  17.         }
  18.  
  19.         // Cloning the request, in case we need to send it again
  20.         var clonedRequest = await CloneRequest(request);
  21.         var response = await base.SendAsync(clonedRequest, cancellationToken);
  22.  
  23.         if (response.StatusCode == HttpStatusCode.Unauthorized)
  24.         {
  25.             // Oh noes, user is not logged in - we got a 401
  26.             // Log them in, this time hardcoded with Facebook but you would
  27.             // trigger the login presentation in your application
  28.             try
  29.             {
  30.                 var user = await this.Client.LoginAsync(MobileServiceAuthenticationProvider.Facebook);
  31.                 // we're now logged in again.
  32.  
  33.                 // Clone the request
  34.                 clonedRequest = await CloneRequest(request);
  35.  
  36.                 // Save the user to the app settings
  37.                 this.saveUserDelegate(user);
  38.  
  39.                 clonedRequest.Headers.Remove("X-ZUMO-AUTH");
  40.                 // Set the authentication header
  41.                 clonedRequest.Headers.Add("X-ZUMO-AUTH", user.MobileServiceAuthenticationToken);
  42.  
  43.                 // Resend the request
  44.                 response = await base.SendAsync(clonedRequest, cancellationToken);
  45.             }
  46.             catch (InvalidOperationException)
  47.             {
  48.                 // user cancelled auth, so lets return the original response
  49.                 return response;
  50.             }
  51.         }
  52.  
  53.         return response;
  54.     }
  55.  
  56.     private async Task<HttpRequestMessage> CloneRequest(HttpRequestMessage request)
  57.     {
  58.         var result = new HttpRequestMessage(request.Method, request.RequestUri);
  59.         foreach (var header in request.Headers)
  60.         {
  61.             result.Headers.Add(header.Key, header.Value);
  62.         }
  63.  
  64.         if (request.Content != null && request.Content.Headers.ContentType != null)
  65.         {
  66.             var requestBody = await request.Content.ReadAsStringAsync();
  67.             var mediaType = request.Content.Headers.ContentType.MediaType;
  68.             result.Content = new StringContent(requestBody, Encoding.UTF8, mediaType);
  69.             foreach (var header in request.Content.Headers)
  70.             {
  71.                 if (!header.Key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase))
  72.                 {
  73.                     result.Content.Headers.Add(header.Key, header.Value);
  74.                 }
  75.             }
  76.         }
  77.  
  78.         return result;
  79.     }
  80. }

Now to the application to test it out. The client is created passing an instance of the AuthHandler class defined above. Since the handler needs a reference to the client (to be able to log in if the token has expired), the client is then passed on to the handler after it’s created. Maybe not the cleanest of the designs, but it solves the chicken-and-egg problem which we have (client needs handler in its constructor; handler also needs a client). When the client calls the operation on the table, the handler will be invoked and re-login the user if necessary.

  1. private async void btnStart_Click(object sender, RoutedEventArgs e)
  2. {
  3.     try
  4.     {
  5.         AddToDebug("Started");
  6.         var authHandler = new AuthHandler(SaveUser);
  7.         var client = new MobileServiceClient(
  8.             MobileService.ApplicationUri,
  9.             MobileService.ApplicationKey,
  10.             authHandler);
  11.         authHandler.Client = client;
  12.         MobileServiceUser user;
  13.         if (TryLoadUser(out user))
  14.         {
  15.             client.CurrentUser = user;
  16.         }
  17.  
  18.         var table = client.GetTable<Item>();
  19.         var items = await table.Take(3).ToEnumerableAsync();
  20.         AddToDebug("Items: {0}", string.Join(", ", items.Select(i => i.Name)));
  21.     }
  22.     catch (Exception ex)
  23.     {
  24.         AddToDebug("Error: {0}", ex);
  25.     }
  26. }
  27.  
  28. private static bool TryLoadUser(out MobileServiceUser user)
  29. {
  30.     object userId, authToken;
  31.     if (ApplicationData.Current.LocalSettings.Values.TryGetValue("userId", out userId) &&
  32.         ApplicationData.Current.LocalSettings.Values.TryGetValue("authToken", out authToken))
  33.     {
  34.         user = new MobileServiceUser((string)userId)
  35.         {
  36.             MobileServiceAuthenticationToken = (string)authToken
  37.         };
  38.         return true;
  39.     }
  40.     else
  41.     {
  42.         user = null;
  43.         return false;
  44.     }
  45. }
  46.  
  47. private static void SaveUser(MobileServiceUser user)
  48. {
  49.     ApplicationData.Current.LocalSettings.Values["userId"] = user.UserId;
  50.     ApplicationData.Current.LocalSettings.Values["authToken"] = user.MobileServiceAuthenticationToken;
  51. }

If you want to test this yourself, you don’t really need to wait for 30 days (the default expiration of the authentication tokens issued by the mobile services) to see if it works. Since the tokens are signed with the mobile service master key, if that key is changed, then all the cached tokens will become invalid. Be careful that any systems or apps which are using that master key will stop working.

To do that, first run the app once to get a token and save it on the application storage. Once that is done, go to your mobile service dashboard page, and select the “manage keys” option in the bottom.

001-ManageKeys

At that point, select the “regenerate” option, and that will change the key used by the service.

002-RegenerateMasterKey

Now if you run the application again, if the client tries to use the cached token the request will fail with an unauthorized (401) response, and the handler will initiate another login action for the client.

And that’s the implementation for caching and revalidating tokens in the current Azure Mobile Services managed SDK.

Comments

  • Anonymous
    June 27, 2014
    Carlos..great article thanks!I am trying to implement this using Xamarin iOS but following is not an option for calling LoginAsync with the SDK provided for Xamarin...any thoughts of a work around?                var user = await this.Client.LoginAsync(MobileServiceAuthenticationProvider.Facebook);
  • Anonymous
    January 25, 2015
    Hi Carlos! Congratulations on your excellent work!I've followed your implementation on my WinRT app, with the azure AAD provider (MobileServiceAuthenticationProvider.WindowsAzureActiveDirectory) but it seems like the token only lasts 1 hour, instead of 30 days...  Any clues about this?Thanks,
  • Anonymous
    January 26, 2015
    Aline, that's governed by the authentication provider. AAD tokens have an expiration time of 1 hour, so the mobile service token that is generated based on that will also have a 1 hour expiration.
  • Anonymous
    January 26, 2015
    The comment has been removed
  • Anonymous
    January 27, 2015
    Hi Carlos,Finally i get this working with concurrent queries using AsyncLock.In addition, the call to LoginAsync should be preceded by an Ui Thread check, because controls that are binded to incrementalfilteringcollections will send the queries from background threads and in that case the code will fail.Now it works fine for me.Regards,
  • Anonymous
    February 23, 2015
    Hi Carlos,a very good article!Thank you very much for your advices.ByeDaniele
  • Anonymous
    February 26, 2015
    Hi Carlos,I have a question because once I stored the token in my application setting or Isolated storage of the phoneand I reuse this for all the next call I have a problem.In-fact on the backend I have a script on the insert  where I make a call toapis.live.net/.../meBut after around ten calls I receive a message that the token is not valid anymore because the call to this url fails.Obviously if I remove the call on the script I don't receive any errors.Do You know if there is a limit of usage for a token in order to call this url?ThanksDaniele