How to setup email verification in Feathers.js

ivanzm123

Ivan Zaldivar

Posted on September 2, 2021

How to setup email verification in Feathers.js

Surely you want to send a verification email to the user when they have registered in your app. In this post, you will learn how to do it using Feathers.js

In this post we will take for granted, that you have knowledges about the framework. If not, visit the following link https://docs.feathersjs.com/guides for more information.

Let's go!

1. First steps.

We create a folder.
mkdir feathers-email-verification

Enter you folder
cd feathers-email-verification

We create an app with feathers-cli
feathers generate app

Select the following options

Note: For use feathers-cli is necessary have it installed. visit this link https://docs.feathersjs.com/guides/basics/generator.html#generating-the-application

Now to verify that installation has been successful. We run the MongoDB database server, we run our development server with the command npm run start

Now if we visit http://localhost:3030 we will see the following

Perfect, now we are going to register a user and then login.

Create a new user

If the user has been created successfully, we will get the following response.

Authenticate user

If the credentials have been correct and everything went well, we will get the following answer.

Wonderful, everything has been correct. It is time to implement the email delivery system.

How it all works.

Before entering with the configuration, it is necessary to understand its operation. What we are going to create is a flow to have the user verify their email address. This goes as follows:

  1. The user create an account in the application of feathers.
  2. The server add field isVerified to the object user in the database and set to false
  3. The server create a token of verification for the user.
  4. The use gets an email that contains a client link with the token as a parameter.
  5. The user clicks the link and, upon visiting the client, this token is sent back to the server.
  6. The server sets the field isVerified on the user object to true
  7. Everything is over.

Excellent, it's all you need to know. Now it's time to get to work.

Setup the email service.

Well, let's create a service that allows us to send email.

But first you need to download the package. So we install nodemailer.
npm i nodemailer

We install the types.
npm i @types/nodemailer

What we will do is create a mails folder, this folder will contain 3 files:

  • mails.class.ts: You will have the logic to send emails, here we initialize nodemailer
  • mails.hooks.ts: This file will contain all of our hooks. If you don't know what hooks are in feathers, visit the following link: Feathers hooks
  • mails.service.ts: Here we register the service, what will its path be, add the hooks, etc.

Well, we create to mails.class.ts

src > services > mails > mails.class.ts

import { Application } from "@feathersjs/express";
import { ServiceMethods } from "@feathersjs/feathers";
import { createTransport, Transporter, SendMailOptions } from "nodemailer";

export class Mails implements Partial<ServiceMethods<SendMailOptions>> {
    private transporter: Transporter;

    constructor(app: Application) {
        // We initialize the transporter.
        this.transporter = createTransport(app.get("mailer"));
    }

    /**
     * We send the email.
     * @param data 
     * @returns 
     */
    async create(data: Partial<SendMailOptions>): Promise<any> {
        return await this.transporter.sendMail(data);
    }
}

Enter fullscreen mode Exit fullscreen mode

Now we create the mails.hooks.ts file

src > services > mails > mails.hooks.ts

import { HooksObject } from "@feathersjs/feathers";

const hooks: HooksObject = {
    before: {},
    after: {},
    error: {}
};

export default hooks;
Enter fullscreen mode Exit fullscreen mode

Now we create the mails.service.ts file

src > services > mails > mails.service.ts

import { Application } from "@feathersjs/express";
import { Service } from "@feathersjs/feathers";
import { SendMailOptions } from "nodemailer";

import hooks from "./mails.hooks";

import { Mails } from "./mails.class";

export default function (app: Application): void {
    // Register service.
    app.use("mails", new Mails(app));

    // Get mail service.
    const service: Service<SendMailOptions> = app.service("mails");

    // Register hooks in the service.
    service.hooks(hooks);
}
Enter fullscreen mode Exit fullscreen mode

Great! Now that everything is almost done, it is necessary to import this service into the global services of the application so that it is available throughout the app.

src > services > index.ts

import { Application } from '../declarations';

import users from './users/users.service';

// We import the mail service.
import mails from "./mails/mails.service";

