Authentication System Architecture: Session Race Condition, Proactive Renewal
আপনি যদি কোনো Authentication Service তৈরি করেন এবং সেখানে Access Token ও Refresh Token ভিত্তিক সেশন সিস্টেম ব্যবহার করে থাকেন, যেখানে টোকেন expire হলে তা রিনিউ করার জন্য একটি নির্দিষ্ট ফ্লো কাজ করে তাহলে খুব সম্ভবত আপনি "Session Race Condition" সমস্যার সম্মুখীন হয়েছেন বা সামনে হবেন।
যেমন ধরুন, একজন ইউজার যখন তার ড্যাশবোর্ড ভিজিট করেন, তখন পেজ লোড হওয়ার সময় ফ্রন্টএন্ড থেকে একসাথে একাধিক API Request সার্ভারে যেতে পারে। আর এই প্রতিটি কনকারেন্ট রিকোয়েস্টেই (Concurrent Request) কিন্তু ইউজারের ওই একই টোকেনগুলো যুক্ত থাকে। এখান থেকেই মূলত সমস্যার শুরু!
সাধারণত Access Token Expire হলে আমরা Refresh Token ব্যবহার করে নতুন Access Token এবং Refresh Token তৈরি করি। এরপর নতুন Token দুটি Database এর Session Table এ সংরক্ষণ করি এবং Browser এ Cookie হিসেবে সেট করে দিই। অর্থাৎ, পুরোনো Token দুটি Rotate করে নতুন Token দিয়ে Replace করি।
ধরুন, Session এর Access Token এর মেয়াদ শেষ হওয়ার ১ সেকেন্ড আগে, ঠিক 14:00:00 তে একজন ইউজার ড্যাশবোর্ডে প্রবেশ করলেন। ড্যাশবোর্ড লোড হওয়ার সময় ফ্রন্টএন্ড থেকে একসাথে ৫টি API Request পাঠানো হলো। ঠিক সেই মুহূর্তে প্রথম রিকোয়েস্টটি ১ সেকেন্ড ধরে সার্ভ হওয়ার পরপরই Access Token টি expired হয়ে গেল।
ফলে বাকি ৪টি রিকোয়েস্ট যখন পুরোনো টোকেন নিয়ে সার্ভারে পৌঁছায়, তখন সেগুলো 401 Unauthorized Error ফেস করে। কারণ ততক্ষণে পুরোনো টোকেনটি ইনভ্যালিড বা রোটেট হয়ে গেছে। একই সময়ে একাধিক রিকোয়েস্ট যাওয়ার কারণে তৈরি হয় Race Condition.
14:00:00 -> Request 1 Arrived
14:00:01 -> Request 1 Processed (Token Expired -> নতুন Access ও Refresh Token জেনারেট হলো এবং পুরোনো দুটিই রোটেট/রিপ্লেস হয়ে গেলো)
14:00:01 -> Request 2 Arrived
14:00:02 -> Request 2 Failed (401 Unauthorized, কারণ পুরোনো Access এবং Refresh Token দুটিই ইতোমধ্যে রোটেট হয়ে গেছে)
14:00:02 -> Request 3 Arrived
14:00:03 -> Request 3 Failed (401 Unauthorized, একই কারণ)
14:00:03 -> Request 4 Arrived
14:00:04 -> Request 4 Failed (401 Unauthorized, একই কারণ)
14:00:04 -> Request 5 Arrived
14:00:05 -> Request 5 Failed (401 Unauthorized, একই কারণ)নোট: এখানে আমরা 'Managed Session' আর্কিটেকচার ব্যবহার করছি। অর্থাৎ, আমাদের ডাটাবেসে ইউজারের Access Token, Refresh Token, সেগুলোর Expiry Time এবং অন্যান্য সেশন ডেটা স্টোর করা থাকে।
এখন কী হবে?
আমরা তো জানলাম প্রথম রিকোয়েস্টটি সাকসেসফুল হওয়ার কারণে ডাটাবেসে ইতোমধ্যে টোকেনগুলো রোটেট হয়ে গেছে।
ফলে বাকি ৪টি রিকোয়েস্ট যখন পুরোনো টোকেন নিয়ে সার্ভারে পৌঁছায়, তখন সেগুলোতে থাকা Access Token এর মেয়াদ শেষ এবং Refresh Token ও ইনভ্যালিড হয়ে যায়। টোকেন রিনিউ করতে না পারার কারণে সার্ভার 401 Unauthorized রেসপন্স রিটার্ন করে। এর সরাসরি প্রভাব পড়ে ইউজারের ওপর তিনি ড্যাশবোর্ডে কাজ করার মাঝখানেই হঠাৎ করে অপ্রত্যাশিতভাবে লগআউট (Logout) হয়ে যান!
সবচেয়ে বিরক্তিকর দিক:
এই সমস্যাটি কিন্তু সবসময় ঘটে না। এটি ঠিক তখনই ঘটে যখন টোকেন এক্সপায়ার হওয়ার মাত্র কয়েক সেকেন্ড আগে একসাথে একাধিক রিকোয়েস্ট সার্ভারে যায়। যেহেতু এটি একটি র্যান্ডম বা ইন্টারমিটেন্ট (Intermittent) সমস্যা, তাই ইউজার কাজের মাঝে হঠাৎ লগআউট হয়ে চরম বিরক্ত হন। অন্যদিকে, ডেভেলপারদের জন্যও এটি বেশ ফ্রাস্ট্রেটিং, কারণ লোকাল এনভায়রনমেন্টে এই সিনারিও সহজে Reproduce বা ডিবাগ করা যায় না।
এখন প্রশ্ন হলো, এর সমাধান কী?
অনেকেই এই সমস্যা সমাধানের জন্য ব্যাকএন্ডে Redis ব্যবহার করে ৫ বা ১০ সেকেন্ডের একটি Grace Period বা Buffer রাখেন। অর্থাৎ, পুরোনো টোকেনগুলো সাথে সাথে ডিলিট না করে Redis এ একটি নির্দিষ্ট TTL সেট করে স্টোর করে রাখা হয়। ফলে ওই বাফার সময়ের মধ্যে পুরোনো টোকেন নিয়ে কোনো রিকোয়েস্ট এলে সার্ভার সেটি Redis থেকে ম্যাচ করে ভ্যালিড হিসেবে কাউন্ট করে এবং রেসপন্সের Set-Cookie হেডারে নতুন জেনারেট হওয়া টোকেন পাঠিয়ে দেয়।
কিন্তু এই পদ্ধতিতেও একটি বড় Catch/সমস্যা রয়েছে!
১. টাইমিং বা লেটেন্সি ইস্যু: নেটওয়ার্ক লেটেন্সি বা স্লো কানেকশনের কারণে বাকি ৪টি রিকোয়েস্ট যদি ৫ বা ১০ সেকেন্ডের বাফার সময়ের পরে সার্ভারে পৌঁছায়, ততক্ষণে Redis-এর TTL শেষ হয়ে যাবে। ফলে ইউজার আবারও সেই 401 Unauthorized Error খাবেন।
২. সিকিউরিটি রিস্ক (Replay Attack): টাইমিং ইস্যু এড়ানোর জন্য আপনি হয়তো ভাবতে পারেন বাফার টাইম বাড়িয়ে ৩০ সেকেন্ড করে দেবেন। কিন্তু এতে সিস্টেমে মারাত্মক সিকিউরিটি রিস্ক তৈরি হবে! কারণ, এই ৩০ সেকেন্ডের বাফার উইন্ডোতে কোনো হ্যাকার যদি পুরোনো টোকেনটি চুরি (Hijack) করতে পারে, তবে সে খুব সহজেই সেটি ব্যবহার করে Malicious Request পাঠাতে পারবে।
অর্থাৎ, বাফার টাইম ৫, ১০ বা ৩০ সেকেন্ড যাই হোক না কেন, একটি রিস্ক থেকেই যাচ্ছে। সোজা কথায়, Session Race Condition সমাধান করতে গিয়ে আমরা সিস্টেমে নতুন একটি Security Loophole তৈরি করে ফেলছি!
এই ধরনের ক্রিটিক্যাল এবং এন্টারপ্রাইজ লেভেল সমস্যার সমাধানের জন্য আমাদের একটি Hybrid Model (Proactive এবং Reactive Renewal এর সমন্বয়) এবং একটি Anti-Hijacking Security Layer প্রয়োজন।
কীভাবে কাজ করে এই Hybrid Architecture?
ধরা যাক, আমাদের সিস্টেমে Access Token এর মেয়াদ ৫ মিনিট, Refresh Token এর মেয়াদ ১৫ মিনিট এবং Renewal Threshold হলো ৩০ সেকেন্ড।
এই মডেলে টোকেন পুরোপুরি এক্সপায়ার হওয়ার আগেই একটি 'Threshold' বা সেফটি উইন্ডো সেট করা থাকে। যখন ইউজারের কোনো রিকোয়েস্ট সার্ভারে আসে, তখন সিস্টেম চেক করে Access Token এর মেয়াদ এই ৩০ সেকেন্ডের উইন্ডোতে ঢুকে পড়েছে কিনা। অর্থাৎ, টোকেনের বয়স যদি ৪ মিনিট ৩০ সেকেন্ড পার হয়ে যায়, তখন সিস্টেম ওই রিকোয়েস্ট প্রসেস করার পাশাপাশি Proactively নতুন Access ও Refresh Token পেয়ার জেনারেট করে ফেলে। এরপর নতুন টোকেনগুলো auth_sessions টেবিলে সেভ করে এবং রেসপন্সের Set-Cookie হেডারে ব্রাউজারে পাঠিয়ে দেয়।
এখানে ম্যাজিকটা কোথায়?
এখানে expire হওয়ার পর আমাদের কোনো এক্সট্রা 'Grace Period' বা বাফার টাইম দরকার হচ্ছে না!
প্রথম রিকোয়েস্টটি যখন নতুন টোকেন জেনারেট করে, তখন সিস্টেম পুরোনো টোকেনটিকে Redis এ ক্যাশ করে রাখে। কিন্তু Redis এর TTL (Time-to-Live) কোনো আরবিট্রারি ৫ বা ১০ সেকেন্ড হয় না, বরং এটি হয় ঠিক Access Token এর বেঁচে থাকা বাকি সময়টুকু (Maximum 30 seconds)।
Access Token এর মূল মেয়াদ শেষ হওয়ার সাথে সাথেই Redis থেকেও সেটি স্বয়ংক্রিয়ভাবে মুছে যায়। ফলে কনকারেন্ট রিকোয়েস্টগুলো যখন পুরোনো টোকেন নিয়ে সার্ভারে আসে, তারা Redis থেকে ভ্যালিডেট হয়ে রেসপন্সের হেডারে নতুন টোকেন নিয়ে ফিরে যায়।
যেহেতু টোকেন এক্সপায়ার হওয়ার পরে অতিরিক্ত কোনো বাফার সময় দেওয়া হচ্ছে না, তাই এখানে Replay Attack এর কোনো Security Risk থাকে না! এবং ইউজারও কখনো 401 Error এর সম্মুখীন হন না।
14:04:30 -> Request 1 Arrived
14:04:31 -> Request 1 Processed (Threshold হিট করেছে! Proactively নতুন Token জেনারেট হলো। পুরোনো Token-টি Redis-এ সেভ হলো এবং TTL সেট হলো ঠিক ২৯ সেকেন্ড, অর্থাৎ 14:05:00 পর্যন্ত)
14:04:31 -> Request 2 Arrived (সেই Old Token নিয়ে)
14:04:32 -> Request 2 Processed (Redis থেকে Old Token ভ্যালিডেট হলো এবং রেসপন্স হেডারে নতুন Token দিয়ে সার্ভ হলো)
14:04:32 -> Request 3 Arrived (Old Token নিয়ে)
14:04:33 -> Request 3 Processed (একইভাবে Redis থেকে ভ্যালিডেট হয়ে সাকসেসফুলি সার্ভ হলো)
14:04:33 -> Request 4 Arrived (Old Token নিয়ে)
14:04:34 -> Request 4 Processed (সাকসেসফুল!)
14:04:34 -> Request 5 Arrived (Old Token নিয়ে)
14:04:35 -> Request 5 Processed (সাকসেসফুল!)এখন আসি, ইউজার দীর্ঘক্ষণ আইডল (Idle) ছিলেন। তার Access Token তো অনেক আগেই এক্সপায়ার হয়ে গেছে, এমনকি Refresh Token এর মেয়াদও শেষ হতে আর মাত্র ১ সেকেন্ড বাকি! অর্থাৎ ১৪ মিনিট ৫৯ সেকেন্ড পার হয়ে গেছে।
ঠিক এই মুহূর্তে ইউজার ড্যাশবোর্ডে ঢুকলেন এবং একসাথে ৫টি কনকারেন্ট রিকোয়েস্ট সার্ভারে হিট করল। প্রতিটি রিকোয়েস্ট প্রসেস হতে ১ সেকেন্ড সময় লাগে।
যেহেতু এখানেও আমরা আমাদের 30 Second Renewal Window মেকানিজম ফলো করছি, তাই ফ্লো টি হবে ঠিক এরকম:
14:59:00 -> Request 1 Arrived
14:59:01 -> Request 1 Processed (মেয়াদ মাত্র ১ সেকেন্ড বাকি থাকায় এটি Threshold এর ভেতরে পড়ে। Proactively নতুন Token জেনারেট হলো। পুরোনো Token-টি Redis এ সেভ হলো, কিন্তু TTL সেট হলো ঠিক ১ সেকেন্ড!)
--- [ 14:59:01 : Refresh Token এর পুরো ১৫ মিনিট মেয়াদ শেষ! Redis এর TTL-ও জিরো! ] ---
14:59:01 -> Request 2 Arrived (Old Token নিয়ে)
14:59:02 -> Request 2 Failed (401 Unauthorized, কারণ Redis থেকে পুরোনো টোকেনটি ইতোমধ্যে ডিলিট হয়ে গেছে)
14:59:02 -> Request 3 Arrived (Old Token নিয়ে)
14:59:03 -> Request 3 Failed (401 Unauthorized)
14:59:03 -> Request 4 Arrived (Old Token নিয়ে)
14:59:04 -> Request 4 Failed (401 Unauthorized)
14:59:04 -> Request 5 Arrived (Old Token নিয়ে)
14:59:05 -> Request 5 Failed (401 Unauthorized)তাহলে কি ইউজার অপ্রত্যাশিতভাবে লগআউট হয়ে যাবে?
না! এখানেই আমাদের Hybrid Architecture এর আসল খেলা।
খেয়াল করে দেখুন, ব্যাকএন্ড এখানে তার সর্বোচ্চ সিকিউরিটি নিশ্চিত করেছে। সে কোনোভাবেই এক্সপায়ার্ড টোকেনকে অতিরিক্ত বাফার টাইম বা গ্রেস পিরিয়ড দেয়নি (যেমনটা অন্য আর্কিটেকচারে ৫ বা ১০ সেকেন্ড দেওয়া হয়)। ফলে হ্যাকারদের জন্য কোনো লুপহোল বা Replay Attack-এর সুযোগ তৈরি হয়নি।
কিন্তু বাকি যে ৪টি রিকোয়েস্ট 401 Error খেলো, সেগুলোকে হ্যান্ডেল করবে আমাদের Frontend Axios Interceptor (Request Queueing)।
ফ্রন্টএন্ডে Axios Interceptor ব্যবহার করে খুব সহজেই 'Request Queueing' ইমপ্লিমেন্ট করা যায়। এতে প্রথম রিকোয়েস্টটি ফেইল হওয়া মাত্রই সিস্টেম বাকি রিকোয়েস্টগুলোকে একটি লাইনে (Queue) হোল্ড করে রাখে। এরপর ব্যাকগ্রাউন্ডে সাইলেন্টলি নতুন টোকেন এনে, আটকে থাকা রিকোয়েস্টগুলো নতুন টোকেনসহ স্বয়ংক্রিয়ভাবে আবার সার্ভারে পাঠিয়ে দেয়। তবে ব্যাকএন্ডেও একটি সেফটি নেট রাখা উচিত, যেটা আমরা আলোচনা korlam.
কী ঘটবে তখন?
১. Request 1 যখন 14:59:01 এ সফলভাবে রেসপন্স নিয়ে ফিরবে, সে ব্রাউজারের Set-Cookie তে নতুন Token পেয়ার সেট করে ফেলবে।
২. ঠিক ওই সময়ে বা এরপর Request 2, 3, 4, 5 যখন 401 Error নিয়ে ফ্রন্টএন্ডে ফিরবে, Axios Interceptor ইউজারকে সাথে সাথে লগআউট করতে দেবে না।
৩. এর পরিবর্তে, Interceptor এই ফেইল হওয়া রিকোয়েস্টগুলোকে একটি লাইনে (Queue) হোল্ড করে রাখবে। যেহেতু Request 1 ইতোমধ্যে ব্যাকগ্রাউন্ডে সাইলেন্টলি নতুন টোকেন এনে কুকিতে সেট করে ফেলেছে, Interceptor ওই আটকে থাকা ৪টি রিকোয়েস্টকে নতুন টোকেনসহ স্বয়ংক্রিয়ভাবে পুনরায় (Retry) সার্ভারে পাঠিয়ে দেবে!
চূড়ান্ত ফলাফল:
- Backend: ১০০% সিকিউর (কোনো রিস্কি বাফার টাইম নেই)।
- Frontend / UX: ইউজার বিন্দুমাত্র টের পেলেন না যে ব্যাকগ্রাউন্ডে কি হয়ে গেছে! তিনি কোনো এরর বা লগআউট ছাড়াই smoothly তার ড্যাশবোর্ড ব্যবহার করতে পারবেন।
#WebDevelopment #SoftwareArchitecture #NodeJS #Redis #Authentication #CyberSecurity #SystemDesign #FullStackDevelopment #Engineering