Intro
This post describes how to implement a simple One Time Password (OTP) system with AWS Serverless services which can be used as a part of two-step verification.
Below tools and technologies used to build this application.
- AWS Lambda
- API Gateway
- DynamoDB
- Simple Email Service - SES
- Amplify Web Hosting
- VueJS for frontend
- Deploy with AWS SAM
Architecture
How it works
In this scenario, I used a login form, which is developed with VueJS and hosted using Amplify static web hosting.
User will enter their email and password and once the credentials are validated, an API endpoint is called to execute "Generate OTP" Lambda function which generates a 6 digit code along with a session id.
Once the code and session id is generated, "Generate OTP" Lambda will save these data into a DynamoDB table.
Then only the session id will be returned as the response of the API endpoint.
DynamoDB streams are enabled in the table. So, once the data are saved, it will trigger the "Send Email" Lambda function.
Within the "Send Email" Lambda function, it will call the Simple Email Service (SES) to send out an email with the generated code to the email address provided.
Meanwhile on the frontend side, once the session id is received from the API, 2nd form is presented to enter the code, which is emailed to the given address.
Once the user enters the code and submits it, it will validate the code along with the session id using another API gateway endpoint that proxy to "Verify OTP" Lambda function.
In "Verify OTP" Lambda function, it queries the DynamoDB table with the given session-id and code and returns the success or error responses.
Key points/Lessons learned
Here I enabled DynamoDB TTL to delete the entries after a specific time to prevent fill out the table very quickly. However, DynamoDB will not delete your record immediately when the TTL is expired. It is deleted eventually and AWS only guarantees it to be deleted within 48 hours. Because of this, when verifying the OTP code, it has to consider the same "expiredAt" field which was used to set TTL.
When design the DynamoDB table, I used sessionId + OTP code as a primary key to easily query the required record. So, when verifying the code, I query by primary key with this combination in the DynamoDB table.
When using DynamoDB steam as a trigger for Lambda, it gets triggers for all the DynamoDB events (ex: insert, delete, update). So, within the Lambda function, had to filter out only the INSERT events using "eventName" of the record.
To send out emails, I have used AWS's own Simple Email Service (SES). However, you need to first verify your sending email address to send emails to any address. This can be done with a support request.
Here, I used Amplify static web hosting to host the frontend of the application. I have used Amplify features - the auto deployments when Github repository modified and custom domain name set up only with few button clicks.
I have used AWS SAM to deploy the backend resources. The expiry time of the OTP and no of digits in the OTP code can be configured at the deployment time.
How to set up
Prerequisites
- AWS CLI
- AWS SAM CLI
- Set up and verified SES send email address
Backend
Clone the repository: https://github.com/pubudusj/simple-otp
-
Run
sam init && sam deploy -g
After providing your stack information and AWS environment parameters, this will create the backend stack. Copy the ApiBaseUrl output value.
#### Frontend
1. Copy the .env.example into .env file and add the ApiBaseUrl value as *VUE_APP_API_BASE_URL*.
2. You may zip the whole frontend directory and use that in Amplify web hosting, or authorize your GitHub repository to automatically deploy the application when a git push is made.
3. If you need to run the frontend in local, navigate to frontend directory and run `npm run serve`
#### To Delete the stack
To remove the backend, run `sam delete`.
## Demo
Demo version of this application is available at :
[https://simple-otp.pubudu.dev/](https://simple-otp.pubudu.dev/)
## Feedback
Your valuable feedback on this project is mostly welcome! I would like you to play around with this and if you have any questions or general comments, please reach out to me via [Personal Blog](https://pubudu.dev), [LinkedIn](https://www.linkedin.com/in/pubudusj/), [Twitter](https://twitter.com/pubudusj) or [Github](https://github.com/pubudusj).
Keep building, keep sharing!
Top comments (17)
Great post! what tool did you use for the architecture diagram, Lucidcharts?
Thanks @karanpratapsingh for the feedback.
I use draw.io for these diagrams.
Thanks
I use it too, it's awesome
In DynamoDB single table design we can't use TTL. I don't know is it possible!
Hey Sekar, can you elaborate more why you cannot use TTL with single table design?
TTL is set for the entire table. In single table design, we have many entities in the same table. We can't set TTL on a single entity.
Very explained post. Thanks for this. 👍🏻👍🏻
Thanks for the feedback @aviboy2006
Great and useful post. 💯✌
Thanks @3much for the feedback!
FYI, the Github repo link is 404, may need to make it public?
Just made it public. Thanks for pointing this out @aaronbrighton .
Excellent post!
I wonder if this implementation limits the number of OTP's generated by email to avoid unnecessary consumption?
Thanks for the feedback @eliasibgerardo
In this implementation there is no limitation enforced. However, there are several ways to protect the unnecessary consumptions.
Since this OTP functionality meant to be consumed by already authenticated users, that will reduce the unnecessary usage since we can track the users who are actually using the system.
Also, in the infrastructure level, we can use Web Application Firewall (WAF) rules with throttling to protect the API end points per IP for example. docs.aws.amazon.com/waf/latest/dev...
Further, in the code level, we can implement our own rate limits per email address using the email address and expiryAt field values.
Great post and well detailed. Although I would say DynamoDB streams is unnecessary, the lambda that stores the function might as well call the SES service. What do you think?
Hi, its a great post, but send-email function gives error that
event.Records is not iterable