JWT (JSON Web Token) הוא מחרוזת משלושה חלקים ב-base64url, header.payload.signature, ששרת נותן למשתמש בהתחברות ובודק בכל בקשה מאוחרת יותר. החתימה מוכיחה שהשרת הנפיק את ה-token ושאף אחד לא שינה אותו, אבל ה-payload רק מקודד, לא מוצפן, כך שכל אחד יכול לקרוא אותו. ב-Node.js יוצרים אחד עם new SignJWT(payload).sign(secret) מהחבילה jose ובודקים אותו עם jwtVerify(token, secret).
הוא מדפיס 3 parts, ואז user-42 admin: jwtVerify בדק את החתימה ואת התוקף והחזיר את ה-payload. שנו תו אחד בסוד שמועבר ל-jwtVerify והריצו שוב: האימות נכשל עם signature verification failed.
מה יש בתוך JWT
ה-header וה-payload הם JSON, מקודדים ב-base64url. כדי לפענח אותם לא צריך מפתח, ובגלל זה JWT אף פעם לא צריך להחזיק סיסמה או כל דבר סודי אחר.
שתי השורות הראשונות מדפיסות {"alg":"HS256"} ו-{"name":"Ada","role":"editor"}, קריאים לכל מי שמחזיק את ה-token. החתימה (43 תווים ב-HS256) היא מה שמונע מהם לערוך אותו: שנו editor ל-admin והחתימה כבר לא תתאים.
| חלק | מכיל | סודי? |
|---|---|---|
| Header | האלגוריתם, כמו HS256 | לא, קריא |
| Payload | claims: sub (מזהה המשתמש), exp (תפוגה), iat (מועד ההנפקה), שדות משלכם | לא, קריא |
| Signature | חתימת HMAC או RSA של שני החלקים הראשונים | צריך את המפתח כדי ליצור אותה |
תוקף ואימות שנכשל
setExpirationTime('15m') כותב claim בשם exp, ו-jwtVerify דוחה token שעבר אותו. כל דחייה היא שגיאה עם code, כך שאפשר להבדיל בין token שפג תוקפו לבין token מזויף.
הבדיקה הראשונה נכשלת עם ERR_JWT_EXPIRED והשנייה, שהחתימה שלה שונתה, עם ERR_JWS_SIGNATURE_VERIFICATION_FAILED. לגבי ה-API שלכם שתיהן אומרות אותו דבר (ענו 401), אבל רישום הקוד ללוג עוזר בדיבאג.
הגנה על route ב-Express עם Bearer token
הלקוח מתחבר פעם אחת, שומר את ה-token ושולח אותו ב-header Authorization בתור Bearer <token>. middleware מאמת אותו ושם את ה-payload ב-req.user בשביל ה-route.
הבקשה הראשונה, GET /me בלי token, מקבלת 401 ו-Missing token. שלחו POST /login עם הגוף {"username": "ada", "password": "lovelace"}, העתיקו את ה-token מהתשובה, ואז שלחו שוב GET /me עם ה-header Authorization: Bearer <token>: התשובה היא {"id":"user-1","name":"ada"}.
החבילה jsonwebtoken
jsonwebtoken (npm install jsonwebtoken) היא הבחירה הנפוצה השנייה. ה-sign וה-verify שלה סינכרוניים (עם callbacks אופציונליים) במקום להחזיר promises:
const jwt = require('jsonwebtoken');
const token = jwt.sign({ role: 'admin' }, process.env.JWT_SECRET, {
subject: 'user-42',
expiresIn: '1h',
});
const payload = jwt.verify(token, process.env.JWT_SECRET); // throws if invalid
השתמשו ב-jose בקוד חדש: אין לה תלויות, והיא רצה גם ב-edge runtimes ובדפדפנים. tokens משתי הספריות הם JWTs סטנדרטיים, כך שאחת יכולה לאמת את מה שהשנייה חתמה.
טעויות נפוצות
שימוש ב-decode במקום ב-verify. jwt.decode ו-decodeJwt קוראים את ה-payload בלי לבדוק את החתימה, כך ש-token מזויף עובר. סמכו רק על מה ש-verify מחזיר.
סוד קצר או סוד שעשו לו commit. סוד HS256 שאפשר לנחש מאפשר לתוקפים לחתום tokens משלהם. השתמשו לפחות ב-32 בייטים אקראיים, קראו אותם מ-process.env, ואל תעשו להם commit אף פעם.
tokens שלא פגים אף פעם. בלי exp, token שדלף עובד לנצח. תמיד הגדירו תפוגה.
שאלות נפוצות
איפה הלקוח צריך לשמור JWT?
באפליקציית דפדפן, cookie עם HttpOnly ו-Secure הוא הבטוח ביותר, כי סקריפטים של הדף לא יכולים לקרוא אותו. localStorage פשוט יותר, אבל כל סקריפט בדף, כולל סקריפט שהוזרק, יכול לקרוא את ה-token. אפליקציות מובייל משתמשות באחסון המאובטח של הפלטפורמה.
איך מנתקים משתמש כשעובדים עם JWTs?
השרת לא יכול לבטל token שהוא כבר חתם; הוא נשאר תקף עד שהתוקף שלו פג. מחקו אותו בצד הלקוח, תנו ל-access tokens תוקף קצר, ואם אתם צריכים ניתוק מיידי, החזיקו בשרת רשימה של ids של tokens שבוטלו (jti) ובדקו אותה בכל בקשה.
HS256 או RS256?
HS256 משתמש בסוד משותף אחד לחתימה ולאימות, וזה מתאים ל-backend יחיד. RS256 חותם עם מפתח פרטי ומאמת עם מפתח ציבורי, כך ששירותים אחרים יכולים לבדוק tokens בלי שיוכלו ליצור אותם.
כמה זמן JWT צריך להיות תקף?
access tokens תקפים בדרך כלל בין 5 ל-60 דקות. לסשנים ארוכים יותר, צרפו אליהם refresh token שנשמר בזהירות רבה יותר, שהלקוח מחליף ב-access token חדש.