Alright folks, here it is: our third and final post detailing how we can use Google's OAuth API with Passport to give our users the ability to login as well as authenticate those users on behalf our applications behalf.
In the first part of this series, we went over how to get your Google credentials for OAuth as well as how to set up the basics for your development environment. Last week, we did a deep dive into Passport's Google Strategy and the callback function we have to give it to store that user in our application's database or retrieve that user's information if they're already in our database.
This week, we'll tie it all together. We'll create the routes we need to handle the authentication process with Express. Plus, we'll need to use another service that gives our user's browser some information that ensures that our application will remember them the next time they visit. And while that might not seem like a big deal, imagine having to log in every time you visit any website or application. That would get old pretty quickly right? We'll use cookies to avoid that.
So let's quickly review what we want to happen:
When the user goes to our login route, whether that be a button or a link, we want to send them to Google so they can sign in.
Once they sign in, we want to Google to redirect them back to our site. But if you remember from last week, our user won't return with their Google profile information in hand. They'll actually just carry a token that we need to send back to Google to get the profile information we're looking for.
Once we get that profile information sent back, we can use our Google Strategy to handle either saving the user to our database or retrieving the previously saved information.
Once the user has been saved or retrieved, we want to send them back to a page within our application. They are now officially logged in.
So now that we have these steps broken down, let's attack them one by one.
Setting up our Login Route
Since we've already initialized our app within our App.js file, we can now start mounting routes on that app, which means if we hit that particular route, from our client, we expect our server to return the appropriate response.
We can name this route whatever we want, but since we want it to handle logging in, it's probably best to name it "login".
app.get('/login', cb);
Now let's fill out the callback function that we will invoke once that route is hit. Instead of a standard request and response handler though, we'll actually use Passport's authenticate method which takes two things: the strategy we're trying to use and then the scope we're trying to retrieve from Google.
Altogether, it looks like this:
app.get('/login', passport.authenticate('google', {
scope: ['profile', 'email'],
}));
But before we try that route out, we need go back to our Google client to handle one thing we put off: our URIs.
Setting up our Google URIs
URI is short for Uniform Resource Identifier, which is standard naming convention for services across the web to talk to each other. One URI you might be quite familiar with is a URL, otherwise known as a web address.
We need to set up URI's within our Google OAuth client to it knows where to expect to receive requests from with our Client ID and Client Secret. You can set up as many as you want, but for this example, we really only need to set up two: one for our local dev environment at port 8080 and the URL of our application (in this case, we're just using example.com).
Next, we need to tell it where to redirect our user after they've successfully logged in. Similar to our login route, this can be whatever we want, but we just need to make sure we're accounting for it in our server routes.
So in this case, we need to make sure we have a route set up for "googleRedirect", because that's where Google will send our user back with their authorization token.
Similar to our "login" route, we set up our redirect route like this:
app.get('/googleRedirect', passport.authenticate('google'), (req, res) => {
// will redirect once the request has been handled
res.redirect('/profile');
});
Triggering our Google Strategy with the redirect route
Again, we use passport.authenticate with our named Google strategy. Since our user is sent back to this route with their token, that will trigger our Google Strategy we built last week. We didn't really touch on this last week, but notice the callback URL listed in our options object:
passport.use(new GoogleStrategy({
// options for the google strategy
callbackURL: '/googleRedirect',
clientID: process.env.GOOGLECLIENTID,
clientSecret: process.env.GOOGLECLIENTSECRET,
}, callback);
This is how we go back to Google with our user token in hand and get their profile information. The user hits our login route, Google redirects them to the route we've set up, and then we go back to Google to quickly exchange the token for profile information and then begin our callback function that saves the user to our database or grabs the user profile we already have saved.
Establishing a session for our user
Okay, so if you remember, I closed last last week's post with these two functions:
passport.serializeUser((user, done) => {
// calling done method once we get the user from the db
done(null, user.googleid);
});
passport.deserializeUser((id, done) => {
// need to find user by id
// calling once we've found the user
getUser(id)
.then(currentUser => {
done(null, currentUser[0]);
});
});
Similar to authenticate, serializeUser and deserializeUser are two Passport methods that we use as either the final piece of logging in or saving the user from logging in when they come back to our application.
In the first case, when the user logs in for the first time, we want to set up what's called a session between the application and our user. We keep track of these sessions by storing a cookie in the user's browser. Within that cookie is a specific identifier that we can use to identify that user in the future when they come back.
But we don't want to store that id directly in the cookie, because that's a little unsafe to be giving a unique identifier that our user could "lose" or have stolen from him by impolite hackers.
That's what we use serializeUser for. After we have our user saved and retrieved from our database, we call serialize with our user and Passport's done method, calling done with that user's googleid, which will be the unique id we choose. But again, it could be something like the user's username or database id.
Once that done method is called, we use one last npm package: cookie-session. This package will dictate how long we want our cookie to last (maxAge property) as well as how we'd like to encrypt that googleid before we send it back to the user (keys property).
Of course, we need to install cookie-session first:
npm i cookie-session
Then we can use it in our app:
app.use(cookieSession({
// age of the cookie in milliseconds
// cookie will last for one day
maxAge: 24 * 60 * 60 * 1000,
// encrypts the user id
keys: [process.env.COOKIEKEY],
}));
The keys property can just be a string of random letters, because cookieSession will use that to encrypt the googleid.
Conversely, deserializeUser will take in a session's cookie data and decrypt that cookie to find the googleid with that same key, thus allowing use to us to go grab that user from our database.
The last thing we need to do to set up a session is call passport's session method and use it in our app.
app.use(passport.session());
Sending our user back to our application
Believe it or not, but almost everything we just did is handled in the passport authenticate method inside of our googleRedirect route. Google's redirect triggers the request and now that we've handled the authentication and established a session with a cookie we're passing back back to our user, we can finally redirect that user to something like their profile page, which will take all the data we just saved and retrieved and send it back to the page, which we can use to render personalized components like an image or other data tied to that user's id.
Our final App.js page
Okay, so we've done quite a lot so let's get one big look at our App.js page that includes everything we've touched in the past three posts.
// bringing express into our project
const express = require('express');
// bringing cookie-session to our project
const cookieSession = require('cookie-session');
// bringing passport into our project
const passport = require('passport');
// bringing a Google "plugin" or Strategy that interacts with Passport
const GoogleStrategy = require('passport-google');
// brining in our getUser and createUser methods from our database methods file
const { getUser, createUser } = require('../db/methods');
// initializing our app by invoking express
const app = express();
// initialize passport to be used
app.use(passport.initialize());
// using session cookies
app.use(passport.session());
// using cookieSession in our app
app.use(cookieSession({
// age of the cookie in milliseconds
// cookie will last for one day
maxAge: 24 * 60 * 60 * 1000,
// encrypts the user id
keys: [process.env.COOKIEKEY],
}));
// setting up our serialize and deserialize methods from passport
passport.serializeUser((user, done) => {
// calling done method once we get the user from the db
done(null, user.googleid);
});
passport.deserializeUser((id, done) => {
// need to find user by id
getUser(id)
.then(currentUser => {
// calling done once we've found the user
done(null, currentUser[0]);
});
// setting our login and redirect routes
app.get('/login', passport.authenticate('google', {
scope: ['profile', 'email'],
}));
app.get('/googleRedirect', passport.authenticate('google'), (req, res) => {
// will redirect once the request has been handled
res.redirect('/profile');
});
// setting up our Google Strategy when we get the profile info back from Google
passport.use(new GoogleStrategy({
// options for the google strategy
callbackURL: '/googleRedirect',
clientID: process.env.GOOGLECLIENTID,
clientSecret: process.env.GOOGLECLIENTSECRET,
}, (accessToken, refreshToken, profile, done) => {
// passport callback function
const {
id: googleId,
displayName: username,
given_name: firstName,
family_name: lastName,
picture: photo,
email: email,
} = profile;
const user = {
googleId,
username,
firstName,
lastName,
photo,
email,
};
getUser(googleId)
.then(currentUser => {
currentUser;
// if the response includes a user object from our database
if (currentUser.length) {
done(null, currentUser[0]);
} else {
// if not, create a new user in the database
createUser(user);
getUser(googleId)
.then(newUser => {
newUser;
done(null, newUser[0]);
})
.catch(err => console.log(err));
}
});
}));
// assigning the port to 8000
const port = 8000;
// calling the listen method on app with a callback that will execute if the server is running and tell us what port
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
Closing thoughts
Authentication can be quite the task. This blog series alone was three pretty big posts. But my hope is that by systematically breaking each piece down, you'll walk away with not only a better idea of how you can incorporate Google's OAuth into your project with the help of Passport, but also a better understanding of Node.js and Express.
While there is a lot of complexity on the surface, it really is as simple as understanding what you're sending out, what you'll get back, and then acting appropriately with that data.
Plus, given the asynchronous nature of the web, authentication with a third party like Google is a great exercise in handing multiple requests with something as simple as a user hitting a "login" route.
If you have any additional questions or feedback, feel free to leave a comment or message me directly.
Top comments (2)
Thanks Mac, nice post! Have you thought about making a similar series for Authentication with JWT ? I'm building a web app and as many devs, I need to configure an API authentication module to access the backend from a mobile app.
nice post! mind sharing what your getUser and createUser functions look like?