Introduction

We will build a todo list app in this tutorial, complete with functionality that allows users to sign in with email. By following along with 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-email

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.

You'll also need a SendGrid account. If you don't already have one, sign up now. This tutorial can be completed using SendGrid's free plan.

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 email-tutorial

You now have a directory named 'email-tutorial'. Let's cd into it:

$ cd email-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 email. For that, we need a login page that prompts the user to enter their address. 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 an email address.

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/email" method="post">
    <section>
        <label for="email">Email</label>
        <input id="email" name="email" type="text" autocomplete="username" required autofocus>
    </section>
    <button type="submit">Sign in with Email</button>
</form>

Refresh the page. We've now got a login page that prompts the user to sign in with email. Next, we will set up SendGrid, in preparation for sending the user a magic link.

Set Up SendGrid

Before we can let users sign in to our app with email, we need a way to send emails.

For that, we are going to use SendGrid.

Go to the SendGrid dashboard.

Navigate to Settings > API Keys.

Click Create API Key.

Enter "email-tutorial" for the API Key Name.

Under API Key Permissions select Restricted Access. Then, under Access Details, ensure that Mail Send > Mail Send is granted full access.

Click the Create & View button.

The following screen will display your API key. Save it someplace safe, as we will use it later.

Next, navigate to Settings > Sender Authentication.

If you've already verified your domain or email address, it will be displayed here. If not, click Get Started under Verify an Address and complete single sender verification.

Now that we have an API key and a verified email address, let's create a '.env' file to store them.

$ touch .env

Add your email and API key. The contents of the file should look something like this:

EMAIL=user@example.com
SENDGRID_API_KEY=__INSERT_API_KEY_HERE__

Next we will configure the strategy.

Configure Strategy

Now that we've set up SendGrid, we are ready to configure Passport and the passport-magic-link strategy.

Install the necessary dependencies:

$ npm install passport
$ npm install passport-magic-link
$ npm install @sendgrid/mail

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 MagicLinkStrategy = require('passport-magic-link').Strategy;
var sendgrid = require('@sendgrid/mail');
var db = require('../db');

The app's database is also require'd.

Add the following code at line 7 to configure the MagicLinkStrategy.

sendgrid.setApiKey(process.env['SENDGRID_API_KEY']);

passport.use(new MagicLinkStrategy({
  secret: 'keyboard cat',
  userFields: [ 'email' ],
  tokenField: 'token',
  verifyUserAfterToken: true
}, function send(user, token) {
  var link = 'http://localhost:3000/login/email/verify?token=' + token;
  var msg = {
    to: user.email,
    from: process.env['EMAIL'],
    subject: 'Sign in to Todos',
    text: 'Hello! Click the link below to finish signing in to Todos.\r\n\r\n' + link,
    html: '<h3>Hello!</h3><p>Click the link below to finish signing in to Todos.</p><p><a href="' + link + '">Sign in</a></p>',
  };
  return sendgrid.send(msg);
}, function verify(user) {
  return new Promise(function(resolve, reject) {
    db.get('SELECT * FROM users WHERE email = ?', [
      user.email
    ], function(err, row) {
      if (err) { return reject(err); }
      if (!row) {
        db.run('INSERT INTO users (email, email_verified) VALUES (?, ?)', [
          user.email,
          1
        ], function(err) {
          if (err) { return reject(err); }
          var id = this.lastID;
          var obj = {
            id: id,
            email: user.email
          };
          return resolve(obj);
        });
      } else {
        return resolve(row);
      }
    });
  });
}));

This configures the MagicLinkStrategy to send emails containing a magic link using SendGrid. When the user clicks on the magic link, the user record associated with the email address will be found. 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 send the user a magic link when they click "Sign in with Email."

Send Email

Now that we are prompting the user for their email address, and have the strategy configured, the next step is to send the user an email when they click "Sign in with Email."

Open 'routes/auth.js', add this route at line 56, below the '/login' route:

router.post('/login/email', passport.authenticate('magiclink', {
  action: 'requestToken',
  failureRedirect: '/login'
}), function(req, res, next) {
  res.redirect('/login/email/check');
});

This route will process the form on the login page and send an email to the user.

Continuing within 'routes/auth.js', add this route at line 63, below the newly added '/login/email' route:

router.get('/login/email/check', function(req, res, next) {
  res.render('login/email/check');
});

This route will render a page instructing the user to check their email and click the link.

Next, we will verify the email when the user clicks that link.

Verify Email

Now that we've sent the user an email with a magic link, the next step is to verify the email address when they click the link.

Open 'routes/auth.js', add this route at line 67, below the '/login/email/check' route:

router.get('/login/email/verify', passport.authenticate('magiclink', {
  successReturnToOrRedirect: '/',
  failureRedirect: '/login'
}));

This route will verify the email address when the link is clicked.

Let's test it out to see what happens.

Start the server:

$ npm start

Open http://localhost:3000, click "Sign in", enter your email address and click "Sign in with Email".

Now, check your email and click the link.

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 email address, 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 50:

passport.serializeUser(function(user, cb) {
  process.nextTick(function() {
    cb(null, { id: user.id, email: user.email });
  });
});

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", enter your email address and click "Sign in with Email".

Now, check your email and click the link.

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 email! 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 84, below the '/login/email/verify' 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!

SEARCH FOR STRATEGIES

0STRATEGIES