Understanding Authentication Security
A deep dive into system design flaws, JWT security, Access & Refresh Token Rotation, HTTPS limitations, Certificate Pinning, and preventing IDOR attacks.
في يوم من الأيام، كنت قاعد مع صاحبي اللي بيدرس Cyber Security وبنتكلم في الثغرات اللي بيلاقيها في برامج ومواقع كتير، وساعتها لفت نظري حاجة، إن كل Request بيبعتها أي تطبيق لسيرفر بيكون لازم يجاوب على سؤال بسيط جدًا:
“أنا أثق في اللي بعتلي الـ Request دي ليه؟”
ومن هنا بدأت أكتشف إن معظم الـ Security Mechanisms اللي الشركات بتستخدمها كل يوم زي الـ JWT، والـ HTTPS، والـ RBAC، والـ Certificate Pinning، وغيرهم هي في الآخر مجرد طرق مختلفة للإجابة على السؤال ده.
وأثناء ما كنت بقرأ عن الموضوع، اكتشفت إن أغلب الـ Attacks أصلًا مش بتحاول تكسر Encryption قوي أو تعمل حاجة خارقة، بس بتحاول تستغل نقطة في النظام خلت السيرفر يثق في شخص أو جهاز أو Request مكنش المفروض يثق فيه.
وده اللي بيفتح الباب لهجمات كتير زي Token Theft، وMan-in-the-Middle، وReplay Attack، وIDOR، وغيرهم كتير.
فقررت أبحث عن الطريقة اللي الأنظمة الحقيقية بتتعامل بيها مع السيناريوهات دي، وإزاي أقدر أبني Authentication System يفضل آمن حتى لو واحدة من طبقات الحماية اتخطت. وأول حاجة اكتشفتها إن الثقة دي مبتتبنيش مرة واحدة وقت الـ Login وخلاص. لأن بعد ما اليوزر يعمل Login، هيبعت عشرات أو مئات الـ Requests بعد كده. فإزاي السيرفر يعرف إن كل Request منهم جاية من نفس اليوزر من غير ما يطلب منه الـ Username والـ Password كل مرة؟ ده بالظبط السؤال اللي الـ Tokens اتعملت عشان تجاوب عليه.
فقبل ما نكمل لازم نفهم يعني ايه Token الأول.
يعني إيه Token؟
لنفترض إن عندك تطبيق لطلب الأكل مثلا. لما يبدأ يجيلك Users فأول حاجة اليوزر من دول هيعملها هو إنه هيعمل أكونت أو يـ Login لو عنده أكونت، ودول هما الحالتين الوحيدين اللي بيتبعت فيهم فعلا الـ Username والـ Password من التطبيق للسيرفر. ولما بيتبعتوا السيرفر بيتأكد إنهم صح من الـ Database وبعد كده بدل ما يخلي التطبيق يبعت كلمة السر مع كل Request، بيديله Token. تقدر تعتبر الـ Token ده كأنه تصريح دخول مؤقت. من اللحظة دي كل Request تطبيقك بيبعته للسيرفر سواء عشان يطلب أوردر أو يعدل بياناته الشخصية أو يشوف ايه المطاعم المتاحة بيكون رايح ومعاه Token بدل الـ Username والـ Password. وبالتالي كلمة السر مبتتنقلش على الشبكة كل شوية وولا بتتخزن في الـ Local Storage بتاع تطبيقك وده في حد ذاته خطوة أأمن.
وأشهر نوع من الـ Tokens هو الـ JSON Web Token (JWT)، وده بيكون بيحتوي على مجموعة من البيانات اسمها Claims، بتكون بيانات زي مثلًا الـ User ID، ومعاد الـ Expiration بتاعه، والـ Role بتاعة اليوزر إذا كان مثلا Customer ولا Restaurant Owner ولا Admin، وأي بيانات تانية السيستم بتاعك بيحتاجها.
وهنا ممكن يجي في بالك سؤال مهم:
“ليه بنحط المعلومات دي كلها في الـ JWT ونسيبها مع اليوزر؟ ما الأسهل إن السيرفر يحتفظ بيها عنده وخلاص؟”
الـ JWT مجرد واحدة من كذا طريقة نقدر نبني بيها نظام الـ Authentication، فقبل ما نقدر نجاوب على السؤال ده ونفهم هو شغال كده ليه، خلينا الأول نقارنه بنظام Authentication غيره شغال بطريقة مختلفة.
Stateful Sessions vs Stateless JWT
زمان، أغلب المواقع كانت بتستخدم نظام اسمه Sessions، ولسه في شركات كتير بتستخدمه لحد النهارده. الفكرة فيه بسيطة جدًا. أول ما اليوزر يعمل Login بالـ Username والـ Password، السيرفر الأول بيتأكد إن البيانات صحيحة، وبعدها بيـ Generate رقم عشوائي طويل اسمه Session ID. الرقم ده لوحده مبيحتويش على أي معلومات عن اليوزر، هو مجرد Identifier عشوائي.
بعد كده السيرفر بيسجل عنده في الـ Database أو في الـ Memory إن الـ Session ID ده خاص باليوزر الفولاني، ويبعت الـ Session ID ده للتطبيق. ومن اللحظة دي فالتطبيق مش بيحتاج يبعت الـ Username والـ Password تاني وكل اللي بيبعته مع أي Request هو الـ Session ID. ولما السيرفر يستقبل الـ Request، أول حاجة بيعملها إنه يقرأ الـ Session ID ويدور عليه عنده، ولما يلاقيه ويعرف اليوزر ده مين بيكمل تنفيذ الـ Request.
وده شكل الـ Flow:
الطريقة دي شغالة كويس جدًا ومتجربة ويعتمد عليها في أنظمة كتير، بس مشكلتها إنها بتخلي السيرفر محتاج يحتفظ بحالة كل يوزر عامل Login. وكل Request بيوصله لازم يرجع يدور على الـ Session الخاصة بيه عشان يعرف هو مين. وده اللي بنسميه Stateful Authentication، لأن السيرفر بيكون محتفظ بـ State أو حالة كل يوزر.
بس مع الأنظمة الحديثة وخصوصًا اللي بتشتغل على أكتر من Server أو فيها Microservices كتير، الاحتفاظ بالـ Sessions وإدارتها بين كل السيرفرات بيبقى أعقد شويتين.
فهنا ظهر اتجاه مختلف اسمه Stateless Authentication.
بدل ما السيرفر يحتفظ بالمعلومات عنده، هو بيحط المعلومات الأساسية اللي هيحتاجها جوا الـ Token نفسه ويبعتها للتطبيق. وبالتالي كل Request بيبقى شايل معاه المعلومات اللي السيرفر محتاجها عشان يعرف هوية اليوزر من غير ما يضطر يرجع يدور على Session متخزنة عنده أو عند سيرفر تاني.
وده اللي بيخلي الـ JWT مناسب جدًا للـ Distributed Systems والـ APIs الحديثة، ﻹن أي سيرفر يقدر يستقبل الـ Request ويتحقق من الـ Token ويكمل شغله عادي من غير ما يعتمد على Session ميعرفش لسه هي متخزنة فين أصلا.
وده شكل الـ Flow في الحالة دي:
وده السبب اللي بيخلي النوع ده اسمه Stateless، لأن السيرفر مش محتاج يحتفظ بحالة كل يوزر بين الـ Requests، وكل Request بيكون مستقل عن اللي قبله.
وده ميخليش الـ JWT أحسن من الـ Sessions بشكل مطلق، كل واحد فيهم له مميزاته واستخداماته، بس في الأنظمة الحديثة المبنية على APIs كتير و Microservices فالـ JWT هو من أكتر الحلول العملية.
بس وجود البيانات دي كلها جوا الـ Token بيفتح سؤال جديد.
“طالما الـ Token موجود عند اليوزر، إيه اللي يمنعه إنه يفتحه، يغير الـ User ID أو الـ Role أو أي Claim تاني، ويبعت النسخة المتعدلة للسيرفر؟”
المفاجأة إن الـ JWT مبيكونش Encrypted أصلًا. أي حد يقدر يعديه على Base64URL Decoder وهيعرف يقرأ كل البيانات اللي جواه. بس ده مش معناه إن أي حد يقدر يعدلها ويبعتها للسيرفر من غير ما ناخد بالنا. ده بالظبط الدور اللي الـ Signature معمول عشانه، لأنه بيخلينا نعرف على طول لو في أي جزء من الـ Token اتغير بعد ما خرج من السيرفر.
بس يعني إيه Signature أصلًا؟ وإزاي بيقدر يكتشف أي تعديل؟ عشان نفهم ده لازم الأول نعرف الـ JWT نفسه متصمم إزاي.
كيفية تصميم JWT Token
الـ Token في JWT بيكون متقسم لـ 3 أجزاء، الجزء الأول بيسموه الـ Header وده بيحتوي على شوية Metadata خاصة بالـ Token وبيتعمله Base64URL encoding، والجزء التاني هو الـ Payload وده بيحتوي على الـ Claims اللي اتكلمنا عليها من شوية اللي بتكون عادة معلومات عن اليوزر زي الـ User ID بتاعه وبرضو بيتعمله Base64URL encoding، والجزء التالت عشان نعمله فده له طريقتين معروفين، HS256 و RS256، فلو هنستخدم HS256 مثلا فهنجمعله أول جزئين اللي هما الـ Header والـ Payload، وهنـ Generate حاجة اسمها Secret Key وده بيكون عبارة عن Random String طويل أوي ومستحيل عمليًا يتخمن أو يتعمله Brute Force، ودول بنعديهم على عملية حسابية معقدة باستخدام Algorithm معين وبنسجل ناتج العملية دي في الجزء التالت نفسه وهو ده اللي بنسميه Signature. فده معناه إن محتوى الجزء ده متحدد على أساس تلاتة Variables أساسيين، Header و Payload و Secret Key. وفي الآخر الـ Formula بتاعتنا بتبقى بالشكل الآتي:
HS256(base64url(header) + "." + base64url(payload), secret key) -> signature
وفي الآخر بنعدي الـ Signature على Base64URL encoding برضو، وبيبقى ده شكل الـ Token على بعضه:
base64url(header).base64url(payload).base64url(signature)
وبعد كل ده، الـ Token بيتبعت للتطبيق. ولما التطبيق يبعته تاني للسيرفر بدل الـ Username والـ Password في أي Request بعد كده السيرفر بياخد الـ Header والـ Payload اللي في الـ Token اللي اتبعتله ده مع الـ Secret Key المتسجل عندنا ويعديهم على نفس الـ Algorithm ويشوف هيطلعوا نفس الـ Signature اللي موجود في الـ Token ولا لا.
عشان ناخد مثال لتوضيح الفكرة، هنفترض إن الهاكر وصل للـ Token بأي شكل من الأشكال وجرب يعمله decoding، فهيعرف يحوله من شكل زي ده:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZW1haWwiOiJqb2huLmRvZUBleGFtcGxlLmNvbSIsInJvbGUiOiJjdXN0b21lciIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MjQyNjIyfQ.qehgEr3CxVP_4z5UAt4FqVhIq-ORPaBNamHac6P0Fd8
للشكل ده:
{"alg":"HS256", "typ":"JWT"}
{"sub":"1234567890", "name":"John Doe", "email":"[email protected]", "role":"customer", "iat":1516239022, "exp":1516242622}
+,U?3@-Z9֦:?A]
السطر الأول هو الـ Metadata زي ما قولنا، والتاني هو الـ Payload، والسطر التالت اللي فيه كلام مش مفهوم ده هو الـ Signature بتاعتنا.
الهاكر لو جرب يعدل الـ Value بتاعة role من customer لـ admin مثلا وعمل encoding للـ Token تاني وبعته للسيرفر، السيرفر هيعمل نفس العملية الحسابية اللي عملها وهو بيعمل الـ Token تاني، بس المرادي بالـ Payload المختلف:
HS256(base64url(header) + "." + base64url(modified payload), secret key) -> new signature
فهنا هنلاقي إن الـ Signature اللي طلعتنا هتبقى مختلفة عن الـ Signature اللي جاية مع الـ Token، عشان الـ Payload اللي تم استخدامه في حسابه اختلف، فمستحيل يحصل Matching بين ناتجين واحنا مغيرين Variable فيهم عن التاني. ففي الحالة دي الهاكر مستحيل يقدر يعمل Signature سليم لإنه هيحتاج الـ Secret Key بتاعك عشان من غيره المعادلة هتبقى ناقصة، ولو بعت الـ Token بنفس الـ Signature اللي طلع من السيرفر بيه بس مغير جزء تاني منه كده ناتج المعادلة هيتغير فمش هيحصل Matching زي ما لسه حاصل. فده معناه إن طالما الـ Secret key بتاعك متسربش، الهاكر معندهوش أي طريقة يعدل بيها على الـ Token من غير ما تاخد بالك.
وده شكل الـ Flow الكامل:
يعني قبل ما أي Endpoint يشتغل أصلًا بيكون السيرفر خلص كل الخطوات دي. ولو أي خطوة فشلت، الـ Request بيقف من قبل ما يدخل على الـ Business Logic.
دلوقتي عرفنا ازاي نصمم JWT Token سليم نقدر نبعته للتطبيق واحنا متطمنين. بس في حاجة زيادة لسه متكلمناش عنها: في أغلب أنظمة الـ JWT، السيرفر مش بيطلع Token واحد بس، بيطلع اتنين.
Access Token vs Refresh Token
نوع الـ Token الأول اللي ببيطلع من الـ JWT اسمه Access Token، وده اللي تطبيقك بيحتاج يبعته مع كل Request عشان يثبت هوية اليوزر، وده عمره بيكون قصير في الغالب كذا دقيقة أو ساعة بالكتير مثلا عشان نقلل من فايدته للهاكر على قد ما نقدر حتى لو حصل وعرف يسرقه من اليوزر.
ونوع الـ Token التاني اسمه Refresh Token، وده عمره أطول بكتير، ممكن يبقى بالأسابيع أو بالشهور. بس التطبيق مش بيبعته مع كل Request، بيبعته بس لما الـ Access Token يحصله Expire بهدف إنه يطلب من السيرفر إنه يطلع Access Token جديد لليوزر من غير ما يضطر يعمل Login كل شوية. بالطريقة دي اليوزر بيفضل مسجل دخوله لأسابيع أو شهور وفي نفس الوقت الـ Access Token نفسه بيفضل عمره قصير.
وطبعًا بسبب أهمية الـ Tokens دي وبالذات الـ Refresh Token فالتطبيق محتاج يحتفظ بيهم في مكان آمن على جهاز اليوزر. ففي تطبيقات الموبايل مثلًا، بيكون الأفضل إنك تخزنها بـ Android Keystore أو iOS Keychain، وفي تطبيقات الويب ففي ناس كتير بتفضل تخزنها جوا حاجة إسمها HttpOnly Secure Cookies بدل أماكن زي localStorage عشان تقلل فرص سرقته عن طريق ثغرات زي XSS.
بس حتى مع كل الاحتياطات دي، لسه في احتمال إن الـ Refresh Token يتسرب بطريقة أو بأخرى، وساعتها هيظهر سؤال جديد:
“لو الهاكر سرق الـ Refresh Token، هيقدر يفضل يطلع Access Tokens جديدة لنفسه عادي؟”
Refresh Token Rotation و Reuse Detection
في تيكنيكس منتشرة مهمتهم إنهم يحلوا المشكلة دي بالتحديد، أشهرهم Refresh Token Rotation و Reuse Detection.
الفكرة فيهم إن كل Refresh Token يتم استخدامه مرة واحدة بس. وأول ما التطبيق يبعته تاني للسيرفر، السيرفر يديله Refresh Token جديد ويـ Invalidate القديم نهائيًا. بس مش بيمسحه من الـ Database، بيفضل متسجل إن ده كان Refresh Token لليوزر الفولاني بس تم استخدامه مرة خلاص.
تخيل بقى إن الهاكر قدر يسرق نسخة Refresh Token من اليوزر بطريقة ما سواء عشان نسيت تسجله في Secure Storage أو بثغرة Man-in-the-Middle نسيت تقفلها (وده هنتكلم عنه قدام) أو لسبب تاني، فبكده بقى في نسختين من نفس الـ Token، نسخة مع اليوزر الحقيقي ونسخة مع الهاكر.
لو اليوزر استخدم الـ Refresh Token الأول، السيرفر هيطلعله واحد جديد وهيعتبر القديم انتهى ومبقاش صالح للاستخدام خلاص، بعدها بدقايق أو ساعات الهاكر هيحاول يستخدم النسخة اللي سرقها.
بالنسبة للسيرفر المفروض الـ Token ده استخدم قبل كده ومات خلاص، فإزاي ظهر تاني؟ أكبر تفسير منطقي هو إن في حد تاني خد نسخة منه، وده معناه إن الـ Refresh Token اتسرب. أغلب الأنظمة ساعتها بتلغي كل Sessions اليوزر وتجبره يعمل Login من الأول. اليوزر الحقيقي هيسجل دخوله تاني من غير مشاكل بالـ Username والـ Password والهاكر هو اللي هيفقد الوصول تمامًا لإن الـ Token اللي معاه مبقاش له لازمة.
وده Diagram بيشرح Cycle الـ JWT كلها:
بس لو وصلت لغاية هنا فممكن يجي على بالك سؤال:
“طب ما إحنا أصلا بنستخدم HTTPS فإيه لازمة كل ده؟”
ليه الـ HTTPS مش كفاية لوحده؟
الـ HTTPS مهم في إنه بيـ Encrypt الـ Connection بالكامل بين التطبيق والسيرفر، يعني أي بيانات ماشية بينهم سواء Password أو Token أو أي بيانات تانية بتتنقل بشكل مشفر، فبالتالي أي حد هيـ Intercept الـ Connection مش هيقدر يفهم محتواه. وكمان بيضمن Integrity البيانات، يعني مش بس بيخليها Encrypted، هو كمان بيمنع أي طرف في النص من إنه يعدل محتوى الـ Request أو الـ Response من غير ما الطرفين يكتشفوا إن الـ Connection تم التلاعب بيه.
لكن بمجرد ما البيانات توصل للتطبيق أو للسيرفر، مهمة الـ HTTPS بتنتهي. لو الـ Token اتسرق من اليوزر بتاعك بعد كده سواء من Malware، أو Browser Extension خبيثة، أو منك أنت شخصيا لو اتسربت من Backup، أو Log، أو بسبب ثغرة XSS في تطبيق الويب، فالـ HTTPS خلاص كده عمل اللي عليه، ميقدرش يمنع استخدام الـ Token بعد سرقته.
وعلى ذكر الـ HTTPS، فمن الحاجات اللي أكتشفتها مؤخرًا وعجبتني برضو حاجة اسمها Certificate Pinning.
Certificate Pinning
لما التطبيق بيتصل بالسيرفر باستخدام HTTPS، السيرفر بيبعت حاجة اسمها SSL/TLS Certificate. تقدر تعتبرها بطاقة هوية رقمية للسيرفر، هي اللي بتثبت لتطبيقك إنه بيتكلم مع سيرفر حقيقي، وكمان دي اللي بتسمح بإنشاء الـ Encrypted Connection بين الطرفين.
في الطبيعي، الجهاز بيثق في أي Certificate صادرة من جهة موثوقة Certificate Authority، بس لو عملت Certificate Pinning فساعتها التطبيق هيوافق على الـ Certificate الخاصة بسيرفرك أنت بس وهيرفض أي Certificate تانية حتى لو كانت صادرة من جهة موثوقة. وده بيصعب جدًا تنفيذ Man-in-the-Middle Attacks لأن الهاكر مش هيقدر يقنع التطبيق إنه هو السيرفر الحقيقي بمجرد إن معاه أي Valid Certificate وخلاص.
بس في المقابل تنفيذ الموضوع ده محتاج إدارة كويسة لإن ممكن الـ Certificate بتاعتك تتغير لأي ظرف وفي أي وقت، فمحتاج تكون قادر تحدث التطبيق في وقتها بالظبط بالـ Certificate الجديدة عشان يفضل قادر يتواصل مع السيرفر. فلو تطبيقك عبارة عن تطبيق موبايل وبيحتاج موافقة من Store على كل تحديث، فهتعوز تبحث شوية في موضوع الـ OTA Updates أو الـ Remote Configs.
Authentication vs Authorization والوقاية من ثغرات IDOR
لحد دلوقتي قضينا معظم المقال بنتكلم عن إزاي السيرفر يتأكد إن الـ Request جاية فعلًا من اليوزر الحقيقي. وكل الحاجات اللي اتكلمنا عنها زي الـ JWT، والـ Signature، والـ Access Tokens كان هدفها الأساسي إنها تجاوب على سؤال واحد:
“مين اللي بعت الـ Request دي؟”
وده اللي بنسميه Authentication.
لكن لسه فيه سؤال تاني مهم:
“بعد ما عرفنا هو مين… هل مسموح له يعمل العملية دي أصلًا؟”
وده اللي بنسميه Authorization، وده بالظبط اللي بتستغله ثغرات زي Insecure Direct Object .Reference (IDOR)
الفكرة الأساسية للثغرة إن الهاكر بيكون عامل Login بشكل طبيعي جدًا، بس بدل ما يطلب الـ Resource بتاعه هو، بيغير الـ ID في الـ URL أو الـ Request ويحاول يوصل لبيانات مستخدم تاني.
فمثلًا لو اليوزر عندك عايز يشوف بيانات الأوردر، بتاعه فلقى إن التطبيق بيبعت Request بالشكل ده:
GET /users/123/orders
فاليوزر جرب يتحنيك ويعدل على الـ Request ويبعته كده:
GET /users/124/orders
لو السيرفر رجعله بيانات أوردر يوزر تاني، فالمشكلة هنا مش في الـ Authentication، عشان اليوزر بالفعل Authenticated. المشكلة إن السيرفر فشل في الـ Authorization، لأنه متأكدش إن اليوزر ده أصلًا مسموح له يوصل للـ Resource دي.
وعشان نتجنب النوع ده من الثغرات، فأول قاعدة مهمة هي إن هوية اليوزر لازم تتحدد من البيانات الموجودة جوا الـ Claims في الـ JWT نفسه، خصوصًا الـ User ID أو الـ sub، مش من أي userId تطبيقك يبعته في الـ URL أو الـ Body.
وبعد ما الهوية تتحدد، بيجي دور Role-Based Access Control (RBAC)، بحيث السيرفر يقرر إذا كان اليوزر ده عنده الصلاحية ينفذ العملية المطلوبة ولا لا بناءً على الـ Role أو الـ Permissions بتاعته، أو بناءً على ملكية الـ Resource نفسها.
يعني حتى لو الـ JWT سليم، فهو بس بيثبت اليوزر مين، بس لسه السيرفر محتاج يتأكد مسموحله يعمل الـ Request ده أصلا ولا لا.
الأمان طبقات
في الآخر، أكتر حاجة خرجت بيها من الكلام ده إن الأمان عمره ما كان Feature واحدة.
- HTTPS بيحل مشكلة.
- Refresh Token Rotation بيحل مشكلة مختلفة.
- Certificate Pinning بيعالج سيناريو مختلف.
- RBAC بيقفل باب هجمات مختلفة.
كل طبقة بتحمي جزء مختلف من النظام، ومفيش طبقة تقدر تعوض غياب الباقي.
لو بتشتغل على Backend أو سيستم مسؤول فيه عن الـ Authentication، اسأل نفسك سؤال بسيط:
“لو حد سرق الـ Token بتاع اليوزر دلوقتي، هيقدر يعمل إيه؟”
الإجابة على السؤال ده غالبًا هتوضحلك فين أكبر نقطة ضعف في السيستم عندك.