Verifying phone numbers and sending SMS messages with Twilio
In our web applications, we often need to send messages to our users. Doing that through email is enough in a lot of cases, but we can also use SMS. In this…
In our web applications, we often need to send messages to our users. Doing that through email is enough in a lot of cases, but we can also use SMS. In this article, we look into how we can use Twilio for verifying phone numbers provided by our users and sending messages. Setting up Twilio First, we need to create a Twilio account. It is a straightforward process that doesn’t require us to provide a credit card number. After creating the Twilio account, we need to set up a service. In Twilio, a service acts as a set of common configurations used to perform phone number verification. To define a service, we need to go to the services dashboard. When choosing a name for the service, remember that our users can see it. The crucial part of the above process is the service id. We need to use it along with the account sid and auth token that we can find in the console. 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({ TWILIO_ACCOUNT_SID: Joi.string().required(), TWILIO_AUTH_TOKEN: Joi.string().required(), TWILIO_VERIFICATION_SERVICE_SID: Joi.string().required() // ... }) }), ], // ... }) export class AppModule {} .env TWILIO_ACCOUNT_SID=... TWILIO_AUTH_TOKEN=... TWILIO_VERIFICATION_SERVICE_SID=... # ... Using Twilio with Node.js To use Twilio with Node.js, we can use the official Twilio library. It comes with all necessary TypeScript declarations built-in.npm install twilio Make sure to install the correct library. A few months ago a malicious package called twilio-npm was published that aimed to compromise the machines of people who downloaded it. If you want to know more, check out this article. Let’s create the SmsService that uses the above library along with our environment variables. sms.service.ts import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Twilio } from 'twilio'; import { UsersService } from '../users/users.service'; @Injectable() export default class SmsService { private twilioClient: Twilio; constructor( private readonly configService: ConfigService ) { const accountSid = configService.get('TWILIO_ACCOUNT_SID'); const authToken = configService.get('TWILIO_AUTH_TOKEN'); this.twilioClient = new Twilio(accountSid, authToken); } } Verifying phone numbers For us, the first step in verifying phone numbers is adding additional fields in the User entity. user.entity.ts import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; @Entity() class User { @PrimaryGeneratedColumn() public id: number; @Column({ unique: true }) public email: string; @Column() public phoneNumber: string; @Column({ default: false }) public isPhoneNumberConfirmed: boolean; // ... } export default User;It is crucial for the phoneNumber to be in the right format. The Twilio documentation suggests a regular expression that we can use. register.dto.ts import { IsEmail, IsString, IsNotEmpty, MinLength, Matches } from 'class-validator'; export class RegisterDto { @IsEmail() email: string; @IsString() @IsNotEmpty() name: string; @IsString() @IsNotEmpty() @MinLength(7) password: string; @IsString() @IsNotEmpty() @Matches(/^\+[1-9]\d{1,14}$/) phoneNumber: string; } export default RegisterDto; If we would like to be more strict with the phone number validation, we could use the Lookup API that Twilio provides. With it, we can make a request to the Twilio API every time our users set a phone number and check if it is valid. Remember that every request we make to the Lookup API costs a little. Initiating the SMS verification Let’s add a function to our SmsService that can initiate SMS verification. sms.service.ts import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Twilio } from 'twilio'; import { UsersService } from '../users/users.service'; @Injectable() export default class SmsService { private twilioClient: Twilio; constructor( private readonly configService: ConfigService, private readonly usersService: UsersService ) { const accountSid = configService.get('TWILIO_ACCOUNT_SID'); const authToken = configService.get('TWILIO_AUTH_TOKEN'); this.twilioClient = new Twilio(accountSid, authToken); } initiatePhoneNumberVerification(phoneNumber: string) { const serviceSid = this.configService.get('TWILIO_VERIFICATION_SERVICE_SID'); return this.twilioClient.verify.services(serviceSid) .verifications .create({ to: phoneNumber, channel: 'sms' }) } }Let’s also create a SmsController that uses it. sms.controller.ts import { Controller, UseGuards, UseInterceptors, ClassSerializerInterceptor, Post, Req, BadRequestException, } from '@nestjs/common'; import SmsService from './sms.service'; import JwtAuthenticationGuard from '../authentication/jwt-authentication.guard'; import RequestWithUser from '../authentication/requestWithUser.interface'; @Controller('sms') @UseInterceptors(ClassSerializerInterceptor) export default class SmsController { constructor( private readonly smsService: SmsService ) {} @Post('initiate-verification') @UseGuards(JwtAuthenticationGuard) async initiatePhoneNumberVerification(@Req() request: RequestWithUser) { if (request.user.isPhoneNumberConfirmed) { throw new BadRequestException('Phone number already confirmed'); } await this.smsService.initiatePhoneNumberVerification(request.user.phoneNumber); } }Requesting the above endpoint results in Twilio sending the SMS to the user. Twilio figures out the language based on the country code in the phone number. We could override it by using the locale property:initiatePhoneNumberVerification(phoneNumber: string) { const serviceSid = this.configService.get('TWILIO_VERIFICATION_SERVICE_SID'); return this.twilioClient.verify.services(serviceSid) .verifications .create({ to: phoneNumber, channel: 'sms', locale: 'en' }) } Confirming the verification code Now, we need to create a way for the users to send the verification code back to our API. To do that, let’s create an additional method in our SmsService: sms.service.ts import { BadRequestException, Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Twilio } from 'twilio'; import { UsersService } from '../users/users.service'; @Injectable() export default class SmsService { private twilioClient: Twilio; constructor( private readonly configService: ConfigService, private readonly usersService: UsersService ) { const accountSid = configService.get('TWILIO_ACCOUNT_SID'); const authToken = configService.get('TWILIO_AUTH_TOKEN'); this.twilioClient = new Twilio(accountSid, authToken); } async confirmPhoneNumber(userId: number, phoneNumber: string, verificationCode: string) { const serviceSid = this.configService.get('TWILIO_VERIFICATION_SERVICE_SID'); const result = await this.twilioClient.verify.services(serviceSid) .verificationChecks .create({to: phoneNumber, code: verificationCode}) if (!result.valid || result.status !== 'approved') { throw new BadRequestException('Wrong code provided'); } await this.usersService.markPhoneNumberAsConfirmed(userId) } // ... }You might notice that we use the usersService.markPhoneNumberAsConfirmed method above. We first need to define it. users.service.ts 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