Windows 8.1: Accounts, Settings, Authentication, Azure Mobile Services

It’s pretty easy with Azure Mobile Services to build either a custom end point or an end point that exposes tabular data and to require users to authenticate before they can access that end point.

For example, if I create myself a free service called mtTimeService sitting on top of a free database;

image

Then I can create a custom API for that service which returns the date/time;

image

and that’s then available (at the time of writing) from http://mtTimeService.azure-mobile.net/api/currenttime (HTTP GET).

But I might not want this valuable IP to be available from every client out there so I might require a user to authenticate before they can call my service which is pretty easy to do with mobile services in that I can demand that a user of the service is authenticated;

image

and I can configure mobile services to use authentication providers like (e.g.) Twitter and Google to authenticate users who want access to this service. This involves visiting the Twitter developer site and the Google Cloud Console and setting the right bits and pieces up but, ultimately, it comes down to gathering some secrets that need to be entered into the Azure Mobile Services portal as below;

image

I can then add a reference to the client-side libraries for Azure Mobile Services (either via the “Add Connected Service…” option in Visual Studio);

image

or via NuGet;

image

and then I could write a little bit of code to use the MobileServiceClient class to login and then invoke the API;

      MobileServiceClient client = new MobileServiceClient(
        "https://mtTimeService.azure-mobile.net");

      MobileServiceUser user = await client.LoginAsync(
        MobileServiceAuthenticationProvider.Twitter);

      if (user != null)
      {
        JToken result = await client.InvokeApiAsync(
          "currenttime", System.Net.Http.HttpMethod.Get, null);
      }

and the Web Authentication Broker pops up its oAuth authentication dialog;

image

and then on Windows 8.1 I would also get the option to store these credentials for re-use at a later point;

image

or if I was logging in via Google’s authentication I could use;

      MobileServiceClient client = new MobileServiceClient(
        "https://mtTimeService.azure-mobile.net");

      MobileServiceUser user = await client.LoginAsync(
        MobileServiceAuthenticationProvider.Google);

      if (user != null)
      {
        JToken result = await client.InvokeApiAsync(
          "currenttime", System.Net.Http.HttpMethod.Get, null);
      }

and see;

image

image

and that works quite nicely and gets my client application a result. I could use JSON deserialization to get the data into a .NET type – e.g.;

  [DataContract(Name="unused")]
  class ServiceResult
  {
    [DataMember(Name="currenttime")]
    public DateTimeOffset CurrentTime { get; set; }
  }

and then change my API call slightly;

        ServiceResult result = await client.InvokeApiAsync<ServiceResult>(
          "currenttime", System.Net.Http.HttpMethod.Get, null);

and that’s interesting but leaves a couple of questions;

  1. What happens if this application suspends/terminates and then the user re-runs the application. That is – we could have a sequence of events like;
    1. User runs app X.
    2. User signs in.
    3. User moves to another app.
    4. Original app X is suspended.
    5. User runs another app.
    6. Original app X is terminated.
    7. User goes back to app X.
    8. App X asks the user to sign in again having lost the user’s logged in status even though they perhaps the gap in time between step 1 and step 8 could be tiny.
  2. What happens if the mobile service times out whatever token it has handed down to the app?
  3. Where does this type of login/logout account management UI need to go in my app?

Surviving Suspend/Terminate/Re-Run

Clearly, to be able to preserve the user’s logged in context across a suspend/terminate/re-run scenario something about the logged in context needs to be stored somewhere that survives the lifetime of the app process.

The post that Josh wrote over here points the way on this one talking about storing the user id and authentication token. They’re pretty easy to get to so we can write code something like;

      MobileServiceClient client = new MobileServiceClient(
        "https://mtTimeService.azure-mobile.net");

      PasswordVault vault = new PasswordVault();
      MobileServiceUser user = null;

      // this seems to throw a genuine System.Exception :-S
      try
      {
        var credentials = vault.FindAllByResource("mtTimeService");
        var passwordCreds = credentials.First();
        passwordCreds.RetrievePassword();
        user = new MobileServiceUser(passwordCreds.UserName);
        user.MobileServiceAuthenticationToken = passwordCreds.Password;
        client.CurrentUser = user;
      }
      catch (Exception)
      {

      }
      if (user == null)
      {
        // this could fail, not catching here.
        user = await client.LoginAsync(
          MobileServiceAuthenticationProvider.Google);

        vault.Add(new PasswordCredential("mtTimeService", user.UserId,
          user.MobileServiceAuthenticationToken));
      }
      var result = await client.InvokeApiAsync<ServiceResult>("currenttime",
        HttpMethod.Get, null);

      client.Logout();

And that should survive suspend/terminate relying on the PasswordVault APIs to do that in a secure way.

