מה זה JWT וכיצד הוא משמש לאימות והרשאות באתרים

סער טויטו

סער טויטו

Software & Web Development


TL;DR
  • JWT (JSON Web Token) תקן פתוח (RFC 7519) שמשמש כ"כרטיס זיהוי" שהשרת מנפיק למשתמש מאומת; משמש לאימות והרשאות בלי לשמור session בשרת.
  • המבנה JWT מורכב משלושה חלקים מופרדים בנקודה: Header (אלגוריתם החתימה), Payload (פרטי המשתמש), ו-Signature (חתימה דיגיטלית לאימות אותנטיות).
  • אחסון מאובטח שמירה ב-HttpOnly cookie מומלצת; localStorage חשוף ל-XSS attacks ולכן לא מתאים לאחסון JWT.
  • חיסרון מרכזי ה-payload מקודד ב-Base64 אך לא מוצפן; כל מי שמיירט את ה-JWT יכול לקרוא את התוכן (אבל לא לזייף אותו).

JWT במבט כללי

JWT (JSON Web Token) הוא תקן פתוח שמוגדר ב-RFC 7519 ומשמש כ"כרטיס זיהוי" שהשרת מנפיק למשתמש מאומת. במקום שהשרת ישמור session על כל משתמש מחובר, ה-JWT מכיל בתוכו את כל המידע הנדרש (id, email, הרשאות), חתום דיגיטלית, ויכול לעבור עם כל בקשה מהלקוח לשרת. הגישה הזו (stateless) מקלה על scaling אופקי באפליקציות גדולות.

JWT משלב שלוש פעולות אבטחה מרכזיות באתרים: אימות (Authentication) - תהליך שבו המערכת בודקת את זהות המשתמש; הרשאה (Authorization) - קביעת אילו פעולות המשתמש זכאי לבצע אחרי שזוהה; ו-קיום session - שמירת ההזדהות לאורך זמן בלי שהשרת יצטרך לזכור אותה. במאמר נעבור על כל אחד מהמושגים, על המבנה של JWT, על איך לאחסן אותו בצורה מאובטחת, ועל דוגמת קוד מעשית ב-Node.js.

אימות (Authentication)

אימות הוא תהליך שבו מערכת בודקת את זהותו של משתמש (user) באמצעות מגוון דרכים כמו כתובת דוא"ל, שם משתמש, וסיסמא. כשאנחנו נרשמים או מתחברים לאתרים כמו Facebook או Twitter, אנחנו מקבלים גישה לפעולות שלא היינו יכולים לקבל אם לא היינו מבצעים תהליך של אימות, כמו פרסום פוסט או תגובה לדוגמה.

כלומר שאנחנו עוברים תהליך של אימות, אנחנו בתוך המערכת, ולכן יש לנו גישה לפעולות מסוימות. תהליך האימות מאפשר לאתר (למערכת) לשמור על בטחון המשתמשים בכך שלא כל אחד יכול לקבל גישה לפעולות מסוימות, אלא רק למשתמשים (users) שביצעו אימות.

הרשאה (Authorization)

הרשאה היא התהליך שבו מערכת קובעת אילו משאבים או פעולות משתמש מורשה לגשת אליהם או לבצע לאחר שהוא עבר את תהליך האימות. במילים אחרות, לאחר שהמערכת מזהה את המשתמש (למשל, על ידי שם משתמש וסיסמה), היא צריכה להחליט מה הוא מורשה לעשות במערכת.

לדוגמה, באתר כמו Facebook, משתמש שעבר אימות יכול לפרסם פוסטים תחת שמו או למחוק פוסטים תחת שמו, אך אין לו הרשאה למחוק פוסטים של משתמשים אחרים. כלומר ההרשאה מבטיחה שלכל משתמש (user) במערכת, יש גבולות ברורים לפעולות שהוא יכול לבצע, ובכך מגנה על המערכת מפני שימוש לא ראוי או התערבות בפרטיות ובזכויות של משתמשים אחרים.

JWT (JSON Web Token)

בחלק הזה, אנחנו נלמד איך JWT (JSON Web Token) עובד וממה הוא מורכב, וגם חסרונות ויתרונות כדי שיהיה לכם את כל המידע הרלוונטי והשלם.

איך JWT עובד?

כשאנחנו מתחברים למערכת לאחר פעולות כמו login או register, מונפק לנו משהו שנקרא JWT. תחשבו על JWT כאל כרטיס זיהוי במערכת שמזהה אתכם בכל פעם שאתם רוצים לבצע פעולה מסוימת או לקבל גישה למשהו מסוים, והוא בודק האם יש לכם הרשאה או לא.

