Introduction
We will build a todo list app in this tutorial, complete with functionality that allows users to sign in with a username and password. 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-password
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 username-password-tutorial
You now have a directory named 'username-password-tutorial'
. Let's cd
into
it:
$ cd username-password-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 a username and password. For that, we need a login page that prompts the user for their username and password. 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 enter a username and password.
For that we need an HTML form. 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>
<form action="/login/password" method="post">
<section>
<label for="username">Username</label>
<input id="username" name="username" type="text" autocomplete="username" required autofocus>
</section>
<section>
<label for="current-password">Password</label>
<input id="current-password" name="password" type="password" autocomplete="current-password" required>
</section>
<button type="submit">Sign in</button>
</form>
Refresh the page. We've now got a login page that prompts the user for their username and password. Next, we will verify the username and password.
Verify Password
Now that we are prompting the user for their username and password, the next step is to verify the password.
To do that, we are going to use Passport and the passport-local
strategy.
Install both as dependencies:
$ npm install passport
$ npm install passport-local
Next, we need to configure Passport. Open 'routes/auth.js'
and require
the
newly installed packages at line 2, below where express
is require
'd:
var express = require('express');
var passport = require('passport');
var LocalStrategy = require('passport-local');
var crypto = require('crypto');
var db = require('../db');
The built-in crypto module and the app's database are also require
'd.
Add the following code at line 7 to configure the LocalStrategy
.
passport.use(new LocalStrategy(function verify(username, password, cb) {
db.get('SELECT * FROM users WHERE username = ?', [ username ], function(err, row) {
if (err) { return cb(err); }
if (!row) { return cb(null, false, { message: 'Incorrect username or password.' }); }
crypto.pbkdf2(password, row.salt, 310000, 32, 'sha256', function(err, hashedPassword) {
if (err) { return cb(err); }
if (!crypto.timingSafeEqual(row.hashed_password, hashedPassword)) {
return cb(null, false, { message: 'Incorrect username or password.' });
}
return cb(null, row);
});
});
}));
This configures the LocalStrategy
to fetch the user record from the app's
database and verify the hashed password that is stored with the record. If
that succeeds, the password is valid and the user is authenticated.
Next, we need to add a route that will process the form submission when the user clicks "Sign in."
Continuing within 'routes/auth.js'
, add this route at line 28, below the
'/login'
route:
router.post('/login/password', passport.authenticate('local', {
successRedirect: '/',
failureRedirect: '/login'
}));
We've now got a route that will process the form on the login page! Let's test it out to see what happens.
Start the server:
$ npm start
Open http://localhost:3000, click "Sign in," and enter the following credentials:
Username: alice
Password: letmein
Click "Sign in."
Uh oh... we are informed that there's an error related to sessions. Next, we will fix that by establishing a session.
Establish Session
Once we've verified the user's password, we need to a login session to remember the fact that the user has authenticated as they navigate the app.
To do that, we'll add session support. 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 passport = require('passport');
var session = require('express-session');
var SQLiteStore = require('connect-sqlite3')(session);
Add the following code at line 29, after express.static
middleware, to
maintain and authenticate the session.
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' })
}));
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 22:
passport.serializeUser(function(user, cb) {
process.nextTick(function() {
cb(null, { id: user.id, username: user.username });
});
});
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 enter the following credentials:
Username: alice
Password: letmein
Click "Sign in."
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 a username and password! 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 45, below the
'/login/password'
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! Next we will let new users sign up.
Sign Up
Users can now sign in and sign out. The only thing that's missing is to let new users sign up.
Open 'routes/auth.js'
and add the following route at line 52, below the
'/logout'
route:
router.get('/signup', function(req, res, next) {
res.render('signup');
});
This route will render the signup page. Let's add an HTML form to that page.
Open 'views/signup.ejs'
and add the form at line 15, below the "Sign up"
heading:
<h1>Sign up</h1>
<form action="/signup" method="post">
<section>
<label for="username">Username</label>
<input id="username" name="username" type="text" autocomplete="username" required>
</section>
<section>
<label for="new-password">Password</label>
<input id="new-password" name="password" type="password" autocomplete="new-password" required>
</section>
<button type="submit">Sign up</button>
</form>
Finally, we need to add a route that will process the form submission when the
user clicks "Sign up." Back within 'routes/auth.js'
, add this route at line
56, below the '/signup'
route:
router.post('/signup', function(req, res, next) {
var salt = crypto.randomBytes(16);
crypto.pbkdf2(req.body.password, salt, 310000, 32, 'sha256', function(err, hashedPassword) {
if (err) { return next(err); }
db.run('INSERT INTO users (username, hashed_password, salt) VALUES (?, ?, ?)', [
req.body.username,
hashedPassword,
salt
], function(err) {
if (err) { return next(err); }
var user = {
id: this.lastID,
username: req.body.username
};
req.login(user, function(err) {
if (err) { return next(err); }
res.redirect('/');
});
});
});
});
This route creates a new user record in the app's database, storing the username and hashed password. Once the record is created, the user is logged in.
Return to the app and create a new account.
That's it! Users can now sign in, sign out, and sign up!