Tokens Will Expire

The problem with the code above is that at some point it will extract the stored token from the PasswordVault and will pass it across to the Azure Mobile Service but the token will have expired and the login call will fail and return a 401, Unauthorized error.

The way that the code is written, it’s a bit difficult to even begin testing for that scenario. I don’t know the exact expiration time for one of these tokens but it’s certainly more than a few minutes so, given the code above, how long do I wait around for a token to expire so that I can test out that scenario?

It’s easier to refactor the code and make the 2 scenarios of;

  1. No token available so the service call is unauthorized.
  2. Expired token available so the service call is unauthorized.

into the same scenario by intercepting the call that the MobileServiceClient makes. This is something that Josh talks about in his post but I think the details of the mechanism have changed slightly.

The code that I want to write is this;

      MobileServiceClient client = new MobileServiceClient(
        "https://mtTimeService.azure-mobile.net");

      // if the client has never logged in, pop a dialog and get a login and
      // store the auth token.
      // if the client has logged in and we have an auth token use it but
      // if it's expired renew it.
      // then make the call below.
      var result = await client.InvokeApiAsync<ServiceResult>("currenttime",
        HttpMethod.Get, null);

and I want to have the call to InvokeApiAsync transparently authenticate if it needs to including handling the initial case where I don’t have any cached credentials and also the case where any cached credentials have expired. If I add in something like this HttpMessageHandler;

  class AutoLoginHandler : HttpClientHandler
  {
    public AutoLoginHandler()
    {
      this.RetrieveCachedCreds();
    }
    public MobileServiceClient ServiceClient
    {
      get
      {
        return (this.serviceClient);
      }
      set
      {
        this.serviceClient = value;
        this.serviceClient.CurrentUser = this.user;
      }
    }
    void RetrieveCachedCreds()
    {
      this.vault = new PasswordVault();

      try
      {
        var credentials = vault.FindAllByResource(VAULT_KEY);
        var passwordCreds = credentials.First();
        passwordCreds.RetrievePassword();

        this.user = new MobileServiceUser(passwordCreds.UserName);
        this.user.MobileServiceAuthenticationToken = passwordCreds.Password;
      }
      catch
      {
      }
    }
    void StoreCachedCreds()
    {
      try
      {
        var credential = this.vault.Retrieve(VAULT_KEY, this.user.UserId);
        this.vault.Remove(credential);
      }
      catch
      {
      }
      this.vault.Add(new PasswordCredential(
        VAULT_KEY,
        this.user.UserId,
        this.user.MobileServiceAuthenticationToken));
    }

    protected async override Task<HttpResponseMessage> SendAsync(
      HttpRequestMessage request, CancellationToken cancellationToken)
    {
      HttpResponseMessage response =
        await base.SendAsync(request, cancellationToken);

      if (!response.IsSuccessStatusCode &&
        (response.StatusCode == System.Net.HttpStatusCode.Unauthorized))
      {
        await this.ServiceClient.LoginAsync(MobileServiceAuthenticationProvider.Google);

        request.Headers.Add(AUTH_HEADER, 
          this.ServiceClient.CurrentUser.MobileServiceAuthenticationToken);

        response = await base.SendAsync(request, cancellationToken);

        this.user = this.ServiceClient.CurrentUser;

        this.StoreCachedCreds();
      }
      return (response);
    }
    MobileServiceClient serviceClient;
    PasswordVault vault;
    MobileServiceUser user;
    static string VAULT_KEY = "mtTimeService";
    static string AUTH_HEADER = "X-ZUMO-AUTH";
  }

and then I can use that by modifying my initial code just slightly to pass the MobileServiceClient instance to the AutoLoginHandler that I’ve written;

      AutoLoginHandler handler = new AutoLoginHandler();

      MobileServiceClient client = new MobileServiceClient(
        "https://mtTimeService.azure-mobile.net", null, handler);

      handler.ServiceClient = client;

      var result = await client.InvokeApiAsync<ServiceResult>("currenttime",
        HttpMethod.Get, null);

and then the first time this code is run it’s not going to find any cached credentials so the call to the API is going to fail with a 401 which will cause the handler to attempt to login and, assuming that works out, it will then re-try the original request with the authentication token attached to it and store that locally (via the PasswordVault) in order to re-use it on future requests until it expires and causes a new 401 when the process should begin again.

Having got something coming together around this idea of logging in and staying logged in, how could this surface to the user?

Windows 8.1 Settings Charm with Accounts Functionality

A change in Windows 8.1 is the addition to the settings charm of functionality to deal with accounts. There’s a little bit on this documented on the Windows Developer Centre and there’s also a discussion about this in this BUILD 2013 video (start at the 37 minute mark);

