JavaScript
Article
By Graham Cox

Add Social Login via Google & Facebook to Your Single-page App

By Graham Cox
Help us help you! You'll get a... FREE 6-Month Subscription to SitePoint Premium Plus you'll go in the draw to WIN a new Macbook SitePoint 2017 Survey Yes, let's Do this It only takes 5 min

SPA Social Login: Authenticate Your Users via Google and Facebook

Increasingly, we are seeing web applications that are developed using a single page architecture, where the entire application is loaded into the browser as JavaScript and then all interactions with the server are carried out using HTTP based APIs that return JSON documents. Often, these applications will require some level of user-restricted interactions, e.g. for storing user profile details. Where this was a relatively simple task to implement in a traditional HTML based application, this is trickier in a single page application that needs to authenticate every API request.

This article will demonstrate a technique using the Passport.js library to implement social logins using a variety of providers, and leading from that to token-based authentication for the later API calls.

All of the source code for this article is available for download from our GitHub repository.

Why Use Social Sign-in for Your SPA?

When implementing a login mechanism on your web application, there are a number of concerns to take into account.

  • How should your UI handle authentication itself?
  • How should you store user information?
  • How should you best secure the user credentials?

These, and many more questions, need to be taken into consideration before you embark on writing a login portal. But, there is a better way.

Many sites, social networks chiefly among them, allow you to use their platforms for authentication of your own applications. This is achieved using a number of different APIs – OAuth 1.0, OAuth 2.0, OpenID, OpenID Connect, etc.

Implementing your login flow by using these social login technologies offers a number of advantages.

  • You are no longer responsible for rendering the UI for the user to authenticate with.
  • You are no longer responsible for storing and securing sensitive user details.
  • The user is able to use a single login for accessing multiple sites.
  • If the user feels their password has been compromised, they can reset it once and benefit across many sites.
  • Often, the service that provides the authentication functionality will make other details available. This can be used, for example, to automatically register users that have never used your site before, or to allow you to post updates to their profile on their behalf.

Why Use Token-based Authentication for Your API?

Any time a client requires access to your API, you will need some way to determine who they are and whether the access is permitted or not. There are several ways of achieving this, but the principal options are:

  • Session-based authentication
  • Cookie-based authentication
  • Token-based authentication

Session-based authentication requires some way for your API service to associate a session with the client. This is often very straightforward to set up, but can suffer if you are deploying your API across multiple servers. You are also at the mercy of the mechanism that your server uses for session management and expiry, which might be out of your control.

Cookie-based is where you simply have some identifier stored in a cookie, and this is used to automatically identify the API request. This means that you need some mechanism of setting the cookie in the first place, and you risk leaking it on subsequent requests, since cookies are automatically included in all (suitable) requests to the same host.

Token-based is a variation on the cookie-based authentication, but putting more control in your hands. Essentially you generate a token in the same way as in a cookie-based authentication system, but you will include it with requests yourself — normally in the “Authorization” header or else directly in the URL. This means that you are completely in control of storing the token, which requests will include it, and so on.

Note: even though the HTTP Header is called “Authorization”, we are actually doing authentication with it. This is because we are using it to ascertain “who” the client is, not “what” the client is allowed to do.

The strategy that is used for generating the token is important as well. These tokens can either be reference tokens, which means that they are nothing more than an identifier that the server uses to look up the real details. Or complete tokens, which means that the token contains all of the information needed already.

Reference tokens have a significant security advantage in that there is absolutely no leakage to the client of the users credentials. There is a performance penalty though, in that you need to resolve the token into the actual credentials on every single request made.

Complete tokens are the opposite. They expose the user credentials to anyone who can understand the token, but because the token is complete there is no performance penalty on looking it up.

Often, Complete Tokens will be implemented using the JSON Web Tokens standard, since this has allowances in it for improving the security of the tokens. Specifically, JWTs allow for the token to be cryptographically signed, meaning that you can guarantee that the token has not been tampered with. There is also provision for them to be encrypted, meaning that without the encryption key the token can not even be decoded.

If you’d like a refresher on using JWTs in Node, check out our tutorial: Using JSON Web Tokens with Node.js.

The other downside to using a complete token is one of size. A reference token could be implemented, for example, using a UUID which would have a length of 36 characters. Conversely, a JWT can easily be hundreds of characters long.

For this article we are going to use JWT tokens to demonstrate how they can work. However, when you implement this for yourself you will need to decide on whether you wish to use reference or complete tokens, and what mechanism you will use for these.

--ADVERTISEMENT--

What Is Passport?

