Организации всегда ищут инновационные способы борьбы с угрозами и уязвимостями на своей платформе. Они постоянно инвестируют в человеческие ресурсы и технологии, помогая создавать и поставлять безопасные приложения. Двухфакторная аутентификация, приложение Authenticator и биометрия, среди прочего, являются одними из инновационных методов, которые организации внедряют для обеспечения безопасности своей платформы.

В этом посте мы узнаем, как создавать API-интерфейсы, которые аутентифицируют пользователей по их номерам телефонов, используя Rust и службу проверки Twilio. В этом посте мы будем использовать Actix Web для создания нашего API. Однако тот же подход применим и к любому фреймворку на основе Rust.


Предпосылки

Чтобы полностью понять концепции, представленные в этом руководстве, применяются следующие требования:

  • Базовое понимание Rust
  • учетная запись Twilio;зарегистрироваться для пробного аккаунта совершенно бесплатно.


Начиная

Для начала нам нужно перейти в нужный каталог и выполнить команду ниже в нашем терминале:

cargo new rust-sms-verification && cd rust-sms-verification
Войти в полноэкранный режим

Выйти из полноэкранного режима

Эта команда создает проект Rust с именем rust-sms-verification и переходит в каталог проекта.

Далее мы приступаем к установке необходимых зависимостей, изменяя [dependencies] раздел Cargo.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" — это фреймворк на основе Rust для создания веб-приложений.

serde = { version = "1.0.145", features = ["derive"] } это фреймворк для сериализации и десериализации структур данных Rust. Например, преобразовать структуры Rust в JSON и наоборот.

dotenv = "0.15.0" представляет собой ящик для управления переменными среды.

reqwest = { version = "0.11", features = ["json"] } представляет собой контейнер HTTP-запросов.

PS: feature флаги, используемые в ящиках выше, включают определенную функцию ящика.

Нам нужно запустить команду ниже, чтобы установить зависимости:

cargo build
Войти в полноэкранный режим

Выйти из полноэкранного режима


Структурирование нашего приложения

Очень важно иметь хорошую структуру проекта, поскольку она делает проект удобным для сопровождения и облегчает нам и другим пользователям чтение нашей кодовой базы.
Для этого нам нужно перейти к src директорию и в этой папке создайте models.rs, services.rsа также handlers.rs файлы.

каталог проекта

models.rs для структурирования данных нашего приложения

services.rs для абстрагирования логики нашего приложения

handlers.rs для структурирования наших API

Наконец, нам нужно объявить эти файлы как модуль и импортировать их в main.rs файл, как показано ниже:

mod handlers;
mod models;
mod services;

fn main() {
  println!("Hello, world!");
}
Войти в полноэкранный режим

Выйти из полноэкранного режима


Настройка Twilio

Чтобы включить проверку OTP в нашем API, нам нужно войти в наш Консоль Twilio чтобы получить наш SID учетной записи и Токен авторизации. Нам нужно держать эти параметры под рукой, поскольку они нужны нам для настройки и создания наших API.

Учетные данные Twilio

Создать службу проверки
Twilio поставляется с безопасными и надежными сервисами для беспрепятственной проверки пользователей с помощью SMS, голосовой связи и электронной почты. В нашем случае мы будем использовать опцию SMS для проверки пользователей по номерам телефонов. Для этого перейдите в Исследуйте продукты вкладку, перейдите к Безопасность аккаунта раздел и нажмите кнопку Проверять кнопка.

Подтвердить учетную запись

Перейдите к Услуги вкладку, нажмите на Создать новый кнопка, ввод смс-сервис как понятное имя, включите смс вариант, и Создавать.

Создать новую услугу
Введите данные

При создании нам нужно скопировать SID службы. Это также пригодится при создании нашего API.

SID службы

включить граммгеографический посвобождение
Географические разрешения — это механизмы, созданные Twilio для контроля использования своих сервисов. Он предоставляет инструмент для включения и отключения стран, принимающих голосовые вызовы и SMS-сообщения из учетной записи Twilio.

Чтобы включить SMS, нам нужно найти SMS Geographic Permissions в строке поиска, нажать на кнопку Географические разрешения SMS результат, а затем Проверьте страна, в которой работает провайдер SMS.

Поиск
Проверить страну работы


Создание API проверки OTP в Rust

Выполнив настройку, мы можем начать создавать наши API, следуя инструкциям.

Добавьте переменные среды
Далее нам нужно создать .env файл в корневом каталоге и добавьте фрагмент ниже:

