6 min read
Original source

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…

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. If you want to know more about JWT, check out API with NestJS #3. Authenticating users with bcrypt, Passport, JWT, and cookies 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. Sending the verification link To be able to send the verification link, we need to set up Nodemailer. We’ve already created the EmailService that does that in the 25th part of this series. 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, ) {} async markEmailAsConfirmed(email: string) { return this.usersRepository.update({ email }, { isEmailConfirmed: true }); } // ... } Resending the confirmation link 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.

Confirming the email address | NestJS.io