Devhammer's Den


May 14, 2013

Building Back-end Data and Services for Windows 8 Apps: Authentication Tips and Tricks

In previous installments of this series, I've shown how you can quickly create REST-based services accessible via HTTP that allow you to easily store and retrieve data in a Windows Store app, using several different approaches including WCF Data Services, ASP.NET Web API, and the new Windows Azure Mobile Services. You can read all of the previous parts of the series here. I recommend reading the intro post and the post on Windows Azure Mobile Services, as well as the most recent installment covering the basics of authentication with mobile services at a minimum, so you're familiar with the games I'm using to demonstrate the concepts in the series, and with the basics of mobile services.

MOAR Authentication!

In the previous post, I showed you how easy it is to leverage one of several providers (Twitter, Facebook, Microsoft Accounts, and Google are currently supported) to provide authentication for your Windows Azure Mobile Services-based apps. In this post, I want to cover a couple of related areas that are of interest. First, I'll show you how to enable persistent login for a mobile service, to simplify the login process for the user. And second,  I'll discuss how we can get additional information about users from the configured identity provider(s).

Persistent Logins

One thing I noticed about the default behavior of my Space Cadet game as coded in the previous post is that it requires the user to enter their username and password every time the game starts, regardless of whether they click the checkbox to indicate that the client should remember this information. Kind of annoying.

Fortunately, this is really easy to solve, and only requires two actions…one on the server, and one on the client.

On the server, we need to provide the mobile service with the package SID for the app. The process of obtaining the Package SID and configuring the mobile services is actually covered in my earlier post on adding push notifications to a mobile service, so if you've followed the steps outlined in that post, you'll already have this in place. To verify, open up your mobile service in the Windows Azure management portal, and click on the Push tab. You should see a value for the Package SID field, as shown in this screen grab from the push notifications post:

PushTab

Note that for the purposes of persistent login, only the Package SID is required.

