DEV Community

loading...
Cover image for Beginner's Guide to Google OAuth with Passport.js

Beginner's Guide to Google OAuth with Passport.js

phyllis_yym profile image Phyllis Yu Yu💫 Updated on ・7 min read

As a user, it's very often we choose to use our Google (or Facebook, Twitter, etc) account to sign into a web application, as it is more convenient and quick than creating a new log in account. Usually the process goes like this:

  1. You click "Sign in with Google" button
  2. You get redirected to a Google consent screen, asking if you grant permission for the app to gain access to your Google profile info
  3. You click "Allow"
  4. Get redirected to the actual application

The process seems very simple. But when I first implemented Google log-in for my web app, I realized there was much more going on behind the scene. I decided to implement it with the help of an authentication middleware known as Passport.js.

Based on what I learned, this article will take you through the process of using Passport.js to set up Google OAuth for an Express.js web application. Please note that this article will not cover any EJS template and refactoring process.

Prerequisites

You'll need to have:

  • An existing app built with Express.js and Mongo database

  • Users collection in your Mongo DB, created by mongoose's users model class. There has to be googleId set up as a String in your user schema.

This collection is for storing users' Google ID so that the app will be able to associate resources (such as posts) with these users and to identify them after they sign out and sign in again.

  • Passport.js and Passport Google OAuth 2.0 strategy installed. To check if they have been installed, run the following command in the terminal.
cat package.json | grep passport 

If they have been installed, the result will be similar to the following.

"passport": "^0.4.1",
"passport-google-oauth20": "^2.0.0"

If the result is empty, run the following command in your terminal to install them.

npm install passport passport-google-oauth20 --save

OAuth Flow

The following flow chart shows the big picture of OAuth flow, from the developer's perspective.

Alt Text

We will discuss these steps in a moment. To kick things off, first we need to set up Passport and configure its strategy.

Preparation Step 1: Passport setup

To set up Passport, you will need to require both Passport and Passport-Google-OAuth2.0 and instruct Passport to use a new instance of Google Strategy.

const passport = require("passport");
const GoogleStrategy = require("passport-google-oauth20").Strategy;

passport.use(new GoogleStrategy());

Preparation Step 2: Getting credentials from Google

In order for Google to identify which application's Passport interacts with their API, you will need to obtain clientID and clientSecret in Google Developers Console. You may refer to this guide for the steps.

