Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save nwalberts/75596a1b523fa8393f718820afdbd096 to your computer and use it in GitHub Desktop.
Save nwalberts/75596a1b523fa8393f718820afdbd096 to your computer and use it in GitHub Desktop.

In this lesson we will be learning how to integrate OAuth into our apps so that our users can login in with sites like Google, LinkedIn, Github, etc. It's vital that you understand each component of this reading in order to integrate those APIs into your app, and we will be learning the basics through Passport's implementation of OAuth with Google as an example.

Know that this lesson can be used to integrate any number of third party APIs as long as they have an established strategy with Passport. Some strategies include:

  • LinkedIn
  • Github
  • Google
  • Facebook
  • Instagram
  • Meetup
  • Twitter
  • Spotify
  • Twitch
  • Reddit
  • Strava or Fitbit

This lesson will not go through token based authentication per se, which these Passport strategies handle. If there is not an existing Passport strategy for your API, and that API does allow for token-based authentication, you will have to implement your own custom strategy (which would be an impressive though difficult endeavor).

Learning Goals

  • Understand what OAuth is and how it allows developers access to priviledged information
  • Know how to integrate OAuth into any Node Express application through passport
  • Understand the basics of passport strategies
  • Review how this process can allow for future interactions with our API

Getting Started

et get authenticating-with-third-party-apis
cd authenticating-with-third-party-apis
createdb authenticating-with-third-party-apis_development
yarn install
cd server
yarn migrate:latest
cd ..
yarn dev

Be prepared to retrieve for Google OAuth API keys from the Google API (which we will briefly cover).

You will need a Google account in order to proceed through this article.

Initial Setup of Engage

Before we begin, it's important to note some changes that will need to be made to any app generated with generator-engage.

  • Ensure passport version of 0.5.0 in your server/package.json
  • In server run yarn add passport-google-oauth20. Ensure that you see this installed in the relevant package.json file.
  • Add two new keys (values to come) in .env: GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET
  • In the User model, remove any presence validations for passwords. You can decide to conditionally validate for a password as needed, but this process will not use a password.

Required Reading

This assignment assumes knowledge of Passport for user authentication, and of Express middlewares and sessions. Finally, the assignment assumes experience in working with third party APIs. Please make sure you have read the following articles:

It is possible to complete this integration without said experience, but it will be both difficult to understand and difficult to modify or adjust for your purposes.

You may wish to also review what the "OAuth2" pattern is for web applications, though this is not required. Simply put, **OAuth2 is a popular pattern for authentication with third party APIs like Google or Spotify that involves tokens." Once we have a token from an API, we can then use that token for subsequent requests to that API. However in this article we are focused just on the initial authentication process.

Our Goal for this App

We will be using a mostly blank slate app consisting of Express-Objection-React in this assignment. Our goal is to be able to login with a Google account. This is an alternative to local authentication, in which a user signs up with information on our app such as an email and password. In this app, a user can sign up with either, but we will not be covering how to link accounts created from local registration with a registration made while signing in with Google. After authentication, the user's id will still be stored in a session for later retrieval.

Here is an overview of the steps we will go through

  • Adding a "Sign in with Google" button
  • Configuring a request to the Google API with our API keys
  • Setting up the Google API console to receive a request to share user information
  • Ensuring our user is returned to the app after they allow Google to share their information
  • Creating a callback that will either create an account for that user in our app, or log them in if they have used Google to login in the past
  • See that the user is logged in with information from the Google People API

Creating the Sign In Button and Beginning the Authentication Process

As long as we have passport-google-oauth20 installed (which we will call Passport-Google for simplicity), we can get started. In the app provided, you should be able to see the "Sign in with Google" button. Don't click the button just yet!

Clicking this button will send a GET request to the authGoogleRouter with a path of /auth/google.

// authGoogleRouter.js
authGoogleRouter.get('/', passport.authenticate('google', { scope: ['profile'] }));

When a user clicks on the google login button, it will issue a request to this route. Instead of handling the request ourselves, we will be having Passport Google handle the request. It will redirect the user to a page where they will be asked to share their Google information with this app.

The scope tells google what pieces of information we wish to retrieve and bring back to our app. Know that you can retrieve a lot of information from the user's account if they allow it, but for this lesson we care about one piece of information: info for that user's "profile".

