Frequently Asked Questions
Use the AWS SDK for JavaScript v3 (@aws-sdk/client-sns) to interact with the SNS service. Configure the SNSClient with your AWS credentials and region. The sendSms function in the provided snsService.js example demonstrates how to publish SMS messages using the PublishCommand, including setting the message type (Transactional or Promotional).
The AWS SDK for JavaScript v3 (@aws-sdk/client-sns) is the recommended way to integrate with AWS SNS in Node.js projects. It offers a modular architecture, improved performance, and better security compared to previous versions.
Transactional SMS messages have higher delivery priority and are more likely to reach users, even those on Do Not Disturb (DND) lists. This makes them ideal for time-sensitive messages like OTPs, ensuring users receive their codes promptly, though they may cost slightly more than Promotional messages.
You should move your AWS account out of the SNS sandbox when you're ready to send SMS messages to unverified phone numbers and need higher spending limits. While in the sandbox, you can only send SMS to verified numbers and have restricted spending.
No, an in-memory store like a JavaScript Map is not suitable for production OTP storage. Data will be lost if the server restarts. Use a persistent store like Redis or DynamoDB for production environments to maintain OTP data reliably.
The provided otpService.js example uses crypto.randomInt to generate secure OTPs of a configurable length (OTP_LENGTH). It pads the OTP with leading zeros to maintain a consistent format.
You need an AWS account, Node.js and npm installed, basic understanding of Node.js, Express, and REST APIs, and access to a phone number for testing.
The verifyOtp function in otpService.js handles OTP verification. It retrieves the stored OTP, checks expiry and attempts remaining. If the submitted OTP matches the stored value, it marks the OTP as used. If the attempt is unsuccessful the number of allowed attempts is decreased.
Rate limiting is crucial to prevent SMS pumping fraud and brute-force attacks. It limits the number of OTP requests and verification attempts from a given IP address within a specified time window.
E.164 is an international standard for phone number formatting, ensuring consistent representation of numbers. It's required by AWS SNS for sending SMS messages and helps avoid formatting issues.
Use keys like otp:{phoneNumber} to store OTP data in Redis. Store data as a JSON string or Redis Hash including the OTP, attempts remaining, and set a TTL for automatic expiry.
Use short expiry times, CSPRNG for generation, avoid logging OTPs in production, ensure single-use, enforce E.164 format, and implement rate limiting.
The snsService.js example includes checks for specific SNS errors like InvalidParameterException and AuthorizationErrorException, providing clearer feedback if sending fails.
Redis is recommended due to its speed and TTL features, which align well with OTP requirements. A SQL database with proper indexing and cleanup mechanisms could also be used if you need more structured data storage.
Implement robust OTP/2FA in Node.js with Express and AWS SNS
One-Time Passwords (OTPs) sent via SMS are a common and effective way to add a second factor of authentication (2FA) or perform phone number verification in applications. This guide provides a complete walkthrough for building a secure and reliable OTP system using Node.js, the Express framework, and Amazon Simple Notification Service (AWS SNS) for SMS delivery.
We will build a simple REST API with two endpoints: one to request an OTP sent to a provided phone number and another to verify the submitted OTP. This guide covers everything from initial AWS setup and project configuration to implementing core logic, security best practices, error handling, and deployment considerations. By the end, you'll have a functional OTP service ready for integration into your applications.
Project overview and goals
Goal: To create a backend service using Node.js and Express that can:
Problem Solved: This addresses the need for phone number verification or adding a second authentication factor (2FA) to enhance application security, using widely adopted and scalable cloud services.
Technologies:
@aws-sdk/client-sns
): To interact with AWS services programmatically using the recommended modular client.Architecture:
Prerequisites:
Setting up the project
Let's initialize our Node.js project and install the necessary dependencies.
Create Project Directory: Open your terminal and create a new directory for the project, then navigate into it.
Initialize Node.js Project: This creates a
package.json
file.Install Dependencies: We need Express for the server, the modular AWS SDK v3 client for SNS, and dotenv for environment variables.
express
: Web framework.@aws-sdk/client-sns
: AWS SDK v3 modular client for SNS. This is the recommended way to use the AWS SDK in Node.js.dotenv
: Loads environment variables from a.env
file.Create Project Structure: Organize the project for clarity.
src/app.js
: Express application setup (middleware, routes).src/server.js
: Starts the HTTP server.src/routes/
: Defines API endpoints.src/controllers/
: Handles request logic and interacts with services.src/services/
: Contains business logic (OTP generation/verification, SNS interaction).src/config.js
: Loads and exports configuration from environment variables..env
: Stores environment variables (AWS keys, configuration). Do not commit this file..gitignore
: Specifies files/directories Git should ignore.Configure
.gitignore
: Addnode_modules
and.env
to prevent committing them.Configure
.env
: Create a.env
file in your project root.Important Security Note: The values below are placeholders. You must replace
YOUR_AWS_ACCESS_KEY_ID
,YOUR_AWS_SECRET_ACCESS_KEY
, andYOUR_AWS_REGION
with your actual AWS credentials and chosen region. Never commit your.env
file containing real secrets to version control (like Git). Use the.gitignore
file to prevent this. For production environments, use more secure methods like AWS Secrets Manager or environment variables provided by your hosting platform.AWS_REGION
: The AWS region where you'll operate SNS (choose one that supports SMS, likeus-east-1
,eu-west-1
,ap-southeast-2
). See AWS Regions and Endpoints documentation for details.SNS_SMS_TYPE
: Set toTransactional
for higher delivery priority, especially for OTPs, even to numbers on Do Not Disturb (DND) lists (may incur slightly higher costs).Promotional
is for marketing messages.AWS setup for SNS
To send SMS messages, we need to configure AWS credentials and SNS settings.
Create an IAM User: It's best practice to create a dedicated IAM user with specific permissions rather than using root account keys.
sns-otp-service-user
).AmazonSNSFullAccess
policy. Note: For production, create a more restrictive custom policy granting onlysns:Publish
permissions.Update
.env
file: Paste the copied credentials and your chosen region into your.env
file:Configure AWS SNS Settings (AWS Console):
+12223334444
).Transactional
(recommended for OTPs) orPromotional
based on your.env
setting and use case.Implementing core functionality
Now let's write the code for OTP generation, storage, verification, and sending SMS via SNS.
Configuration Loader
OTP Storage (In-Memory)
For this guide, we'll use a simple JavaScript
Map
as an in-memory store. Remember: This is not suitable for production as data is lost on server restart. Use Redis, DynamoDB, or a similar persistent store in a real application. The basicsetTimeout
cleanup used here is also rudimentary and may not be perfectly reliable or efficient at scale.AWS SNS Service
Building the API layer
Now_ let's connect the logic to Express endpoints.
OTP Controller
OTP Routes
Express App Setup
Server Entry Point
Update
package.json
start scriptAdd a start script to
package.json
for easy execution.(Note: Update dependency versions like
^3.XXX.X
inpackage.json
based on your actual installation)Implementing error handling and logging
success
flag and amessage
. We use appropriate HTTP status codes (200 for success, 400 for bad requests/invalid OTPs, 404 for not found, 429 for rate limits, 500 for server errors).console.log
,console.warn
, andconsole.error
statements. For production, use a structured logger likewinston
orpino
to log in JSON format, include request IDs, control log levels (info
,warn
,error
), and potentially ship logs to a centralized logging service (like CloudWatch Logs).snsService.js
includes specific checks forInvalidParameterException
andAuthorizationErrorException
, providing clearer error messages when sending SMS fails.app.use((err, req, res, next) => ...)
inapp.js
catches any unhandled errors that occur during request processing, logs them, and returns a generic 500 error to the client.async-retry
. However, for OTPs, automatically retrying might send multiple SMS messages, which is usually undesirable. It's often better to return an error and let the user trigger a retry manually.Database schema and data layer (Conceptual)
As mentioned, the in-memory store is unsuitable for production. If using Redis (recommended for OTPs due to speed and TTL features):
otp:{phoneNumber}
(e.g.,otp:+12223334444
).{ otp: '123456', attempts: 3 }
.config.OTP_EXPIRY_MINUTES
* 60 seconds. Redis handles automatic expiry efficiently.If using a SQL database:
Schema:
Data Layer: Use an ORM (like Sequelize, Prisma) or a query builder (like Knex.js) to interact with the table, handling insertions, lookups (finding by
phone_number
), updates (decrementingattempts_remaining
), and deletions (on successful verification or expiry). Implement a background job or scheduled task to periodically delete rows whereexpires_at
is in the past.Adding security features
Input Validation: We added basic validation for phone number (E.164) and OTP format using regex. For more robust validation (checking types, lengths, allowed characters, sanitizing inputs), use libraries like
express-validator
orjoi
.Rate Limiting: Crucial to prevent SMS pumping fraud (maliciously triggering SMS costs) and brute-force attacks on both requesting and verifying OTPs. Use
express-rate-limit
:Update
src/app.js
to include and apply the limiters:Brute Force Protection (Verification): The
OTP_MAX_ATTEMPTS
logic implemented inotpService.js
provides specific protection against guessing the OTP for a given phone number.HTTPS: Always deploy your application behind a reverse proxy (like Nginx) or load balancer (like AWS ELB/ALB) that terminates SSL/TLS, ensuring all traffic is encrypted via HTTPS. Do not handle TLS directly in Node.js unless necessary.
Secure Credential Handling: Reiterate: Use
.env
for local development only. In production, inject secrets securely via your hosting environment's mechanisms (e.g., AWS Secrets Manager, Parameter Store, platform environment variables). Never commit secrets to Git.OTP Security Best Practices:
crypto.randomInt
.NODE_ENV
.otpService.js
deletes the OTP upon successful verification).Handling special cases
+
followed by country code and number, no spaces or symbols) before storing or sending to SNS. The validation regex/^\+[1-9]\d{1,14}$/
and checks insnsService.js
help enforce this.message
variable inotpController.js
) is currently hardcoded in English. For multi-language support, you would need a localization library (likei18next
) and potentially pass a language preference from the client or detect it based on the phone number prefix to select the correct message template.Date.now()
provides milliseconds since epoch, which is timezone-agnostic). If using a database, ensureTIMESTAMP WITH TIME ZONE
data types are used to avoid ambiguity.Implementing performance optimizations
Map
to Redis for OTP storage will significantly improve performance and scalability, especially under load, due to Redis's optimized in-memory operations and built-in TTL handling.SNSClient
is instantiated once insnsService.js
and reused for allsendSms
calls. This avoids the overhead of creating a new client and establishing connections for every request.snsClient.send
call) correctly useasync/await
, preventing the Node.js event loop from being blocked and ensuring the server remains responsive.