Social Authentication with Node.js Passport and JWT in SPA

Login with Facebook, Google, and other such authentication providers can provide a seamless login experience to users. For a website supporting both email and social provider logins, almost 50% of users will choose a social provider to log in. I am going to outline how to add login with Facebook, Google, and Linkedin on your website with Vue.js frontend and Node.js backend. We are going to use the popular Passport.js library on Express which will make it really easy for us to add more providers if needed. This article outlines the flow where the authentication happens server-side — there are other approaches to do it on the client — but I won’t be talking about them here.

The authentication mechanism uses OAuth 2.0 and is supported by most social identity providers including Facebook, Google, Linkedin, Dropbox, Github, Microsoft, etc. A high-level understanding of the Oauth 2.0 flow will be useful to understand how the code works. I found this article to be a great explanation of the entire flow in details. The figure below summarizes the Oauth 2.0 flow:

OAuth 2.0 Flow with our website as example.com and Oauth Provider as Facebook

Once the server returns the profile details of the user, we can store them in our database and return JWT tokens to the client from our application server. The JWT tokens are used for authentication, irrespective of what medium (email or social auth) the user used for initial login. To build this, we will have to modify the client, server, and DB schema accordingly.

Client

Login page at example.com/login

Database schema

We want to support both emails and social auth providers for users to log in. To support both these use cases, I created my database schema to be something like:

The provider column stores the authentication medium and will be either of email, facebook, google, linkedin. The provide user id column stores the user id of the user for that provider — for e.g. the facebook userid of that user. This column will be blank if the user signs up with email. We also make the email column unique.

Developer Apps

We will have to create an app for each of the Oauth providers. The process might be slightly different for each of them, but you will come across the fields below for all of them:

  1. App Name — The name of your app (for e.g. MyApp). This will be shown to the user while providing their credentials.
  2. App Logo — The logo for your app
  3. Client ID — This is ID of your application. We will need this to get the access token.
  4. Client Secret — This should never be mentioned anywhere in the client. Only server code should have this. Its also used to get the access_token.
  5. Redirect URI — The server route where the provider will callback with the authorization code.

To create a Facebook app, you can follow this article. For Google app, this is a great read.

Server

We will first start with adding the routes. For each provider, we usually have 3 routes:

  1. /api/auth/<provider> — This is called from our client (example.com)
  2. /api/<provider>/callback — This is called by the provider after authorizing the user. This is where we can then call the provider to get the access_token.
  3. /api/<provider>/delete — This can be called by the provider whenever it wants us to delete the userdata. This should be implemented to comply with the terms and conditions of the Oauth provider.
Node.js routes

The passport.authenticate(<provider_name>, scope) redirects us to the Facebook login screen where we can authorize access to Facebook data. Once the authorization completes, Facebook redirects us back to the redirect uri (/api/facebook/callback) with the authorization code. We then execute authController.loginFacebook — let’s have a look at what it does.

loginFacebook code

This function executes the passport Facebook strategy which we will configure below. The code will get the user email from Facebook and check if it exists in our users table. If the email doesn’t exist, we create the user in the users table. If it already exists, that means the user had earlier registered on our website. In either case, once we get the user, we compose the JWT token and redirect to a page on the client with route /loginsuccess. The code below shows the Passport strategies for Google, Facebook, Linkedin and email.

passport_auth.js

Let’s look at a few important things in the above code:

Environment variables

We store the variables in a .env file which can then be read using the dotenv package. The key variables to include in the .env file for this authentication are:

BASE_SERVER_URL = <your server url>
JWT_SECRET = 'a random string'
# Facebook app credentials
FACEBOOK_APP_ID = 'XXXXXXXXXX'
FACEBOOK_APP_SECRET = 'XXXXXXXXXX'
# Google app credentials
GOOGLE_CLIENT_ID='XXXXXXX'
GOOGLE_CLIENT_SECRET='XXXXXXXXXXX'

getOrCreateNewUserWithMedium

I used this function to actually look in the db if a user with that email id exists in which case we can directly log the user into the website. If the user doesn’t exist, we create the user with the credentials provided and return the user id and metadata.

passReqToCallback

This is useful if you want to transfer some state from our request to Facebook and want the data back in the callback. An example use-case can be if we want to support multiple roles — like driver and rider in the case of Uber. We can send the roleId and then get it back in the callback so that we can create the user with the corresponding details. The way to pass this data to the Facebook API call is to modify the auth route:

Pass data to callback

Composing the JWT Token and passing it to client

As we saw above in the login Facebook code, the callback composes the JWT token from the user details, composes the cookie and redirects to the loginsuccess page.

var payload = { id: user.id, email: user.email, firstName: user.firstName, lastName: user.lastName }const token = jwt.sign(payload, process.env.JWT_SECRET);var cookiePayload = { user, token }res.cookie('auth', JSON.stringify(cookiePayload), { domain: process.env.DOMAIN_NAME });res.redirect(process.env.BASE_CLIENT_URL + '/loginsuccess'

The reason we have to pass the JWT token in a cookie is because the server needs to redirect to a route on a client which is hosted as a static website. The setup here is assuming the client is hosted at example.com and the server is hosted at api.example.com. We have to attach the domain name (example.com) to the cookie, otherwise the client cannot read a cookie from api.example.com. The LoginSuccess page on the client will then parse the cookie and store the authentication token for future requests.

The LoginSuccess.vue file looks something like:

Similar logic can be replicated in other frontend frameworks like React or Angular. This finally completed the integration of social authentication and makes it easy to include additional social providers. The codes above can be refactored into function to avoid deduplication across multiple providers — I just kept it this way to make the explanation simpler.

Enterprenuer | Ex-Facebook Hacker | Travel | Musician by aspirations

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store