Node Authentication With Google OAuth: Part 1 (With Sessions)
Introduction
This article will show you how to quickly and easily "outsource" your website's user authentication to Google.
The problem
Building a user sign-up flow for a new website is a pain in the butt, and comes with a number of disadvantages:
- It's a lot of work for you (and any other developers on the team).
- Must store salted / encrypted user passwords securely in your database.
- Must help users reset their passwords when they forget.
- Have to write all the back-end AND front-end code for most of those flows (though admittedly there are libraries that can help).
- Your users have to create a new account, then remember their new password.
- Someone can now steal your hashed passwords, and then you'd have to tell your users "well, the bad guys probably can't use these passwords bc we salted / encrypted them, but technically there are still some risks..."
The solution
Anyway, kind of a drag. Often, it's way easier for you AND your users to let someone else worry about all that:
Turns out, you can set your site up so that any time a user wants to sign in, they get redirected to another site (eg. Google, Facebook, Github, Twitter, or many others), then that site can come back and say "Margaret? Yeah we know Margaret. She's cool." And boom, now you can log Margaret into your site without you EVER having to see or worry about her password.
Motivation
I set something like this up a few years ago. I'm finishing up a new side project now (a fantasy basketball analysis tool), and want to make it usable by more people than just me (so I need user login). For the reasons listed above, I wanted to let my (hypothetical) future users log into my site through Google.
However, it's been tough to find a clear walkthrough on how to set all this up, especially one that doesn't require signing up with a service like Auth0 or Okta. So I decided to write this walkthrough.
Walkthrough
Pre-reqs
This overview will assume basic familiarity with Node, Express, and some knowledge of deploying a Node app to Heroku.
Step 1: Set up a basic API and homepage
This step should be straightforward for those with Express experience, so I won't spend much time on it. Put together a basic server + a basic html homepage, and in the next steps we'll add to it:
<!-- public/index.html -->
<html>
<head>
</head>
<body>
<h1>Attention, everyone!</h1>
<p>This is a homepage.</p>
<a href="/auth/google">Log In With Google</a>
</body>
</html>
// This file should be named `server.js`
const express = require('express');
const app = express();
const port = process.env.PORT || 8050;
// Serve static files
app.use(express.static(__dirname + '/public'));
// Serve a test API endpoint
// This is just to test your API -- we're gonna delete this endpoint later
app.get('/test', (req, res) => {
res.send('Your api is working!');
});
// Start server
const server = app.listen(port, function() {
console.log('Server listening on port ' + port);
});
Run this server (with node server.js
), then test that both the homepage and the "/test" API are working by going http://localhost:8050 and http://localhost:8050/test.
Note -- the login link on the homepage won't work yet, but it will be useful later on.
Step 2: Deploy your basic API to Heroku
We're gonna start deploying to heroku now because it's better to get it set up early before we get into more complicated things.
- Make sure you have a Heroku account, and that the Heroku CLI is installed.
- Either manually create a new heroku app, or do so automatically via the
heroku create
command. Either way, you should now have a git remote called "heroku". (Verify this by running
git remote -v
in the root directory of your app).You should see something like this:
$ git remote -v heroku https://git.heroku.com/something-something-123.git (fetch) heroku https://git.heroku.com/something-something-123.git (push)
- Commit your code to your local git, then push it to the heroku remote repository with
git push heroku master
. - Once this completes, you should be able to verify that everything is working by running
heroku open
, which will open your (now-deployed) site in your default browser.
- For all future steps, we will no longer be using
localhost
to test our app, since we need a publicly available site in order to log in with Google.
- For all future steps, we will no longer be using
Step 3: Set things up so Google knows about your app
Ok, so you have a basic website, and it's deployed to heroku. Now let's start setting up the authentication.
In order to use Google to log users in, you have to set up OAuth credentials with Google. This allows your app to redirect login requests to Google, and tells Google where they can redirect users once they're done authenticating (logging in) with Google.
This article will walk you through the process in detail, but I'll include the basic steps below:
- Go to the Google Developer Console.
- Create a new "app" in that console.
- Go to the credentials page
- Your new app should be selected by default if it's the first one you've made.
- If not, make sure it's selected in the dropdown at the top of the page.
- Select Create credentials -> OAuth client ID
- Select the type of app (in this case, "Web Application")
- Eventually, Google will give you a Client ID and a Client Secret.
- This Client ID and a Client Secret will be needed by your application to handle authentication requests (we need to send them over to Google when we ask for Google's help authenticating).
- We don't want to commit them to our application code directly.
- So, instead of hard-coding them in our app, let's save them in our Heroku app's environmental variables:
heroku config:set GOOGLE_OAUTH_TEST_APP_CLIENT_ID="your-id-here"
heroku config:set GOOGLE_OAUTH_TEST_APP_CLIENT_SECRET="your-secret-here"
- Verify that these both got saved by running
heroku config
At some point you'll also reach a page where you'll need to set these:
- "Authorized JavaScript origins": just put the url of your Heroku app here (mine was https://whispering-retreat-73590.herokuapp.com)
- "Authorized redirect URIs": This is where Google will redirect your users after they've signed in. Set this to your-heroku-website-address.com/auth/google/callback. (For me, it was https://whispering-retreat-73590.herokuapp.com/auth/google/callback)
Go back to the developer console and Enable your API
Step 4: Add the Passport JS libraries and set up the Google integration
We'll be integrating with Google via the passport.js package. This package is great at authentication, and far better than us trying to write everything from scratch (remember, we want an easy solution that is also safe and reliable).
Passport can help with all sorts of login strategies, including "local" login where users create new accounts specifically for your site. It also allows you to integrate with other 3rd party authentication providers (not just Google).
Step 4.1: Install the passport packages
We'll need two npm packages: passport
(the base passport package) and passport-google-oauth20
(the passport "strategy" package that allows integration with Google):
npm install --save passport
npm install --save passport-google-oauth20
Step 4.2: Add the Google "Strategy"
Passport handles authentication via "strategies". Basically, these just tell Passport how to authenticate a user with a given approach. (So in the future, if your app allows login with Google OR Twitter, you'll need to set up a "strategy" for both). For now, though, we're just gonna set up Google.
Here's the new server code with the Google strategy code:
// server.js
const express = require('express');
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const app = express();
const port = process.env.PORT || 8050;
// Serve static files
app.use(express.static(__dirname + '/public'));
// Set up passport strategy
passport.use(new GoogleStrategy(
{
clientID: process.env.GOOGLE_OAUTH_TEST_APP_CLIENT_ID,
clientSecret: process.env.GOOGLE_OAUTH_TEST_APP_CLIENT_SECRET,
callbackURL: 'https://your-apps-name.herokuapp.com/auth/google/callback',
scope: ['email'],
},
// This is a "verify" function required by all Passport strategies
(accessToken, refreshToken, profile, cb) => {
console.log('Our user authenticated with Google, and Google sent us back this profile info identifying the authenticated user:', profile);
return cb(null, profile);
},
));
// Serve a test API endpoint
app.get('/test', (req, res) => {
res.send('Your api is working!');
});
// Start server
const server = app.listen(port, function() {
console.log('Server listening on port ' + port);
});
The code above still doesn't do anything yet, but let's pause here to go in depth a bit more. The passport-google-oauth20
package gave us access to this "Strategy" for authentication, we just had to fill in a few blanks: our client id, our client secret, and the URL where we expect Google to redirect users back to our application. You can read about the scope
value here, but this just tells Google what data to give us for that user.
We also provided a verify
function as part of the Strategy setup, but we'll cover that later.
In the next few steps, we'll write a couple more endpoints that allow us to finish the authentication flow using this "strategy".
Step 4.3: Add the login endpoint
So far, we've set up this "Strategy" object with our config values and a verify function, but we aren't using the "Strategy" yet. Let's change that by adding a place where users can log in:
app.get('/auth/google', passport.authenticate('google'));
This is just a regular GET endpoint defined on our express server, but we've added passport's authenticate
middleware. In a nutshell, this middleware looks for a "strategy" with the lookup name "google", then it does whatever that strategy tells it.
Turns out, the "strategy" we just defined in step 4.2 has the lookup name "google". And that strategy will just redirect the user directly to Google so they can log in.
So, passport says "what strategy has the name 'google'", gives control to that strategy, and that strategy (the one we set up in 4.2) redirects the user to google to login.
Step 4.4: Add the Google callback endpoint
In the step above, you'll notice that our "login" link just sends the user directly to Google via the authenticate
middleware. This is necessary so they can login with Google, but after they're done Google needs to know where to redirect those users so they end up back on our website. This is the importance of defining the "callbackURL" from Step 4.2 and the "authorized redirect URI" from step 3.
Let's set that up.
// This is where Google sends users once they authenticate with Google
// Make sure this endpoint matches the "callbackURL" from step 4.2 and the "authorized redirect URI" from Step 3
app.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/', session: false }),
(req, res) => {
console.log('wooo we authenticated, here is our user object:', req.user);
res.json(req.user);
}
);
Like in Step 4.3, this new GET endpoint uses the same authenticate
middleware as the step above.
If that's the case, why doesn't it cause an infinite loop? (eg. user hits our login endpoint, they get sent to Google to login, Google sends them back to an endpoint with the same middleware, they get sent to Google to login, etc.)
Turns out, when Google redirects users back to our callback URL, they add a GET parameter to the URL. So, instead of redirecting to https://yourapp.herokuapp.com/auth/google/callback, Google redirects to https://yourapp.herokuapp.com/auth/google/callback?code=some-code-here.
What difference does that make? Well, turns out that the Google Strategy we set up is smart enough to look for the ?code=some-code-here
parameter. If the code parameter is there, our Google strategy knows it doesn't need to authenticate again, and instead it takes that "code" and converts it to user data.
End result: instead of telling the user to authenticate again, the Google Strategy turns that code into user data (for the curious, this happens behind-the-scenes via another request to Google), then passes that user data to the "verify" function from Step 4.2.
That "verify" function we set up can do whatever it wants (ours just passed through the entire user info, but in most production use cases we'd want to convert the google profile data into a user object that is relevant for our website, perhaps by looking up the equivalent user in our database). Regardless, whatever gets passed into cb
from the "verify" function will be automatically saved to req.user
...which is why we can have a console.log
statement here that prints out req.user
.
(To test the above, try changing what the verify
function from Step 4.2 passes to the cb
callback. You'll notice that whatever you pass back shows up in req.user
).
Q: If both /auth/google
and /auth/google/callback
use the same middleware, couldn't users actually log in by going to auth/google/callback
?
A: Yep! Turns out we don't really need the /auth/google/
endpoint, unless we want it there for clarity. I included /auth/google/
here because it is the standard way to set things up for Passport auth, but it is actually not necessary for this particular Google Strategy (so let's remove it in our next step!)
Step 4.5: The app so far
On your own, remove the /test
endpoint and the auth/google
login endpoint (we'll need to update the front-end to point to /auth/google/callback
.
Here is what your application should look like at this point:
<!--public/index.html-->
<html>
<head>
</head>
<body>
<h1>Attention, everyone!</h1>
<p>This is a homepage.</p>
<a href="/auth/google/callback">Log In With Google</a>
</body>
</html>
// server.js
const express = require('express');
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const app = express();
const port = process.env.PORT || 8050;
// Serve static files
app.use(express.static(__dirname + '/public'));
// Set up passport strategy
passport.use(new GoogleStrategy(
{
clientID: process.env.GOOGLE_OAUTH_TEST_APP_CLIENT_ID,
clientSecret: process.env.GOOGLE_OAUTH_TEST_APP_CLIENT_SECRET,
callbackURL: 'https://whispering-retreat-73590.herokuapp.com/auth/google/callback',
scope: ['email'],
},
(accessToken, refreshToken, profile, cb) => {
console.log('Our user authenticated with Google, and Google sent us back this profile info identifying the authenticated user:', profile);
return cb(null, profile);
},
));
// Create API endpoints
// This is where users point their browsers in order to get logged in
// This is also where Google sends back information to our app once a user authenticates with Google
app.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/login.html', session: false }),
(req, res) => {
console.log('wooo we authenticated, here is our user object:', req.user);
// Send the user data back to the browser for now
res.json(req.user);
}
);
// Start server
const server = app.listen(port, function() {
console.log('Server listening on port ' + port);
});
Go ahead and deploy that to heroku (remember that your heroku app name will not be the same as mine, so make sure your callbackURL
doesn't match the one above exactly!)
If you go to your site, you should see the following:
Assuming you've set everything up correctly in Step 3, clicking "Log In With Google" should send you to Google. After logging in, it should send you back to /auth/google/callback
, which will print out the user data that we got from Google.
Note: If anything goes wrong, you can run heroku logs --tail
to see any console.log
statements or anything else that your heroku app is logging as it runs. This is a great way to debug.
The most likely issues you'll encounter are
- Missing something from Step 3 (eg. not enabling your API)
- Incorrect configuration of your Google Strategy (eg. using my heroku app name instead of your own heroku app name)
Step 5: Persistent Login
Alright, this blog post is already WAY longer than I'd planned, so I'll try to wrap this up soon.
Step 5.1: Quick Recap
So far, we've set up a way for our app's users to authenticate with Google. Once they do that, Google sends us back some info basically saying "this is the user".
- Users don't need to create a new account & password to use our app.
- Our app never sees the user's login information -- Google does all the heavy lifting, then lets us know the identity of the user (eg. their Google ID or email).
- Password resets, storing passwords, password security, are all handled by Google.
Step 5.2: What's missing?
We're almost done, but not quite. You may have noticed that in step 4.4 above, we set session: false
in our login / callback endpoint. By default, once a user logs in using a Passport strategy, Passport will store a cookie on the user's browser that maintains the session. Setting session: false
disabled this behavior, and would normally allow us to keep track of users however we want (ie. if we wanted to use JWTs, or track users through some other approach, we could).
At the moment, with session: false
, we get back a response from Google with some basic data about our user, but then our app immediately forgets it, since that data isn't stored anywhere and we haven't set up a way for our users to identify their future requests.
So, let's set up sessions.
Step 5.3: Enable Express Sessions
In order for Passport to automatically sign users in, we need to set up sessions in express.
- Install the
express-session
package:npm install --save express-session
. This package is for handling any type of session in Express, and is not specific to Passport. - Add the new session middleware to your application by adding the following to
server.js
:
// Add session support
app.use(session({
secret: process.env.SESSION_SECRET || 'default_session_secret',
resave: false,
saveUninitialized: false,
}));
This blog post is not about sessions, so I won't go too in-depth with what's going on here, but the basic summary is:
- Once you enable this middleware via
app.use
, it allows your server's endpoints to "save" values to the client's browser by modifying thereq.session
object. - The way your app saves the session data is by telling the client's browser to save a cookie. The client's browser will then automatically send that cookie's data to our server every time a request is made.
- Things to note: the code above is not good for production environments for a number of reasons (eg. it saves session state to application memory by default, so if you have more than 1 server handling requests, this will break). If you're curious, check out the express-session page for more details.
However, for the purposes of this tutorial, this session set-up is good enough for now.
Step 5.4: Enable Passport To Use Sessions
Now that our application understands the concept of "sessions", and can save session data to the client's browser, let's also set up Passport so it can take advantage of this.
After the lines we added in Step 5.3, add these lines:
app.use(passport.initialize());
app.use(passport.session());
As you'll notice, these are 2 more middleware functions that get run every time our server receives a request.
The first line (app.use(passport.initialize());
), simply transfers passport-specific session data from req.session
(where express-session
puts everything) onto req._passport.session
. TL;DR: This just sets things up for the next piece of middleware.
The second line (app.use(passport.session());
) actually tells passport to authenticate all routes with the 'session' Strategy. This Strategy is similar to the "Google" strategy we set up in Step 4.2, but it is always available to Passport by default. Instead of re-directing to Google or checking for a "code" parameter, the session strategy just reads the session
to find out if the user is already logged in. If so, it will automatically add the user to req.user
.
Step 5.5: Set up session serialization
There's one last thing missing before saving user data to the session will work: We need to define what User data to store in the cookie on a client's browser.
Since the browser will automatically send cookie data to our server on every request, it's usually better to store as little data as possible into client-side cookies for performance reasons. Usually, the way to do this is to store just a user id, then have a database or lookup table that can convert user id into a full user object on the server side.
We don't have a database set up for this example, so to keep this as simple as possible, we're going to store the entire user object in the client-side cookie.
// This will tell passport what to put into client-side cookies
// We are just saving the entire user object for this tutorial
// Normally, we'd usually want to save just a user_id
passport.serializeUser((user, done) => {
done(null, user);
});
serializeUser
tells passport what to put into the cookie on the client's browser, deserializeUser
tells passport how to convert what's in that cookie back into a full user object.
Since we just stored the entire user object, we can just return it straight from the cookie.
passport.deserializeUser((userDataFromCookie, done) => {
done(null, userDataFromCookie);
});
Step 5.6: Add a protected endpoint
Alright, we've got our sessions ready to go, which means that Passport will now "remember" when a user logs in to the server, since the client will store a cookie with the user's info.
Let's add an endpoint to test that all this is working, and "protect" access to it using custom middleware.
// Checks if a user is logged in
const accessProtectionMiddleware = (req, res, next) => {
if (req.isAuthenticated()) {
next();
} else {
res.status(403).json({
message: 'must be logged in to continue',
});
}
};
// A secret endpoint accessible only to logged-in users
app.get('/protected', accessProtectionMiddleware, (req, res) => {
res.json({
message: 'You have accessed the protected endpoint!',
yourUserInfo: req.user,
});
});
Step 5.7: The finished product
Ok, now we have everything set up. Here's what your files should look like now:
Note: I added a link to the protected endpoint, and added a redirect to the login endpoint that takes you back to the homepage.
<!-- public/index.html-->
<html>
<head>
</head>
<body>
<h1>Attention, everyone!</h1>
<p>This is a homepage.</p>
<a href="/auth/google/callback">Log In With Google</a>
<a href="/protected">Go To Protected Endpoint</a>
</body>
</html>
// server.js
const express = require('express');
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const session = require('express-session');
const app = express();
const port = process.env.PORT || 8050;
// Serve static files
app.use(express.static(__dirname + '/public'));
// Add session support
app.use(session({
secret: process.env.SESSION_SECRET || 'default_session_secret',
resave: false,
saveUninitialized: false,
}));
app.use(passport.initialize());
app.use(passport.session());
passport.serializeUser((user, done) => {
done(null, user);
});
passport.deserializeUser((userDataFromCookie, done) => {
done(null, userDataFromCookie);
});
// Checks if a user is logged in
const accessProtectionMiddleware = (req, res, next) => {
if (req.isAuthenticated()) {
next();
} else {
res.status(403).json({
message: 'must be logged in to continue',
});
}
};
// Set up passport strategy
passport.use(new GoogleStrategy(
{
clientID: process.env.GOOGLE_OAUTH_TEST_APP_CLIENT_ID,
clientSecret: process.env.GOOGLE_OAUTH_TEST_APP_CLIENT_SECRET,
callbackURL: 'https://whispering-retreat-73590.herokuapp.com/auth/google/callback',
scope: ['email'],
},
(accessToken, refreshToken, profile, cb) => {
console.log('Our user authenticated with Google, and Google sent us back this profile info identifying the authenticated user:', profile);
return cb(null, profile);
},
));
// Create API endpoints
// This is where users point their browsers in order to get logged in
// This is also where Google sends back information to our app once a user authenticates with Google
app.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/', session: true }),
(req, res) => {
console.log('wooo we authenticated, here is our user object:', req.user);
// res.json(req.user);
res.redirect('/');
}
);
app.get('/protected', accessProtectionMiddleware, (req, res) => {
res.json({
message: 'You have accessed the protected endpoint!',
yourUserInfo: req.user,
});
});
// Start server
const server = app.listen(port, function() {
console.log('Server listening on port ' + port);
});
Step 6: Test it
Ok, everything is done, let's make sure it works.
- Push everything to heroku:
git push heroku master
- Go to your site.
- Click the protected link -- you should see an error message.
- Log in with Goolge.
- Now go to the protected link -- it should work!
Congratulations, now you have Google OAuth set up in your app!
Part 2 covers setting up Google auth with JWTs instead of cookies, which can work better with "single page apps" (eg. like a front-end built with React that just has an API back-end) and mobile apps.
(Spoiler alert: Cookie monster will not feature in Part 2, since we're gonna avoid using cookies).