DEV Community

Cover image for Create a simple OTP system with AWS Serverless
Pubudu Jayawardana for AWS Community Builders

Posted on • Updated on • Originally published at pubudu.dev

Create a simple OTP system with AWS Serverless

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.

Architecture

Simple OTP Architecture

How it works

  1. In this scenario, I used a login form, which is developed with VueJS and hosted using Amplify static web hosting.

  2. 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.

  3. Once the code and session id is generated, "Generate OTP" Lambda will save these data into a DynamoDB table.

  4. Then only the session id will be returned as the response of the API endpoint.

  5. DynamoDB streams are enabled in the table. So, once the data are saved, it will trigger the "Send Email" Lambda function.

  6. 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.

  7. 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.

  8. 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.

  9. 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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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

  1. Clone the repository: https://github.com/pubudusj/simple-otp

  2. 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!
Enter fullscreen mode Exit fullscreen mode

Top comments (17)

Collapse
 
karanpratapsingh profile image
Karan Pratap Singh

Great post! what tool did you use for the architecture diagram, Lucidcharts?

Collapse
 
pubudusj profile image
Pubudu Jayawardana

Thanks @karanpratapsingh for the feedback.
I use draw.io for these diagrams.

Collapse
 
karanpratapsingh profile image
Karan Pratap Singh

Thanks

Collapse
 
wiley19 profile image
Wilson

I use it too, it's awesome

Collapse
 
sakar_dhana profile image
Sakar

In DynamoDB single table design we can't use TTL. I don't know is it possible!

Collapse
 
pubudusj profile image
Pubudu Jayawardana

Hey Sekar, can you elaborate more why you cannot use TTL with single table design?

Collapse
 
sakar_dhana profile image
Sakar

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.

Collapse
 
avinashdalvi_ profile image
Avinash Dalvi

Very explained post. Thanks for this. 👍🏻👍🏻

Collapse
 
pubudusj profile image
Pubudu Jayawardana

Thanks for the feedback @aviboy2006

Collapse
 
3much profile image
3much

Great and useful post. 💯✌

Collapse
 
pubudusj profile image
Pubudu Jayawardana

Thanks @3much for the feedback!

Collapse
 
aaronbrighton profile image
Aaron Brighton

FYI, the Github repo link is 404, may need to make it public?

Collapse
 
pubudusj profile image
Pubudu Jayawardana

Just made it public. Thanks for pointing this out @aaronbrighton .

Collapse
 
eliasibgerardo profile image
Gerardo Eliasib

Excellent post!
I wonder if this implementation limits the number of OTP's generated by email to avoid unnecessary consumption?

Collapse
 
pubudusj profile image
Pubudu Jayawardana

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.

Collapse
 
wiley19 profile image
Wilson

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?

Collapse
 
pawan_kumar profile image
pawan kumar

Hi, its a great post, but send-email function gives error that
event.Records is not iterable