מה זה JWT?
אסימון ה-JSON Web Token (JWT) הוא תקן המגדיר דרך בטוחה, קומפקטית, ועצמאית להעברת מידע בין לקוח לשרת בצורת אובייקט JSON.
אחד הדברים שחשוב להבין ש JWT הוא מושג אבסטרקטי שהמימוש שלו בא לידי ביטוי ע״י:- JWS (JSON Web Signature) – אסימון שמאומת באמצעות חתימה
- JWE (JSON Web Encryption) – אסימון שמאומת באמצעות הצפנה

JWS - Json Web Signature
מורכב מ: Header , Payload , Signature
ו - JWE- Json Web Encryption
מורכב מ: Header, Encrypted Key,Initialization Vector,Ciphertext, Authentication Tag
דבר שגם אני מודה בפה מלא לא הכרתי עד שממש צללתי על הנושא 🙂
תיאור ויזואלי מעולה של JWS vs JWE ניתן לראות כאן
JWT היא כאמור דרך אחת ליצירת אסימון. ישנם מספר חלופות ל-JWT כמו Branca, Paseto, Macaroon
במאמר זה אדבר על הדברים המשותפים ל JWS ו JWE שהם Header ,Payload ואז נמשיך ל Signature שקשור ל JWS
מבנה ה-JWS (סקירה מהירה):
אסימון ה-JSON Web Token הוא למעשה שלוש מחרוזות המופרדות זו מזו באמצעות '.' (נקודה)

Header – מכיל מידע על הסוג של האלגוריתם שישמש להצפנת ה- JWT
Payload – מכיל את המידע העיקרי שהשרת משתמש בו לזיהוי המשתמש וההרשאות שלו. חשוב להבין שלמרות שה-JWT מאובטח, לא טוב לשמור בו מידע רגיש. המטרה של ה-payload היא לאחסן מידע שנחוץ לאימות ולבדיקת הרשאות בלבד.
Signature – תפקידו לאשר את האמינות של ה-JWT. קרי החתימה מבטיחה שה-JWT לא עבר שינויים לא מורשים לאחר שהונפק.
כותרת (Header) -
זהו החלק הראשון של ה-JWT. הוא ידוע גם בשם JOSE (JSON Object Signing and Encryption).
הכותרת תפקידה לתאר איזה אלגוריתם משמש לחתימה , במקרה שברצוננו להפיק JWS
או לחלופין איזה אלגוריתם משמש להצפנת התוכן במקרה שברצוננו להפיק JWE.
הכותרת ב JWS מגדירה שני מאפיינים:
- alg האלגוריתם המשמש לחתימה או הצפנה של ה-JWT.
- typ התוכן שנחתם או מוצפן.
כותרת ה-JSON נראית כפי שמוצג למטה.
{
"alg": "HS256",
"typ": "JWT"
}
שימו לב, כאשר אנו ממירים את התוכן הזה ל-base64encode, אנו מקבלים את החלק הראשון של אסימון ה-JSON שלנו. זה רק קידוד ולא הצפנה. כל אחד יכול בקלות לפענח את המחרוזת זו ולקבל את ה-JSON.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

