المصادقة
يُنشئ FORGE نظام مصادقة كامل مبني على JWT مع دعم لطرق تسجيل دخول متعددة وتجديد الـ token ومعالجة آمنة لكلمات المرور. النظام يستخدم access tokens قصيرة العمر مقترنة بـ refresh tokens أطول عمراً، متبعاً أفضل الممارسات في الصناعة للمصادقة الـ stateless لواجهات API.
استراتيجية الـ Token
نظام المصادقة يستخدم نهج الـ dual-token:
| Token | العمر | الغرض | التخزين |
|---|---|---|---|
| Access Token | 15 دقيقة | تفويض طلبات API | ترويسة Authorization أو HttpOnly cookie |
| Refresh Token | 7 أيام | الحصول على access tokens جديدة | HttpOnly cookie |
┌──────────┐ تسجيل دخول ┌──────────┐
│ العميل │ ──────────────▶│ الخادم │
│ │◀────────────── │ │
│ │ Access Token │ │
│ │ Refresh Token │ │
│ │ │ │
│ │ طلب API │ │
│ │ ──────────────▶│ │
│ │ (Access Token)│ │
│ │ │ │
│ │ Token منتهي │ │
│ │ ──────────────▶│ │
│ │ (Refresh Token)│ │
│ │◀────────────── │ │
│ │ Tokens جديدة │ │
└──────────┘ └──────────┘نصيحة
Access tokens قصيرة العمر عمداً. عندما تنتهي صلاحية access token، يجب على العميل استدعاء نقطة نهاية التجديد للحصول على زوج جديد من الـ tokens بدون الحاجة لإعادة تسجيل دخول المستخدم.
طرق تسجيل الدخول
يدعم FORGE ثلاث طرق لتسجيل الدخول مباشرة:
البريد الإلكتروني + كلمة المرور
تدفق تسجيل الدخول القياسي باستخدام البريد الإلكتروني وكلمة المرور.
// POST /api/auth/login
#[derive(Deserialize, Validate)]
pub struct LoginRequest {
#[validate(email)]
pub email: String,
#[validate(length(min = 8))]
pub password: String,
}الجوال + كلمة المرور
للتطبيقات التي تستخدم أرقام الهاتف كمعرّف رئيسي.
// POST /api/auth/login
#[derive(Deserialize, Validate)]
pub struct MobileLoginRequest {
#[validate(length(min = 8, max = 15))]
pub mobile: String,
#[validate(length(min = 8))]
pub password: String,
}الجوال + OTP
مصادقة بدون كلمة مرور باستخدام كلمات مرور لمرة واحدة تُرسل عبر SMS.
// Step 1: Request OTP
// POST /api/auth/send-otp
#[derive(Deserialize, Validate)]
pub struct SendOtpRequest {
#[validate(length(min = 8, max = 15))]
pub mobile: String,
}
// Step 2: Verify OTP and authenticate
// POST /api/auth/verify-otp
#[derive(Deserialize, Validate)]
pub struct VerifyOtpRequest {
#[validate(length(min = 8, max = 15))]
pub mobile: String,
#[validate(length(equal = 6))]
pub otp: String,
}المصادقة الثنائية للوحة الإدارة (2FA)
عند تفعيل كل من email_password و mobile_otp في إعدادات مشروعك، يُنشئ FORGE تلقائياً مصادقة ثنائية للوصول إلى لوحة الإدارة. هذا يوفر طبقة أمان إضافية للحسابات الإدارية.
تدفق المصادقة الثنائية
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ البريد/كلمة │ ──▶ │ إرسال OTP │ ──▶ │ التحقق من │ ──▶ لوحة الإدارة
│ المرور │ │ لجوال المدير │ │ OTP (6 أرقام) │
└─────────────────┘ └─────────────────┘ └─────────────────┘كيف تعمل
- المستخدم يُرسل البريد وكلمة المرور ← الخادم يتحقق من البيانات
- إذا كان لدى المستخدم رقم جوال ← الخادم يُنشئ OTP ويرسله للجوال ويُرجع
requires_otp: trueمع token مؤقت - المستخدم يُدخل OTP ← الخادم يتحقق من OTP والـ token المؤقت ويُرجع access tokens
- إذا لم يكن للمستخدم جوال ← يُتخطى 2FA ويستمر تسجيل الدخول العادي
أنواع استجابة تسجيل الدخول
عند تفعيل 2FA، تُرجع نقطة نهاية تسجيل الدخول استجابة موحدة:
تسجيل دخول عادي (بدون 2FA):
{
"success": true,
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "eyJhbGciOiJIUzI1NiIs...",
"user": { "id": "...", "name": "...", "email": "..." }
}
}مطلوب 2FA:
{
"success": true,
"data": {
"requires_otp": true,
"temp_token": "dXNlcl9pZDoxNzA2MTIzNDU2...",
"mobile_masked": "+971*****567"
}
}نقطة نهاية التحقق من 2FA
أكمل تدفق 2FA بالتحقق من OTP:
POST /api/auth/otp/verify-2fa
Content-Type: application/json
{
"temp_token": "dXNlcl9pZDoxNzA2MTIzNDU2...",
"code": "123456"
}الاستجابة (200):
{
"success": true,
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "eyJhbGciOiJIUzI1NiIs...",
"user": { "id": "...", "name": "...", "email": "..." }
}
}ميزات الأمان
| الميزة | الوصف |
|---|---|
| Token مؤقت | موقّع بـ HMAC-SHA256، صلاحية 5 دقائق |
| الحد الأقصى للمحاولات | 3 محاولات OTP قبل طلب إعادة تسجيل الدخول |
| إخفاء رقم الجوال | يُعرض +971*****567 للخصوصية |
| انتهاء صلاحية OTP | 5 دقائق (قابل للتعديل عبر OTP_EXPIRY) |
وضع التطوير
لتسهيل الاختبار، عندما يكون APP_ENV=development، رمز OTP دائماً 123456:
fn generate_otp_code(length: usize) -> String {
if std::env::var("APP_ENV").unwrap_or_default() == "development" {
return "123456".to_string();
}
// Production: generate random OTP
// ...
}بيانات المستخدمين الافتراضية
عند تفعيل mobile_otp، يتضمن المستخدمون الافتراضيون أرقام جوال:
| المستخدم | البريد | الجوال | كلمة المرور |
|---|---|---|---|
| مدير | admin@ | +971501234567 | password |
| مستخدم | user@ | +971509876543 | password |
اختبار المصادقة الثنائية
- شغّل
forge new myappمع تفعيل كل منemail_passwordوmobile_otp - شغّل API بـ
APP_ENV=development - سجّل الدخول في
/admin/loginبـadmin@{domain}/password - أدخل OTP الثابت
123456في شاشة التحقق
نقاط نهاية API
تسجيل الدخول
مصادقة مستخدم واستلام tokens.
POST /api/auth/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "securepassword"
}الاستجابة (200):
{
"success": true,
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 900,
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "محمد أحمد",
"email": "user@example.com"
}
}
}التسجيل
إنشاء حساب مستخدم جديد.
POST /api/auth/register
Content-Type: application/json
{
"name": "سارة علي",
"email": "sara@example.com",
"password": "securepassword",
"password_confirmation": "securepassword"
}تجديد Token
استبدال refresh token صالح بزوج tokens جديد.
POST /api/auth/refresh
Content-Type: application/json
{
"refresh_token": "eyJhbGciOiJIUzI1NiIs..."
}تحذير
Refresh tokens تُستخدم مرة واحدة. كل استدعاء لنقطة نهاية التجديد يُبطل refresh token السابق ويُصدر زوجاً جديداً. إذا استُخدم refresh token مرتين، يُلغى كلا الـ tokens كإجراء أمني (refresh token rotation).
تسجيل الخروج
إبطال tokens الجلسة الحالية.
POST /api/auth/logout
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...نسيت كلمة المرور
بدء تدفق إعادة تعيين كلمة المرور بإرسال رابط إعادة التعيين لبريد المستخدم.
POST /api/auth/forgot-password
Content-Type: application/json
{
"email": "user@example.com"
}إعادة تعيين كلمة المرور
إكمال إعادة تعيين كلمة المرور باستخدام الـ token من البريد الإلكتروني.
POST /api/auth/reset-password
Content-Type: application/json
{
"token": "reset-token-from-email",
"password": "newsecurepassword",
"password_confirmation": "newsecurepassword"
}إرسال OTP
طلب كلمة مرور لمرة واحدة للمصادقة عبر الجوال.
POST /api/auth/send-otp
Content-Type: application/json
{
"mobile": "+966500000000"
}التحقق من OTP
التحقق من OTP ومصادقة المستخدم.
POST /api/auth/verify-otp
Content-Type: application/json
{
"mobile": "+966500000000",
"otp": "123456"
}تجزئة كلمات المرور
جميع كلمات المرور تُجزّأ باستخدام Argon2id، الفائز في مسابقة Password Hashing Competition والخوارزمية المُوصى بها للتطبيقات الجديدة.
use argon2::{
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
Argon2,
};
/// Hash a plaintext password using Argon2id
pub fn hash_password(password: &str) -> Result<String, AppError> {
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let hash = argon2
.hash_password(password.as_bytes(), &salt)
.map_err(|_| AppError::InternalError("Failed to hash password".into()))?;
Ok(hash.to_string())
}
/// Verify a password against a stored hash
pub fn verify_password(password: &str, hash: &str) -> Result<bool, AppError> {
let parsed_hash = PasswordHash::new(hash)
.map_err(|_| AppError::InternalError("Invalid password hash".into()))?;
Ok(Argon2::default()
.verify_password(password.as_bytes(), &parsed_hash)
.is_ok())
}نصيحة
يستخدم FORGE خوارزمية Argon2id مع الإعدادات الافتراضية (19 ميجابايت ذاكرة، تكرارين، درجة واحدة من التوازي). هذه الإعدادات توفر أماناً قوياً مع إبقاء أوقات استجابة تسجيل الدخول أقل من 500 ملي ثانية على الأجهزة الحديثة.
بنية JWT Token
Access tokens تحمل هوية المستخدم وتُوقّع بـ HMAC-SHA256:
#[derive(Serialize, Deserialize)]
pub struct Claims {
pub sub: String, // User ID (UUID)
pub email: String, // User email
pub exp: usize, // Expiration timestamp
pub iat: usize, // Issued at timestamp
pub token_type: String, // "access" or "refresh"
}توليد Token:
use jsonwebtoken::{encode, Header, EncodingKey};
pub fn generate_access_token(user: &User, secret: &str) -> Result<String, AppError> {
let now = chrono::Utc::now();
let claims = Claims {
sub: user.id.to_string(),
email: user.email.clone(),
exp: (now + chrono::Duration::minutes(15)).timestamp() as usize,
iat: now.timestamp() as usize,
token_type: "access".to_string(),
};
encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(secret.as_bytes()),
)
.map_err(|_| AppError::InternalError("Failed to generate token".into()))
}تسليم Token
يمكن تسليم Tokens بطريقتين حسب نوع العميل:
HTTP-Only Cookie (مُوصى به لتطبيقات الويب):
let cookie = format!(
"access_token={}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=900",
access_token
);ترويسة Authorization (لعملاء الجوال/SPA):
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...تحذير
فضّل دائماً HttpOnly cookies للتطبيقات المبنية على المتصفح. إنها محصنة ضد هجمات XSS لأن JavaScript لا يمكنها الوصول لقيمة الـ token. استخدم ترويسة Authorization فقط للعملاء غير المتصفح (تطبيقات الجوال، أدوات CLI).
متغيرات البيئة
عدّل سلوك المصادقة عبر متغيرات البيئة:
# JWT signing secret (required, use a strong random value)
JWT_SECRET=your-256-bit-secret-key-here
# Token lifetimes
ACCESS_TOKEN_EXPIRY=900 # 15 minutes in seconds
REFRESH_TOKEN_EXPIRY=604800 # 7 days in seconds
# Password reset
RESET_TOKEN_EXPIRY=3600 # 1 hour in seconds
# OTP settings
OTP_LENGTH=6
OTP_EXPIRY=300 # 5 minutes in secondsانظر أيضاً
- التفويض — التحكم بالوصول المبني على الأدوار بعد المصادقة
- الـ Middleware — كيف يستخرج auth middleware الـ tokens ويتحقق منها
- معالجة الأخطاء — استجابات أخطاء المصادقة