DEV Community

Cover image for Double Opt-In in Node.js: Sending confirmation emails
webdeasy.de
webdeasy.de

Posted on • Originally published at webdeasy.de

Double Opt-In in Node.js: Sending confirmation emails

Originally published on webdeasy.de!

The double opt-in procedure has established itself as the standard for all types of registrations on the Internet in recent years. I will show you here what exactly is behind it and how you can integrate Double Opt-In into your Node.js application.

With the double opt-in procedure, in addition to the registration in a form, e.g. on a website, an activation link is also sent by e-mail and only after its confirmation is the registration process completed or the registration verified.

Classical areas of application are for example:

  • Newsletter subscriptions and unsubscriptions,
  • lottery registrations, or
  • Login Systems

In this tutorial we will build a Double Opt-In procedure into an existing RestAPI of a Node.js login system. I have already shown in another tutorial how we can create a RestAPI with Node.js and connect it to our frontend, e.g. Vue.js.

Tutorial Node.js Login System

If you already have a finished system and only want to extend the Double Opt-In procedure, you can read on. Otherwise I recommend you first have a look at the linked tutorial.

If you are not yet familiar with Node.js, you can first take a look at the basics of Node.js.

Table Of Contents

1. Advantages of Double Opt-In

Besides the normal Single Opt-In procedure, where you simply sign up for something – without further confirmation – there is also the better Double Opt-In procedure. The following diagram should explain the difference in the registration process:

Single Opt-In vs. Double Opt-In<br>

The Double Opt-In has many advantages and is now an established standard for any web application, app, etc.

The most important fact is that all confirmed registrations are genuine (verified). This means that bots can sign up in the form, but the account will never be verified, because the bots cannot click on the verification links in the confirmation emails.

Furthermore, it is probably advantageous from a data protection point of view, since the user explicitly agrees to a registration. Especially in EU countries it may be required by the DSGVO in some cases.

But attention: I cannot give a professional opinion on legal issues!

Now we start directly and send some confirmation emails (with the help of nodemailer) with activation links, have fun! 🙂

2. Install dependencies

So the first step is to install the required packages:

Alt Text

We can install these packages with the following command:

npm install nodemailer email-validator
Enter fullscreen mode Exit fullscreen mode

3. Adjust MySQL database structure

We now extend our existing users table by three more columns:

Alt Text

The complete MySQL table now looks like this:
phpMyAdmin view of the extended “users” table<br>

Here again the hint: This tutorial is based on another tutorial from me: Node.js Login System with Express, JWT and MySQL (RestAPI)

4. Adjust Registration (/sign-up) and Login (/login) routes

To fill in the new columns, we adjust our existing registration route (/sign-up) and login route (/route) a bit.

To do this, we change the following lines: 11, 21-31, 40-44.

Unfortunately I can't give line numbers or highlighted lines here. 😔 For a clearer representation look at the post on my blog webdeasy.de.

The SQL query is changed here so that our new values, i.e. the e-mail address, the token and the account status (active) are entered into the database. As token we use a unique ID from the uuid package as well as for the user ID.

From line 40 we call the function sendOptInMail() from the mailer. We implement the function in step 8.

// routes/router.js
router.post("/sign-up", userMiddleware.validateRegister, (req, res, next) => {
  db.query(
    `SELECT * FROM users WHERE LOWER(username) = LOWER(${db.escape(
      req.body.username
    )}) OR LOWER(email) = LOWER(${db.escape(req.body.email)});`,
    (err, result) => {
      if (result.length) {
        return res.status(409).send({
          msg: "This username or email is already in use!",
        });
      } else {
        // username is available
        bcrypt.hash(req.body.password, 10, (err, hash) => {
          if (err) {
            return res.status(500).send({
              msg: err,
            });
          } else {
            let email = req.body.email;
            let userID = uuid.v4();
            let token = uuid.v4();
            // has hashed pw => add to database
            db.query(
              `INSERT INTO users (id, username, email, password, registered, active, token) VALUES ('${userID}', ${db.escape(
                req.body.username
              )}, '${email}', ${db.escape(
                hash
              )}, now(), 0, '${token}')`,
              async (err, result) => {
                if (err) {
                  throw err;
                  return res.status(400).send({
                    msg: err,
                  });
                }
                await mailer.sendOptInMail(
                  email,
                  userID,
                  token
                );
                return res.status(201).send({
                  msg: "Registered!",
                });
              }
            );
          }
        });
      }
    }
  );
});
Enter fullscreen mode Exit fullscreen mode

To call sendOptInMail() we have to include mailer. For this you can simply create the file already, content will come – as said – immediately.

// routes/router.js
[...]
const mailer = require("../lib/mailer.js");
[...]
Enter fullscreen mode Exit fullscreen mode

5. Validate e-mail address

To verify the entered e-mail address, we extend our middleware in users.js with the lines 11-16.

There it is checked whether a value named email was passed in the body of our request and whether it is a valid email address. If not, an error message is returned. Otherwise the route will be executed with next() at the end.

To validate the email we use the email-validator package. You can also do this with your own regular expression, but for simplicity we use an external module here.