JWE (JSON Web Encryption) Header:
בכותרת JWE יש מידע הנועד לפענח את ההודעה, כולל את האלגוריתם המשמש להצפנת התוכן והשיטה לאחזור המפתח המוצפן. בדרך כלל היא כוללת את המאפיינים הבאים:
1. alg: האלגוריתם המשמש להצפנת המפתח, לדוגמה, RSA-OAEP.
2. enc: אלגוריתם ההצפנה של התוכן, לדוגמה, A256GCM.
3. typ
דוגמה לכותרת JWE:
{
"alg": "RSA-OAEP",
"enc": "A256GCM",
"typ": "JWT"
}
אלגוריתמים שניתנים לקינפוג ב header:
לאסימוני JWT (JSON Web Tokens), ישנם מספר אלגוריתמים קריפטוגרפיים מלבד HS256 שניתן להשתמש בהם לחתימה והאבטחה של אסימונים. לכל אחד מהאלגוריתמים אלו יש תכונות ייחודיות לשימושים שונים, הנה כמה מהנפוצים:
HS256 : כאמור, הוא משתמש במפתח סימטרי לצורך חתימה ואימות האסימון. המימוש שלו יחסית פשוט והוא נמצא בשימוש נרחב. החיסרון איתו שהוא דורש שיתוף מפתח סודי בין הצדדים.
HS384 ו HS512 : אלגוריתמים אלו דומים ל-HS256 אך משתמשים באלגוריתמי הצפנה SHA-384 ו-SHA-512 בהתאמה. הם מספקים ערכי גיבוב ארוכים יותר, ומציעים בטיחות גבוהה יותר אך במחיר של משאבי מחשוב יקרים יותר.
RS256: אלגוריתם זה משתמש בקריפטוגרפיה ציבורית של RSA. האסימון נחתם באמצעות מפתח פרטי, והמפתח הציבורי המתאים משמש לאימות החתימה. זה שימושי בתרחישים שבהם גורם המנפיק וגורם המאמת שונים זה מזה.
ES256: משתמש באלגוריתם חתימה דיגיטלי של עקומה אליפטית (ECDSA) עם עקומת P-256 ו-SHA-256. ידוע ביעילותו ביחס ל-RSA מבחינת משאבי מחשוב ודורש מפתחות קטנים יותר לאותו רמת בטיחות.
PS256 : זו גרסה חדשה יותר של חתימות RSA, המשתמשת ב-RSASSA-PSS (מערכת חתימה פרובביליסטית ל RSA) עם SHA-256. נחשב לטוב יותר כנגד התקפות קריפטוגרפיות מסוימות לעומת RS256. לכל אחד מהאלגוריתמים הללו יש את היתרונות והשימושים שלו:
HS256, HS384, HS512: טובים למצבים בהם אותו גורם אחראי על יצירה ואימות האסימון.
RS256, ES256, PS256: מתאימים יותר לתרחישים בהם גורם המנפיק וגורם המאמת שונים, מה שמאפשר להפיץ את המפתח הציבורי בעוד שומרים על הפרטי בבטחה. בעת בחירת אלגוריתם, חשוב לשקול את דרישות הבטיחות, את משאבי המחשוב הזמינים, ואת נוחות ניהול המפתחות. הבחירה לעיתים תלויה בשימוש הספציפי ובסביבה שבה ה-JWT ישמש.
Payload
זהו החלק השני של ה-JWT. הוא מכיל את המידע העיקרי שהשרת משתמש בו לזיהוי המשתמש וההרשאות שלו. הפיילוד מורכב מ-claims (הצהרות).
"הצהרות" – הן טענות/הצהרות לגבי ישות (בדרך כלל המשתמש) ונתונים נוספים. או במילים יותר קלילות ה״הצהרות״ הן זוגות של שם וערך (שדות) המעבירים מידע על המשתמש, כמו מזהה משתמש, כתובת אימייל, תפקידים וכו'.. ישנם שלושה סוגים של claims.
א) שמות הצהרה רשומים – Registered Claim Names
אביא לפניכם את ה"הצהרות הרשומות" הנפוצות ביותר:
sub: מזהה את הרכיב המרכזי שהוא הנושא של ה-JWT.
לדוגמה, בהקשר של אימות משתמש, ההצהרה "sub" עשויה להכיל מזהה ייחודי (כמו מספר משתמש) שמייצג את המשתמש שאומת. זה מאפשר למקבל ה-JWT לקבוע את המשתמש הספציפי שהטוקן מיועד לו.
aud: מזהה את המקבלים שה-JWT מיועד להם.
exp: מזהה את זמן התפוגה שלאחריו ה-JWT לא חייב להתקבל לעיבוד.
nbf: מזהה את הזמן לפניו ה-JWT לא חייב להתקבל לעיבוד.
iat: מזהה את הזמן בו ה-JWT הונפק.
ב) שמות הצהרה ציבוריים – Public claims
הצהרות ציבוריות: אלו הן הצהרות שניתן להגדירם לפי הרצון של המשתמש אך אמורים להיות מוגדרים ברישום של ״IANA "JSON Web Token Claims או להיות מוגדרים כ-URI שכולל מרחב שמות שאינו נפגע מהתנגשויות . ההצהרות הציבוריות מיועדות להסכמה כללית ויכולות להיות בשימוש בין יישומים ושירותים שונים.
דוגמא:
{
"sub": "1234567890", // Registered claim
"iat": 1516239022, // Registered claim
"name": shay // הצהרה ציבורית עם שם שאינו נפגע מהתנגשויות
}
אם ניכנס לאתר של https://www.iana.org/ נראה את שמות ההצהרה הציבוריים שניתן לתת לטוקן שלנו.

*ניתן גם לראות בהתחלה ב IANA מה שלא מסומן במרקר הן הצהרות רשומות שניתן לתת לטוקן.
ג. שמות הצהרה פרטיים- Private Claims
מייצר וצרכן של JWT יכולים להסכים על כל "הצהרה פרטית" שאינו הצהרה רשומה או הצהרה ציבורית. בניגוד להצהרות הציבוריות, הצהרות אלו פרטיות וחשופות להתנגשויות ויש להשתמש בהם בזהירות..!!.
הערה: לרוב, רבים מהיישומים מתבססים בעיקר על הצהרות רשומות ופרטיות כדי להעביר את המידע הנדרש. יחד עם זאת, אם יש צורך בהצהרה שיש לה פוטנציאל להיות שימושית באופן אוניברסלי במגוון יישומים או פלטפורמות, או אם יש רצון למנוע התנגשויות שמות עתידיות, כדאי לשקול לרשום את הצהרה זו כציבורית ברישום של ״IANA "JSON Web Token Claims.
*כדי לרשום ״הצהרה״ כציבורית ב INNA הכנתי את הצעדים שיש ליישם, נמצא בנספח (סוף המאמר).
כעת ניקח דוגמא לפיילוד עם שדה פרטי
{
"sub": "1234567890", // Registered claim
"department": "Human Resources" // Private claim
"iat": 1516239022 // Registered claim
}
כמו שהזכרתי קודם אם הצהרה כלשהי מוגדרת ברישום ההצהרות של IANA ל-JWT, אז הוא הצהרה ציבורית או רשומה.
זה אומר שניתן לעשות איתו שימוש במגוון יישומים ופלטפורמות.
במקרה כאן department לא מוגדר ב רישום של INNA ולכן הוא מוגדר כהצהרה פרטית.
עד כאן לנושא של ההבדלים בין ההצהרות השונות חומר מסכם.
לאחר שלמדנו על סוגי ההצהרות השונים נמשיך בהמרה של הפיילוד:
כמו שבטח תיארתם לעצמכם ההמרה של ה payload נעשה באותו אופן שהמרנו את ה header, המרה ל-base64encode ונקבל את החלק השני של אסימון ה-JSON שלנו.
eyJzdWIiOiIxMjM0NTY3ODkwIiwiZGVwYXJ0bWVudCI6Ikh1bWFuIFJlc291cmNlcyIsImlhdCI6MTUxNjIzOTAyMn0

נשים לב שפיילוד הוא מושג די ״חמקמק״ מכיוון שמצד אחד ב -JWS (JSON Web Signature), הפיילוד אינו מוצפן וכל אחד שיש לו את ה-JWT יכול לקרוא אותו על ידי פיענוח של הקידוד ב-Base64Url.
ומצד שני ב-JWE (JSON Web Encryption), ה-Payload מוצפן כדי להגן על מידע רגיש. הפעולה הזו מבטיחה שהמידע לא יהיה נגיש לאנשים שאין להם את המפתח המתאים לפענוח.
שימו לב בטח אתם שואלים היכן הפיילוד ב JWE ? הרי לא הזכרנו רכיב כזה :
אז אני אסביר שכאן החלק שנקרא Ciphertext הוא בעצם טקסט מאובטח ומוצפן שלמעשה משמש כפיילוד.
תזכורת:
JWE- Json Web Encryption – מורכב מ:
Header, Encrypted Key,Initialization Vector,Ciphertext, Authentication Tag

שימו לב טוב, ב JWE אנו בעצם נטמיע את אובייקט ה Payload => ״גייסון״ עם ה״הצהרות״ (שדות) (the key – value pairs)
כאשר ה Payload מוצפן ב -Ciphertext ..!!!
(בניגוד לדבר שקורה ב JWS שהוא אינו מוצפן כמו שראינו)
בהמשך המאמר נרחיב על מושג ה Signature
איך רושמים הצהרה כציבורית ב INNA?:
כדי לרשום הצהרה ציבורית ברישום ההצהרות של IANA ל-JSON Web Token, יש לבצע את השלבים הבאים:
- דרישת מפרט: ההצהרה שלך חייבת לעמוד במפרט טכני מסוים. זה אומר שההצהרה שאתה רוצה לרשום צריכה להיות מוגדרת היטב ולשרת מטרה ברורה.
- התייחסות ל-RFC 7519: ההצהרה שלך צריכה להיות בהתאם להנחיות שנקבעו ב-RFC 7519, שהוא המסמך הבסיסי ל-JWT.
- שליחת בקשת רישום: שלח את בקשת הרישום שלך לרשימת התפוצה כפי שמתואר ב-RFC 7519. זה כולל את הצגת ההצהרה שלך, מטרתה, ואיך שהיא מתאימה למבנה JWT.
- המתנה לאישור: לאחר שליחת הבקשה שלך, המנהלים/מומחים בנושא זה יבדקו אותה.
אם ההצהרה שלך תתקבל, הם יודיעו ל- IANA תוך שלושה שבועות.
לעזרה: אם אתה זקוק לעזרה בתהליך, תוכל לפנות ישירות ל- IANA בכתובת iana@iana.org.
Signature
חתימות סימטריות
כאשר JWT נחתם באמצעות מפתח סודי, זה נקרא חתימה סימטרית. סוג זה של חתימה מתבצע כשיש רק שרת אחד שחותם ומאמת את האסימון, אותו מפתח סודי משמש ליצירה ולאימות של האסימון ונחתם באמצעות HMAC. HMAC הוא ראשי תיבות של Hashing for Message Authentication Code. זהו קוד אימות להודעה שנקבע על ידי הרצת פונקציה קריפטוגרפית של חשיבה (כמו MD5, SHA1, ו-SHA256) על הנתונים (שמיועדים לאימות) ומפתח סודי משותף (secret key) כדי ליצור את החלק של החתימה צריך ארבעה רכיבים: – כותרת (header) מקודדת ב-Base64 – (payload) מקודד ב-Base64 – מפתח סודי – secret key – אלגוריתם קריפטוגרפי – (למשל כאן נשתמש ב HMACSHA256) נניח שהמפתח הסודי שלנו הוא "123456" נשתמש בheader ו בpayload שיצרנו בחלקים 1 ו 2 (יכולים לדפדף אחורה לחלקים אלו)Header:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
payload:
TY3ODkwIiwiZGVwYXJ0bWVudCI6Ikh1bWFuIFJlc291cmNlcyIsImlhdCI6MTUxNjIzOTAyMn0
ונשתמש באלגוריתם HMACSHA256 קרי:
HMACSHA256(
base64UrlEncode(header) + "."
+ base64UrlEncode(payload),
secret key
)
אם נריץ את זה יווצר לנו:
NrWrYTgCx8ikHIqExDG_69kYfCBDWH3x-8y36yH5BCM
לבסוף אם אני נחבר את שלושת המרכיבים ל jws : (שעשיתי בדוגמא , אני מקבל)
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZGVwYXJ0bWVudCI6Ikh1
bWFuIFJlc291cmNlcyIsImlhdCI6MTUxNjIzOTAyMn0.NrWrYTgCx8ikHIqExDG_69kYfCBDWH3x-8y36yH5BCM
אתם כמובן יכולים לראות את התוצאה ב jwt.io : (לא לשכוח להכניס מפתח סודי, ולעשות וי על הצ׳קבוקס של secret base64 encoded ) ולראות בעצמכם שהטוקן תקין.