Passport is a set of modules for Node.js that implements authentication in your web application. It plugs into many Node based web servers very easily, and works with a modular structure to implement the login mechanisms that you need with a minimal of bloat.

Passport is a powerful suite of modules that cover a large range of authentication requirements. Using these we are able to have a pluggable setup that allows different authentication requirements for different endpoints. The authentication systems used can be as simple as checking for a special value in the URL all the way up to depending on third party providers to do all of the work for us.

For this article we are going to be making use of the passport-google-oauth, passport-facebook and passport-jwt modules, allowing us to implement both social logins and JWT token-based authentication for the API endpoints.

The passport-jwt module will be used to require that certain endpoints — our actual API endpoints that need authentication to access — will have a valid JWT present in the request. The passport-google-oauth and passport-facebook modules will be used to provide endpoints that authenticate against Google and Facebook respectively, and then generate a JWT that can be used to access the other endpoints in the application.

Implementing Social Logins for Your Single Page Application

From here, we are going to walk through taking a trivial single page application and implementing social logins in it. This application is written using Express, with a simple API providing one secured and one insecure endpoint. The source code for this can be checked out from https://github.com/sitepoint-editors/social-logins-spa if you wish to follow along. This application can be built by executing npm install inside the downloaded source code — to download all of the dependencies — and then run by executing node src/index.js.

In order to successfully use the application you will need to register for social login credentials with Google and Facebook, and make the credentials available to the application. Full instructions are available in the demo application’s README file. These are accessed as environment variables. As such, the application can be run as follows:

# Linux / OS X
$ export GOOGLE_CLIENTID=myGoogleClientId
$ export GOOGLE_CLIENTSECRET=myGoogleClientSecret
$ export FACEBOOK_CLIENTID=myFacebookClientId
$ export FACEBOOK_CLIENTSECRET=myFacebookClientSecret
$ node src/index.js
# Windows
> set GOOGLE_CLIENTID=myGoogleClientId
> set GOOGLE_CLIENTSECRET=myGoogleClientSecret
> set FACEBOOK_CLIENTID=myFacebookClientId
> set FACEBOOK_CLIENTSECRET=myFacebookClientSecret
> node src/index.js

The end result of this process is going to add token authentication support (using JSON Web Tokens) to our secured endpoints, and then add social login support (using Google and Facebook) in order to obtain a token for the rest of the application to use. This means that you need to authenticate with the social provider once, and having done so, use the generated JWT for all future API calls in to the application.

JWTs are a particular good choice for our scenario because they are entirely self contained whilst still being secure. A JWT is made of a JSON payload and a cryptographic signature. The payload contains details of the authenticated user, the authenticating system and the validity period of the token. The signature then ensures that it can not be forged by malicious third parties — only someone with the signing key would be able to produce the tokens.

As you follow this article, you will see frequent references to a config.js module included as part of the application. This is used to configure the application, and makes use of the Node-convict module for external configuration. The configuration that is used throughout this article is as follows:

  • http.port – The port that the application runs on. This defaults to 3000, and is overridden using the “PORT” environment variable.
  • authentication.google.clientId – The Google client ID used for Google authentication. This is provided to the application using the “GOOGLE_CLIENTID” environment variable
  • authentication.google.clientSecret – The Google client secret used for Google authentication. This is provided to the application using the “GOOGLE_CLIENTSECRET” environment variable.
  • authentication.facebook.clientId – The Facebook client ID used for Facebook authentication. This is provided to the application using the “FACEBOOK_CLIENTID” environment variable
  • authentication.facebook.clientSecret – The Facebook client secret used for Facebook authentication. This is provided to the application using the “FACEBOOK_CLIENTSECRET” environment variable.
  • authentication.token.secret – The secret used to sign the JWT used for our authentication token. This defaults to “mySuperSecretKey”.
  • authentication.token.issuer – The Issuer stored inside the JWT. This is an indication of which service issued the token, in scenarios where one authentication service serves many applications.
  • authentication.token.audience – The audience stored inside the JWT. This is an indication of which service the token is intended for, in scenarios where one authentication service serves many applications.

Integrating Passport

Before it can be used in your application, Passport needs a small amount of setup. This is nothing more than making sure the module is installed, and initializing the middleware in your Express application.

The module we need for this stage is the passport module, and then to set up the middleware we simply need to add it to our Express app.

// src/index.js
const passport = require('passport');
.....
app.use(passport.initialize());

If you were to follow the instructions from the Passport website then it would have you set up session support – by use of the passport.session() call. We are not making use of any session support in our application, so this is unnecessary. This is because we are implementing a stateless API so we will provide authentication on every request, instead of persisting it in a session.

Implementing JWT Token Auth for the Secured Endpoint

