Today we'll learn how to log into our app using OAuth provided by FB & Google.

What You'll Learn

Today we're going to learn how to implement OAuth authentication, allowing users to sign in to our app using a third-party such as Google or Facebook.

At a high level, this diagram explains the steps we have to follow in order to implement OAuth authentication.

Requirements

Rockets

npm install react-facebook-login react-google-login
touch .env
REACT_APP_BACKEND_API="http://localhost:5000"
REACT_APP_FB_APP_ID=""
REACT_APP_GOOGLE_CLIENT_ID=""

We haven't defined these environment variables yet, but let's set up our App.js first.

const FB_APP_ID = process.env.REACT_APP_FB_APP_ID;
const GOOGLE_CLIENT_ID = process.env.REACT_APP_GOOGLE_CLIENT_ID;
 <FacebookLogin
                appId={FB_APP_ID}
                fields="name,email,picture"
                callback={loginWithFacebook}
                icon="fa-facebook"
                onFailure={(err) => {
                  console.log("FB LOGIN ERROR:", err);
                }}
                containerStyle={{
                  textAlign: "center",
                  backgroundColor: "#3b5998",
                  borderColor: "#3b5998",
                  flex: 1,
                  display: "flex",
                  color: "#fff",
                  cursor: "pointer",
                  marginBottom: "3px",
                }}
                buttonStyle={{
                  flex: 1,
                  textTransform: "none",
                  padding: "12px",
                  background: "none",
                  border: "none",
                }}
              />

              <GoogleLogin
                className="google-btn d-flex justify-content-center"
                clientId={GOOGLE_CLIENT_ID}
                buttonText="Login with Google"
                onSuccess={loginWithGoogle}
                onFailure={(err) => {
                  console.log("GOOGLE LOGIN ERROR:", err);
                }}
                cookiePolicy="single_host_origin"
              />
const loginWithFacebook = (response) => {
  dispatch(authActions.loginFacebookRequest());
};

const loginWithGoogle = (response) => {
  dispatch(authActions.loginGoogleRequest());
};

It should look something like these - do NOT use these variables, but instead use your own.

REACT_APP_FB_APP_ID="1060822310719146x"
REACT_APP_GOOGLE_CLIENT_ID="499012344960-4qodmmb4g8dn94i7q5m14bh2lts4cttk.apps.googleusercontent.comx"

We're front loading the work of installing necessary packages so we don't have to stop and do so later.

npm install passport passport-facebook-token passport-google-token bcryptjs dotenv jsonwebtoken nodemon cors

Here are all the variables we'll need, we get them from Google & Facebook.

PORT=5000
MONGODB_URI="mongodb://localhost:27017/coderbook"
JWT_SECRET_KEY="secretsecret"
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""
FACEBOOK_APP_ID=""
FACEBOOK_APP_SECRET=""

Passport is the package we'll use to manage authentication more easily.

const passport = require("passport");
require("./middlewares/passport");
app.use(passport.initialize());

This is where we configure our authentication strategy.

We define how we'll parse the data coming from Facebook, Google and our frontend app using one of two strategies, FacebookTokenStrategy, GoogleTokenStrategy. FacebookTokenStrategyGoogleTokenStrategy

const passport = require("passport");
const FacebookTokenStrategy = require("passport-facebook-token");
const GoogleTokenStrategy = require("passport-google-token").Strategy;

const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET;

const FACEBOOK_APP_ID = process.env.FACEBOOK_APP_ID;
const FACEBOOK_APP_SECRET = process.env.FACEBOOK_APP_SECRET;

const User = require("../models/User");

passport.serializeUser(function (user, done) {
  done(null, user);
});

passport.deserializeUser(function (user, done) {
  done(null, user);
});

passport.use(
  new FacebookTokenStrategy(
    {
      fbGraphVersion: "v3.0",
      clientID: FACEBOOK_APP_ID,
      clientSecret: FACEBOOK_APP_SECRET,
    },
    function (_, _, profile, done) {
      User.findOrCreate(
        {
          facebookId: profile.id,
          name: profile.displayName,
          email: profile.emails[0].value,
          avatarUrl: profile.photos[0].value,
        },
        function (error, user) {
          return done(error, user);
        },
      );
    },
  ),
);

passport.use(
  new GoogleTokenStrategy(
    {
      clientID: GOOGLE_CLIENT_ID,
      clientSecret: GOOGLE_CLIENT_SECRET,
    },
    function (_, _, profile, done) {
      User.findOrCreate(
        {
          googleId: profile.id,
          name: profile.displayName,
          email: profile.emails[0].value,
          avatarUrl: profile._json.picture,
        },
        function (err, user) {
          return done(err, user);
        },
      );
    },
  ),
);
var express = require("express");
var router = express.Router();

const passport = require("passport");

const authController = require("../controllers/auth.controller");

router.post(
  "/login/google",
  passport.authenticate("google-token", { session: false }),
  authController.loginWithFacebookOrGoogle,
);

router.post(
  "/login/facebook",
  passport.authenticate("facebook-token", { session: false }),
  authController.loginWithFacebookOrGoogle,
);

module.exports = router;

Take note of the arguments send to router.post().

  1. Path.
  2. A middleware, Passport's authentication strategy; google-token or facebook-token.
  3. The controller action/function we want fire when the endpoint is hit.

The different paths result in different authentication strategies. Passport's job is to normalize incoming data. In other words, it makes data consistent when it hits out backend endpoint. Notice all three paths hit the same controller action.

We import bcrypt because we use this library to encrypt our users password when they create an account. We also import User because we use that model to

const bcrypt = require("bcryptjs");

const User = require("../models/User");

const authController = {};

authController.loginWithFacebookOrLogin = async ({ user }, res) => {
  if (user) {
    user = await User.findByIdAndUpdate(
      user._id,
      { avatarUrl: user.avatarUrl },
      { new: true },
    );
  } else {
    throw new Error("There is No User");
  }

  const accessToken = await user.generateToken();
  res.status(200).json({ status: "success", data: { user, accessToken } });
};
module.exports = authController;

We'll use these packages to secure our app.

const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");
const JWT_SECRET_KEY = process.env.JWT_SECRET_KEY;

This function exists on the class User. Learn more here

userSchema.statics.findOrCreate = function findOrCreate(profile, cb) {
  const userObj = new this(); // create a new User class
  this.findOne({ email: profile.email }, async function (err, result) {
    if (!result) {
      let newPassword =
        profile.password || "" + Math.floor(Math.random() * 100000000);
      const salt = await bcrypt.genSalt(10);
      newPassword = await bcrypt.hash(newPassword, salt);

      userObj.name = profile.name;
      userObj.email = profile.email;
      userObj.password = newPassword;
      userObj.googleId = profile.googleId;
      userObj.facebookId = profile.facebookId;
      userObj.avatarUrl = profile.avatarUrl;
      userObj.save(cb);
    } else {
      cb(err, result);
    }
  });
};