מאמרים הנדסיים
למה משתמשים מתנתקים באקראי, ואיך מתקנים באגים בסשנים של JWT
מאת Muhammad Shahid · · 3 דקות קריאה
תשובה קצרה
התנתקויות אקראיות עם סשנים של JWT מגיעות כמעט תמיד מכמה סיבות: שתי בקשות שמרעננות את אותו טוקן בבת אחת (לרוב משתי לשוניות), בדיקות תוקף שנפגעות מהפרשי שעון, הגדרות cookie שמאבדות את הטוקן, או שרתים שלא חולקים את אותו סוד. משחזרים את ההתנתקות, מזהים איזו סיבה זו ומתקנים אותה. תיקנתי באגים כאלה ב־TheCryptoBasic, פלטפורמת חדשות קריפטו עם יותר מ־2 מיליון משתמשים.
| תסמין | משתמשים מתנתקים באקראי, לרוב אחרי זמן בלי פעילות או עם כמה לשוניות פתוחות |
|---|---|
| הסיבה הנפוצה ביותר | מרוצי refresh token בין בקשות מקבילות או לשוניות |
| סיבות נוספות | הפרשי שעון, הגדרות cookie, סודות שונים בין שרתים |
| איפה תיקנתי | TheCryptoBasic, יותר מ־2 מיליון משתמשים |
| טכנולוגיות | Node.js, Express, JWT |
למה כל כך קשה לאתר התנתקויות אקראיות?
כי הן תלויות בתזמון. אותו משתמש שעושה אותו דבר מחובר פעם אחת ומנותק בפעם הבאה. לפני ששינים קוד, רושמים כל כישלון אימות עם הסיבה שלו (פג תוקף, חתימה לא תקינה, refresh token לא נמצא, refresh token כבר נוצל) ועם השעה בשרת. עמודת הסיבה בדרך כלל מצביעה ישר על הבעיה.
סיבה 1: שתי בקשות מרעננות את אותו טוקן בבת אחת
תוקף ה־access token פג. הדף שולח שלוש קריאות API באותו רגע; שלושתן מקבלות 401 ושלושתן מנסות לרענן. עם רוטציה של refresh tokens, הרענון הראשון מצליח ומבטל את ה־refresh token הישן, כך ששני האחרים נכשלים והאפליקציה מנתקת את המשתמש. שתי לשוניות פתוחות עושות בדיוק את אותו הדבר.
מתקנים בצד הלקוח על ידי שיתוף רענון אחד בכל פעם:
let refreshing = null
function refreshOnce() {
if (!refreshing) {
refreshing = api.post("/auth/refresh").finally(() => { refreshing = null })
}
return refreshing // every caller waits for the same refresh
}
בצד השרת מקבלים refresh token שעבר רוטציה לפני כמה שניות ומחזירים את אותו זוג חדש, כך שרענון כפול בחלון הקצר הזה לא נחשב גניבה.
סיבה 2: תוקף והפרשי שעון
אם השרת שמנפיק טוקנים והשרת שבודק אותם לא מסכימים על השעה, טוקן חדש יכול להיראות כפג תוקף. access tokens קצרים מאוד מחמירים את זה. מסנכרנים את שעוני השרתים, מאפשרים סבילות שעון קטנה בבדיקה, ומרעננים קצת לפני שפג התוקף ולא אחרי ה־401 הראשון.
jwt.verify(token, secret, { clockTolerance: 30 }) // seconds
סיבה 3: הגדרות cookie שמאבדות את הטוקן
אם ה־refresh token נשמר ב־cookie, בודקים את ההגדרות שלו: ה־domain וה־path צריכים לכסות את ה־API, SameSite צריך לאפשר את הבקשה שהאפליקציה שולחת, Secure דורש HTTPS, ותוקף ה־cookie עצמו לא יכול להיות קצר מתוקף ה־refresh token. cookie שלא נשלח בשקט נראה בדיוק כמו התנתקות אקראית.
סיבה 4: שרתים שלא חולקים את אותו סוד
מאחורי load balancer, כל מופע צריך לחתום ולבדוק עם אותו סוד או אותה קבוצת מפתחות. אם למופע אחד יש סוד ישן או שונה, משתמשים מתנתקים בכל פעם שבקשה נוחתת עליו, וזה נראה אקראי. אותו דבר קורה ברוטציית סוד שנעשית בלי לקבל את שני המפתחות לזמן מה.
איך מוודאים שהתיקון עבד?
- פותחים את האפליקציה בשתי לשוניות, מחכים שתוקף ה־access token יפוג ומשתמשים בשתיהן.
- שולחים כמה קריאות API בבת אחת מיד אחרי שפג התוקף.
- עוקבים אחרי יומן כישלונות האימות לפי סיבה; הסיבה שראיתם קודם צריכה להיעלם.
שאלות נפוצות
למה האפליקציה שלי מנתקת משתמשים באקראי?
בדרך כלל מרוץ refresh (כמה בקשות או לשוניות מרעננות בבת אחת), הפרשי שעון, הגדרות cookie או שרתים עם סודות שונים. רושמים כל כישלון אימות עם הסיבה כדי לזהות איזו.
מה זה מרוץ refresh token?
כמה בקשות מנסות לרענן טוקן שפג באותו זמן; עם רוטציה, הראשונה מבטלת את הטוקן שהאחרות משתמשות בו, הן נכשלות והמשתמש מתנתק.
איך עוצרים בקשות refresh מקבילות?
משתפים הבטחת רענון אחת בצד הלקוח כך שכל בקשה מחכה לאותו רענון, ונותנים בשרת חלון חסד קצר לטוקן שעבר רוטציה הרגע.
מה זה JWT clock skew?
הפרש שעות בין שרתים, שיכול לגרום לטוקן תקף להיראות כפג תוקף. מסנכרנים שעונים ומאפשרים clockTolerance קטן בבדיקה.
לשמור JWT ב־cookie או ב־local storage?
אפליקציות רבות שומרות את ה־refresh token ב־cookie מסוג httpOnly ו־Secure ואת ה־access token הקצר בזיכרון, וזה מגביל מה סקריפט יכול לגנוב.
אתה יכול לתקן באגי אימות באפליקציה שלי?
כן. תיקנתי באגים בסשנים של JWT שניתקו משתמשים באקראי בפלטפורמה עם 2 מיליון משתמשים. ראו את עמוד שירות הבקאנד ב־Node.js.