export default function (app: Application): void {
  // We expose the service to the rest of the application.
  app.configure(mails);
  app.configure(users);
}

Enter fullscreen mode Exit fullscreen mode

As you may have already noticed, we have an error, this is because in our environment variables file defaul.json it does not find the mailer property, what we have to do is add it and be all set.

Note: By the way, do not expose the user and pass since this is confidential content, I did it for didactic reasons. But in the same way I have already reset the credentials.

We start the development server again with npm run dev and error will be gone.

To the check that everything is correct, we will send this request.

If everything went well, we should see this in mailtrap.

If you wonder what service he is using to test the sending of emails, that is nothing more and nothing less than mailtrap

Obviously we do not want our mailer to be misused for spam or something, so after testing we are going to close it off by adding a before hook on the all mailer routes. For this we install the feathers-hooks-common

src > services > mails > mails.hooks.ts

import { HooksObject } from "@feathersjs/feathers";
import { disallow } from "feathers-hooks-common";

const hooks: HooksObject = {
    before: {
        // Reject all requests that come from outside.
        all: [disallow("external")]
    },
    after: {},
    error: {}
};

export default hooks;
Enter fullscreen mode Exit fullscreen mode

To check if it rejects the requests, we send email again, as we did in the previous step, and this should give us an error.

Wonderful, we have finished with the configuration of sending emails.

Setting up the Feathers-Authentication-Managment module.

Now we are going to set up the feathers-authentication-management module. First let’s install it.

npm i feathers-authentication-management-ts

Once installed, we are going to create a folder with the name authmanagement inside the services folder. This folder will have the following structure.

index.strategies.ts
This file will contains all strategies. The strategies that will be present are verificationEmail and verifiedEmail. Copy the following content.

src > services > authmanagement > strategies > index.strategies.ts

import { User, Types } from "feathers-authentication-management-ts";
import { SendMailOptions } from "nodemailer";

export interface MailOptions extends Partial<SendMailOptions> {
    user: User;
    token?: string;
    domain: string;
}

export interface ParamsLink {
    type?: string;
    token?: string;
    domain: string;
}

export type StrategiesAuthManagement = Record<
    Types,
    (options: MailOptions) => MailOptions
>;

export function generateLink(data: ParamsLink): string {
    const { domain, type, token } = data;
    return `${ domain }/${ type }?token=${ token }`;
}

export function verificationEmail(options: MailOptions): MailOptions {
    const { token, domain } = options;
    const link: string = generateLink({ token, domain, type: "verifyEmail" });

    return {
        ...options,
        subject: "Email Verification",
        text: "Feathers welcomes you, check your email to access our services 📧",
        html: `
            <h1>Thanks for registering 🥰</h1>
            <p>Verify your email and everything will be ready.</p>
            <a href="${ link }">Verify your email</a>
        `
    };
}

export function confirmationEmail(options: MailOptions): MailOptions {
    const html: string = `
        <h1>Your email has been verified</h1>
        <p>Great, now that your account is verified. It is time to get down to work.</p>
    `;

    return {
        ...options,
        subject: "Verified Email",
        text: "Congratulations! Your email has been verified 🏆",
        html
    };
}

export const strategies: Partial<StrategiesAuthManagement> = {
    resendVerifySignup: verificationEmail,
    verifySignup: confirmationEmail
}

Enter fullscreen mode Exit fullscreen mode

authmanagement.controller.ts
This file will contain all the logic of how to obtain the strategies according to their types, it will be responsible for interacting with the email service. That we had previously configured. Copy the following content.

src > services > authmanagement > authmanagement.controller.ts

import { SendMailOptions } from "nodemailer";
import { Application } from "@feathersjs/express";
import { MethodNotAllowed } from "@feathersjs/errors";
import { Service } from "@feathersjs/feathers";
import { Options, Types, User } from "feathers-authentication-management-ts";

import { strategies, MailOptions } from "./strategies/index.strategies";