כל פעם שאתם מגיבים למשתמש בFacebook או מעלים פוסט וכדומה, אתם שולחים בקשה לשרת ביחד עם ה-JWT שהונפק לכם בשעת האימות, ולמה ביחד עם ה-JWT? כי השרת משתמש ב-JWT שהונפק לכם כדי לוודא שאתם משתמשים אמתיים, וכדי לקרוא אלו הרשאות יש לכם, ואם יש לכם את ההרשאה המתאימה, אתם תוכלו לבצע את אותה פעולה. אם האימות נכשל מסיבה כזו או אחרת, המשתמש לא יוכל לבצע שום פעולה באתר, וברוב הפעמים, ייצא מן המערכת בצורה programmatically.

JWT מורכב משלושה חלקים:

JWT מורכב משלושה חלקים, שכל אחד מהם מופרד על ידי נקודה (.).

  • Header - מכיל מידע על הסוג של האלגוריתם ששימש להצפנת ה-JWT.

  • Payload - מאחסן את פרטי המשתמש, כגון כתובת אימייל, שם משתמש ועוד. חשוב לזכור שלמרות שה-JWT מאובטח, לא נהוג לשמור בו מידע רגיש כמו סיסמאות או פרטי כרטיסי אשראי. המטרה של ה-payload היא לאחסן מידע שנחוץ לאימות ולבדיקת הרשאות בלבד.

  • Signature - תפקידה לאשר את אותנטיות של ה-JWT. החתימה מבטיחה שה-JWT נותר נאמן למקור שיצר אותו ולא עבר שינויים לא מורשים לאחר שהונפק.

אחסון ה-JWT

שימו לב שמתכנתים צעירים שומרים את אותו JWT ב-local storage וזה נחשב security issue כי local storage מיועד להתקפה של XSS (Cross-Site Scripting). במקום זאת עדיף לשמור אותו ב-HttpOnly cookie כי לא ניתן לגשת לשם באמצעות JavaScript ו-HttpOnly cookie חסין מפני התקפות XSS.

יתרונות השימוש ב-JWT:

אחד היתרונות העיקריים של השימוש ב-JWT זה שהוא קטן וקומפקטי, וקל להעביר אותו בין צד לקוח לשרת. יתרון שני הוא הפופולריות שלו. הרבה מאוד ארגונים קטנים וגדולים משתמשים בו על מנת לחזק ולאבטח את האתרים שלהם. כמובן שיש עוד יתרונות, אבל אנחנו נתמקד בחשובות והבסיסיות.

חסרונות השימוש ב-JWT:

אחד החסרונות העיקריים של השימוש ב-JWT הוא שה-payload אינו מוצפן כברירת מחדל. המשמעות היא שכל מי שמיירט את ה-JWT יכול לקרוא את המידע הכלול. כדי להפחית סיכון זה, ניתן להצפין JWT באמצעות זוג מפתחות ציבורי/פרטי.

יישום JWT (JSON Web Token) באפליקציית Node.js :

יצירת ה-JWT:

import jwt from 'jsonwebtoken';

const generateUserToken = ({ _id, firstName, lastName, email, isAdmin }: IToken) => {
  return jwt.sign({
    _id,
    firstName,
    lastName,
    email,
    isAdmin
  },
  process.env.ACCESS_TOKEN_USER as string, { expiresIn: '3 days' });
}

עם הקוד שכתבנו למעלה, אנחנו יוצרים JWT באמצעות ספריית jsonwebtoken. הפונקציה מקבלת אובייקט עם המאפיינים _id, firstName, lastName, email ו-isAdmin, והאובייקט הזה יהיה ה-payload של אותו JWT. כשאנחנו יוצרים את ה-JWT, אנחנו מאבטחים אותו עם secretOrPrivateKey שרק אתו אנחנו נייצר JWT.

בגלל שה-secretOrPrivateKey צריך להיות פרטי ולא חשוף לעולם החיצוני, אנחנו ניישם אותו תחת משתנה סביבתי (Environment variable), מה שמבטיח שה-JWT יהיה תקף, רק אם המפתח הסודי (secretOrPrivateKey) תואם. בנוסף, אנחנו מגדירים את ה-JWT לפוג לאחר שלושה ימים, כפי שצוין במאפיין expiresIn.

שימוש בפונקציה של יצירת ה-JWT:

export const signIn = () => async (req: Request, res: Response) => {
  const { firstName, lastName, email, isAdmin, _id } = user;

  const propsForToken = { firstName, lastName, email, isAdmin, _id };
  const token = generateUserToken(propsForToken);

  res.cookie("access_token", token, {
    httpOnly: true,
    // secure: true // Enable it just in the production environment
  }).status(200).send("Some response");
};