The profile information will contain the user's email and their unique id on Google, which we will use to create their User account on our app.

passport.authenticate is available to us through Passport-Google, and we will be going over the configuration for this tool shortly. First let us make sense of what happens to the user in the authentication process.

Authorizing Access

At this point, the user will now see a Google sign in page after clicking the login button. If the user is not signed in, they will be asked to provide their email and password. Otherwise they will see something like this:

google-sign-in

It's important to note that at this point the user is no longer in our app. The user is on a Google webpage.

If we've configured our application correctly, then when the user is redirected to this page, they will also be sent along with required API keys from our app, and a callback url that will direct the user back to our app. But for this to happen we have to configure settings both in our Express app and in Google's Cloud Console.

The Google Cloud Console

If you are using a different Passport strategy e.g. LinkedIn, then your experience will based on that API's dev console. The things we wish to do on an API's console include:

  • getting an API secret key and client id.
  • designate a callback URL to sender the user back to e.g. "http://localhost:3000/auth/google/callback"
  • designate which pieces of information are allowed to be shared to the web app

OAuth Credentials and App Creation

Note: the next steps may change if Google decides to change up their UI.

To view your own Google Cloud console, navigate to https://console.cloud.google.com/apis/dashboard after signing in to Google. Now it's time to register our project so that we can get API keys and designate a callback URL. Follow these steps:

  • Click "APIs and Services"
  • Click "Create New Project". This should be near the top of the page. If you have made a project in the past, click on the project dropdown near the top of the page.
  • Provide a project name and organization/location. You may put "launchacademy.com" for the organization if you wish.
  • Credentials --> "Create Credentials" --> OAuth Client ID
  • Application Type --> Web Application
  • Name --> e.g. Teaching Passport Strategies
  • Authorized redirect URIs --> Add URI --> "http://localhost:3000/auth/google/callback" --> click on "CREATE"

You should now see a pop-up with your GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET that will look something like this:

  • Client secret: XyLuTLIKFiy28WRHX6Ov_93IP
  • Client ID: 3184723834901-tn6figvte2381.apps.googleusercontent.com

Copy both of these to the GOOGLE_CLIENT_SECRET and GOOGLE_CLIENT_ID in your .env file respectively.

Take note of the step where we designated a "redirect URI". This is also sometimes called a callback URL. The callback URL is where Google will send the user to after they have given their permission to Google to share their personal details. This is important if we want to get the user back into our app!

For production: you will need to update your redirect URI to be "https://www..com/auth/google/callback"

Designating a Google Sub-API

Click on "Enabled APIs & Services" in the side bar.

  • Click "ENABLE APIS AND SERVICES" with the plus icon.
  • Search "Google People API". You may choose a different API if there is different info, such as Youtube videos, that you wish to access on the user's account.
  • Select the Google People API, then click to enable the API.

We've told Google which subapp we want information from!

OAuth Consent for sharing Data

Next click on OAuth Consent Screen in the side panel:

  • Designate the app name if it isn't already populated
  • provide your own Google email for the "User Support Email"
  • you can skip providing a logo, but it may be needed later for production purposes
  • App domain should be "http://localhost:3000" until you publish your app. Once published, change this to your app's domain name.
  • Publishing status should be "Testing". This will need to be changed to "Publish App" later, and at that time you will have to go through a verification process.
  • User type --> External
  • provide your own email for the developer email

Click "Save and Continue" to provide to the scopes page.

  • Click "ADD OR REMOVE SCOPES"
  • This form may be a bit frustrating to use, but you want to type "People API" and then select the API
  • then type "profile" and then enter
  • finally select "/auth/userinfo.profile"

We are saying that the Google may share this user's profile information when they login. This will allow us to get the user's email and google ID upon authentication.

Click "Save and Continue" to proceed to "Test Users"

  • provide your own gmail if you have not already
  • Click "Save and Continue"

Then review your info and return to the dashboard!

That should be it, but know that the Google Console page, and other API pages, change constantly. What's most important is that you have designate the information that we originally mentioned.

Returning to the authGoogleRouter to Log In

The setting we designated in the Google Console helped lead the user back to our app at the URL of http://localhost:3000/auth/google/callback. Let's look at that endpoint in our application.