To experiment with this I wrote a simple page with a button on it that would invoke the same currenttime API of the mtTimeService. I ended up with a bunch of code as below which needs a bunch of refactoring but seems to more or less hang together in the first instance;

 public sealed partial class MainPage : Page
  {
    MobileServiceClient serviceClient;
    PasswordVault vault;

    public MainPage()
    {
      this.InitializeComponent();
      this.Loaded += OnLoaded;
    }
    bool HasCurrentUser
    {
      get
      {
        return (this.serviceClient.CurrentUser != null);
      }
    }
    bool UserIdIsCurrentUser(string userId)
    {
      return (this.HasCurrentUser &&
        (this.serviceClient.CurrentUser.UserId == userId));
    }
    async void OnLoaded(object sender, RoutedEventArgs args)
    {
      this.vault = new PasswordVault();

      this.serviceClient = new MobileServiceClient(
        "https://mtTimeService.azure-mobile.net");

      SettingsPane settingsPane = SettingsPane.GetForCurrentView();
      AccountsSettingsPane accountSettingsPane = AccountsSettingsPane.GetForCurrentView();
      settingsPane.CommandsRequested += OnCommandsRequested;
      accountSettingsPane.AccountCommandsRequested += OnAccountCommandsRequested;
    }
    void OnCommandsRequested(SettingsPane sender, SettingsPaneCommandsRequestedEventArgs args)
    {
      args.Request.ApplicationCommands.Add(SettingsCommand.AccountsCommand);
    }
    void OnAccountCommandsRequested(AccountsSettingsPane sender,
      AccountsSettingsPaneCommandsRequestedEventArgs args)
    {
      args.WebAccountProviderCommands.Add(
        new WebAccountProviderCommand(
          new WebAccountProvider(
            MobileServiceAuthenticationProvider.Twitter.ToString(), 
            "Twitter", 
            new Uri("ms-appx:///Assets/twitter.png")),
          OnWebAccountProviderInvoked));

      args.WebAccountProviderCommands.Add(
        new WebAccountProviderCommand(
          new WebAccountProvider(
            MobileServiceAuthenticationProvider.Google.ToString(), 
            "Google", 
            new Uri("ms-appx:///Assets/google.png")),
            OnWebAccountProviderInvoked));

      this.AddWebCommandsForStoredCredentials(args);
    }
    void AddWebCommandsForStoredCredentials(AccountsSettingsPaneCommandsRequestedEventArgs args)
    {
      foreach (var providerCmd in args.WebAccountProviderCommands)
      {
        try
        {
          var list = this.vault.FindAllByResource(providerCmd.WebAccountProvider.Id);

          foreach (var creds in list)
          {
            SupportedWebAccountActions accountActions = SupportedWebAccountActions.Remove;
            
            if (!this.UserIdIsCurrentUser(creds.UserName))
            {
              accountActions |= SupportedWebAccountActions.Reconnect;
            }

            WebAccount account = new WebAccount(
              providerCmd.WebAccountProvider,
              creds.UserName,
              this.UserIdIsCurrentUser(creds.UserName) ? 
                WebAccountState.Connected : WebAccountState.None);

            args.WebAccountCommands.Add(
              new WebAccountCommand(
                  account,
                  OnWebAccountInvoked,
                  accountActions));
          }
        }
        catch // previous call seems to throw System.Exception if nothing is found
        {

        }
      }
    }
    async void OnWebAccountProviderInvoked(WebAccountProviderCommand cmd)
    {
      MobileServiceAuthenticationProvider provider = (MobileServiceAuthenticationProvider)
        Enum.Parse(typeof(MobileServiceAuthenticationProvider), cmd.WebAccountProvider.Id);

      this.serviceClient.Logout();

      await this.serviceClient.LoginAsync(provider);

      if (this.HasCurrentUser)
      {
        this.AddCredentialsToStore(provider);
      }
    }
    void OnWebAccountInvoked(object sender, WebAccountInvokedArgs args)
    {
      WebAccountCommand webAccountCmd = (WebAccountCommand)sender;
      WebAccount webAccount = webAccountCmd.WebAccount;

      // not expecting this to not return the entry...
      PasswordCredential cred = this.vault.FindAllByUserName(webAccount.UserName).Single();
      
      if (args.Action == WebAccountAction.Remove)
      {
        if (this.UserIdIsCurrentUser(webAccount.UserName))
        {
          this.serviceClient.Logout();
        }
        this.vault.Remove(cred);
      }
      else if (args.Action == WebAccountAction.Reconnect)
      {
        if (this.HasCurrentUser)
        {
          this.serviceClient.Logout();
        }
        cred.RetrievePassword();
        this.serviceClient.CurrentUser = new MobileServiceUser(cred.UserName);
        this.serviceClient.CurrentUser.MobileServiceAuthenticationToken = cred.Password;
      }
    }
    void AddCredentialsToStore(MobileServiceAuthenticationProvider provider)
    {
      string key = provider.ToString();

      bool existing = false;

      try
      {
        var list = vault.FindAllByResource(key);
        var count = list.Count;
        existing = list.Any(c => c.UserName == this.serviceClient.CurrentUser.UserId);
      }
      catch // that FindAllByResource call seems to throw System.Exception
      {

      }
      if (!existing)
      {
        vault.Add(new PasswordCredential(
          key,
          this.serviceClient.CurrentUser.UserId,
          this.serviceClient.CurrentUser.MobileServiceAuthenticationToken));
      }
    }
    async void OnButtonClickInvokeService(object sender, RoutedEventArgs e)
    {
      try
      {
        ServiceResult result =
          await this.serviceClient.InvokeApiAsync<ServiceResult>(
            "currenttime",
            HttpMethod.Get, 
            null);
      }
      catch (Exception ex)
      {
        MessageDialog dialog = new MessageDialog(ex.Message);
        dialog.ShowAsync();
      }
    }
  }

