8 min read

Confirming the email address

In a lot of web applications, emails play a significant role. If we create an online ordering system, we need to be confident that our users get a confirmation…

July 12, 2021

In a lot of web applications, emails play a significant role. If we create an online ordering system, we need to be confident that our users get a confirmation email. When our services include a mailing list, we want to make sure that the provided email is valid. We also might want to implement the password resetting feature, for which the email address is essential. Requiring our users to confirm the email address might also serve as an additional layer of security against bots. Therefore, in this article, we look into confirming the email addresses.

Confirming the email address#

First, we need a way to store the information about whether the email is confirmed. To do that, let’s expand on the entity of the user.

user.entity.ts#
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"
@Entity()
class User {
  @PrimaryGeneratedColumn()
  public id: number
  @Column({ unique: true })
  public email: string
  @Column({ default: false })
  public isEmailConfirmed: boolean // ...
}
export default User

To confirm the email address, we aim to send an email message with an URL containing the JWT. To do that, we need additional environment variables.

app.module.ts#
import { Module } from "@nestjs/common"
import { ConfigModule } from "@nestjs/config"
import * as Joi from "@hapi/joi"
@Module({
  imports: [
    ConfigModule.forRoot({
      validationSchema: Joi.object({
        JWT_VERIFICATION_TOKEN_SECRET: Joi.string().required(),
        JWT_VERIFICATION_TOKEN_EXPIRATION_TIME: Joi.string().required(),
        EMAIL_CONFIRMATION_URL: Joi.string().required(), // ...
      }),
    }), // ...
  ],
  controllers: [],
})
export class AppModule {}

We’ve already used JWT in other parts of this series. To increase security, we want to use a different secret token to encode and decode JWT for email verification. We also want the token to expire after a few hours in case the email account of our user gets hijacked.

.env#
JWT_VERIFICATION_TOKEN_SECRET=7AnEd5epXmdaJfUrokkQ
JWT_VERIFICATION_TOKEN_EXPIRATION_TIME=21600
EMAIL_CONFIRMATION_URL=https://my-app.com/confirm-email

Above, we define the expiration time in seconds.

To be able to send the verification link, we need to set up Nodemailer. We’ve already created the EmailService that does that. Let’s reuse it in a new service that manages email confirmation.

emailConfirmation.service.ts#
import { Injectable } from "@nestjs/common"
import { JwtService } from "@nestjs/jwt"
import { ConfigService } from "@nestjs/config"
import VerificationTokenPayload from "./verificationTokenPayload.interface"
import EmailService from "../email/email.service"
import { UsersService } from "../users/users.service"
@Injectable()
export class EmailConfirmationService {
  constructor(
    private readonly jwtService: JwtService,
    private readonly configService: ConfigService,
    private readonly emailService: EmailService,
  ) {}
  public sendVerificationLink(email: string) {
    const payload: VerificationTokenPayload = { email }
    const token = this.jwtService.sign(payload, {
      secret: this.configService.get("JWT_VERIFICATION_TOKEN_SECRET"),
      expiresIn: `${this.configService.get("JWT_VERIFICATION_TOKEN_EXPIRATION_TIME")}s`,
    })
    const url = `${this.configService.get("EMAIL_CONFIRMATION_URL")}?token=${token}`
    const text = `Welcome to the application. To confirm the email address, click here: ${url}`
    return this.emailService.sendMail({
      to: email,
      subject: "Email confirmation",
      text,
    })
  }
}
verificationTokenPayload.interface.ts#
interface VerificationTokenPayload {
  email: string
}
export default VerificationTokenPayload

Let’s modify our AuthenticationController and use the above service.

authenticationController.ts#
import { Body, Controller, Post, ClassSerializerInterceptor, UseInterceptors } from "@nestjs/common"
import { AuthenticationService } from "./authentication.service"
import RegisterDto from "./dto/register.dto"
import { UsersService } from "../users/users.service"
import { EmailConfirmationService } from "../emailConfirmation/emailConfirmation.service"
@Controller("authentication")
@UseInterceptors(ClassSerializerInterceptor)
export class AuthenticationController {
  constructor(
    private readonly authenticationService: AuthenticationService,
    private readonly usersService: UsersService,
    private readonly emailConfirmationService: EmailConfirmationService,
  ) {}
  @Post("register")
  async register(@Body() registrationData: RegisterDto) {
    const user = await this.authenticationService.register(registrationData)
    await this.emailConfirmationService.sendVerificationLink(registrationData.email)
    return user
  } // ...
}

Now, as soon as users sign in, they receive a link through email.

Feel free to make the contents of the email more refined.

Confirming the email address#

Once the user goes to the link above, our frontend application needs to get the token from the URL and send it to our API. To do support that, we need to create an endpoint for it.

emailConfirmation.controller.ts#
import {
  Controller,
  ClassSerializerInterceptor,
  UseInterceptors,
  Post,
  Body,
  UseGuards,
  Req,
} from "@nestjs/common"
import ConfirmEmailDto from "./confirmEmail.dto"
import { EmailConfirmationService } from "./emailConfirmation.service"
import JwtAuthenticationGuard from "../authentication/jwt-authentication.guard"
import RequestWithUser from "../authentication/requestWithUser.interface"
@Controller("email-confirmation")
@UseInterceptors(ClassSerializerInterceptor)
export class EmailConfirmationController {
  constructor(private readonly emailConfirmationService: EmailConfirmationService) {}
  @Post("confirm")
  async confirm(@Body() confirmationData: ConfirmEmailDto) {
    const email = await this.emailConfirmationService.decodeConfirmationToken(
      confirmationData.token,
    )
    await this.emailConfirmationService.confirmEmail(email)
  }
}
confirmEmail.dto.ts#
import { IsString, IsNotEmpty } from "class-validator"
export class ConfirmEmailDto {
  @IsString()
  @IsNotEmpty()
  token: string
}
export default ConfirmEmailDto

Above, a few notable things are happening. We expect the frontend application to send the token from the URL in the request body back to the API. We then decode it and confirm the email.

emailConfirmation.service.ts#
import { BadRequestException, Injectable } from "@nestjs/common"
import { JwtService } from "@nestjs/jwt"
import { ConfigService } from "@nestjs/config"
import EmailService from "../email/email.service"
import { UsersService } from "../users/users.service"
@Injectable()
export class EmailConfirmationService {
  constructor(
    private readonly jwtService: JwtService,
    private readonly configService: ConfigService,
    private readonly emailService: EmailService,
    private readonly usersService: UsersService,
  ) {}
  public async confirmEmail(email: string) {
    const user = await this.usersService.getByEmail(email)
    if (user.isEmailConfirmed) {
      throw new BadRequestException("Email already confirmed")
    }
    await this.usersService.markEmailAsConfirmed(email)
  }
  public async decodeConfirmationToken(token: string) {
    try {
      const payload = await this.jwtService.verify(token, {
        secret: this.configService.get("JWT_VERIFICATION_TOKEN_SECRET"),
      })
      if (typeof payload === "object" && "email" in payload) {
        return payload.email
      }
      throw new BadRequestException()
    } catch (error) {
      if (error?.name === "TokenExpiredError") {
        throw new BadRequestException("Email confirmation token expired")
      }
      throw new BadRequestException("Bad confirmation token")
    }
  } // ...
}

Please notice, that we throw an error if the email is already confirmed. Therefore, our JWT can’t be used more than once.

If the confirmEmail method, we use the UsersService to mark the email as confirmed. We need to implement this functionality.

import { Injectable } from "@nestjs/common"
import { InjectRepository } from "@nestjs/typeorm"
import { Repository } from "typeorm"
import User from "./user.entity"
@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) {}
  async markEmailAsConfirmed(email: string) {
    return this.usersRepository.update(
      { email },
      {
        isEmailConfirmed: true,
      },
    )
  } // ...
}