export default function (app: Application): Partial<Options> {
    return {
        notifier(types: Types, user: User): void {
            // Get strategy by types.
            const strategy = strategies[types];

            // Check if the strategy exists.
            if (typeof strategy !== "function") throw new MethodNotAllowed({
                name: "StrategyNotAllowed",
                message: `The <${types}> strategy has not been implemented`
            });

            // Get email service.
            const email: Service<SendMailOptions> = app.service("mails");

            // Set payload.
            const payload: MailOptions = strategy({
                from: app.get("email_domain"),
                to: user.email,
                token: user.verifyToken,
                domain: app.get("domain"),
                user
            });

            // Dispatch email.
            email.create(payload)
                .then(() => console.log("Sent email successfully"))
                .catch(console.error)
        }
    };
}

Enter fullscreen mode Exit fullscreen mode

Note: If the url of your users service is different from users for example /api/users, it is important that you add the url of the users service that you assigned, since by default it will look in users

src > services > authmanagement > authmanagement.controller.ts

export default function (): Partial<Options> {
    return {
        // Name of the user service.
        service: "<Your name user service>",
        // Notifier.
        notifier(type: Types, user: User) {}
    }
}
Enter fullscreen mode Exit fullscreen mode

authmanagement.hooks.ts
It contains all the hooks of the service. Copy the following content.

src > services > authmanagement > authmanagement.hooks.ts

import { HooksObject } from "@feathersjs/feathers";

const hooks: HooksObject = {
    before: {},
    after: {},
    error: {}
}

export default hooks;

Enter fullscreen mode Exit fullscreen mode

authmanagement.service.ts
You will be responsible for registering the service. Copy the following content.

src > services > authmanagement > authmanagement.service.ts

import { Application } from "@feathersjs/express";
import { Service } from "@feathersjs/feathers";
import authmanagement from "feathers-authentication-management-ts";

import hooks from "./authmanagement.hooks";

import controller from "./authmanagement.controller";

export default function (app: Application): void {
    // Initialize service.
    app.configure(authmanagement(controller(app)));

    // Get service.
    const service: Service<any> = app.service("authManagement");

    // Add hooks.
    service.hooks(hooks);
}
Enter fullscreen mode Exit fullscreen mode

Now add the authmanagement service to the global services.

src > services > index.ts

import authmanagement from "./authmanagement/authmanagement.service";

export default function (app: Application): void {
  // Configure my auth management.
  app.configure(authmanagement);
  // More services...
}

Enter fullscreen mode Exit fullscreen mode

Note: If you use any ORMs like Mongoose or Sequelize you need to add the verification fields manually to the user model. These need to be the following fields.


const schema = new Schema({
    // More properties...
    isVerified: { type: Boolean },
    verifyToken: { type: String },
    verifyExpires: { type: Date },
    verifyChanges: { type: Object },
    resetToken: { type: String },
    resetExpires: { type: Date }
});
Enter fullscreen mode Exit fullscreen mode

Finally, we need to add two after create hooks to our user model. One to call our notifier function and one to remove the verification again. That looks like this.

src > services > users > users.hooks.ts

import * as feathersAuthentication from '@feathersjs/authentication';
import * as local from '@feathersjs/authentication-local';
import { Application } from '@feathersjs/express';
import { HooksObject } from '@feathersjs/feathers';
import { BadRequest } from "@feathersjs/errors";
import authmanagement from "feathers-authentication-management-ts";

import notifier from "../authmanagement/authmanagement.controller";

const { authenticate } = feathersAuthentication.hooks;
const { hashPassword, protect } = local.hooks;

const hooks: HooksObject = {
  before: {
    all: [],
    find: [ authenticate('jwt') ],
    get: [ authenticate('jwt') ],
    create: [
      hashPassword('password'),
      // Sets values to some properties of the users model.
      authmanagement.hooks.addVerification()
    ],
    update: [ hashPassword('password'),  authenticate('jwt') ],
    patch: [ hashPassword('password'),  authenticate('jwt') ],
    remove: [ authenticate('jwt') ]
  },

  after: {
    all: [ protect('password') ],
    create: [
      ({ app, result }) => {
        const sender = notifier(app as Application);

        if (typeof sender.notifier !== "function") throw new BadRequest({
          name: "EmailNotSupported",
          message: "Sending emails not supported"
        });

        sender.notifier("resendVerifySignup", result);
      },
      // Protects sensitive properties before they are shipped to the customer.
      authmanagement.hooks.removeVerification()
    ]
  },
  error: {}
};