authGoogleRouter.get('/callback', passport.authenticate('google', 
  { 
    successRedirect: "/profile",
    failureRedirect: "/auth/google/failure"
  })
)

You may note that this route looks very similar to the route that sent us to Google. This time though, a request has been made to this endpoint along with the user's profile information, and an access token and a refresh token. These tokens would be needed if we want to make additional requests to the Google API without needing to go through the auth process once more. For that, you would make requests with an HTTP client library like got with these tokens as headers.

The profile information includes that user's unique Google ID, which we can store and even use for future requests. A more relevant example might be Instagram. If you wanted to retrieve all of a user's posts for some reason, then that user would need to authenticate so that you can retrieve an access token for their connection. Then use an HTTP client against the instagram API for posts by a user with the instagram ID you get back from the API.

The second argument to the passport.authenticate method this time is an object with a successRedirect and failureRedirect. After the login is successful, we redirect the user to their profile page where they may be able to see info from google (like their email). If something wen't wrong, then we can redirect the user to a failure page that displays some informative error.

Passport also allows you to supply an additional callback function if you wanted to do something more specific upon a successful authentication. Since the authenticate method is a middleware, the ensuing callback function has access to the standard Express (req, res):

authGoogleRouter.get('/callback', passport.authenticate('google', 
  { failureRedirect: "/auth/google/failure" }, 
  (req, res) => res.send("You logged in to Google!")
  )
)

However for this example, on a successful login, we are redirected to the "/profile" page. In React, this results in a simple page with the user's profile information for display, specifically their Google email from the authentication process! But how did we get this email?

After the redirect to /profile, the getCurrentUser method in our App component went to this endpoint before rendering the Profile component:

sessionRouter.get("/current", async (req, res) => {
  if (req.user) {
    res.status(200).json(req.user);
  } else {
    res.status(401).json(undefined);
  }
});

All we are doing is sending up the information that is stored on req.user, which is why this works! This property seems to magically have access to the user object. In fact, this is usually a part of the way we configure passport: after login, Passport makes the user available via session on req.user. But there were a few steps our app actually went through behind the scenes that we haven't yet covered.

Behind the Scenes: The Passport Google Strategy

Let's review how Passport is added to our application via middleware.

// ...
const addMiddlewares = async app => {
  addExpressSession(app);
  addPassport(app); // the function that adds passport to express
  await addClientMiddlewares(app);
  await addEnvironmentMiddlewares(app);
};
// ...

addMiddlewares is called in our app.js file which gets run when we boot up the app.

Inside of addPassport, we actually pass our application in as an argument, and then ensure that passport is both initialized and is using sessions for storing a login for a user. serializeUser determines what gets saved in a session upon authentication (the user's id is saved to a cookie), and deserializeUser determines how a User object is retrieved using that stored id when we call req.user in a router.

import passport from "passport";
import strategy from "../authentication/passportStrategy.js";
import googleStrategy from "../authentication/googleStrategy.js";
import deserializeUser from "..//authentication/deserializeUser.js";
const addPassport = (app) => {
  app.use(passport.initialize());
  app.use(passport.session());
};
passport.use(strategy);
passport.use(googleStrategy) // this is new
passport.serializeUser((user, done) => {
  console.log(user)
  done(null, user.id);
});
passport.deserializeUser(deserializeUser);
export default addPassport;

But of note in this lesson is the second strategy we have told passport to work with. passport.use(googleStrategy) tells passport to use googleStrategy when we call passport.authenticate("google") in our router. Let's view that googleStrategy that was imported.

import { Strategy as GoogleStrategy } from "passport-google-oauth20"
import dotenv from "dotenv";
import User from "../models/User.js";
dotenv.config();
// this gets run after the GoogleAuth process is completed.
const googleAuthHandler = (accessToken, refreshToken, profile, done) => {
  User.query()
    .findOne({ googleId: profile?.id })
    .then((user) => {
      // if user logged in before
      if (user) {
          return done(null, user);
      }
      // if you look at this log, all of the profile info on this user is available from Google!
      console.log(profile)
      // if user hasn't logged in before
      User.query().insertAndFetch({ 
        googleId: profile.id, 
        email: profile.emails[0].value 
      }).then((user) => {
        return done(null, user);
      })
    });
}
const googleStrategy = new GoogleStrategy({
    clientID: process.env.GOOGLE_CLIENT_ID,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    callbackURL: "http://localhost:3000/auth/google/callback",
  }, 
  googleAuthHandler 
)
export default googleStrategy

Start at the bototm with new GoogleStrategy. This is where we import from passport-google-oauth20. GoogleStrategy is doing a lot of work with us under the head as long as we provide a google cliendID, clientSecret and callbackURL. The original passport.authenticate("google") call in our authGoogleRouter uses this strategy to perform a multi-step authentication process on our behalf. Behind the scenes, token based authentication is handled. The API keys are used to retrieve an authorization code from Google, and then an additional request is made with that code to get an accessToken, refreshToken, and profile information. The two token can be stored in a session so that we could make further requests for user information if we wished, but in this case we are only concerned with the information from the user's profile. After the Google authentication process takes place, the googleAuthHandler function is run and passed accessToken, refreshToken, profile, done as arguments. The done argument is actually passed to us from Express and Passport, and when called can be passed an error (which we don't provide, so just pass null) and the user to be serialized into a session. This authHandler will receive the Google profile data of the user and either create a new account for them or find their existing account using the unique Google profile id that we get back from the auth process. Whichever user is passed to done() will get logged into our application. If we console.log the profile returned to us, we can see a swath of new Google information that would be inaccessible to us without authentication, and then use that to verify our user:

{
  id: '110581127932666555400',
  displayName: 'Nicholas Alberts',
  name: { familyName: 'Alberts', givenName: 'Nicholas' },
  emails: [ { value: 'nick.alberts@gmail.com', verified: true } ],
  photos: [
    {
      value: 'https://lh3.googleusercontent.com/a/AGNmyxYx6-alidotv_tsd9ZAB9enC7a5gpRN0w41CaWW=s96-c'
    }
  ],
  provider: 'google',
  _raw: '{\n' +
    '  "sub": "110581127932666555400",\n' +
    '  "name": "Nicholas Alberts",\n' +
    '  "given_name": "Nicholas",\n' +
    '  "family_name": "Alberts",\n' +
    '  "picture": "https://lh3.googleusercontent.com/a/AGNmyxYx6-alidotv_tsd9ZAB9enC7a5gpRN0w41CaWW\\u003ds96-c",\n' +
    '  "email": "nick.alberts@gmail.com",\n' +
    '  "email_verified": true,\n' +
    '  "locale": "en"\n' +
    '}',
  _json: {
    sub: '110581127932666555400',
    name: 'Nicholas Alberts',
    given_name: 'Nicholas',
    family_name: 'Alberts',
    picture: 'https://lh3.googleusercontent.com/a/AGNmyxYx6-alidotv_tsd9ZAB9enC7a5gpRN0w41CaWW=s96-c',
    email: 'nick.alberts@gmail.com',
    email_verified: true,
    locale: 'en'
  }
}

We could edit this googleAuthHandler function to behave differently if we wished, but even if you are using a different Passport strategy, your logic will likely be similar.

Why This Matters

This strategy will allow users to log in with Google, but we could extend it further if we had a more involved API integration. The refreshToken and accessToken we received from this authentication process are valuable, time-limited keys that will allow us to make additional requests against the API for more information without having to use Passport-Google again. Knowing this pattern thus allows developers to access a vast amount of secure, privileged information. Need a user's playlists from Spotify? Or a user's profile images from Facebook? Or access to a user's repositories from Github? In those cases, you'll need to go through this authentication process to access that information, all using token based authentication.

Summary

In this article, we explored a Passport strategy that will handle token based authentication for us with Google. We explored how to handle the Google authorization process, and what routes we need to add to have Passport-Google properly handle authentication. In our config, we saw how we can implement a strategy for either verifying an existing user or creating a new user record so that we can then log that user in to our application. After authentication, we were able to received secure information on that user's Google account, as well as tokens that could allow us to make further requests to the Google API. At the time of writing this article, nearly every Passport strategy that uses OAuth2 will loosely follow this same pattern. You can use this article to help you configure connection to various APIs accordingly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment