How to build a One-Time-Password(OTP) Verification API with Rust and Twilio
Tags: actixweb, rust, twilio, otp
Published: 2022-10-29 10:04:09

---
title: How to build a One-Time-Password(OTP) Verification API with Rust and Twilio
published: true
description: Learn how to build APIs that authenticate users with their phone numbers using Rust and Twilio’s Verification Service.
tags: #actixweb #rust #twilio #otp
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tzjf9961oe1027hwvjad.png
# Use a ratio of 100:42 for best results.
# published_at: 2022-10-21 13:56 +0000
---
Organizations are always looking for innovative ways to tackle threats and vulnerabilities on their platform. They are constantly investing in human resources and technologies to help build and ship secure applications. Two-factor authentication, Authenticator app, and Biometrics, among others, are some of the innovative methods organizations are adopting to keep their platform safe.

In this post, we will learn how to build APIs that authenticate users with their phone numbers using Rust and Twilio’s Verification Service. For this post, we will be using Actix Web to build our API. However, the same approach also applies to any Rust-based framework.

## Prerequisites

To fully grasp the concepts presented in this tutorial, the following requirements apply:

- Basic understanding of Rust
- A Twilio account;[**signup**](https://www.twilio.com/try-twilio) for a trial account is completely free.

## Getting started

To get started, we need to navigate to the desired directory and run the command below in our terminal:

```bash
cargo new rust-sms-verification && cd rust-sms-verification
```

This command creates a Rust project called `rust-sms-verification` and navigates into the project directory.

Next, we proceed to install the required dependencies by modifying the `[dependencies]` section of the `Cargo.toml` file as shown below:

```toml
//other code section goes here

[dependencies]
actix-web = "4"
serde = { version = "1.0.145", features = ["derive"] }
dotenv = "0.15.0"
reqwest = { version = "0.11", features = ["json"] }
```

`actix-web =` `"4"` is a Rust-based framework for building web applications.

`serde = { version = "1.0.145", features = ["derive"] }` is a framework for serializing and deserializing Rust data structures. E.g. convert Rust structs to JSON and vice versa.

`dotenv = "0.15.0"` is a crate for managing environment variables.

`reqwest = { version = "0.11", features = ["json"] }` is a HTTP request crate.

***PS****: The* `feature` *flags used in the crates above enable a specific feature of the crate.*

We need to run the command below to install the dependencies:

```bash
cargo build
```

## Structuring our application

It is essential to have a good project structure as it makes the project maintainable and makes it easier for us and others to read our codebase.
To do this, we need to navigate to the `src` directory and, in this folder, create a `models.rs`, `services.rs`, and `handlers.rs` files.

![project directory](https://paper-attachments.dropbox.com/s_DB6836ECE0A8A06D12EEC3D38A3E08BCD6C29CEB67E496D0241BCADF43286DB6_1665733392370_Screenshot+2022-10-14+at+08.42.45.png)


`models.rs` is for structuring our application data

`services.rs` is for abstracting our application logic

`handlers.rs` is for structuring our APIs

Finally, we need to declare these files as a module and import them into the `main.rs` file as shown below:

```rs
mod handlers;
mod models;
mod services;

fn main() {
  println!("Hello, world!");
}
```

## Setting up Twilio

To enable OTP verification in our API, we need to sign into our [Twilio Console](https://console.twilio.com/) to get our **Account SID** and an **Auth Token**. We need to keep these parameters handy as we need them to configure and build our APIs.

![Twilio credentials](https://paper-attachments.dropbox.com/s_5B9EC91BD44DDEBF4CBA993B23ED14FA855B0F9A80137ECFCB22BC7B67C871FC_1665673572014_Screenshot+2022-10-13+at+16.05.12.png)


**Create a Verification Service**
Twilio ships with secure and robust services for seamlessly validating users with SMS, Voice, and Email. In our case, we will use the SMS option to verify users through phone numbers.  To do this, navigate to the **Explore Products** tab, scroll to the **Account security** section, and click on the **Verify** button.

![Verify account](https://paper-attachments.dropbox.com/s_5B9EC91BD44DDEBF4CBA993B23ED14FA855B0F9A80137ECFCB22BC7B67C871FC_1665673638888_Screenshot+2022-10-08+at+16.44.44.png)


Navigate to the **Services** tab, click on the **Create new** button, input **sms-service** as the friendly name, toggle-on the **SMS** option, and **Create**.

![Create new service](https://paper-attachments.dropbox.com/s_5B9EC91BD44DDEBF4CBA993B23ED14FA855B0F9A80137ECFCB22BC7B67C871FC_1665674577307_Screenshot+2022-10-08+at+16.46.42.png)
![Input details](https://paper-attachments.dropbox.com/s_5B9EC91BD44DDEBF4CBA993B23ED14FA855B0F9A80137ECFCB22BC7B67C871FC_1665674577353_Screenshot+2022-10-08+at+16.53.00.png)


Upon creation, we need to copy the **Service SID.** It will also come in handy when building our API.

![Service SID](https://paper-attachments.dropbox.com/s_5B9EC91BD44DDEBF4CBA993B23ED14FA855B0F9A80137ECFCB22BC7B67C871FC_1665674754011_Screenshot+2022-10-08+at+16.53.39.png)


**Enable** **g****eographical** **p****ermission**
Geographical Permissions are mechanisms put in place by Twilio to control the use of their services. It provides a tool for enabling and disabling countries receiving voice calls and SMS messages from a Twilio account.

To enable SMS, we need to search for SMS Geographic Permissions in the search bar, click on the **SMS Geographic Permissions** result, and then **check** the country where the SMS provider operates.

![Search](https://paper-attachments.dropbox.com/s_3BFCDA37590D5822C6FEFC1F3DD4F8A442FBEC418FAB7D6351861F1A86D14D5A_1663779185609_Screenshot+2022-09-21+at+11.36.38.png)
![Check country of operation](https://paper-attachments.dropbox.com/s_3BFCDA37590D5822C6FEFC1F3DD4F8A442FBEC418FAB7D6351861F1A86D14D5A_1663779216189_Screenshot+2022-09-21+at+17.53.12.png)



## Creating OTP Verification APIs in Rust

With the configuration done, we can start building our APIs by following the steps.

**Add environment variables**
Next, we need to create a `.env` file in the root directory and add the snippet below:

```bash
TWILIO_ACCOUNT_SID=<ACCOUNT SID>
TWILIO_AUTHTOKEN=<AUTH TOKEN>
TWILIO_SERVICES_ID=<SERVICE ID>
```

As mentioned earlier, we can get the required credentials from Twilio console and Service Setting.

![twilio console](https://paper-attachments.dropbox.com/s_5B9EC91BD44DDEBF4CBA993B23ED14FA855B0F9A80137ECFCB22BC7B67C871FC_1665676428573_Screenshot+2022-10-13+at+16.05.12.png)
![service settings](https://paper-attachments.dropbox.com/s_5B9EC91BD44DDEBF4CBA993B23ED14FA855B0F9A80137ECFCB22BC7B67C871FC_1665676428561_Screenshot+2022-10-08+at+16.53.39.png)


**Create the API models**
Next, we need to create models to represent our application data. To do this, we need to modify the `model.rs` file as shown below:

```rs
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct OTPData {
    pub phone_number: String,
}

#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct VerifyOTPData {
    pub user: OTPData,
    pub code: String,
}

#[derive(Deserialize, Debug, Clone)]
pub struct OTPResponse {
    pub sid: String,
}

#[derive(Deserialize, Debug, Clone)]
pub struct OTPVerifyResponse {
    pub status: String,
}

#[derive(Serialize, Debug, Clone)]
pub struct APIResponse {
    pub status: u16,
    pub message: String,
    pub data: String,
}
```

The snippet above does the following:

- Imports the required dependency
- Creates `OTPData` and `VerifyOTPData` structs with required properties needed for the API request body 
- Creates `OTPResponse`, `OTPVerifyResponse`, and `APIResponse` structs with required properties  needed for the API response

***PS****: The* `#[serde(rename_all = "camelCase")]` *macro converts snake case properties to camel case and the* `derive` *macro adds implementation support for serialization, deserializations, and cloning.*

**Create the API service**
Twilio ships with an open API specification for accessing their services. In this section, we will use the Verification Service to send and verify OTP. To do this, first, we need to update the `services.rs` file as shown below:

```rs
use std::{collections::HashMap, env};

use dotenv::dotenv;
use reqwest::{header, Client};

use crate::models::{OTPResponse, OTPVerifyResponse};

pub struct TwilioService {}

impl TwilioService {
    fn env_loader(key: &str) -> String {
        dotenv().ok();
        match env::var(key) {
            Ok(v) => v.to_string(),
            Err(_) => format!("Error loading env variable"),
        }
    }

    pub async fn send_otp(phone_number: &String) -> Result<OTPResponse, &'static str> {
        let account_sid = TwilioService::env_loader("TWILIO_ACCOUNT_SID");
        let auth_token = TwilioService::env_loader("TWILIO_AUTHTOKEN");
        let service_id = TwilioService::env_loader("TWILIO_SERVICES_ID");

        let url = format!(
            "https://verify.twilio.com/v2/Services/{serv_id}/Verifications",
            serv_id = service_id
        );

        let mut headers = header::HeaderMap::new();
        headers.insert(
            "Content-Type",
            "application/x-www-form-urlencoded".parse().unwrap(),
        );

        let mut form_body: HashMap<&str, String> = HashMap::new();
        form_body.insert("To", phone_number.to_string());
        form_body.insert("Channel", "sms".to_string());

        let client = Client::new();
        let res = client
            .post(url)
            .basic_auth(account_sid, Some(auth_token))
            .headers(headers)
            .form(&form_body)
            .send()
            .await;

        match res {
            Ok(response) => {
                let result = response.json::<OTPResponse>().await;
                match result {
                    Ok(data) => Ok(data),
                    Err(_) => Err("Error sending OTP"),
                }
            }
            Err(_) => Err("Error sending OTP"),
        }
    }
}
```

The snippet above does the following:

- Imports the required dependencies
- Creates a `TwilioService` struct
- Creates an implementation block that adds methods to the `TwilioService` struct
- Adds an `env_loader` helper method to load the environment variable
- Adds a `send_otp` method that takes in the `phone_number` as a parameter and returns the `OTPResponse` or a string describing the error. The method also does the following:
    - **Line 20 - 22**: - Uses the `env_loader` helper method to create the required environment variable
    - **Line 24 - 27**: Creates the `url` by formatting it with the required parameter
    - **Line 29 - 33**: Creates the API request header and sets it to a form type
    - **Line 35 - 37**: Creates a key-value pair form body to set the phone number that will receive the OTP and set the channel for sending the OTP as `sms`
    - **Line 39 - 46**: Creates a `client` instance using the `reqwest` library to make HTTP call to Twilio API by passing in the required authentication parameters, request header, and the form body
    - **Line 48 - 57**: Returns the appropriate response

Lastly, we need to add a method to verify the sent OTP as shown below:

```rs
//imports goes here

pub struct TwilioService {}

impl TwilioService {
    fn env_loader(key: &str) -> String {
        //code goes here
    }

    pub async fn send_otp(phone_number: &String) -> Result<OTPResponse, &'static str> {
        //code goes here
    }

  //add
  pub async fn verify_otp(phone_number: &String, code: &String) -> Result<(), &'static str> {
        let account_sid = TwilioService::env_loader("TWILIO_ACCOUNT_SID");
        let auth_token = TwilioService::env_loader("TWILIO_AUTHTOKEN");
        let service_id = TwilioService::env_loader("TWILIO_SERVICES_ID");

        let url = format!(
            "https://verify.twilio.com/v2/Services/{serv_id}/VerificationCheck",
            serv_id = service_id,
        );

        let mut headers = header::HeaderMap::new();
        headers.insert(
            "Content-Type",
            "application/x-www-form-urlencoded".parse().unwrap(),
        );

        let mut form_body: HashMap<&str, &String> = HashMap::new();
        form_body.insert("To", phone_number);
        form_body.insert("Code", code);

        let client = Client::new();
        let res = client
            .post(url)
            .basic_auth(account_sid, Some(auth_token))
            .headers(headers)
            .form(&form_body)
            .send()
            .await;

        match res {
            Ok(response) => {
                let data = response.json::<OTPVerifyResponse>().await;
                match data {
                    Ok(result) => {
                        if result.status == "approved" {
                            Ok(())
                        } else {
                            Err("Error verifying OTP")
                        }
                    }
                    Err(_) => Err("Error verifying OTP"),
                }
            }
            Err(_) => Err("Error verifying OTP"),
        }
    }
}
```

The snippet above works similarly to the `send_otp` method. However, we changed the `url` to a Verification Check URL, changed the `Channel` to `Code` in the form body, and returned the appropriate response by checking that the response status is `approved`.

**Create the API handlers**
With services fully configured, we can use them to create our API handler. To do this, we need to update the `handlers.rs` as shown below:

```rs
use actix_web::{post, web::Json, HttpResponse};
use reqwest::StatusCode;

use crate::{
    models::{APIResponse, OTPData, VerifyOTPData},
    services::TwilioService,
};

#[post("/otp")]
pub async fn send_otp(new_data: Json<OTPData>) -> HttpResponse {
    let data = OTPData {
        phone_number: new_data.phone_number.clone(),
    };

    let otp_details = TwilioService::send_otp(&data.phone_number).await;

    match otp_details {
        Ok(otp) => HttpResponse::Ok().json(APIResponse {
            status: StatusCode::ACCEPTED.as_u16(),
            message: "success".to_string(),
            data: otp.sid,
        }),
        Err(e) => HttpResponse::InternalServerError().json(APIResponse {
            status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
            message: "failure".to_string(),
            data: e.to_string(),
        }),
    }
}

#[post("/verifyOTP")]
pub async fn verify_otp(new_data: Json<VerifyOTPData>) -> HttpResponse {
    let data = VerifyOTPData {
        user: new_data.user.clone(),
        code: new_data.code.clone(),
    };

    let otp_details = TwilioService::verify_otp(&data.user.phone_number, &data.code).await;

    match otp_details {
        Ok(_) => HttpResponse::Ok().json(APIResponse {
            status: StatusCode::ACCEPTED.as_u16(),
            message: "success".to_string(),
            data: "OTP verified successfully".to_string(),
        }),
        Err(e) => HttpResponse::InternalServerError().json(APIResponse {
            status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
            message: "failure".to_string(),
            data: e.to_string(),
        }),
    }
}
```

The snippet above does the following:

- Imports the required dependencies 
- Creates a `send_otp` handler with a `/otp` API route that uses the `send_otp` service to send OTP and returns the appropriate response using the `APIResponse`
- Creates a `verify_otp` handler with a `/verifyOTP` API route that uses the `verify_otp` service to verify OTP and returns the appropriate response using the `APIResponse`

**Putting it all together**
With the that done, we need to update the `main.rs` file to include our application entry point and use the `send_otp` and `verify_otp` handlers.

```rs
use actix_web::{App, HttpServer}; //add
use handlers::{send_otp, verify_otp}; //add
mod handlers;
mod models;
mod services;

//add
#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(move || App::new().service(send_otp).service(verify_otp))
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
}
```

The snippet above does the following:

- Imports the required dependencies
- Creates a new server that adds the `send_otp` and `verify_otp` handlers and runs on `localhost:8080`

With that done, we can start a development server using the command below:

```bash
cargo run main.rs 
```

![send OTP](https://paper-attachments.dropbox.com/s_DB6836ECE0A8A06D12EEC3D38A3E08BCD6C29CEB67E496D0241BCADF43286DB6_1665755886829_Screenshot+2022-10-12+at+15.49.06.png)
![OTP](https://paper-attachments.dropbox.com/s_5B9EC91BD44DDEBF4CBA993B23ED14FA855B0F9A80137ECFCB22BC7B67C871FC_1665700959117_IMG_8827.PNG)
![Verifying OTP](https://paper-attachments.dropbox.com/s_5B9EC91BD44DDEBF4CBA993B23ED14FA855B0F9A80137ECFCB22BC7B67C871FC_1665700959120_Screenshot+2022-10-08+at+18.06.00.png)


We can also verify the message logs by navigating to the **Verify** tab of the **Logs** on Twilio

![Message log](https://paper-attachments.dropbox.com/s_5B9EC91BD44DDEBF4CBA993B23ED14FA855B0F9A80137ECFCB22BC7B67C871FC_1665701089112_Screenshot+2022-10-08+at+18.07.02.png)



## Conclusion

This post discussed how to create APIs that check and verify users with their phone numbers using Rust and Twilio’s Verification Service. Beyond SMS-based verification, Twilio ships multiple services to seamlessly integrate authentication into a new or existing codebase.

This resources might be helpful:

- [Twilio Verification Service](https://www.twilio.com/docs/verify/api/service)
- [Reqwest HTTP Library](https://github.com/seanmonstar/reqwest)