בקוד שכתבנו למעלה, תוכלו לראות שבנינו route פשוט שאחראי ל-signIn. אנחנו לוקחים את נתוני המשתמש כמו שם, כתובת אימייל וכדומה, ושולחים את האובייקט "propsForToken" לפונקציה שראיתם למעלה "generateUserToken". אנחנו משתמשים ב-httpOnly cookie כדי לאחסן את ה-JWT שלנו בצורה מאובטחת.

בדיקת הרשאות:

// #PATCH
router.patch('/update-me', [user], updateMe());

// Regular User
const user = (req: any, res: any, next: any) => {
  if (req.headers.cookie) {
    const token = req.headers.cookie.split('=')[1];
    const decoded = jwt.verify(token, process.env.ACCESS_TOKEN_USER as string);
    req.user = decoded;
    return next();
  }
  return res.status(401).send("Access denied. No token provided.");
};

בקוד למעלה, ,תוכל לראות שיש route שאמור לעדכן את פרטי המשתמש, אבל כמובן לא כל אחד יכול, אלא רק למי שיש את ההרשאות המתאימות יכול לעדכן את פרטי המשתמש, לדוגמה המשתמש עצמו והמנהל. שימו לב שבאמצע של ה-route אנחנו משתמשים ב-middleware שמספקת לנו שכבת הגנה לפני שאנחנו מגיעים ל-route's method עצמה.

בעצם בתוך ה-middleware אנחנו מוודאים שהכל תקין, כלומר שיש למשתמש שלנו JWT אמיתי, ואם הוא אמיתי, הוא יוכל לבצע את הפעולה שהוא רצה (עדכון פרטי המשתמש).

שאלות נפוצות

מה ההבדל בין Authentication ל-Authorization

Authentication (אימות) זה לבדוק "מי אתה" - תהליך שמאשר את זהות המשתמש, בדרך כלל על ידי אימייל וסיסמה או OAuth. Authorization (הרשאה) זה לבדוק "מה מותר לך" - אחרי שהמשתמש זוהה, המערכת בודקת אילו פעולות הוא רשאי לבצע (לדוגמה, רק admin יכול למחוק משתמשים). שני התהליכים שונים אבל קשורים: צריך לאמת לפני שמרשים, ולא צריך לאמת לכל פעולה אם כבר יש JWT תקף.

מה ההבדל בין JWT ל-Session-based authentication

ב-Session-based, השרת שומר את ה-session של המשתמש (בזיכרון, ב-Redis או ב-DB), ושולח ללקוח cookie עם session ID. בכל בקשה השרת מחפש את ה-session במאגר. JWT הוא stateless - השרת לא שומר כלום, כל המידע על המשתמש נמצא בתוך ה-token עצמו. JWT מקל על scaling אופקי (אין צורך לחלוק session בין שרתים), אבל קשה לבטל token לפני שפג תוקפו. Session-based קל יותר לבטל אבל דורש storage מרכזי.

האם JWT באמת מאובטח

JWT מאובטח רק אם משתמשים בו נכון. הסיכונים העיקריים: שמירה ב-localStorage חושפת ל-XSS; שימוש באלגוריתם חלש (כמו none) מאפשר זיוף; secret key חלש מאפשר brute-force; expiration time ארוך מדי מאריך את חלון ההתקפה אם token נגנב. הגישה הנכונה: HS256 או RS256, secret key חזק, expiration קצר (15-60 דקות), refresh tokens, ואחסון ב-HttpOnly cookie עם secure ו-SameSite מוגדרים.

מה זה Refresh Token ולמה צריך אותו

Refresh Token הוא token נפרד עם תוקף ארוך (כמה שבועות) שמשמש להנפיק access tokens חדשים. הרעיון: ה-access token הראשי יהיה קצר תוקף (15 דקות) כדי לצמצם את החלון של התקפה במקרה של גניבה, אבל המשתמש לא צריך להתחבר מחדש כל 15 דקות - הקוד שולח את ה-refresh token לשרת ומקבל access token חדש בשקט. הפרדה זו משלבת אבטחה גבוהה עם חוויית משתמש טובה.

איך לבטל JWT לפני שתוקפו פג

זו אחת המגבלות של JWT - בגלל שהוא stateless, אי אפשר "למחוק" אותו מצד השרת. הפתרונות המקובלים: (1) Token blocklist - שמירת רשימה של JWT-ים שבוטלו ב-Redis או DB, ובדיקה של כל בקשה; (2) Short-lived access tokens עם refresh tokens - הצמצום הטבעי של חלון התוקף מקטין את הצורך בביטול ידני; (3) שינוי ה-secret key - מבטל את כל ה-JWT-ים אבל גם מנתק את כל המשתמשים ולכן לא מומלץ אלא במצב חירום.