Введение: зачем OAuth 2.0 и JWT в Rust?
Современные веб-приложения редко обходятся без системы аутентификации и авторизации. OAuth 2.0 — это отраслевой стандарт для делегирования доступа, а JWT (JSON Web Tokens) — компактный и безопасный способ передачи утверждений между сторонами. Rust, благодаря своей производительности и безопасности памяти, становится всё более популярным для создания высоконагруженных серверов. В этой статье мы на практике разберём, как реализовать OAuth 2.0 авторизацию с JWT на Rust, используя популярные крейты и современные подходы.
1. Подготовка проекта и установка зависимостей
Для начала создадим новый проект на Rust и добавим необходимые зависимости. Мы будем использовать actix-web для веб-сервера, jsonwebtoken для работы с JWT, serde для сериализации и reqwest для HTTP-запросов к внешним провайдерам OAuth.
# Cargo.toml
[package]
name = "oauth-jwt-rust"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-web = "4"
actix-rt = "2"
jsonwebtoken = "9"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
reqwest = { version = "0.12", features = ["json"] }
tokio = { version = "1", features = ["full"] }
dotenv = "0.15"
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1", features = ["v4"] }
Создадим файл .env для хранения секретных ключей:
JWT_SECRET=your_super_secret_key_here
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
2. Базовая структура JWT токенов
JWT состоит из трёх частей: заголовок, полезная нагрузка и подпись. Определим структуры для работы с токенами.
// src/jwt.rs
use chrono::{Utc, Duration};
use jsonwebtoken::{encode, decode, Header, Validation, EncodingKey, DecodingKey};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
pub sub: String, // идентификатор пользователя
pub email: String, // email пользователя
pub exp: usize, // время истечения
pub iat: usize, // время выдачи
}
pub fn create_jwt(user_id: &str, email: &str) -> Result {
let now = Utc::now();
let expiration = now + Duration::hours(24); // токен живёт 24 часа
let claims = Claims {
sub: user_id.to_string(),
email: email.to_string(),
exp: expiration.timestamp() as usize,
iat: now.timestamp() as usize,
};
let secret = std::env::var("JWT_SECRET").expect("JWT_SECRET must be set");
encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(secret.as_bytes()),
)
}
pub fn validate_jwt(token: &str) -> Result {
let secret = std::env::var("JWT_SECRET").expect("JWT_SECRET must be set");
let token_data = decode::(
token,
&DecodingKey::from_secret(secret.as_bytes()),
&Validation::default(),
)?;
Ok(token_data.claims)
}
3. Реализация OAuth 2.0 с GitHub
Рассмотрим классический сценарий OAuth 2.0 — авторизация через GitHub. Пользователь перенаправляется на GitHub, после подтверждения получает код, который мы обмениваем на access token.
3.1. Эндпоинт для начала OAuth-потока
// src/oauth.rs
use actix_web::{web, HttpResponse, HttpRequest};
use reqwest::Client;
use serde_json::Value;
pub async fn github_login() -> HttpResponse {
let client_id = std::env::var("GITHUB_CLIENT_ID").unwrap();
let redirect_uri = "http://localhost:8080/auth/github/callback";
let auth_url = format!(
"https://github.com/login/oauth/authorize?client_id={}&redirect_uri={}&scope=user:email",
client_id, redirect_uri
);
HttpResponse::Found()
.append_header(("Location", auth_url))
.finish()
}
3.2. Callback-обработчик
pub async fn github_callback(query: web::Query<std::collections::HashMap<String, String>>) -> HttpResponse {
let code = match query.get("code") {
Some(c) => c,
None => return HttpResponse::BadRequest().body("Missing code parameter"),
};
let client = Client::new();
let client_id = std::env::var("GITHUB_CLIENT_ID").unwrap();
let client_secret = std::env::var("GITHUB_CLIENT_SECRET").unwrap();
// Обмен кода на access token
let token_params = [
("client_id", client_id.as_str()),
("client_secret", client_secret.as_str()),
("code", code),
];
let token_response = client
.post("https://github.com/login/oauth/access_token")
.header("Accept", "application/json")
.form(&token_params)
.send()
.await
.unwrap();
let token_json: Value = token_response.json().await.unwrap();
let access_token = token_json["access_token"].as_str().unwrap();
// Получение информации о пользователе
let user_response = client
.get("https://api.github.com/user")
.header("Authorization", format!("Bearer {}", access_token))
.header("User-Agent", "rust-oauth-app")
.send()
.await
.unwrap();
let user_info: Value = user_response.json().await.unwrap();
let user_id = user_info["id"].to_string();
let email = user_info["email"].as_str().unwrap_or("no-email@github.com");
// Создаём JWT токен
match crate::jwt::create_jwt(&user_id, email) {
Ok(token) => HttpResponse::Ok().json(serde_json::json!({
"token": token,
"user": user_info
})),
Err(e) => HttpResponse::InternalServerError().body(format!("JWT error: {}", e)),
}
}
4. Защита эндпоинтов с помощью JWT middleware
Теперь создадим middleware, который будет проверять JWT токен в заголовке Authorization.
// src/middleware.rs
use actix_web::{dev::ServiceRequest, Error, HttpMessage};
use actix_web::error::ErrorUnauthorized;
use crate::jwt::validate_jwt;
pub async fn jwt_middleware(req: ServiceRequest) -> Result<ServiceRequest, Error> {
let auth_header