Setting up JWT Token Authentication is relatively simple with Passport. We will be making use of the passport-jwt module, which does all of the hard work for us. This module looks for an “Authorization” header where the value starts “JWT “, and treats the rest of the header as the JWT token to use for authentication. It then decodes the JWT and makes the values stored inside it available for your own code to manipulate — to do user lookups, for example. If the JWT is not valid, e.g. if the signature is invalid, the token has expired… then the request will be unauthenticated without any extra involvement from your own code.

Configuring the JWT token authentication is then a case of doing as follows:

// src/authentication/jwt.js
const passport = require('passport');
const passportJwt = require('passport-jwt');
const config = require('../config');
const users = require('../users');

const jwtOptions = {
  // Get the JWT from the "Authorization" header.
  // By default this looks for a "JWT " prefix
  jwtFromRequest: passportJwt.ExtractJwt.fromAuthHeader(),
  // The secret that was used to sign the JWT
  secretOrKey: config.get('authentication.token.secret'),
  // The issuer stored in the JWT
  issuer: config.get('authentication.token.issuer'),
  // The audience stored in the JWT
  audience: config.get('authentication.token.audience')
};

passport.use(new passportJwt.Strategy(jwtOptions, (payload, done) => {
  const user = users.getUserById(parseInt(payload.sub));
  if (user) {
      return done(null, user, payload);
  }
  return done();
}));

In the above, we have a couple of internal modules that we make use of:

  • config.js – This contains our configuration properties for the entire application. It can be assumed that these are configured already, and that values are readily available for use
  • users.js – This is the user store for the application. This allows for users to be loaded and created — here we simply load a user by their internal ID.

Here, we are configuring the JWT decoder with a known secret, issuer and audience, and we are informing the strategy that it should get the JWT from the Authorization header. If either of the issuer or audience do not match what is stored in the JWT then authentication will fail. This gives us another level of anti-forgery protection, albeit a very simple one.

Token decoding is handled entirely by the passport-jwt module, and all we need to do is provide the configuration that corresponds to the configuration that was used to generate the token in the first place. Because JWT is a standard, any modules that follow the standard are able to work together perfectly well.

When the token is successfully decoded, it is then passed it as a payload to our callback. Here we simply try to look up the user identified by the “subject” from the token. In reality you might do extra checks, for example to ensure the token has not been revoked.

If the user is found, we provide it to Passport, which will then make it available to the rest of the request processing as req.user. If the user is not found, then we provide no user to Passport, which will then consider authentication to have failed.

This can now be wired in to a request handler so that the request needs authentication to succeed:

// src/index.js
app.get('/api/secure',
  // This request must be authenticated using a JWT, or else we will fail
  passport.authenticate(['jwt'], { session: false }),
  (req, res) => {
    res.send('Secure response from ' + JSON.stringify(req.user));
  }
);

Line 3 above is the magic that makes Passport process the request. This causes Passport to run the “jwt” strategy that we have just configured on the incoming request, and either allow it to proceed or else fail immediately.

We can see this in action by running the application — by executing node src/index.js — and trying to access this resource:

$ curl -v http://localhost:3000/api/secure
> GET /api/secure HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.51.0
> Accept: */*
>
< HTTP/1.1 401 Unauthorized
< X-Powered-By: Express
< Date: Tue, 13 Jun 2017 07:53:10 GMT
< Connection: keep-alive
< Content-Length: 12
<
Unauthorized

We don’t provide any Authorization header, and it fails to allow us to proceed.
However, if you were to provide a valid Authorization header you would get a successful response:

$ curl -v http://localhost:3000/api/secure -H "Authorization: JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE0OTczNDAzNzgsImV4cCI6MTQ5NzM0Mzk3OCwiYXVkIjoic29jaWFsLWxvZ2lucy1zcGEiLCJpc3MiOiJzb2NpYWwtbG9naW5zLXNwYSIsInN1YiI6IjAifQ.XlVnG59dX-SykXTJqCmvz_ALvzPW-yGZKOJEGFZ5KUs"
> GET /api/secure HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.51.0
> Accept: */*
> Authorization: JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE0OTczNDAzNzgsImV4cCI6MTQ5NzM0Mzk3OCwiYXVkIjoic29jaWFsLWxvZ2lucy1zcGEiLCJpc3MiOiJzb2NpYWwtbG9naW5zLXNwYSIsInN1YiI6IjAifQ.XlVnG59dX-SykXTJqCmvz_ALvzPW-yGZKOJEGFZ5KUs
>
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Content-Type: text/html; charset=utf-8
< Content-Length: 60
< ETag: W/"3c-2im1YD4hSDFtwS8eVcEUzt3l5XQ"
< Date: Tue, 13 Jun 2017 07:54:37 GMT
< Connection: keep-alive
<
Secure response from {"id":0,"name":"Graham","providers":[]}