That’s quite a bit of code and it’s important to say that this does not really do any of the stuff I talked about in the previous sections in terms of handling token timeouts or anything like that – that would need adding.

When this code is run for the first time, the user is going to be unauthorized so if they click on the button that invokes the service they’ll get;

image

but then the code implements the Settings Contract via the method OnCommandsRequested. So, when I swipe in from the right hand side of the screen;

image

the OnCommandsRequested method is going to add a single command into the commands collection which is the new (in 8.1) SettingsCommand.AccountsCommand.

When that command is invoked (via a tap) the OnLoaded method has already registered the OnAccountCommandsRequested handler. The UI that gets displayed in the first instance is;

image

The OnAccountCommandsRequested handler adds 2 different WebAccountProviderCommands into the passed AccountsSettingsPaneCommandsRequestedEventArgs.WebAccountProviderCommands property passed as an event args;

  • One for Twitter authentication
  • One for Google authentication

If the user invokes either of these commands then the system calls back to the code’s OnWebAccountProviderInvoked implementation to indicate that the user has chosen one of these providers.

That handler implementation figures out which of the providers has been chosen and then passes that on to MobileServiceClient.LoginAsync and asks it to log in so I get the WebAuthenticationBroker dialog;

image

and then I can log in and if I attempt to call the service API again now that the MobileServiceClient has authenticated then that call will work fine (subject to time outs etc).

There’s a bit more going on in the code though. After logging in to the service, the OnWebAccountProviderInvoked handler grabs the user id and authentication token that have been returned from the login and attempts to store them in PasswordVault under a key of either Twitter/Google depending on which provider we’re using.

When the Settings charm is invoked again, the rest of the code in OnAccountCommandsRequested invokes the AddWebCommandsForStoredCredentials method which essentially goes through all the accounts that it finds in the PasswordVault under both the Twitter and Google keys and for each one of those entries it adds a WebAccountCommand into the AccountsSettingsPaneCommandsRequestedEventArgs.WebAccountCommands collection.

This is how the app code is trying to help the user by keeping any accounts that they have used to authenticate so that they can easily use them again. In the real world, the app might be returning different data based on which set of credentials the user is logging in with and might want to offer the user an easy way to choose between them.

In the UI this looks like;

image

and the Add an Account option now leads to the option to effectively add another account;

image

whereas tapping on the specific account offers actions that can be taken on that account;

image

and in the AddWebCommandsForStoredCredentials method, these entries offer either one or two options per each known account;

  • The Remove option if an account is the account that the user is currently logged in with
  • The Remove and Reconnect options if an account is not the account that the user is currently logged in with

If I followed through and add a secondary account then that panel looks as below (where I’m currently logged in via the Twitter credentials);

image

and if I tap on the Google account I can Remove/Reconnect it;

image

and the handler behind these verbs ( OnWebAccountInvoked ) figures out the action being taken and either attempts to replace the MobileServiceClient.CurrentUser based on the account that is being reconnected or, in the remove case, it removes the details of those credentials from the PasswordVault.

I spent a bit of time playing with this – I’m sure I don’t have it 100% correct here but from experimenting it feels like I’ve got a clearer view of how the flow of API calls might work in a real world app and where this sort of functionality now lives in an 8.1 app.

This code would then need refactoring into some reasonable classes and then combining with the code from the previous two sections in order that the current set of credentials was maintained across the app being terminated (either by the user or by the system) and also so that the code dealt with credentials being timed out by the mobile service.

I could also add support for Facebook and Microsoft Accounts ( given that Mobile Services have built-in support for that ) and that might all then build up to a starting point for handling this sort of account management.