Introduction
We will build a todo list app in this tutorial, complete with functionality that allows users to sign in with Google. In this tutorial, you will learn how to use Passport for authentication.
If you want to see where we are headed, here's an example of the final result: https://github.com/passport/todos-express-google
Before we dive in, you'll need a working development environment with Node.js and Git, as well as an editor and terminal of your choosing. Take a moment to set up these tools if you have not already done so.
Let's get started!
We are going to start with a starter app, which has all the scaffolding needed to build a todo list. Let's clone the app:
$ git clone https://github.com/passport/todos-express-starter.git google-tutorial
You now have a directory named 'google-tutorial'
. Let's cd
into
it:
$ cd google-tutorial
Take a moment browse through the files in the starter app. As we work through this tutorial, we'll be using Express as our web framework, along with EJS as our template engine and CSS for styling. We will use SQLite as our database for storing data. Don't worry if you are not familiar with these technologies -- the necessary code will be provided at each step.
Now, let's install the dependencies:
$ npm install
And start the server:
$ npm start
Let's check to see if its working. Open http://localhost:3000 in your browser. You should be greeted with a page explaining how todos help you get things done.
Next, we will add a login page to the app.
Login Prompt
We want to let users sign in with Google. For that, we need a login page that prompts the user to do that. Let's add that now.
Let's create a file that will contain authentication-related routes:
$ touch routes/auth.js
Add the following code to that file, which creates a login route that will render the login page.
var express = require('express');
var router = express.Router();
router.get('/login', function(req, res, next) {
res.render('login');
});
module.exports = router;
Next, we need to add this route to the app. Open 'app.js'
and require
the
newly created auth routes at line 10, below where 'routes/index'
is
require
'd:
var indexRouter = require('./routes/index');
var authRouter = require('./routes/auth');
Continuing within 'app.js'
, use the newly require
'd authRouter
at line 27,
below where indexRouter
is use
'd.
app.use('/', indexRouter);
app.use('/', authRouter);
The login page has been added to our app! Let's see how it looks.
Start the server:
$ npm start
And open http://localhost:3000 and click "Sign in." We are prompted to sign in, but there's no place to choose Google.
For that we need a button. Let's add that now.
Open 'views/login.ejs'
and add the form at line 15, below the "Sign in"
heading:
<h1>Sign in</h1>
<a class="button google" href="/login/federated/google">Sign in with Google</a>
Refresh the page. We've now got a login page that prompts the user to sign in with Google. Next, we will redirect the user to Google.
Redirect to Google
Now that we are prompting the user to sign in with Google, the next step is to redirect the user to Google.
To do that, we are going to use Passport and the passport-google-oidc
strategy. Install both as dependencies:
$ npm install passport
$ npm install passport-google-oidc
Open 'routes/auth.js'
and require
the newly installed packages, as well as
the app's database, at line 2, below where express
is require
'd:
var express = require('express');
var passport = require('passport');
var GoogleStrategy = require('passport-google-oidc');
var db = require('../db');
Next, we need to add a route that will redirect the user when they click "Sign in with Google".
Continuing within 'routes/auth.js'
, add this route at line 12, below the
'/login'
route:
router.get('/login/federated/google', passport.authenticate('google'));
We've now got a route that will redirect the user to Google! Let's test it out to see what happens.
Start the server:
$ npm start
Open http://localhost:3000, click "Sign in" and then click "Sign in with Google".
Uh oh... we are informed the Google authentication strategy is unknown. We will fix that by configuring the strategy. But first, we need to register our app with Google.
Register App
Before we can let users sign in to our app with Google, we need to register the app with Google.
Go to the Google Cloud Platform console.
From the projects list, select a project or create a new one.
Navigate to the APIs & Services page and select Credentials.
If you have an existing application, it will be listed under OAuth 2.0 Client IDs. Click Edit OAuth client to obtain the client ID and secret, and proceed to configure the strategy. Otherwise, continue.
If you have not already done so, configure the OAuth consent screen. Select External to make your application available to any user with a Google account. Complete the app registration process by entering the app name, support email, and developer contact information.
Click Create Credentials, then select OAuth client ID.
Select Web application as Application type.
Click Add URI under Authorized Redirect URIs. Enter
http://localhost:3000/oauth2/redirect/google
.
Click Create to create the OAuth client. The following screen will display your client ID and secret. Proceed to configure the strategy.
Configure Strategy
Now that we've registered our app with Google, we can configure Passport.
First, let's create a '.env'
file to store the client ID and secret we just
obtained from Google.
$ touch .env
Then, add the client ID and secret. The contents of the file should look something like this:
GOOGLE_CLIENT_ID=__INSERT_CLIENT_ID_HERE__
GOOGLE_CLIENT_SECRET=__INSERT_CLIENT_SECRET_HERE__
Of course, your client ID and secret should be inserted where noted.
Open 'routes/auth.js'
and add the following code at line 6 to configure the
GoogleStrategy
.
passport.use(new GoogleStrategy({
clientID: process.env['GOOGLE_CLIENT_ID'],
clientSecret: process.env['GOOGLE_CLIENT_SECRET'],
callbackURL: '/oauth2/redirect/google',
scope: [ 'profile' ]
}, function verify(issuer, profile, cb) {
db.get('SELECT * FROM federated_credentials WHERE provider = ? AND subject = ?', [
issuer,
profile.id
], function(err, row) {
if (err) { return cb(err); }
if (!row) {
db.run('INSERT INTO users (name) VALUES (?)', [
profile.displayName
], function(err) {
if (err) { return cb(err); }
var id = this.lastID;
db.run('INSERT INTO federated_credentials (user_id, provider, subject) VALUES (?, ?, ?)', [
id,
issuer,
profile.id
], function(err) {
if (err) { return cb(err); }
var user = {
id: id,
name: profile.displayName
};
return cb(null, user);
});
});
} else {
db.get('SELECT * FROM users WHERE id = ?', [ row.user_id ], function(err, row) {
if (err) { return cb(err); }
if (!row) { return cb(null, false); }
return cb(null, row);
});
}
});
}));
This configures the GoogleStrategy
to fetch the user record associated with
the Google account. If a user record does not exist, one is created the first
time someone signs in.
The strategy is now configured. Next we need to maintain state when redirecting to Google.
Maintain State
When a user signs in to our app with Google, they are redirected to Google. Google takes care of authenticating the user and then redirects them back to our app.
For security, state needs to be maintained between these two redirects. Passport does this automatically, but the app first needs session support. Let's add that now.
Begin by installing the necessary dependencies:
$ npm install express-session
$ npm install connect-sqlite3
Open 'app.js'
and require
the additional dependencies at line 8, below
where 'morgan'
is require
'd:
var logger = require('morgan');
var session = require('express-session');
var SQLiteStore = require('connect-sqlite3')(session);
Add the following code at line 28, after express.static
middleware, to
add sessions to the application.
app.use(express.static(path.join(__dirname, 'public')));
app.use(session({
secret: 'keyboard cat',
resave: false,
saveUninitialized: false,
store: new SQLiteStore({ db: 'sessions.db', dir: './var/db' })
}));
Now that the app has session support, we are ready to handle the redirect back to our app.
Redirect Back to App
When a user signs in to our app with Google, they are redirected to Google. Google takes care of authenticating the user and then redirects them back to our app.
Let's close the loop by adding a route that will handle this redirect.
Open 'routes/auth.js'
and add this route at line 55, below the
'/login/federated/google'
route:
router.get('/oauth2/redirect/google', passport.authenticate('google', {
successRedirect: '/',
failureRedirect: '/login'
}));
We've now got routes that will redirect the user to Google and handle the redirect back to our app! Let's test it out to see what happens.
Start the server:
$ npm start
Open http://localhost:3000, click "Sign in" and then click "Sign in with Google".
Uh oh... we are informed that there's an error related to sessions. Next, we will fix that by establishing a login session.
Establish Session
Once the user has signed in with Google, we need to a login session to remember the fact that the user has authenticated as they navigate the app.
Open 'app.js'
and require
Passport at line 9, below where
'express-session'
is require
'd:
var session = require('express-session');
var passport = require('passport');
Add the following code at line 35, after session
middleware, to authenticate
the session.
app.use(session({
secret: 'keyboard cat',
resave: false,
saveUninitialized: false,
store: new SQLiteStore({ db: 'sessions.db', dir: './var/db' })
}));
app.use(passport.authenticate('session'));
Finally, we need to configure Passport to manage the login session. Open
'routes/auth.js'
and add the following code at line 47:
passport.serializeUser(function(user, cb) {
process.nextTick(function() {
cb(null, { id: user.id, username: user.username, name: user.name });
});
});
passport.deserializeUser(function(user, cb) {
process.nextTick(function() {
return cb(null, user);
});
});
Now, let's retry signing in. Open http://localhost:3000, click "Sign in" and then click "Sign in with Google".
We are logged in! Go ahead and enter some tasks you've been needing to get done.
At this point, users can sign in with Google! Next, we will add the ability to sign out.
Log Out
Now that users can sign in, they'll need a way to sign out.
Open 'routes/auth.js'
and add this route at line 72, below the
'/oauth2/redirect/google'
route:
router.post('/logout', function(req, res, next) {
req.logout(function(err) {
if (err) { return next(err); }
res.redirect('/');
});
});
Return to the app, where you should already be signed in, and click "Sign out."
We've now got a working app where users can sign in and sign out!