כדי ליצור אסימון משלכם , אתם יכולים להשתמש בקוד (פייתון) הבא:
import jwt
# Define the header and payload
header = {
"alg": "HS256",
"typ": "JWT"
}
payload = {
"sub": "1234567890",
"department": "Human Resources",
"iat": 1516239022
}
# Secret key
secret = "123456"
# Create the JWT
encoded_jwt = jwt.encode(payload, secret, algorithm='HS256', headers=header)
print(encoded_jwt)
מה שתקבלו בהרצת קוד זה הוא קוד Base64
למשל יכול לצאת לכם הפלט הבא:

או לחלופין להיכנס לאתר של https://jwt.io/ וליצור טוקן באופן ידני:
בדוגמא הנ״ל אני ממש משנה את הערכים בheader וב payload כרצוני ושימו לב שכל פעם שאני אשנה את הערכים אז הקוד של ה Encoded משתנה בהתאם.

שימו לב ➕
שאם אני כביכול אנסה ״להתחכם״ ולשנות את התוכן כלומר את הפיילוד, ואז אנסה לקודד את זה ולשלוח את הטוקן לשרת עם הפיילוד החדש , הטוקן לא יאומת לי…!!!
וזה בעצם תפקידו של החתימה בטוקן קרי לוודא שהתוכן שנשלח אל השרת הוא התוכן המקורי שהשולח רצה לשלוח לי.
למשל בואו ננסה ליצור אסימון חדש וננסה לשנות לו את החתימה.

- ניצור טוקן חדש עם האתר של jwt :
קוד ה encoded הוא :
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMTExMSIsImRlcGFydG1lbnQiOiJzaGF5IiwiaWF0IjoxNTE2MjM5MDIyfQ.6vOHVCXwRQx3ESdtS
5e98c8LOSLfP0K0-CnJYe9ooqc
כעת נשנה את ה payload ל
{
"sub": "11111",
"department": "shay 4", <= הוספתי 4 אחרי ה שי
"iat": 1516239022
}

אם נתייחס רק לחלק של הפיילוד שנוצר לי כעת הוא:
עתה אם נחליף את הפיילוד הזה (שלמעלה) עם הפיילוד הקודם
לפני ההחלפה:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMTExMSIsImRlcGFydG1lbnQiOiJzaGF5IiwiaWF0IjoxNTE2MjM5MDIyfQ.6vOHVCXwRQx3ESd
tS5e98c8LOSLfP0K0-CnJYe9ooqc
אחרי ההחלפה:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMTExMSIsImRlcGFydG1lbnQiOiJzaGF5IDQiLCJpYXQiOjE1MTYyMzkwMjJ9.6vOHVCXwRQx3ESd
tS5e98c8LOSLfP0K0-CnJYe9ooqc
ונכניס את את הטוקן המזוייף שיצרנו לאתר של jwt נקבל Invalid Signature:

הסיבה לכך היא מאוד פשוטה והיגיונית שימו לב כשבנינו את החתימה בנינו אותה ע"י:
HMACSHA256(
base64UrlEncode(header) + "."
+ base64UrlEncode(payload),
secret key
)
אם אני פוגע ב פיילוד => אני אפגע בחתימה => טוקן לא מאומת עקב חתימה שאינה נכונה
חתימות אסימטריות
חתימה זו מתאימה לתרחישים מבוזרים. נניח שישנן מספר סרוויסים שיכולים לאמת JWT נתון. אם נשתמש במפתח סודי לחתימת JWT (כמו מקודם), אזי כל הסרוויסים יצטרכו להשתמש במפתח (secret key) יחיד כדי לאמת את האסימון.
הבעיה שיכולה להיווצר שאם אני אשתמש באותו מפתח סודי בכל הסרוויסים אז ישנו סיכוי רב יותר שהוא ידלף ואילו חלילה ידלף אז כל הסרוויסים שלי כעת לא מוגנים..!!.
בכדי לפתור את הבעיה הזו, פותחה שיטה של חתימה אסימטרית.
החתימה האסימטרית משתמשת בזוג מפתחות: פרטי ו ציבורי לחתימה.
כאשר המימוש נעשה באופן הבא:
בדר״כ יש שרת/סרוויס אחד שבו יש את המפתח הפרטי שזהו שרת שתפקידו לייצר את האסימונים והוא אחראי לחתום אותם באמצעות המפתח הפרטי, ולבסוף מעביר אותו לסרוויס אחר.
לאחר מכן הסרוויס שצריך לאמת את האסימון הנקרא בדוגמה ה ״סרוויס האחר״ אינו צריך להחזיק במפתח (secret key) אלא הוא יאמת את הטוקן באמצעות מפתח הציבורי בלבד. כאשר החתימה זו תבוצע באמצעות RSA(זוהי הצפנה אסימטרית) ואלגוריתם חתימה דיגיטלית.
דוגמא קוד עם נוד ואקספרס (באופן לוקלי – לא לפרודקשיין):
- נתקין את החבילות: npm install jsonwebtoken express
- ניצור זוג מפתחות RSA:
נשתמש בכלי OpenSSL כדי ליצור מפתח פרטי וציבורי:
openssl genpkey -algorithm RSA -out private.pem
openssl rsa -pubout -in private.pem -out public.pem
אם אתם רוצים לראות את הקוד שנוצר במפתח הציבורי והפרטי שלכם אתם יכולים להריץ:
openssl rsa -in private.pem -text -noout
openssl rsa -pubin -in public.pem -text -noout