export default hooks;
Enter fullscreen mode Exit fullscreen mode

Securing the application.

Now that the app works there is only one step to complete and that is adding some security to the users service. Since we have a nice authentication flow running we don’t want any users to meddle with the user service directly anymore. For this we create two before hooks. One on the update method and one on the patch method. With the one on the update method we are going to disallow this method in its entirety. After all, we wouldn’t want someone to be able to replace our carefully verified user by a new one. The one on the patch method we want to restrict the user from touching any of the authentication field methods directly. To do this we update the user before hooks to:

src > services > users > users.hooks.ts

import { HooksObject } from '@feathersjs/feathers';
import feathersCommon from "feathers-hooks-common";

const hooks: HooksObject = {
  before: {
    update: [
      feathersCommon.disallow("external")
    ],
    patch: [
      feathersCommon.iff(
        feathersCommon.isProvider('external'),
        feathersCommon.preventChanges(true,
          'email',
          'isVerified',
          'verifyToken',
          'verifyShortToken',
          'verifyExpires',
          'verifyChanges',
          'resetToken',
          'resetShortToken',
          'resetExpires'
        )
      )
    ]
  }
};

export default hooks;

Enter fullscreen mode Exit fullscreen mode

This is the final result.

src > services > users > users.hooks.ts

import * as feathersAuthentication from '@feathersjs/authentication';
import * as local from '@feathersjs/authentication-local';
import { Application } from '@feathersjs/express';
import { HooksObject } from '@feathersjs/feathers';
import { BadRequest } from "@feathersjs/errors";
import authmanagement from "feathers-authentication-management-ts";
import feathersCommon from "feathers-hooks-common";

import notifier from "../authmanagement/authmanagement.controller";

const { authenticate } = feathersAuthentication.hooks;
const { hashPassword, protect } = local.hooks;

const hooks: HooksObject = {
  before: {
    all: [],
    find: [ authenticate('jwt') ],
    get: [ authenticate('jwt') ],
    create: [
      hashPassword('password'),
      // Sets values to some properties of the users model.
      authmanagement.hooks.addVerification()
    ],
    update: [
      hashPassword('password'),
      authenticate('jwt'),
      feathersCommon.disallow("external")
    ],
    patch: [
      feathersCommon.iff(
        feathersCommon.isProvider('external'),
        feathersCommon.preventChanges(true,
          'email',
          'isVerified',
          'verifyToken',
          'verifyShortToken',
          'verifyExpires',
          'verifyChanges',
          'resetToken',
          'resetShortToken',
          'resetExpires'
        )
      ),
      hashPassword('password'),
      authenticate('jwt')
    ],
    remove: [ authenticate('jwt') ]
  },

  after: {
    all: [ protect('password') ],
    create: [
      ({ app, result }) => {
        const sender = notifier(app as Application);

        if (typeof sender.notifier !== "function") throw new BadRequest({
          name: "EmailNotSupported",
          message: "Sending emails not supported"
        });

        sender.notifier("resendVerifySignup", result);
      },
      // Protects sensitive properties before they are shipped to the customer.
      authmanagement.hooks.removeVerification()
    ]
  },
  error: {}
};

export default hooks;

Enter fullscreen mode Exit fullscreen mode

To check that everything is fine, it is time to register a user.

If we go to mailtrap we will look at something like this.

Excellent, we are done with the server configuration. In another post, we will build the client part, using Vuejs and Angular.

I leave you the project repository: https://github.com/IvanZM123/feathers-email-verification

Follow me on social networks.

💖 💪 🙅 🚩
ivanzm123
Ivan Zaldivar

Posted on September 2, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related