Once the mobile service is properly configured for your app, the client-side change is even easier. Simply open the app, and find the call to the MobileServiceClient.login function, and add a second parameter to the call, with a value of "true" (no quotes), like so:

   1:  leaderboardClient.login("twitter", true).done(function (result) {
   2:     // remaining code omitted...

This parameter indicates that we want the app to persist the login (the mobile service refers to this as single sign-on), so the next time we call the login function, the user will not need to enter their credentials (though they will still see the UI for the WebAuthenticationBroker, though it will automatically disappear once authentication is complete).

Essentially, we've done here is that by associating the app's Package SID to the mobile service, and adding the second parameter, we've enabled the use of HTTP cookies from our app to persist the login information, so our game now behaves essentially the same as if we'd authorized our app via a web browser. It's also important to note that this particular parameter of the login function is only applicable to Windows 8 clients.

Now, if we fire up the game, and have already authenticated (and authorized our app to use the selected identity provider, in this case, Twitter), we'll see the WebAuthenticationBroker UI appear briefly, and will be automatically logged in. This functionality leverages the Windows 8 WebAuthenticationBroker's support for single sign-on (SSO). Check out this page on the Windows Developer Center for more information on how single sign-on works with the Web authentication broker.

IMPORTANT:

The mobile services documentation refers to the feature we're using as single sign-on, but conceptually, it's more of a persistent login, in that it enables the user to only provide their authentication credentials the first time the app is authorized with the configured identity provider. If you wish to provide single sign-on using the Microsoft Account of the logged in user of a Windows 8 app, see this article from the Live Connect Developer Center.

I Need Moar Info!

The other thing that's less than ideal about our initial implementation of authentication against Twitter as an identity provider in the last post is that by default, the mobile service client library only provides access to the ID for the given identity provider. In the case of Twitter, this is the string "Twitter:[ID]" where ID is the unique identifier for the logged in user.

In Space Cadet, one piece of information that the app tracks is the "Cadet Name". In Dave Isbitski's original game starter kit, this value is simply a user-entered field. And in my game leaderboard service, I use the Cadet Name along with the game name to uniquely identify a given leaderboard score. But as you might have guessed already, there's a problem. If the user can choose their own username, then there's the potential for more than one user to choose the same name, in which case, data might get overwritten.

Once implementing login with Twitter as the identity provider, it would be pretty easy to solve the problem of uniqueness, simply by using the ID as the unique identifier, but in its current state, the playerName property of the game logic (in default.js) isn't settable from our Leaderboard code. So the first step in providing better integration with our logged in identity is to shift the game code to read and write to the Leaderboard player identity. Here's what I'll do…first, I'll modify the Leaderboard namespace to enable the game code to get the "playerName" variable:

   1:  WinJS.Namespace.define("Leaderboard", {
   2:      getPlayerName: getPlayerName,
   3:      setPlayerName: setPlayerName,
   4:      init: init,
   5:      addWin: incrementWins,
   6:      addLoss: incrementLosses,
   7:      addTie: incrementTies,
   8:      updateScore: updateHighScore,
   9:      getTopTenScores: getTopTenScores,
  10:      getTopTenWins: getTopTenWinLossTie,
  11:      leaderboardList: leaderboardList //,
  12:  });

And create the getPlayerName function to return the property value:

   1:  function getPlayerName() {
   2:      return playerName;
   3:  }

I also need to modify the Leaderboard.init function so that it no longer requires the player name to be passed in, which I can do by simply commenting out a line of code:

   1:  function init(player, game) {
   2:      return new WinJS.Promise(
   3:          function (completed, error, progress) {
   4:              if (player) {
   5:                  playerName = player;
   6:              }
   7:              else {
   8:                  // error("Player Name is required for initialization");
   9:              }
  10:   
  11:              if (game) {
  12:                  gameName = game;
  13:              }
  14:              else {
  15:                  error("Game Name is required for initialization");
  16:              }
  17:              // remaining code omitted for brevity

And then we need one last change in Leaderboard.init, to set the playerName to the ID returned from the login:

   1:  // Login with Twitter, and don't process additional 
   2:  // initialization code until the authentication is complete
   3:  leaderboardClient.login("twitter", true).done(function (result) {
   4:      userId = result.userId;
   5:      playerName = userId;
   6:      // remaining code omitted for brevity

Then, in the game code, I'll update the initialization code to pull the game's playerName value from the leaderboard, once leaderboard initialization is complete:

   1:  // set or retrieve player name and initialize leaderboard
   2:  playerName = appdata.current.roamingSettings.values["playerName"];
   3:  playerName = !playerName ? "Player1" : playerName; 
   4:  txtPlayerName.textContent = playerName;
   5:  Leaderboard.init(playerName, "Space Cadet").done(function () {
   6:      // any initialization dependent on leaderboard
   7:      txtPlayerName.textContent = Leaderboard.getPlayerName();
   8:  },

Of course, now that we're relying on the logged in identity, it would probably be a good idea to disable the code that can update the cadet name, but I'll leave that as an exercise for the reader.

But even with the changes we've made, that only solves half of our problem, as this is what I see when I'm playing the game after logging in:

TwitterID

This is fine in terms of uniqueness, but we still want to be able to display the username, which is something that the mobile service client does not provide. So it's back off to the server we go…

Ideally, I'd like to use the Twitter screen name of the logged in user for the display name of the player. I could do that from the client side code, but I'd need to do it every time I launched the app, which ends up being a fair number of extra calls over the life of the app. Instead, I can grab the screen name on the server and ensure that when a new score record is inserted, we add a new column called playerDisplayName, and populate it with the screen_name property, which we can get from Twitter's Users API (note that I'm using the v1 API, which is being sunsetted, to keep things simple…once the v1 API is retired, you'll need to add code to handle the OAuth request process as well, but that's outside the scope of this post). Thanks to the server-side support for making HTTP requests, this is simple to do:

   1:  function insert(item, user, request) {
   2:      item.playerDisplayName = "";
   3:      var id = user.userId.substring(user.userId.indexOf(':') + 1);
   4:      var url = 'https://api.twitter.com/1/users/show.json?user_id=' + id;
   5:      var httpRequest = require('request');
   6:      httpRequest(url, function (err, resp, body) {
   7:          if (err || resp.statusCode !== 200) {
   8:              request.respond(500, body);
   9:          } else {
  10:              try {
  11:                  item.playerDisplayName = JSON.parse(body).screen_name;
  12:                  console.log("updated score record for: " + item.playerDisplayName);
  13:                  request.execute();
  14:              } catch (ex) {
  15:                  request.respond(500, ex);
  16:              }
  17:          }
  18:      });
  19:  }

(My thanks to Sasha Goldshtein for this blog post which included the code which I've modified slightly for the above example)

The code is pretty straightforward, and will run each time a new score record is inserted. It grabs the userId(which will contain the ID of the logged in user) from the user object, parses it to get just the numeric portion following the ":", and then creates the API request URL. Next, the code creates an HTTP request object, and executes the request, passing in the API request URL, and a callback function. If the request succeeds, the code parses the response and sets item.playerDisplayName to the screen_name property value, then logs the request status, and executes the insert request. As long as dynamic schema is enabled for the mobile service (the default), the code above will add the playerDisplayName column even if it does not yet exist.

Once the value has been stored at the server, it only takes a few tweaks on the client to bring everything together. First, we'll add a variable named playerDisplayName to leaderboardWAMS.js to store the new value:

   1:  var leaderboardClient, gameScoresTable, leaderboardList, playerName, playerDisplayName, gameName, diag, channel, userId;

Next, we'll modify the login code inside Leaderboard.init to set the variable's value, based on the value returned from the server:

   1:  leaderboardClient.login("twitter", true).done(function (result) {
   2:      userId = result.userId;
   3:      playerName = userId;
   4:   
   5:      // Replace "gamescore" with your table name, if different
   6:      gameScoresTable = leaderboardClient.getTable('gamescore');
   7:      // make sure that the score table has at least one record for the player name / game name combo
   8:      gameScoresTable.where({ player: playerName, game: gameName })
   9:          .read().done(
  10:          function (results) {
  11:              if (results.length == 0) {
  12:              // initialize table
  13:              var gameScore = {
  14:                  game: gameName, player: playerName, score: 0, wins: 0, losses: 0, ties: 0
  15:              };
  16:              gameScoresTable.insert(gameScore).done(function (insertResults) {
  17:                  playerDisplayName = insertResults.playerDisplayName;
  18:                  completed();
  19:              });
  20:          }
  21:          else {
  22:              playerDisplayName = results[0].playerDisplayName;
  23:              completed();
  24:          }
  25:      },
  26:      function (e) {
  27:          showMessage(e.message);
  28:      });
  29:  });

Note that since the gameScoresTable.insert method is async, it returns a Promise object with the results of the insert operation. If a score record already exists for this player, the gameScoresTable.where method will have already returned it, so we can simply grab the playerDisplayName property from the existing object.

Last, but not least, we need to update the getPlayerName function to return the new playerDisplayName property:

   1:  function getPlayerName() {
   2:      return playerDisplayName;
   3:  }

Now, if I fire up the game, the login code executes, and this is what I see during game play:

TwitterScreenName

Because the code to get the screen name is on the server, it runs only when the record is first inserted. Each subsequent run of the app will locate the existing record based on the game name and the Twitter ID, which provide a reliable unique identifier for the score record, and the retrieved record will include the playerDisplayName information, which we can use as a display value for the player.

Wrap-up

In this post, I've shown you how I quick and easy it is to authenticate users against a third-party provider using Windows Azure Mobile Services. Once you've configured the proper app credentials, the mobile services client takes care of all the heavy lifting of making the appropriate API calls for the app platform you're working with, and returns the user ID and the auth token from the configured service.

You can explore more about authentication in Windows Azure Mobile Services here.

For Review

If you haven't already, I highly recommend that you read the rest of the posts in this series:

 

What's Next?

Given how far we've come with this series, I may be ready to put the wraps on it. But if you readers have topics you'd like to see covered, please drop me a note, and I'll be happy to add to the list.

While you're waiting for the next installment, why not sign up for App Builder? There are lots of great resources available for building Windows 8 apps (and now for Windows Phone 8 as well), including new information on a variety of app frameworks from partners that make it fast and easy to build apps and games for Windows 8. It's free, and you control how often updates are sent, so there's no good reason to pass it up. Sign up now!

Tags: Windows 8, Windows Store, Windows Azure, Apps

Comments powered by Disqus

Visitors

Disclaimer

The views expressed on this weblog are mine and do not necessarily reflect the views of my employer.
All postings are provided "AS IS" with no warranties, and confer no rights.

Unless otherwise noted, all code provided in this blog is copyright © G. Andrew Duthie, and licensed under the Microsoft Limited Public License (Ms-LPL). All rights reserved.



worldmaps