ניתן גם לראות את המפתחות שיצרנו בתיקייה:
3. ניצור את סרוויס האותנטיקציה:
const express = require('express');
const jwt = require('jsonwebtoken');
const fs = require('fs');
const app = express();
const privateKey = fs.readFileSync('private.pem', 'utf8');
app.get('/login', (req, res) => {
const user = { id: 123, username: "exampleUser" };
// This should be fetched after validating user credentials
const token = jwt.sign(user, privateKey, {
expiresIn: '1h',
algorithm: 'RS256'
});
res.send({ token });
});
app.listen(3000, () => {
console.log('Auth Service started on port 3000');
});
כעת נניח שעשיתי לוגין '/login' (הכל בסדר) יש לי טוקן אצלי ביד שקיבלתי אותו מסרוויס האותנטיקציה:
res.send({ token });
עתה נניח שיש לנו סרוויס אחר בשם User Profile שרוצה להשתמש בסרוויס האימות , אזי נממש זאת כך:
const express = require('express');
const jwt = require('jsonwebtoken');
const fs = require('fs');
const app = express();
const publicKey = fs.readFileSync('public.pem', 'utf8');
const verifyToken = (req, res, next) => {
const token = req.headers['authorization'];
if (!token) return res.sendStatus(403);
jwt.verify(
token,
publicKey,
{ algorithms: ['RS256']},
(err, user) => {
if (err) return res.sendStatus(403);
req.user = user;
next();
});
};
app.get('/profile', verifyToken, (req, res) => {
// Fetch user data based on req.user.id
res.send(`Hello ${req.user.username}`);
});
app.listen(4000, () => {
console.log('User Profile Service started on port 4000');
});
שימו לב מה קורה בסרוויס זה !! אם אני רוצה להפעיל את השיטה '/profile' אז קודם כל תפעל הפונקציה verifyToken ושם אני אבדוק את ה token ששמתי בauthorization לפני שביצעתי את הבקשה למשל:

verifyToken כך:
const token = req.headers['authorization'];
ואז לבדוק אותו כך:
jwt.verify(
token,
publicKey,
{ algorithms: ['RS256']},
(err, user) => {
if (err) return res.sendStatus(403);
req.user = user;
next();
});
כמה הדגשים שימו לב 👍
- לשם הפשטות לא החזרתי את הטוקן ב cookie דבר שמאוד מומלץ לעשות כדי להימנע מהתקפת Cross-Site Scripting (XSS)
- דוגמה זאת היא לא לפקודקשן בשום צורה זאת דוגמה להבנה בלבד.
- נהוג לחתום את החתימה של authirazation עם Bearer

4. שימו לב שאפשר לממש את הרעיון הזה עם AWS KMS בשילוב עם AWS Lambda כפי שמתואר במאמר של Yossi Ittach.
פוסטים ומאמרים נוספים ניתן למצוא בדף הלינקדין shay Sarussi Elshten