TWILIO_ACCOUNT_SID=<ACCOUNT SID>
TWILIO_AUTHTOKEN=<AUTH TOKEN>
TWILIO_SERVICES_ID=<SERVICE ID>
Войти в полноэкранный режим

Выйти из полноэкранного режима

Как упоминалось ранее, мы можем получить необходимые учетные данные из консоли Twilio и настроек службы.

твилио консоль
сервисные настройки

Создание моделей API
Далее нам нужно создать модели для представления данных нашего приложения. Для этого нам нужно изменить model.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,
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Фрагмент выше делает следующее:

  • Импортирует необходимую зависимость
  • Создает OTPData а также VerifyOTPData структуры с требуемыми свойствами, необходимыми для тела запроса API
  • Создает OTPResponse, OTPVerifyResponseа также APIResponse структуры с требуемыми свойствами, необходимыми для ответа API

PS: #[serde(rename_all = "camelCase")] макрос преобразует свойства змеиного случая в верблюжий и derive макрос добавляет поддержку реализации для сериализации, десериализации и клонирования.

Создайте службу API
Twilio поставляется с открытой спецификацией API для доступа к своим сервисам. В этом разделе мы будем использовать службу проверки для отправки и проверки OTP. Для этого, во-первых, нам нужно обновить services.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"),
        }
    }
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Фрагмент выше делает следующее:

  • Импортирует необходимые зависимости
  • Создает TwilioService структура
  • Создает блок реализации, который добавляет методы в TwilioService структура
  • Добавляет env_loader вспомогательный метод для загрузки переменной окружения
  • Добавляет send_otp метод, который принимает в phone_number в качестве параметра и возвращает OTPResponse или строку, описывающую ошибку. Метод также делает следующее:
    • Строка 20 — 22: — Использует env_loader вспомогательный метод для создания необходимой переменной среды
    • Строка 24 — 27: Создает url отформатировав его с требуемым параметром
    • Строка 29 — 33: создает заголовок запроса API и устанавливает для него тип формы
    • Строка 35 — 37: создает тело формы пары «ключ-значение», чтобы указать номер телефона, который будет получать одноразовый пароль, и установить канал для отправки одноразового пароля как sms
    • Строка 39 — 46: создает client экземпляр с использованием reqwest библиотека для выполнения HTTP-вызова API Twilio путем передачи необходимых параметров аутентификации, заголовка запроса и тела формы.
    • Строка 48 — 57: возвращает соответствующий ответ

Наконец, нам нужно добавить метод для проверки отправленного OTP, как показано ниже:

//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"),
        }
    }
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Фрагмент выше работает аналогично send_otp метод. Тем не менее, мы изменили url на URL проверки подтверждения, изменил Channel к Code в теле формы и вернул соответствующий ответ, проверив, что статус ответа approved.

Создайте обработчики API
С полностью настроенными службами мы можем использовать их для создания нашего обработчика API. Для этого нам нужно обновить handlers.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(),
        }),
    }
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Фрагмент выше делает следующее:

  • Импортирует необходимые зависимости
  • Создает send_otp обработчик с /otp Маршрут API, который использует send_otp сервис для отправки OTP и возвращает соответствующий ответ, используя APIResponse
  • Создает verify_otp обработчик с /verifyOTP Маршрут API, который использует verify_otp сервис для проверки OTP и возвращает соответствующий ответ, используя APIResponse

Собираем все вместе
Сделав это, нам нужно обновить main.rs файл, чтобы включить нашу точку входа в приложение и использовать send_otp а также verify_otp обработчики.

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
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Фрагмент выше делает следующее:

  • Импортирует необходимые зависимости
  • Создает новый сервер, который добавляет send_otp а также verify_otp обработчики и работает на localhost:8080

После этого мы можем запустить сервер разработки, используя следующую команду:

cargo run main.go 
Войти в полноэкранный режим

Выйти из полноэкранного режима

отправить одноразовый пароль
одноразовый пароль
Проверка одноразового пароля

Мы также можем проверить журналы сообщений, перейдя к Проверять вкладка Журналы Твилио

Журнал сообщений


Вывод

В этом посте обсуждалось, как создавать API-интерфейсы, которые проверяют и верифицируют пользователей по их номерам телефонов, используя Rust и службу проверки Twilio. Помимо проверки на основе SMS, Twilio предоставляет несколько сервисов для беспрепятственной интеграции аутентификации в новую или существующую кодовую базу.

Эти ресурсы могут быть полезны: