ഒരു ലൈവ് സൈറ്റിൽ 'ടോക്കൺ ട്രാപ്പ്' (token trap) സംഭവിച്ചു. ഒരേ സമയം ഒരു ഉപയോക്താവ് തുറന്നിരുന്ന അഞ്ച് ടാബുകളും കാലാവധി കഴിഞ്ഞ ഒരു JWT റിഫ്രഷ് ചെയ്യാൻ ശ്രമിച്ചപ്പോൾ, ബാക്കെൻഡിലേക്ക് ഒരേസമയം ഒരേ റിഫ്രഷ് റിക്വസ്റ്റുകൾ അയക്കപ്പെടുകയും സെഷൻ ഉടൻ തന്നെ റദ്ദാക്കപ്പെടുകയും ചെയ്തു. എല്ലാ ടാബുകളും ഉപയോക്താവിനെ ലോഗ് ഔട്ട് ചെയ്തു, ഇത് ടോക്കൺ പുതുക്കുന്നതിന് ഒരു സിംഗിൾ-ടാബ് പരിഹാരം മാത്രം മതിയാകില്ലെന്ന് തെളിയിക്കുന്നു.
എന്തുകൊണ്ടാണ് ടോക്കൺ ട്രാപ്പ് പ്രധാനമാകുന്നത്
ആധുനിക സിംഗിൾ-പേജ് ആപ്പുകൾ (SPAs) കുറഞ്ഞ കാലയളവിലേക്ക് മാത്രം നിലനിൽക്കുന്ന access tokens ഉം കൂടുതൽ കാലം നിലനിൽക്കുന്ന refresh token ഉം ആണ് ഉപയോഗിക്കുന്നത്. Access token കാലാവധി കഴിയുമ്പോൾ, ക്ലയന്റ് ഒരു റിഫ്രഷ് റിക്വസ്റ്റ് അയക്കുകയും പുതിയ ടോക്കൺ ജോഡി സ്വീകരിക്കുകയും തുടർന്ന് യഥാർത്ഥ കോൾ വീണ്ടും ശ്രമിക്കുകയും ചെയ്യുന്നു. മിക്ക ഡെവലപ്പർമാരും ഒരു ഇൻ-മെമ്മറി ഫ്ലാഗ് (ഉദാഹരണത്തിന്, isRefreshing = true) അല്ലെങ്കിൽ ഒരു റിക്വസ്റ്റ് ക്യൂ ഉപയോഗിച്ച് ഈ പ്രക്രിയ നിയന്ത്രിക്കാറുണ്ട്, ഇത് ഒരു സിംഗിൾ ടാബിൽ മാത്രം പരീക്ഷിച്ചു നോക്കുന്നു. എന്നാൽ യഥാർത്ഥ ലോകത്ത് ഉപയോക്താക്കൾ സെറ്റിംഗ്സ് പേജ്, അനലിറ്റിക്സ് ഡാഷ്ബോർഡ്, ചില ഡാറ്റാ വ്യൂകൾ എന്നിങ്ങനെ ഒന്നിലധികം ടാബുകൾ തുറന്നിട്ടുണ്ടാകും. Access token കാലാവധി കഴിയുമ്പോൾ, ഓരോ ടാബും സ്വതന്ത്രമായി 401 error കണ്ടെത്തുകയും ഓരോന്നും ഒരു റിഫ്രഷ് റിക്വസ്റ്റ് അയക്കുകയും ചെയ്യുന്നു. ബാക്കെൻഡ്—പ്രത്യേകിച്ച് refresh-token rotation നടപ്പിലാക്കുമ്പോൾ—രണ്ടാമത്തെ റിക്വസ്റ്റിനെ ഒരു replay ആയി കണക്കാക്കുകയും മുഴുവൻ സെഷനും റദ്ദാക്കുകയും ചെയ്യുന്നു.
JavaScript isolation പ്രശ്നം സൃഷ്ടിക്കുന്നു
ഓരോ ബ്രൗസർ ടാബും അതിന്റെ സ്വന്തം JavaScript context ആണ് പ്രവർത്തിപ്പിക്കുന്നത്. വേരിയബിളുകൾ, ടൈമറുകൾ, ഇൻ-മെമ്മറി ഫ്ലാഗുകൾ എന്നിവ ഒരേ ഒറിജിൻ (origin) പങ്കിടുന്നുണ്ടെങ്കിൽ പോലും മറ്റ് ടാബുകൾക്ക് അവ കാണാൻ കഴിയില്ല. “ഒരു റിഫ്രഷ് നടന്നുകൊണ്ടിരിക്കുന്നു” എന്ന് സൂചിപ്പിക്കുന്ന ഒരു ഫ്ലാഗ് ആ ഫ്ലാഗ് സെറ്റ് ചെയ്ത ടാബിൽ മാത്രമേ നിലനിൽക്കുന്നുള്ളൂ. ടോക്കൺ മറ്റൊരിടത്ത് റിഫ്രഷ് ചെയ്യപ്പെടുന്നുണ്ടെന്ന് അറിയാൻ മറ്റ് ടാബുകൾക്ക് കഴിയില്ല, അതിനാൽ അവയെല്ലാം സ്വന്തം നെറ്റ്വർക്ക് കോൾ ആരംഭിക്കുന്നു. ഇത് ഇൻ്റർസെപ്റ്ററിലെ (interceptor) ഒരു ബഗ് അല്ല; മറിച്ച് ക്ലയൻ്റ് സൈഡ് സ്റ്റേറ്റ് ഐസൊലേഷന്റെ (client-side state isolation) അടിസ്ഥാനപരമായ പരിമിതിയാണ്.
Web Locks API രക്ഷകനായി വരുന്നു
ഒരു ടാബ് ഒരു ലോക്ക് (lock) കൈവശം വെക്കുമ്പോൾ, അതേ ലോക്കിനായി ആവശ്യപ്പെടുന്ന മറ്റ് ടാബുകൾ അത് റിലീസ് ചെയ്യുന്നത് വരെ കാത്തിരിക്കണം.
ടോക്കൺ റിഫ്രഷിനായി ഇത് എങ്ങനെ പ്രവർത്തിക്കുന്നു
- Detect a 401 – ഏതെങ്കിലും ടാബ് അൺതോറൈസ്ഡ് (unauthorized) റെസ്പോൺസ് ലഭിക്കുമ്പോൾ
navigator.locks.request('auth_token_refresh_lock', async lock => { … })വിളിക്കുന്നു. - Acquire the lock – മറ്റ് ടാബുകളൊന്നും ലോക്ക് കൈവശം വെക്കുന്നില്ലെങ്കിൽ, നിലവിലെ ടാബ് മുന്നോട്ട് പോകുന്നു; അല്ലെങ്കിൽ ലോക്ക് ഒഴിവുള്ളതുവരെ അത് കാത്തിരിക്കുന്നു.
- Refresh once – ലോക്ക് കൈവശമുള്ള ടാബ് റിഫ്രഷ് റിക്വസ്റ്റ് അയക്കുന്നു, പുതിയ access token ഉം ഒരു ടൈംസ്റ്റാമ്പും
localStorage-ൽ സൂക്ഷിക്കുന്നു, തുടർന്ന് കോൾബാക്ക് (callback) പൂർത്തിയാകുമ്പോൾ ലോക്ക് സ്വയമേവ റിലീസ് ചെയ്യുന്നു. - Skip duplicate work – കാത്തിരിക്കുന്ന ഒരു ടാബ് ഒടുവിൽ ലോക്ക് ലഭിക്കുമ്പോൾ, അത്
localStorage-ൽ നിന്ന് ടൈംസ്റ്റാമ്പ് വായിക്കുന്നു. ടോക്കൺ ഒരു നിശ്ചിത സമയത്തിനുള്ളിൽ (ഉദാഹരണത്തിന്, കഴിഞ്ഞ ഏതാനും സെക്കൻഡുകൾക്കുള്ളിൽ) റിഫ്രഷ് ചെയ്തിട്ടുണ്ടെങ്കിൽ, ആ ടാബ് നെറ്റ്വർക്ക് കോൾ ഒഴിവാക്കിlocalStorage-ൽ നിന്ന് ഇൻ-മെമ്മറി ടോക്കൺ അപ്ഡേറ്റ് ചെയ്യുന്നു. - Handle crashes – ലോക്ക് കൈവശം വെച്ചുകൊണ്ട് ഒരു ടാബ് ക്രാഷ് ആകുകയോ അല്ലെങ്കിൽ അടച്ചുപൂട്ടുകയോ ചെയ്താൽ, ബ്രൗസർ ലോക്ക് റിലീസ് ചെയ്യുകയും മറ്റൊരു ടാബിന് റിഫ്രഷ് വീണ്ടും ശ്രമിക്കാൻ അനുവദിക്കുകയും ചെയ്യുന്നു.
ഒറ്റനോട്ടത്തിൽ ഗുണങ്ങൾ
- Zero redundant network calls – ആദ്യത്തെ ടാബ് മാത്രമേ ബാക്കെൻഡുമായി സംസാരിക്കൂ.
- No session destruction – Refresh-token rotation ഒരു തവണ മാത്രമേ കാണുന്നുള്ളൂ, ഇത് സെഷൻ നിലനിർത്താൻ സഹായിക്കുന്നു.
- Graceful recovery – ഒരു ടാബ് പെട്ടെന്ന് അടച്ചുപൂട്ടിയാലും ബ്രൗസർ നിയന്ത്രിക്കുന്ന ലോക്ക് റിലീസ് സംവിധാനം ഡെഡ്ലോക്കുകൾ (deadlocks) ഒഴിവാക്കുന്നു.
Implementation checklist
- നിങ്ങളുടെ Axios (അല്ലെങ്കിൽ fetch) ഇൻ്റർസെപ്റ്ററിൽ ഒരു ലോക്ക് റിക്വസ്റ്റോടെ റിഫ്രഷ് ലോജിക് ഉൾപ്പെടുത്തുക.
- റിഫ്രഷ് ചെയ്ത ടോക്കണും ഒരു മില്ലിസെക്കൻഡ് ടൈംസ്റ്റാമ്പും
localStorage-ൽ (അല്ലെങ്കിൽ ഓരോ സെഷനും വേണമെങ്കിൽsessionStorage-ൽ) സൂക്ഷിക്കുക. - ലോക്ക് ലഭിക്കുമ്പോൾ, സ്റ്റോർ ചെയ്ത ടൈംസ്റ്റാമ്പിനെ
Date.now()உடன் താരതമ്യം ചെയ്യുക. വ്യത്യാസം നിങ്ങളുടെ നിശ്ചിത പരിധിയേക്കാൾ കുറവാണെങ്കിൽ, ബാക്കെൻഡ് വിളിക്കുന്നതിന് പകരം സ്റ്റോറേജ് നിന്ന് ടോക്കൺ വായിക്കുക. - യഥാർത്ഥ API കോൾ വീണ്ടും ശ്രമിക്കുന്നതിന് മുമ്പ്, ഇൻ്റർസെപ്റ്റർ സ്റ്റോറേജ് നിന്ന് ലഭിച്ച ടോക്കൺ ഉപയോഗിച്ച് റിക്വസ്റ്റ് ഹെഡറുകൾ അപ്ഡേറ്റ് ചെയ്യുന്നുണ്ടെന്ന് ഉറപ്പാക്കുക.
- ഒരു റിഫ്രഷ് റിക്വസ്റ്റ് മാത്രമേ സെർവറിൽ എത്തുന്നുള്ളൂ എന്ന് പരിശോധിക്കാൻ നെറ്റ്വർക്ക് ലേറ്റൻസി (latency) സിമുലേറ്റ് ചെയ്തുകൊണ്ട് ഒന്നിലധികം ടാബുകളിൽ ഈ പ്രക്രിയ പരീക്ഷിക്കുക.
എന്ത് തെറ്റുകൾ സംഭവിക്കാം
അടുത്തതായി ശ്രദ്ധിക്കേണ്ടവ
ചുരുക്കത്തിൽ
ഓരോ ബ്രൗസർ ടാബിനെയും ഒരു ചെറിയ ഡിസ്ട്രിബ്യൂട്ടഡ് സിസ്റ്റത്തിലെ (distributed system) ഒരു നോഡ് ആയി പരിഗണിക്കുക. JWT റിഫ്രഷുകൾ ക്രമീകരിക്കാൻ Web Locks API ഉപയോഗിക്കുന്നതിലൂടെ, നിങ്ങൾക്ക് ഡ്യൂപ്ലിക്കേറ്റ് കോളുകൾ ഒഴിവാക്കാനും, refresh-token rotation സംരക്ഷിക്കാനും, ഉപയോക്താവ് തുറന്നിരിക്കുന്ന എല്ലാ ടാബുകളിലും ലോഗിൻ നിലനിർത്താനും കഴിയും.