Since we set an expiration time for our tokens, the user might not use the token on time. Therefore, we should implement a feature of resending the link.

emailConfirmation.controller.ts#
import {
  Controller,
  ClassSerializerInterceptor,
  UseInterceptors,
  Post,
  UseGuards,
  Req,
} from "@nestjs/common"
import { EmailConfirmationService } from "./emailConfirmation.service"
import JwtAuthenticationGuard from "../authentication/jwt-authentication.guard"
import RequestWithUser from "../authentication/requestWithUser.interface"
@Controller("email-confirmation")
@UseInterceptors(ClassSerializerInterceptor)
export class EmailConfirmationController {
  constructor(private readonly emailConfirmationService: EmailConfirmationService) {}
  @Post("resend-confirmation-link")
  @UseGuards(JwtAuthenticationGuard)
  async resendConfirmationLink(@Req() request: RequestWithUser) {
    await this.emailConfirmationService.resendConfirmationLink(request.user.id)
  } // ...
}

A thing worth noting is that we require the user to authenticate before resending the confirmation link. Thanks to that, users can’t require email confirmation for other people.

emailConfirmation.service.ts#
import { BadRequestException, Injectable } from "@nestjs/common"
import { UsersService } from "../users/users.service"
@Injectable()
export class EmailConfirmationService {
  constructor(private readonly usersService: UsersService) {}
  public async resendConfirmationLink(userId: number) {
    const user = await this.usersService.getById(userId)
    if (user.isEmailConfirmed) {
      throw new BadRequestException("Email already confirmed")
    }
    await this.sendVerificationLink(user.email)
  } // ...
}

A crucial thing to notice security-wise is that sending a new confirmation link doesn’t invalidate the previous links. If we would like to achieve that, we could, for example, store the most recent confirmation token in the database and check it before confirming.

Requiring the email address to be confirmed#

Depending on the use case, we might want to prevent the user from accessing certain endpoints if the user didn’t confirm the email. To do that, we can create an additional guard.

emailConfirmation.guard.ts#
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from "@nestjs/common"
import RequestWithUser from "../authentication/requestWithUser.interface"
@Injectable()
export class EmailConfirmationGuard implements CanActivate {
  canActivate(context: ExecutionContext) {
    const request: RequestWithUser = context.switchToHttp().getRequest()
    if (!request.user?.isEmailConfirmed) {
      throw new UnauthorizedException("Confirm your email first")
    }
    return true
  }
}

For our guard to work, we need to attach it to an endpoint.

creditCards.controller.ts#
import { Controller, Req, UseGuards, Get } from "@nestjs/common"
import JwtAuthenticationGuard from "../authentication/jwt-authentication.guard"
import RequestWithUser from "../authentication/requestWithUser.interface"
import StripeService from "../stripe/stripe.service"
import { EmailConfirmationGuard } from "../emailConfirmation/emailConfirmation.guard"
@Controller("credit-cards")
export default class CreditCardsController {
  constructor(private readonly stripeService: StripeService) {}
  @Get()
  @UseGuards(EmailConfirmationGuard)
  @UseGuards(JwtAuthenticationGuard)
  async getCreditCards(@Req() request: RequestWithUser) {
    return this.stripeService.listCreditCards(request.user.stripeCustomerId)
  } // ...
}

In Typescript, decorators resolve from bottom to top. In our implementation, the EmailConfirmationGuard requires the request.user object to work properly. Because of that, the crucial thing is to first use the EmailConfirmationGuard, then apply the JwtAuthenticationGuard.

Summary#

In this article, we’ve implemented the feature of confirming email addresses. To do that, we had to send emails containing JWT.  We’ve also created a NestJS guard that we can use to block certain endpoints if the user didn’t confirm the email. With it, we’ve added a feature that might prove to be useful if we depend on email messaging a lot in our application.

Confirming the email address | NestJS.io