Skip to content

المصادقة

يُنشئ FORGE نظام مصادقة كامل مبني على JWT مع دعم لطرق تسجيل دخول متعددة وتجديد الـ token ومعالجة آمنة لكلمات المرور. النظام يستخدم access tokens قصيرة العمر مقترنة بـ refresh tokens أطول عمراً، متبعاً أفضل الممارسات في الصناعة للمصادقة الـ stateless لواجهات API.

استراتيجية الـ Token

نظام المصادقة يستخدم نهج الـ dual-token:

Tokenالعمرالغرضالتخزين
Access Token15 دقيقةتفويض طلبات APIترويسة Authorization أو HttpOnly cookie
Refresh Token7 أيامالحصول على access tokens جديدةHttpOnly cookie
┌──────────┐     تسجيل دخول  ┌──────────┐
│  العميل  │ ──────────────▶│  الخادم  │
│          │◀────────────── │          │
│          │  Access Token  │          │
│          │  Refresh Token │          │
│          │                │          │
│          │  طلب API       │          │
│          │ ──────────────▶│          │
│          │  (Access Token)│          │
│          │                │          │
│          │  Token منتهي   │          │
│          │ ──────────────▶│          │
│          │ (Refresh Token)│          │
│          │◀────────────── │          │
│          │  Tokens جديدة  │          │
└──────────┘                └──────────┘

نصيحة

Access tokens قصيرة العمر عمداً. عندما تنتهي صلاحية access token، يجب على العميل استدعاء نقطة نهاية التجديد للحصول على زوج جديد من الـ tokens بدون الحاجة لإعادة تسجيل دخول المستخدم.

طرق تسجيل الدخول

يدعم FORGE ثلاث طرق لتسجيل الدخول مباشرة:

البريد الإلكتروني + كلمة المرور

تدفق تسجيل الدخول القياسي باستخدام البريد الإلكتروني وكلمة المرور.

rust
// POST /api/auth/login
#[derive(Deserialize, Validate)]
pub struct LoginRequest {
    #[validate(email)]
    pub email: String,
    #[validate(length(min = 8))]
    pub password: String,
}

الجوال + كلمة المرور

للتطبيقات التي تستخدم أرقام الهاتف كمعرّف رئيسي.

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

rust
// 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 أرقام) │
└─────────────────┘     └─────────────────┘     └─────────────────┘

كيف تعمل

  1. المستخدم يُرسل البريد وكلمة المرور ← الخادم يتحقق من البيانات
  2. إذا كان لدى المستخدم رقم جوال ← الخادم يُنشئ OTP ويرسله للجوال ويُرجع requires_otp: true مع token مؤقت
  3. المستخدم يُدخل OTP ← الخادم يتحقق من OTP والـ token المؤقت ويُرجع access tokens
  4. إذا لم يكن للمستخدم جوال ← يُتخطى 2FA ويستمر تسجيل الدخول العادي

أنواع استجابة تسجيل الدخول

عند تفعيل 2FA، تُرجع نقطة نهاية تسجيل الدخول استجابة موحدة:

تسجيل دخول عادي (بدون 2FA):

json
{
  "success": true,
  "data": {
    "access_token": "eyJhbGciOiJIUzI1NiIs...",
    "refresh_token": "eyJhbGciOiJIUzI1NiIs...",
    "user": { "id": "...", "name": "...", "email": "..." }
  }
}

مطلوب 2FA:

json
{
  "success": true,
  "data": {
    "requires_otp": true,
    "temp_token": "dXNlcl9pZDoxNzA2MTIzNDU2...",
    "mobile_masked": "+971*****567"
  }
}

نقطة نهاية التحقق من 2FA

أكمل تدفق 2FA بالتحقق من OTP:

bash
POST /api/auth/otp/verify-2fa
Content-Type: application/json

{
  "temp_token": "dXNlcl9pZDoxNzA2MTIzNDU2...",
  "code": "123456"
}