// middleware/users.js
validateRegister: (req, res, next) => {
    // username min length 3
    if (!req.body.username || req.body.username.length < 3) {
      return res.status(400).send({
        msg: "Please enter a username with min. 3 chars",
      });
    }
    // valide email
    if (!req.body.email || !validator.validate(req.body.email)) {
      return res.status(400).send({
        msg: "Please enter a valid email address",
      });
    }
    // password min 6 chars
    if (!req.body.password || req.body.password.length < 6) {
      return res.status(400).send({
        msg: "Please enter a password with min. 6 chars",
      });
    }
    // password (repeat) does not match
    if (
      !req.body.password_repeat ||
      req.body.password != req.body.password_repeat
    ) {
      return res.status(400).send({
        msg: "Both passwords must match",
      });
    }
    next();
  },
Enter fullscreen mode Exit fullscreen mode

6. Check account activation at login

In the /login route you can add this query, so that you can’t login if the account hasn’t been confirmed yet. In my case the query is after checking if a corresponding database entry exists. From line 20 of the /login route, if you want to orientate yourself at my other tutorial.

// routes/router.js
[...]
if (!result[0]["active"]) {
  return res.status(401).send({
    msg: "Your account is not activated!",
  });
}
[...]
Enter fullscreen mode Exit fullscreen mode

If you have already extended the system with your own function, you should add this query in other relevant places, e.g. in a forget-password function.

7. Create verification route (/verify)

With the verification route we can activate a registered account. We will send the link in the last step by mail. The link consists of the user ID and the unique token.

To do this, we create a new route that reads the user from the database using the user ID. First we check if the entry exists at all and return an error message if necessary.

From line 25 we check if the account is already activated and from line 32 we check the token.

If all checks are correct, we set the user account active (from line 39) and return a success message with status code 200.

// routes/router.js
router.get("/verify/:userID/:token", (req, res, next) => {
  let userID = req.params.userID;
  let token = req.params.token;
  db.query(
    `SELECT * FROM users WHERE id = ${db.escape(userID)}`,
    (err, result) => {
      // user does not exists
      if (err) {
        throw err;
        return res.status(400).send({
          msg: err,
        });
      }
      // no result from database
      if (!result.length) {
        return res.status(409).send({
          msg: "The requested parameters are incorrect!",
        });
      }
      // already activated
      if (result[0]["active"]) {
        return res.status(409).send({
          msg: "Account is already activated!",
        });
      }
      // wrong activation token
      if (result[0]["token"] !== token) {
        return res.status(401).send({
          msg: "The requested parameters are incorrect!",
        });
      }
      // set account active
      db.query(
        `UPDATE users SET active = 1 WHERE id = '${userID}'`,
        (err, result) => {
          if (err) {
            throw err;
            return res.status(400).send({
              msg: err,
            });
          }
          return res.status(200).send({
            msg: "Account activated",
          });
        }
      );
    }
  );
});
Enter fullscreen mode Exit fullscreen mode

8. Send confirmation mail

The confirmation mail contains a verification link, when called the account should be confirmed, i.e. activated. We assemble the link ourselves and send it with the nodemailer package.

For this we can create a mailer.js in the folder lib and include the nodemailer.

Now we set up the nodemailer to send mails via our mailserver. This is done with the createTransport() function from line 5 to 16.

You should declare the access data to the mail server as environment variables of Node.js (process.env.*), so that no sensitive data is directly in the source code and you can easily adapt the data on different development and production stages.

// lib/mailer.js
const nodemailer = require("nodemailer");
let transporter = nodemailer.createTransport({
  host: process.env.MAIL_HOST,
  port: 465,
  secure: true,
  auth: {
    user: process.env.MAIL_USER,
    pass: process.env.MAIL_PASSWORD,
  },
  tls: {
    rejectUnauthorized: false,
  },
});
Enter fullscreen mode Exit fullscreen mode

Now we create the asynchronous function sendOptInMail(). We build the activation link from the user ID and the token, as we defined it in the /verify route.

For the e-mail we specify some parameters:

  • from: Sender’s address
  • to: Recipient address
  • subject: Subject
  • text: Text content of the e-mail (if the mail client does not support HTML)
  • html: HTML content of the e-mail

The most important thing is that we embed the activation link in the email so that the user can use it to activate their account.

// lib/mailer.js
module.exports = {
  async sendOptInMail(email, userID, token) {
    let activationLink = `${process.env.BASE_URL}api/verify/${userID}/${token}`;
    let mail = {
      from: process.env.SENDER_MAIL,
      to: email,
      subject: "Please active your account",
      text: `To activate your account, please click this link: ${activationLink}`,
      html: `<p>To activate your account, please click this link: <a href="${activationLink}">${activationLink}</a></p>`,
    };
    await transporter.sendMail(mail);
  },
};
Enter fullscreen mode Exit fullscreen mode

At the end the configured e-mail is sent via transporter.sendMail(mail);.

9. Test Double Opt-In

All ready! Now you can test your RestAPI. You can use Postman for this. Also try if you can’t register again, if you can’t log in if the account is not confirmed and if the confirmation link works.

If everything works out: Congratulations & happy coding! 🙂

Top comments (0)