The purpose of this article is to provide the reader with a quick guide to setting up Passport.JS Google Strategy (OAuth2) with their React frontend and PostgreSQL backend to maintain a user session and authentication. While recently working on my latest web application, I noticed there are very few articles or videos that guides a developer on how to implement Passport specifically with React and PostgreSQL. We will use pg-pool to manage the connection between our express backend and our database. This article assumes the reader has a basic understanding of React and Express, and has already setup their credentials with Google Developers Console.
Getting Started
Once you have created your react app, and established your express server, cd
to your root folder for your server (I named mine 'server') open your terminal and run this command to install the required packages:
npm install passport, passport-google-oauth20, pg, express-session
Additionally, you may want to install 'cors' to assist with any cors-error messages you might get, and 'dotenv' to establish your environment variables you don't want accessible to the public:
npm install cors, dotenv
.
Backend
Setup & Initialization
DOTENV
After installing the required packages, let's setup our 'dotenv' file. At the top-level of our server, create a dotenv file and name it like so: .env
. Make sure to include this file in your .gitignore
file. This is where we will store our sensitive information. Inside this file should be:
server>.env
CLIENT_ID="FROM YOUR GOOGLE DEVELOPERS CONSOLE"
CLIENT_SECRET="FROM GOOGLE DEVELOPERS CONSOLE"
CLIENT_CALLBACK_URL="FROM GOOGLE DEVELOPERS CONSOLE"
CLIENT_URL="URL of your frontend. i.e. localhost:3000"
COOKIE_SECRET="ANY STRING YOU WANT. USED TO VERIFY COOKIE CREDENTIALS"
NODE_ENV="production"
DB_USER="name of user in your PostgreSQL database: normally defaults to 'postgres'"
DB_PASSWORD="The password you established for your database. You might not have set one up"
DB_HOST="The name of the host domain: i.e., localhost"
DB_PORT="The port your database uses: normally defaults to '5432'" **This is NOT the port your server 'listens' on**
DB_NAME="The name you have given your database"
Whenever we need to use a variable from this file, we will require it in like so: require("dotenv").config();
PG-POOL
Next we will setup our connection to our database using the variables in our dotenv file. I created a folder called 'db' then a file within called 'index.js', but you may use whatever you like. Establish your pool connection like so:
server>db>index.js
require("dotenv").config();
const { Pool } = require("pg");
const pool = new Pool({
database: process.env.DB_NAME,
port: process.env.DB_PORT,
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
});
module.exports = pool;
Passport Google Strategy Setup
Next, create a file to hold our setup for Passport (I named mine 'auth.js'). This file will get fairly large fairly quick depending on the information you would like passport to maintain regarding your user. For the purposes of this article, I will keep it as simple as possible. Keep in mind you may need to adjust some variables to better suit your needs. At the top of the file, require in the following:
server>auth.js
const passport = require("passport");
const { Strategy: GoogleStrategy } = require("passport-google-oauth20");
require("dotenv").config();
const pool = require("./db/index.js");
You may want to 'require' in any helper functions you'd like to use to further authenticate a user to your database, such as checking if the user google email is already registered with your app. Next, still inside 'auth.js', we will create a new instance of 'passport' and 'GoogleStrategy':
server>auth.js
//---required in variables---//
passport.use(
new GoogleStrategy(
{
clientID: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
callbackURL: process.env.GOOGLE_CALLBACK_URL,
},
async(accessToken, refreshToken, profile, done) =>{
const userData = profile._json;
let user = {};
try {
const currentUserQuery = await pool.query("SELECT * FROM users WHERE google_id = $1", [userData.sub]);
if(currentUserQuery.rows.length > 0){
user = {
user_id: currentUserQuery.rows[0].user_id,
username: currentUserQuery.rows[0].username,
};
} else {
Quick explanation of what's happening in this first portion of the strategy. We initialized a new GoogleStrategy, passed in our credentials, then began our async function, which takes 4 parameters: accessToken, refreshToken, profile, done. We then grabbed the information from the google response and stored it in a variable called userData. We created a variable called user and set it equal to an empty object. We did this so we could determine what information to store in session. We then queried our database to determine if the user is already registered and, if so, store in our 'user' object the information we want kept in session: user_id, username. Let's continue where we left off, our else
logic:
//---Continuing logic---//
} else {
const newUser = await pool.query("INSERT INTO users (username, email, firstName, lastName, img, googleID)
VALUES ($1, $2, 3$, $4, $5, $6) RETURNING user_id, username", [userData.name, userData.email, userData.given_name, userData.family_name, userData.picture, userData.sub]);
user = {
user_id: newUser.rows[0].user_id,
username: newUser.rows[0].username
}
}
done(null, user);
} catch (error) {
done(error, false, error.message)
}
}
));
In this portion of our logic, we're stating that if the user is not already in our database, to insert their information and return only the information we want stored in session, again: user_id, username. done(null, user)
is a callback function passport uses to determine when a block of logic is complete, whether from error or otherwise. The first parameter is used if there is an error, the second parameter is used to store the session information. Next, we have to serialize and deserialize. Still inside the same file, under passport.use
:
//---Continuing from previous code block---//
passport.serializeUser((user, done) => {
done(null, user);
});
passport.deserializeUser((user, done) => {
done(null, user)
});
This is a simplified call for serialization and deserialization of user. NOTE: Passport stores in session whatever is contained in the deserializeUser done() callback function as req.user. So if you want to update your session object, here is where it must be done.
Server.js
Now that we have setup pool, dotenv, and passport, we can move onto our server's main file (I named mine server.js). First, we will require
in the needed modules:
server>server.js
const express = require("express");
const session = require("express-session");
const cors = require("cors");
const passports = require("passport");
require("dotenv").config();
require("./auth.js")
Next, we'll initialize, then utilize a router. NOTE: The order in which we initialize our packages (express, session, passport, etc.) is important. Still within server.js:
server>server.js
//---required modules---//
const app = express();
app.use(
cors({
credentials: true,
origin: process.env.CLIENT_URL,
})
);
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(express.static("public"));
app.use(
session({
secret: [process.env.COOKIE_SECRET],
cookie: {
secure: process.env.NODE_ENV === "production" ? "true" : "auto",
sameSite: process.env.NODE_ENV === "production" ? "none" : "lax",
maxAge: 30 * 24 * 60 * 60 * 1000,
},
resave: false,
saveUninitialized: false,
})
);
app.use(passport.session());
app.use(passport.authenticate("session"));
const authRouter = require("./routes/authRouter.js);
app.use("/auth", authRouter);
app.listen(8080, () => {
console.log("Listening on port: 8080")
};
Setting up Authentication Route
In the previous section, at the end of the code block, we setup a route to handle "/auth"
. Now we will setup the actual router. We could have done everything inside our server.js file, but best practices dictate we maintain a separation of concerns. I created a folder called routes, then created a file named 'authRouter.js'. You may call it whatever you like.
server>routes>authRouter.js
const express = require("express");
const router = express.Router();
const passport = require("passport");
require("dotenv").config();
router.get("/google",
passport.authenticate("google", { scope: "profile", })
);
router.get("/google/callback",
passport.authenticate("google", { session: true }),
(req, res) => {
res.redirect(`${process.env.CLIENT_URL}`);
});
module.exports = router;
Frontend
Login Component
For simplicities sake, I will just use a very basic login component that returns a <div>
with a <button>
. You will need to provide your own styles, and imports as your app requires. Your environment variables may be different as well. I created a folder in my 'src' folder named 'pages' and a react file named 'Login.js'. The baseApiURL
will be where your server is 'listening'. In this article, the server is listening on localhost:8080.
client>src>pages>Login.js
import { baseApiURL } "../App.js";
export default function Login(){
const handleOAuth = () => {
window.open(`${baseApiURL}/auth/google`, "_self");
};
return (
<div>
<button onClick={handleOAuth}>
Login with Google
</button>
</div>
)
};
When the user clicks the button, a new window will appear (the reason for "_self"), asking the user to login with their google credentials. If they accept, they will be redirected according to the redirect we setup in our authRouter.
Managing Authentication
You now have everything setup for Google Authentication using Passport! Last thing to remember: whenever your frontend React app needs to verify the authentication of the user, whether in a GET
or POST
request, you must include credentials: "inlcude",
like so:
//---POST Route---//
fetch(`${baseApiURL}${routeNeedingAuth}`, {
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify("DATA BEING SENT"),
})
//---GET Route---//
fetch(`${baseApiURL}${routeNeedingAuth}`, {
credentials: "include",
})
Conclusion
Remember:
Your web application's needs and variables may vary.
The order in which you initialize the modules in your server matters.
Routes needing authentication after the user has logged in requires
credentials: "include",
If you would like a visual guide, checkout this YouTube Playlist by Lester Fernandez. He does a phenomenal job explaining the steps. Happy Coding!
Top comments (0)