الاستجابة (200):

json
{
  "success": true,
  "data": {
    "access_token": "eyJhbGciOiJIUzI1NiIs...",
    "refresh_token": "eyJhbGciOiJIUzI1NiIs...",
    "user": { "id": "...", "name": "...", "email": "..." }
  }
}

ميزات الأمان

الميزةالوصف
Token مؤقتموقّع بـ HMAC-SHA256، صلاحية 5 دقائق
الحد الأقصى للمحاولات3 محاولات OTP قبل طلب إعادة تسجيل الدخول
إخفاء رقم الجواليُعرض +971*****567 للخصوصية
انتهاء صلاحية OTP5 دقائق (قابل للتعديل عبر OTP_EXPIRY)

وضع التطوير

لتسهيل الاختبار، عندما يكون APP_ENV=development، رمز OTP دائماً 123456:

rust
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@+971501234567password
مستخدمuser@+971509876543password

اختبار المصادقة الثنائية

  1. شغّل forge new myapp مع تفعيل كل من email_password و mobile_otp
  2. شغّل API بـ APP_ENV=development
  3. سجّل الدخول في /admin/login بـ admin@{domain} / password
  4. أدخل OTP الثابت 123456 في شاشة التحقق

نقاط نهاية API

تسجيل الدخول

مصادقة مستخدم واستلام tokens.

bash
POST /api/auth/login
Content-Type: application/json

{
  "email": "user@example.com",
  "password": "securepassword"
}

الاستجابة (200):

json
{
  "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"
    }
  }
}

التسجيل

إنشاء حساب مستخدم جديد.

bash
POST /api/auth/register
Content-Type: application/json

{
  "name": "سارة علي",
  "email": "sara@example.com",
  "password": "securepassword",
  "password_confirmation": "securepassword"
}

تجديد Token

استبدال refresh token صالح بزوج tokens جديد.

bash
POST /api/auth/refresh
Content-Type: application/json

{
  "refresh_token": "eyJhbGciOiJIUzI1NiIs..."
}

تحذير

Refresh tokens تُستخدم مرة واحدة. كل استدعاء لنقطة نهاية التجديد يُبطل refresh token السابق ويُصدر زوجاً جديداً. إذا استُخدم refresh token مرتين، يُلغى كلا الـ tokens كإجراء أمني (refresh token rotation).

تسجيل الخروج

إبطال tokens الجلسة الحالية.

bash
POST /api/auth/logout
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

نسيت كلمة المرور

بدء تدفق إعادة تعيين كلمة المرور بإرسال رابط إعادة التعيين لبريد المستخدم.

bash
POST /api/auth/forgot-password
Content-Type: application/json

{
  "email": "user@example.com"
}

إعادة تعيين كلمة المرور

إكمال إعادة تعيين كلمة المرور باستخدام الـ token من البريد الإلكتروني.

bash
POST /api/auth/reset-password
Content-Type: application/json

{
  "token": "reset-token-from-email",
  "password": "newsecurepassword",
  "password_confirmation": "newsecurepassword"
}

إرسال OTP

طلب كلمة مرور لمرة واحدة للمصادقة عبر الجوال.

bash
POST /api/auth/send-otp
Content-Type: application/json

{
  "mobile": "+966500000000"
}

التحقق من OTP

التحقق من OTP ومصادقة المستخدم.

bash
POST /api/auth/verify-otp
Content-Type: application/json

{
  "mobile": "+966500000000",
  "otp": "123456"
}

تجزئة كلمات المرور

جميع كلمات المرور تُجزّأ باستخدام Argon2id، الفائز في مسابقة Password Hashing Competition والخوارزمية المُوصى بها للتطبيقات الجديدة.

rust
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:

rust
#[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:

rust
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 (مُوصى به لتطبيقات الويب):

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

متغيرات البيئة

عدّل سلوك المصادقة عبر متغيرات البيئة:

bash
# 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

انظر أيضاً

Released under the MIT License.