In order to perform this test, I manually generated a JWT by visiting https://www.jsonwebtoken.io, and filling in the form there. The “Payload” that I used was

{
  "iat": 1497340378, // Tuesday, 13 June 2017 07:52:58 UTC
  "exp": 1497343978, // Tuesday, 13 June 2017 08:52:58 UTC
  "aud": "social-logins-spa",
  "iss": "social-logins-spa",
  "sub": "0"
}

And the “Signing Key” was “mySuperSecretKey”, as taken from the configuration.

Supporting Token Generation

Now that we can access resources only with a valid token, we need a way to actually generate the tokens.
This is done using the jsonwebtoken module, building a JWT containing the correct details and signed with the same key as used above.

// src/token.js
const jwt = require('jsonwebtoken');
const config = require('./config');

// Generate an Access Token for the given User ID
function generateAccessToken(userId) {
  // How long will the token be valid for
  const expiresIn = '1 hour';
  // Which service issued the token
  const issuer = config.get('authentication.token.issuer');
  // Which service is the token intended for
  const audience = config.get('authentication.token.audience');
  // The signing key for signing the token
  const secret = config.get('authentication.token.secret');

  const token = jwt.sign({}, secret, {
    expiresIn: expiresIn,
    audience: audience,
    issuer: issuer,
    subject: userId.toString()
  });

  return token;
}

Note that we use the exact same configuration settings for the audience, issuer and secret when generating the JWT. We also specify that the JWT will have an expiry period of one hour. This could be any period that you deem sensible for your application, or even pulled from the configuration so that it can be changed easily.

In this case, no JWT ID is specified, but this can be used to generate a completely unique ID for the token — using a UUID for example. This then gives you a way to revoke tokens and store a collection of revoked IDs in a data store and check that the JWT ID is not on the list when processing the JWT in the Passport strategy.

Social Login Providers

Now that we have the ability to generate tokens, we need a way for users to actually log in. This is where the social login providers come in. We will add the ability for a user to be redirected to a social login provider, and on success to generate a JWT Token and provide it to the browser’s JavaScript engine for use in future requests.
We have almost all of the pieces for this in place, we just need to plug them together.

Social login providers in Passport come in two parts. First there is the need to actually configure Passport for the social login provider, using the appropriate plugins. Secondly there is a need to have Express routes that the user is directed to in order to start the authentication, and for the user to be redirected back to when authentication is successful.

We will be opening these URLs in a new child browser window, which we will be able to close on completion, and which is able to call JavaScript methods inside of the window that opened it. This means that the process is relatively transparent to the user – at most they will see a new window open asking for their credentials, but at best they might see nothing apart from the fact that they are now logged in.

The browser side of this will need to consist of two parts. The view for the popup window, and the JavaScript that handles this in the main window. This can be done easily enough to integrate with any framework, but for this example we are going to use vanilla JavaScript for simplicity reasons.

The main page JavaScript simply needs something like this:

// src/public/index.html
let accessToken;

function authenticate(provider) {
  window.authenticateCallback = function(token) {
    accessToken = token;
  };

  window.open('/api/authentication/' + provider + '/start');
}

This registers a global function object on the window (named authenticateCallback) which will store the access token, and then opens our route to start authentication, which we are accessing on /api/authentication/{provider}/start.

This function can then be triggered by whatever means you desire to initiate authentication. This is normally a login link in the header area somewhere, but the details are entirely up to your application.

The second part of this is the view to be rendered on successful authentication. In this case we are using Mustache for simplicity, but this would use whatever view technology makes the most sense to you.

<!-- src/public/authenticated.html -->
<!DOCTYPE html>
<html>
  <head>
    <title>Authenticated</title>
  </head>
  <body>
    Authenticated successfully.

    <script type="text/javascript">
      window.opener.authenticateCallback('{{token}}');
      window.close();
    </script>
  </body>
</html>

Here we simply have a bit of JavaScript that calls the authenticateCallback method from above on the opener of this window, i.e. on the main application window, and then we close ourselves.

At this point, the JWT Token will be available in the main application window for whatever purpose you desire.

Implementing Google Authentication

Authenticating against Google will be done using the passport-google-oauth module. This needs to be supplied three pieces of information:

  • Client ID
  • Client Secret
  • Redirect URL

The Client ID and secret are obtained by registering your application at the Google Developer Console. The redirect URL is the URL inside your application that the user will be sent back to when they have signed in with their Google credentials. This will be dependent on how and where the application is deployed, but for now we are going to hard-code it.

Our Passport configuration for Google Authentication will then look like this:

// src/authentication/google.js
const passport = require('passport');
const passportGoogle = require('passport-google-oauth');
const config = require('../config');
const users = require('../users');

const passportConfig = {
  clientID: config.get('authentication.google.clientId'),
  clientSecret: config.get('authentication.google.clientSecret'),
  callbackURL: 'http://localhost:3000/api/authentication/google/redirect'
};

if (passportConfig.clientID) {
  passport.use(new passportGoogle.OAuth2Strategy(passportConfig, function (request, accessToken, refreshToken, profile, done) {
    // See if this user already exists
    let user = users.getUserByExternalId('google', profile.id);
    if (!user) {
      // They don't, so register them
      user = users.createUser(profile.displayName, 'google', profile.id);
    }
    return done(null, user);
  }));
}

When the user is redirected back to us after a successful authentication, we are provided their ID inside of Google’s system and some profile information. The first thing we try to see if this user has logged in before.
If so then we grab their user record and we’re done. If not then we will register a new account for them, and this new account is the one we will then use. This gives us a transparent mechanism where user registration is done on first login. We can do this differently if you so choose, but for now there’s no need.

The next part is to set up the route handlers to manage this login. These will look like this:

// src/index.js
function generateUserToken(req, res) {
  const accessToken = token.generateAccessToken(req.user.id);
  res.render('authenticated.html', {
    token: accessToken
  });
}

app.get('/api/authentication/google/start',
  passport.authenticate('google', { session: false, scope: ['openid', 'profile', 'email'] }));
app.get('/api/authentication/google/redirect',
  passport.authenticate('google', { session: false }),
  generateUserToken);

Note the routes for /api/authentication/google/start and /api/authentication/gogle/redirect. As noted above, the /start variation is the URL that we open, and the /redirect variant is the one that Google redirects the user back to on success. This then renders our authenticated view as shown above, providing the generated JWT for it to use.

Implementing Facebook Authentication

Now that we’ve got our first social login provider, let’s expand and add a second. This time it’s going to be Facebook, using the passport-facebook module.

This module works virtually the same as the Google module, requiring the same configuration and the same setup. The only real differences are in the fact that it’s a different module and a different URL structure to access it.

In order to configure Facebook Authentication you will also need a Client ID, Client Secret and Redirect URL.
The Client ID and Client Secret (referred to as an App ID and App Secret by Facebook) can be obtained by creating a Facebook Application in the Facebook Developer Console.
You will need to ensure that you add the “Facebook Login” product to your application in order for this to work.

Our Passport configuration for Facebook Authentication will be:

// src/authentication/facebook.js
const passport = require('passport');
const passportFacebook = require('passport-facebook');
const config = require('../config');
const users = require('../users');

const passportConfig = {
  clientID: config.get('authentication.facebook.clientId'),
  clientSecret: config.get('authentication.facebook.clientSecret'),
  callbackURL: 'http://localhost:3000/api/authentication/facebook/redirect'
};

if (passportConfig.clientID) {
  passport.use(new passportFacebook.Strategy(passportConfig, function (accessToken, refreshToken, profile, done) {
    let user = users.getUserByExternalId('facebook', profile.id);
    if (!user) {
      user = users.createUser(profile.displayName, 'facebook', profile.id);
    }
    return done(null, user);
  }));
}

This is almost identical to that for Google, only with the term “facebook” instead. And the URL Routes are similar:

// src/index.js
app.get('/api/authentication/facebook/start',
  passport.authenticate('facebook', { session: false }));
app.get('/api/authentication/facebook/redirect',
  passport.authenticate('facebook', { session: false }),
  generateUserToken);

Here we don’t need to specify the scopes that we want to use, because the default set are already good enough. Otherwise, the configuration between Google and Facebook is almost identical.

Summary

The use of social login providers makes it very quick and easy to add user login and registration to your application. The fact that this uses browser redirects to send the user to the social login provider and then back to your application can make this tricky to integrate into a single page application, even though it’s relatively easy to integrate into a more traditional application.

This article has shown a way to integrate these social login providers into your single page application in a way that is hopefully both easy to use, and is easy to extend for future providers that you may wish to work with.
Passport has a large number of modules to work with different providers, and it is a case of finding the right one and configuring it in the same way that we did above for Google and Facebook.

This article was peer reviewed by James Kolce. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be

Login or Create Account to Comment
Login Create Account
Recommended
Sponsors
Get the latest in JavaScript, once a week, for free.