دليل شامل لـ OAuth 2.0 و JWT
OAuth 2.0 ورموز الويب JSON (JWT) هي تقنيات أساسية للمصادقة والترخيص في واجهات API الحديثة. فهم آلياتها الداخلية وتدفقاتها وأفضل ممارساتها أمر بالغ الأهمية لبناء واجهات API آمنة وقابلة للتطوير.
فهم OAuth 2.0
OAuth 2.0 هو إطار ترخيص يمكّن التطبيقات من الحصول على وصول محدود لحسابات المستخدمين على خدمة HTTP. يعمل عن طريق تفويض مصادقة المستخدم للخدمة التي تستضيف حساب المستخدم.
مصطلحات OAuth 2.0 الرئيسية:
- مالك المورد: المستخدم الذي يملك البيانات
- العميل: التطبيق الذي يطلب الوصول إلى بيانات المستخدم
- خادم الترخيص: يصدر رموز الوصول بعد مصادقة المستخدم
- خادم الموارد: خادم API الذي يستضيف الموارد المحمية
- رمز الوصول: بيانات الاعتماد المستخدمة للوصول إلى الموارد المحمية
- رمز التحديث: بيانات الاعتماد المستخدمة للحصول على رموز وصول جديدة
- النطاق: يحدد مستوى الوصول الممنوح
أنواع منح OAuth 2.0 (التدفقات)
1. تدفق رمز الترخيص (الأكثر أماناً)
الأفضل لتطبيقات الويب من جانب الخادم حيث يمكن تخزين سر العميل بشكل آمن.
<?php
// الخطوة 1: إعادة توجيه المستخدم إلى نقطة نهاية الترخيص
Route::get('/auth/authorize', function () {
$query = http_build_query([
'client_id' => config('oauth.client_id'),
'redirect_uri' => route('auth.callback'),
'response_type' => 'code',
'scope' => 'read write',
'state' => Str::random(40), // حماية CSRF
]);
return redirect('https://auth-server.com/oauth/authorize?' . $query);
});
// الخطوة 2: معالجة الاستدعاء مع رمز الترخيص
Route::get('/auth/callback', function (Request $request) {
// التحقق من الحالة لمنع CSRF
if ($request->state !== session('oauth_state')) {
abort(403, 'حالة غير صالحة');
}
// تبادل رمز الترخيص برمز الوصول
$response = Http::asForm()->post('https://auth-server.com/oauth/token', [
'grant_type' => 'authorization_code',
'client_id' => config('oauth.client_id'),
'client_secret' => config('oauth.client_secret'),
'redirect_uri' => route('auth.callback'),
'code' => $request->code,
]);
$tokens = $response->json();
// تخزين الرموز بشكل آمن
auth()->user()->update([
'access_token' => encrypt($tokens['access_token']),
'refresh_token' => encrypt($tokens['refresh_token']),
'expires_at' => now()->addSeconds($tokens['expires_in'])
]);
return redirect('/dashboard');
});
// الخطوة 3: استخدام رمز الوصول لإجراء طلبات API
Route::get('/api/data', function () {
$accessToken = decrypt(auth()->user()->access_token);
$response = Http::withToken($accessToken)
->get('https://api.example.com/data');
return $response->json();
});
2. تدفق بيانات اعتماد العميل
يستخدم للمصادقة من آلة إلى آلة حيث لا يوجد مستخدم متورط.
<?php
// طلب رمز الوصول باستخدام بيانات اعتماد العميل
$response = Http::asForm()->post('https://auth-server.com/oauth/token', [
'grant_type' => 'client_credentials',
'client_id' => config('oauth.client_id'),
'client_secret' => config('oauth.client_secret'),
'scope' => 'api.read api.write'
]);
$accessToken = $response->json('access_token');
// استخدام الرمز لطلبات API
$apiResponse = Http::withToken($accessToken)
->get('https://api.example.com/resources');
// مثال Laravel Passport
use Laravel\Passport\Client;
Route::post('/oauth/token', function (Request $request) {
$client = Client::where('id', $request->client_id)
->where('secret', $request->client_secret)
->firstOrFail();
// التحقق من بيانات اعتماد العميل وإصدار الرمز
$token = $client->createToken('API Access', [
'api.read',
'api.write'
])->accessToken;
return response()->json([
'access_token' => $token,
'token_type' => 'Bearer',
'expires_in' => 3600
]);
});
3. منحة كلمة المرور (قديمة، غير موصى بها)
استخدم فقط لتطبيقات الطرف الأول التي تثق بها تماماً.
<?php
// ⚠️ استخدم فقط لعملاء الطرف الأول الموثوق بهم
$response = Http::asForm()->post('https://auth-server.com/oauth/token', [
'grant_type' => 'password',
'client_id' => config('oauth.client_id'),
'client_secret' => config('oauth.client_secret'),
'username' => $request->email,
'password' => $request->password,
'scope' => '*'
]);
$tokens = $response->json();
إلغاء منحة كلمة المرور: OAuth 2.1 يزيل منحة كلمة المرور. استخدم رمز الترخيص مع PKCE بدلاً من ذلك، حتى لتطبيقات الطرف الأول.
4. رمز الترخيص مع PKCE
أمان محسّن للأجهزة المحمولة وتطبيقات الصفحة الواحدة التي لا يمكنها تخزين أسرار العميل بشكل آمن.
<?php
// توليد مُحقق الكود والتحدي
function generatePKCE(): array
{
$verifier = Str::random(128);
$challenge = rtrim(strtr(base64_encode(hash('sha256', $verifier, true)), '+/', '-_'), '=');
return [
'verifier' => $verifier,
'challenge' => $challenge
];
}
// الخطوة 1: طلب الترخيص مع PKCE
Route::get('/auth/login', function () {
$pkce = generatePKCE();
session(['code_verifier' => $pkce['verifier']]);
$query = http_build_query([
'client_id' => config('oauth.client_id'),
'redirect_uri' => route('auth.callback'),
'response_type' => 'code',
'scope' => 'read write',
'code_challenge' => $pkce['challenge'],
'code_challenge_method' => 'S256'
]);
return redirect('https://auth-server.com/oauth/authorize?' . $query);
});
// الخطوة 2: طلب الرمز مع مُحقق الكود
Route::get('/auth/callback', function (Request $request) {
$response = Http::asForm()->post('https://auth-server.com/oauth/token', [
'grant_type' => 'authorization_code',
'client_id' => config('oauth.client_id'),
// لا حاجة لـ client_secret مع PKCE!
'redirect_uri' => route('auth.callback'),
'code' => $request->code,
'code_verifier' => session('code_verifier')
]);
return $response->json();
});
شرح رموز الويب JSON (JWT)
JWT هي وسيلة مدمجة وآمنة للعنوان URL لتمثيل المطالبات المراد نقلها بين طرفين. يتكون JWT من ثلاثة أجزاء مفصولة بنقاط: الرأس.الحمولة.التوقيع
بنية JWT
// مثال JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
// فك الترميز:
// الرأس
{
"alg": "HS256", // الخوارزمية: HMAC SHA-256
"typ": "JWT" // نوع الرمز
}
// الحمولة (المطالبات)
{
"sub": "1234567890", // الموضوع (معرف المستخدم)
"name": "John Doe", // مطالبة مخصصة
"iat": 1516239022, // صدر في
"exp": 1516242622, // وقت انتهاء الصلاحية
"aud": "https://api.example.com", // الجمهور
"iss": "https://auth.example.com" // المُصدر
}
// التوقيع
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
إنشاء والتحقق من JWT في Laravel
<?php
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
// إنشاء JWT
function createJWT(User $user): string
{
$payload = [
'iss' => config('app.url'), // المُصدر
'sub' => $user->id, // الموضوع (معرف المستخدم)
'iat' => time(), // صدر في
'exp' => time() + (60 * 60), // ينتهي خلال ساعة واحدة
'nbf' => time(), // ليس قبل
'jti' => Str::uuid()->toString(), // معرف JWT (معرف فريد)
// مطالبات مخصصة
'email' => $user->email,
'role' => $user->role,
'scopes' => ['read', 'write']
];
return JWT::encode($payload, config('jwt.secret'), 'HS256');
}
// التحقق وفك ترميز JWT
function verifyJWT(string $token): object
{
try {
$decoded = JWT::decode(
$token,
new Key(config('jwt.secret'), 'HS256')
);
// التحقق الإضافي
if ($decoded->exp < time()) {
throw new Exception('انتهت صلاحية الرمز');
}
if ($decoded->iss !== config('app.url')) {
throw new Exception('مُصدر غير صالح');
}
return $decoded;
} catch (Exception $e) {
throw new AuthenticationException('رمز غير صالح: ' . $e->getMessage());
}
}
// وسيطة للتحقق من JWT
class JwtAuthentication
{
public function handle(Request $request, Closure $next)
{
$token = $request->bearerToken();
if (!$token) {
return response()->json([
'errors' => [[
'status' => '401',
'title' => 'غير مصرح',
'detail' => 'لم يتم تقديم رمز'
]]
], 401);
}
try {
$decoded = verifyJWT($token);
// إرفاق المستخدم بالطلب
$request->merge(['authenticated_user' => User::find($decoded->sub)]);
return $next($request);
} catch (AuthenticationException $e) {
return response()->json([
'errors' => [[
'status' => '401',
'title' => 'غير مصرح',
'detail' => $e->getMessage()
]]
], 401);
}
}
}
رموز التحديث
يجب أن يكون لرموز الوصول عمر قصير. تسمح رموز التحديث للعملاء بالحصول على رموز وصول جديدة دون إعادة المصادقة.
<?php
// توليد زوج الرموز
public function login(Request $request)
{
$credentials = $request->only('email', 'password');
if (!auth()->attempt($credentials)) {
return response()->json([
'errors' => [[
'status' => '401',
'title' => 'فشلت المصادقة',
'detail' => 'بيانات اعتماد غير صالحة'
]]
], 401);
}
$user = auth()->user();
// إنشاء رمز الوصول (قصير الأجل: 15 دقيقة)
$accessToken = createJWT($user);
// إنشاء رمز التحديث (طويل الأجل: 30 يوماً)
$refreshToken = Str::random(64);
$user->refreshTokens()->create([
'token' => hash('sha256', $refreshToken),
'expires_at' => now()->addDays(30)
]);
return response()->json([
'access_token' => $accessToken,
'refresh_token' => $refreshToken,
'token_type' => 'Bearer',
'expires_in' => 900 // 15 دقيقة
]);
}
// تحديث رمز الوصول
public function refresh(Request $request)
{
$refreshToken = $request->input('refresh_token');
if (!$refreshToken) {
return response()->json([
'errors' => [[
'status' => '400',
'title' => 'طلب سيء',
'detail' => 'رمز التحديث مطلوب'
]]
], 400);
}
// ابحث عن الرمز في قاعدة البيانات
$tokenRecord = RefreshToken::where('token', hash('sha256', $refreshToken))
->where('expires_at', '>', now())
->where('revoked', false)
->first();
if (!$tokenRecord) {
return response()->json([
'errors' => [[
'status' => '401',
'title' => 'غير مصرح',
'detail' => 'رمز تحديث غير صالح أو منتهي الصلاحية'
]]
], 401);
}
$user = $tokenRecord->user;
// إصدار رمز وصول جديد
$newAccessToken = createJWT($user);
// اختياري: تنفيذ دوران الرمز
// إلغاء رمز التحديث القديم وإصدار واحد جديد
$tokenRecord->update(['revoked' => true]);
$newRefreshToken = Str::random(64);
$user->refreshTokens()->create([
'token' => hash('sha256', $newRefreshToken),
'expires_at' => now()->addDays(30)
]);
return response()->json([
'access_token' => $newAccessToken,
'refresh_token' => $newRefreshToken,
'token_type' => 'Bearer',
'expires_in' => 900
]);
}
دوران الرمز والكشف عن إعادة الاستخدام
يحسن دوران الرمز الأمان من خلال إصدار رمز تحديث جديد مع كل طلب تحديث.
<?php
// ترحيل رموز التحديث
Schema::create('refresh_tokens', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('token', 64)->unique();
$table->string('family', 36); // تتبع عائلة الرمز لاكتشاف إعادة الاستخدام
$table->boolean('revoked')->default(false);
$table->timestamp('expires_at');
$table->timestamp('used_at')->nullable();
$table->timestamps();
$table->index(['user_id', 'family']);
});
// تنفيذ التحديث مع الدوران والكشف عن إعادة الاستخدام
public function refresh(Request $request)
{
$refreshToken = $request->input('refresh_token');
$hashedToken = hash('sha256', $refreshToken);
$tokenRecord = RefreshToken::where('token', $hashedToken)
->where('expires_at', '>', now())
->first();
if (!$tokenRecord) {
return response()->json(['errors' => [[
'status' => '401',
'detail' => 'رمز تحديث غير صالح'
]]], 401);
}
// الكشف عن إعادة الاستخدام: إذا تم استخدام الرمز بالفعل، ألغِ العائلة بأكملها
if ($tokenRecord->used_at !== null) {
RefreshToken::where('family', $tokenRecord->family)
->update(['revoked' => true]);
return response()->json(['errors' => [[
'status' => '401',
'detail' => 'تم اكتشاف إعادة استخدام الرمز. تم إلغاء جميع الرموز.'
]]], 401);
}
$user = $tokenRecord->user;
// وضع علامة على الرمز الحالي كمستخدم
$tokenRecord->update(['used_at' => now()]);
// إصدار رموز جديدة في نفس العائلة
$newAccessToken = createJWT($user);
$newRefreshToken = Str::random(64);
$user->refreshTokens()->create([
'token' => hash('sha256', $newRefreshToken),
'family' => $tokenRecord->family,
'expires_at' => now()->addDays(30)
]);
return response()->json([
'access_token' => $newAccessToken,
'refresh_token' => $newRefreshToken,
'token_type' => 'Bearer',
'expires_in' => 900
]);
}
فوائد دوران الرمز: إذا تمت سرقة رمز التحديث، يمكن استخدامه مرة واحدة فقط. عندما يحاول المستخدم الشرعي استخدام نسخته، يتم اكتشاف إعادة الاستخدام ويتم إلغاء جميع الرموز في تلك العائلة.
أفضل ممارسات JWT
توصيات الأمان:
- اجعل رموز الوصول قصيرة الأجل (5-15 دقيقة)
- استخدم RS256 (RSA) بدلاً من HS256 لواجهات API العامة
- لا تخزن أبداً بيانات حساسة في حمولة JWT (إنها ليست مشفرة، موقعة فقط)
- تحقق دائماً من مطالبات `exp`، `iss`، `aud`
- قم بتنفيذ إلغاء الرمز لتسجيل الخروج
- استخدم دوران رمز التحديث مع الكشف عن إعادة الاستخدام
- قم بتخزين رموز التحديث بشكل آمن (مُجزأة في قاعدة البيانات)
- استخدم HTTPS فقط
- قم بتنفيذ تحديد المعدل على نقاط نهاية الرمز
تمرين:
- قم بتنفيذ تدفق رمز ترخيص OAuth 2.0 مع PKCE
- أنشئ نظام مصادقة JWT مع مطالبات مخصصة
- ابنِ نظام رمز تحديث مع دوران الرمز
- قم بتنفيذ الكشف عن إعادة الاستخدام لمنع سرقة الرمز
- أضف ترخيصاً قائماً على النطاق إلى نقاط نهاية API الخاصة بك
- أنشئ نقطة نهاية إلغاء الرمز لتسجيل الخروج
أخطاء JWT الشائعة:
- استخدام `alg: none` - حدد دائماً خوارزمية
- عدم التحقق من التوقيع - تحقق دائماً من الرموز
- تخزين كلمات المرور أو البيانات الحساسة في JWT - لا تفعل هذا أبداً
- استخدام رموز وصول طويلة الأجل - اجعلها قصيرة
- عدم تنفيذ إلغاء الرمز - خطط لتسجيل الخروج/الرموز المخترقة