You will also be asked to fill out the "Authorized JavaScript Origins" and "Authorized redirect URI". Authorized JavaScript should be your localhost URL that your app listens to (e.g. http://localhost:3000), whereas Authorized redirect URI would be the route that you would like the user to be sent to after he/she grants permission to your app. In this example, we set it up as http://localhost:3000/auth/google/redirect.

Preparation step 3: Securing the keys

For security, before hooking the Google credentials to our Google strategy, we need to save them in a keys.js file of config folder. We should add this file to gitignore so that it won't be committed to Git.

module.exports = {
  mongodb:{
    dbURI: "your_mongo_atlas_SRV_address"
  },
  google:{
    clientID:"your_client_ID",
    clientSecret:"your_client_secret"
  }
};

Preparation step 4: Configuring Google Strategy

The next step would be requiring the keys file we just created in app.js as well as configuring Google Strategy by hooking it to your Google credentials and Authorized redirect URI you just created in the Google Developers Console.

For now, don't worry about the second argument accessToken - I will explain in implementation Step 3 below.

const passport = require("passport");
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const keys = require("./config/keys");

passport.use(
  new GoogleStrategy(
    {
      clientID: keys.google.clientID,
      clientSecret: keys.google.clientSecret,
      callbackURL: "/auth/google/redirect"
    },
    accessToken => {
      console.log("access token: ", accessToken);
    }
  )
);

Now let's go over the basic OAuth flow again. I've included code snippets to show how to implement the flow.

Implementation Step 1: When user clicks "Log in with Google"

The app then hands control to Passport to communicate with Google to handle authentication. User will be directed to the Google page asking for user's permission.

Passport has already been set up previously so it will start the OAuth flow.

app.get("/auth/google", passport.authenticate("google", {
    scope: ["profile", "email"]
  }));

A quick explanation of scope: it specifies which user's Google information that you want your app to get access to. In this example, I need access to the user's Google profile and email address, which is why I put profile and email next to the scope. A whole list of scopes can be found here.

Implementation Step 2: After user clicks "Allow" on the consent screen

The page will redirect to your redirect URI which you set up in Google Developers Console. The URI also contains a code from Google, which will be used by Passport to request Google for user's info.

app.get("/auth/google/redirect",passport.authenticate('google'));

Implementation Step 3: When Google replies with the user's profile info

The server fires passport callback function, which looks up or creates user in our app's database.
Since we only want one user record no matter how many times the user signs in, first our app needs to check whether our database already has this user with the given Google profile ID. If the answer is yes, we can skip creating a user in our database. However, if it's not the case, we need to create a new user.

To achieve this, we need to modify the passport callback function to the following. We also need to require mongoose and the users model of our database.

const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const mongoose = require('mongoose');
const keys = require('./config/keys');
const User = require("your_user_model_file_path");

passport.use(
  new GoogleStrategy({
      clientID: keys.google.clientID,
      clientSecret: keys.google.clientSecret,
      callbackURL: '/auth/google/redirect'
  }, (accessToken, refreshToken, profile, done) => {
      // passport callback function
      //check if user already exists in our db with the given profile ID
      User.findOne({googleId: profile.id}).then((currentUser)=>{
        if(currentUser){
          //if we already have a record with the given profile ID
          done(null, currentUser);
        } else{
             //if not, create a new user 
            new User({
              googleId: profile.id,
            }).save().then((newUser) =>{
              done(null, newUser);
            });
         } 
      })
    })
);

A quick explanation of the arguments in the callback function:

  • accessToken: Access tokens are the thing that applications use to make API requests on behalf of a user. In this example, we won't use any access token.

  • refreshToken: Access tokens typically have a limited lifetime. When they expire and if the app wants to gain access to a protected resource, refresh token are used to allow an app to obtain a new access token without prompting the user. We also don't need to use any refresh token in this example.

  • done function: this is called to tell passport that we have finished looking up or creating a user and it should now proceed with the authentication flow.

  • null: an error object which tells passport things are fine and there is no error.

Implementation Step 4: Calling Passport's serializeUser function.

In other words, Passport generates some identifying token, stuff it inside a cookie and send to the user's browser.
user in the code refers to the user model instance we just looked up or created in our database, whereas user.id is the ID assigned by Mongo, instead of Google profile ID.
Imagine a scenario when the user can log in your app with Facebook or Twitter account apart from Google. That means he/ she can be logged in without a Google ID. That's why we use user.id instead of Google ID in the serializeUser function.

passport.serializeUser((user, done) => {
  done(null, user.id);
});

Implementation Step 5: Later the user needs some resources from our app on the browser, say, asking for some posts...

the cookie will be automatically added to the request sent to the server. Server will then take the identifying token from cookie, pass into deserializeUser function to turn it into a user.
Passport then figures out the user has already been authenticated and directs the server to send the requested posts to the user's browser.

passport.deserializeUser((id, done) => {
  User.findById(id).then(user => {
    done(null, user);
  });
});

Implementation Step 6: Instruct Passport to use cookies to handle authentication for us

First, let's install cookie-session by running the following command in the terminal.

npm install cookie-session --save

We then need to require cookie-session in our app.js file.

const cookieSession = require("cookie-session");

Next, let's create a key to encrypt the cookie and store it inside keys.js file in config folder. The key can be any random characters.

module.exports = {
   mongodb:{
    dbURI: "your_mongo_atlas_SRV_address"
  },
  google:{
    clientID:"your_client_ID",
    clientSecret:"your_client_secret"
  },
  session:{
    cookieKey:"cookie_key_set_up_by_you"
  }
};

After this step, let's add the following to your app.js file to tell your app to use cookie session and to tell passport to use cookie-session to handle authentication. maxAge refers to the milliseconds of the duration that the cookie will last.

app.use(cookieSession({
  // milliseconds of a day
  maxAge: 24*60*60*1000,
  keys:[keys.session.cookieKey]
}));

app.use(passport.initialize());
app.use(passport.session());

Implementation Step 7: Test if it works!!

User model instance we got from deserializeUser function is now added to the req object as req.user .

We can make use of req.user to test if the whole OAuth works.

First, we can modify the callback route for Google to redirect to to the following.

router.get("auth/google/redirect",passport.authenticate("google"),(req,res)=>{
  res.send(req.user);
  res.send("you reached the redirect URI");
});

Then, we set up a logout route.

app.get("/auth/logout", (req, res) => {
    req.logout();
    res.send(req.user);
  });

To test, let's restart the server and go to http://localhost:3000/auth/google.
If it works, you will be directed to Google consent screen.
After you click "Allow", you should be able to see req.user object which contains information that you set up for your user model (i.e. at least google ID) and the message "you reached the redirect URI".
To check if logout function works, you can go to http://localhost:3000/auth/logout. If you don't see any req.user object, it means the cookie has been destroyed and now you logged out!

🎉🎉🎉 Congratulations! We did it! We have successfully used Passport.js to set up Google OAuth in our web application.

References:

Buy Me A Coffee

Discussion (29)

pic
Editor guide
Collapse
vickyktk profile image
vickyktk

A good one !!!!

Collapse
phyllis_yym profile image
Phyllis Yu Yu💫 Author

Thank you so much !! 😄

Collapse
vickyktk profile image
vickyktk

Welcome

Collapse
andrewmartin profile image
Andrew Martin

Just a heads up, you can use the upsert option to make this even easier (and reduce some logic to create the user:

user = await User.findOneAndUpdate(
          { email: profile.emails[0].value },
          { googleId: profile.id },
          { upsert: true, new: true, setDefaultsOnInsert: true }, // upsert creates a new document if not found.
        )
Enter fullscreen mode Exit fullscreen mode

I think this makes it a bit easier to follow the logic. Thanks the post, great stuff!

Collapse
sarvagya2545 profile image
Sarvagya Sharma • Edited

In our mongoose model, do we need to have a field which says googleId?
And do we need their gmail address?

Collapse
lxxmi3 profile image
Teabag

You don't need an explicit googleId field, if u use react Google o auth package. Where in responds with a jwt token and a Google user id, which u can store in normal Id field of your model.
Very late, but if you want you can check that approach, it's very simple

Collapse
sarvagya2545 profile image
Sarvagya Sharma

Yeah this approach looks good too. I assume you are talking about this package? npmjs.com/package/react-google-oauth2

Thread Thread
lxxmi3 profile image
Teabag

Yeah

Collapse
phyllis_yym profile image
Phyllis Yu Yu💫 Author

Hi Sarvagya, yes, there has to be googleId set up as a String in your user schema. It’s one of the prerequisites in this article. We don’t need to set up the field of gmail address in the schema for Google log-in to work. If you want your app to get access to users’ gmail address, you will need to specify it in the scope.

Collapse
tatianacodes profile image
Tatiana

This was very helpful to me! I've only used local strategy before, and was trying to implement this!

Collapse
phyllis_yym profile image
Phyllis Yu Yu💫 Author

Thanks! I’m glad you found it helpful! 🙂

Collapse
venomfate619 profile image
Syed Aman Ali

How to do this it with jwt token
Or
Without passport. Js

Collapse
jainpawan21 profile image
Pawan Jain

Can you share whole code in a single repository please! It will be very helpful.

Collapse
monfernape profile image
Usman Khalil

Clear and concise guide

Collapse
phyllis_yym profile image
Phyllis Yu Yu💫 Author

Thanks! Appreciate your feedback.

Collapse
kwizeraelvis profile image
kwizera elvis

Thanks for the article. It was really helpful.

Collapse
phyllis_yym profile image
Phyllis Yu Yu💫 Author

Thanks! Glad you find it helpful 🙂

Collapse
saswatamcode profile image
Saswata Mukherjee

Really clear way of describing authentication flow!!

Collapse
phyllis_yym profile image
Phyllis Yu Yu💫 Author

Thanks! Glad you find it clear :)

Collapse
harshakns profile image
Narasimha Sriharsha KANDURI

Really liked it, the diagram of auth flow is very helpful.

Collapse
phyllis_yym profile image
Phyllis Yu Yu💫 Author

Glad you found it helpful, thanks!

Collapse
tmthekid profile image
tmthekid

cookies only work on the same domain right? what if the backend and frontend are from two different domains?

Collapse
jdonesky profile image
Jonathan Donesky • Edited

You should really credit Stephen Grider in your source material. You copied all of this directly from his course Node with React: Full Stack Web Development. Including the diagrams.

Collapse
abdulloooh profile image
Abdullah Oladipo

This is really brilliant. IMO, better than the documentation teself. Thank you

Collapse
chan_austria777 profile image
chan 🤖

How were you able to test this one in a e2e test context? I mean, let's say you want to test this on cypress?

Collapse
rochakgupta profile image
Rochak Gupta

Finally an explanation that has the perfect balance of conciseness and depth. Thanks :)

Collapse
rinshasaheer profile image
rinshasaheer

How to logout from the application when the user sign out from Google.

Collapse
emmanuelkaranja profile image
emmanuel-karanja

Thank you so much Phyllis, your explanations are clear and straight to the point, I finally understand it.

Collapse
sarezas profile image
Comment marked as low quality/non-constructive by the community. View Code of Conduct
sharisM

Is this a direct copy-paste instructions from a Udemy Course by Stephen Grider called Node with React: Fullstack Web Development ? LOL