Adding third-party authentication to your Web app

This tutorial will explain how the recent addition of the Become API allows developers to integrate "Login with GitHub" to a web app, with code that can be modified to support other OAuth providers.

JavaScript
Cloud Code

Download code for this tutorial:

GitHub

Many third-party identity services provide an interface for developers to integrate "Login with X" to their app. Commonly, this is done with the OAuth protocol which involves redirecting the user to the third-party and then being sent back with some authorization. More explicitly, the steps are:

  1. Create and save a reference to this request
    • Include a unique identifier
  2. Redirect the user to the third-party login endpoint
    • Include the app id registered with the third-party provider
    • Include the unique identifier for the request
    • Optionally adjust the permissions you are requesting access to
  3. Process an inbound redirect from the third-party site
    • Verify the data provided matches a previously stored request
    • Destroy the previously stored request, so it cannot be used again
  4. Validate the request with the third-party service
    • Call another endpoint on the third-party to exchange the code provided with the redirect for an access token

At this point, if the user accepted the authorization and no errors occurred, the developer would now have an access token. The token can be used to identify a specific user, and make additional calls using the third-party API on behalf of the user.

In this example, we use Cloud Code to enable "Login with GitHub" on a web app hosted on Parse. The GitHub documentation which this is based on can be found here.

Prerequisites

Create a Parse App and select a web hosting sub-domain in the Dashboard (Settings->Web Hosting)

Parse Dashboard

You'll need to register an app on GitHub

GitHub app registration

Setup

Overview

The example is very basic and uses 4 express routes:

Securing Objects

Two Parse Data classes are used, 'TokenRequest' and 'TokenStorage', and their permissions should be limited to prevent any access from the outside. You can create the TokenRequest and TokenStorage classes in the data browser, and choose "Set Permissions" from the "More" drop-down to lock them down by unchecking the "Any user can perform this action" checkbox on every permission.

In the code, we create a restrictive ACL to explicitly protect any objects we create from being tampered with:

var restrictedAcl = new Parse.ACL();
restrictedAcl.setPublicReadAccess(false);
restrictedAcl.setPublicWriteAccess(false);

Later, in the /authorize route, we create a TokenRequest, secure it, and then use the Master Key in order to save it:

var tokenRequest = new Parse.Object("TokenRequest");
tokenRequest.setACL(restrictedAcl);
tokenRequest.save(null, { useMasterKey: true });

Creating Users

When we've passed the GitHub authentication flow successfully, we ask GitHub for the users information. Using that info, we search for a previous login by that user. If no user is found, we create a new Parse User with a random username and password:

var username = new Buffer(24);
var password = new Buffer(24);
_.times(24, function(i) {
  username.set(i, _.random(0, 255));
  password.set(i, _.random(0, 255));
});
user.set("username", username.toString('base64'));
user.set("password", password.toString('base64'));

The User is then saved, and a TokenStorage object is created to link this User with this GitHub identity:

var ts = new Parse.Object("TokenStorage");
ts.set('githubId', githubData.id);
ts.set('githubLogin', githubData.login);
ts.set('accessToken', accessToken);
ts.set('user', user);
ts.setACL(restrictedAcl);
// Use the master key because TokenStorage objects should be protected.
return ts.save(null, { useMasterKey: true });
...

Becoming the User on the Client

Once we have a valid Parse User in Cloud Code, we can call the .getSessionToken() method to access the session token. This token can be used to become the user on the client as described in this previous blog entry. We render the 'cloud/views/store_auth.ejs' template with the Parse User sessionToken:

res.render('store_auth', { sessionToken: user.getSessionToken() });

This template uses the following Javascript to set the current user on the users browser:

Parse.User.become('<%= sessionToken %>').then(function (user) {
  window.location.href='/main';
},
function (error) {
  alert('Login with GitHub Failed.');
  window.location.href='/';
});

Accessing the GitHub API on behalf of the user

When the /main page is loaded, we check if the user is logged in, and if so execute a Cloud Code function:

if (!Parse.User.current()) {
  window.location.href='/';
} else {
  Parse.Cloud.run('getGitHubData', {}).then(function(response) {
  ...
  });
}

That Cloud function, getGitHubData, also validates the current user before making a request:

Parse.Cloud.define('getGitHubData', function(request, response) {
  if (!request.user) {
    return response.error('Must be logged in.');
  }
  var query = new Parse.Query(TokenStorage);
  query.equalTo('user', request.user);
  query.ascending('createdAt');
  Parse.Promise.as().then(function() {
    return query.first({ useMasterKey: true });
  }).then(function(tokenData) {
    if (!tokenData) {
      return Parse.Promise.error('No GitHub data found.');
    }
    return getGitHubUserDetails(tokenData.get('accessToken'));
  }).then(function(userDataResponse) {
    var userData = userDataResponse.data;
    response.success(userData);
  }, function(error) {
    response.error(error);
  });
});

Demo

You can try the live demo of this here.

Other OAuth providers

It should be relatively easy to alter this code, change the endpoints and a few variable names, and get this code to support other login providers.


Take a look through the source on GitHub, which makes heavy usage of Promises, and let us know what you think!