OAuth2: היכן נשברת הכניסה ומדוע זה יקר ממה שזה נראה
OAuth2 כמעט אף פעם לא נשבר "בכניסה" — הוא נשבר בפרטים שאינם נראים ב-happy path. הקלאסיקה: לקוח ציבורי ללא PKCE, וקוד ההרשאה עובר בגלוי דרך יירוט הפניה מחדש — במכשיר נייד או בדפדפן כל אפליקציה שרשמה את אותה סכמת URI תופסת אותו. לאחר מכן התוקף מחליף את הקוד בטוקן לפני הלקוח הלגיטימי, ובלוגים זה נראה כמו כניסה מוצלחת רגילה.
המוקש השני — טוקנים נצחיים. Access token ללא תוקף קצר ו-refresh ללא רוטציה חי עד לביטול ידני: דליפה מהלוגים, מהזיכרון של המכשיר או דרך סקריפט צד שלישי מעניקה גישה קבועה, ואין במה לבטל אותו — אין רשימת ביטול ואין הצמדה למכשיר. את הביטול נאלצים לבצע בהחלפת סודות, מה שמפיל את כל המשתמשים בבת אחת.
התרחיש השלישי — חוסר סנכרון בין סשנים בין שירותים. המשתמש נכנס באפליקציה אחת, אך השירות הסמוך אינו רואה אותו: מנפיקי טוקנים שונים, קהלים שאינם תואמים, מפתחות חתימה שהתפצלו. מבחוץ זו "סירוב כניסה" ללא שגיאה ברורה, והתמיכה מנתחת אותו בעיוורון.
בנפרד — פרצי סירובים בעת רענון הטוקן. כאשר בקשות refresh יוצאות במקביל מכמה לשוניות או רפליקות, הרוטציה פועלת בבקשה הראשונה, והשאר מקבלות סירוב ומנתקות את המשתמש. שיא התקלות הללו חל בשעות הבוקר וברגעי פריסה, כאשר סשנים מבקשים מחדש גישה בהמוניהם.
המחיר של כל ארבעת התרחישים זהה: הזמנות שאבדו, עומס על התמיכה ואובדן מוניטין שאינם נראים במדדי הזמינות. מפרקים כיצד זה נסגר.
תרשים כניסה: PKCE, סבב טוקנים, כניסה דרך רשתות חברתיות
PKCE במקום implicit flow
תהליך implicit flow הקלאסי מעביר את הטוקן ישירות לשורת הכתובת — הוא נשמר בהיסטוריית הדפדפן, בלוגים של פרוקסי ובכותרת Referer. PKCE סוגר את המחלקה הזו: הלקוח מייצר code_verifier אקראי, שולח את ה-hash שלו (code_challenge) לאימות, והספק משווה את הזוג בעת החלפת הקוד בטוקן. קוד שיורט ללא verifier חסר תועלת — התקיפה על ה-redirect מפסיקה לעבוד באופן עקרוני.
סבב טוקנים ותוחלת חיי הסשן
טוקן ה-refresh חד-פעמי: בעת ההחלפה מונפק זוג חדש, והישן מבוטל. אם אותו refresh מגיע שוב — זה סימן לגניבה, וכל שרשרת הסשנים של המשתמש נשללת אוטומטית. טוקן ה-access חי דקות, ולכן חשיפה נותנת חלון של דקות בודדות, לא ימים. תוחלת חיי הסשן נקבעת לפי מדיניות: עבור אזורי ניהול — שעות, עבור רגילים — ימים עם הארכה לפי פעילות.
כניסה דרך רשתות חברתיות ושלילת גישה
ספק חיצוני מחזיר מזהה חתום, שלפיו אנו מאתרים או יוצרים חשבון מקומי — הקישור נעשה לפי מזהה יציב, ולא לפי דואר אלקטרוני, ולכן שינוי כתובת אינו יוצר כפילות. שלילת הגישה מיידית: הרשומה ברשימת הטוקנים הנשללים נבדקת בכל בקשה, והסשן מת בגבולות מטמון הבדיקה.
| אפשרות | מתי מתאימה | מגבלות |
|---|---|---|
| PKCE + סבב | לקוחות ציבוריים, ניידים ו-SPA | נדרש חילוף קוד בצד השרת |
| זרימת שרת קלאסית | לקוחות חסויים עם סוד | הסוד דולף יחד עם ה-backend |
| טוקן ארוך-טווח | שירותים פנימיים ללא משתמש | השלילה מתעכבת, חלון התקיפה גדל |
את התרשים מרכיבים על Keycloak או על ספק מובנה — הבחירה תלויה בשאלה אם נדרשת כניסה אחודה למערכות חיצוניות.
סדר העבודה: מביקורת הכניסה הנוכחית ועד מסירת ההגדרות
העבודה מתבצעת ברצף: כל שלב עוקב נשען על התוצרים של השלב הקודם, ולכן אין צורך לבחון מחדש החלטות ביניים באמצע הדרך. להלן — כיצד בנוי התהליך מביקורת הכניסה ועד מסירת ההגדרות.
-
ביקורת הכניסה הנוכחית. מנתחים כיצד האפליקציה מנפיקה ומאחסנת טוקנים, היכן נמצאות הסשנים, אילו ספקים כבר מחוברים ואילו זרימות OAuth2 נמצאות בשימוש. מתעדים נקודות תורפה: טוקני refresh ארוכי-טווח ללא רוטציה, היעדר PKCE ללקוחות ציבוריים, טוקנים ב-localStorage, היעדר ביטול בעת יציאה.
-
תכנון הזרימות. בוחרים Authorization Code + PKCE ללקוחות שאינם יכולים לאחסן סוד, וקובעים את משך החיים של טוקני access ו-refresh. מתכננים רוטציה של טוקני refresh עם זיהוי שימוש חוזר: בניסיון להשתמש בטוקן שכבר בוטל, כל השרשרת מבוטלת.
-
הגדרת ספקים ומדיניות. רושמים את האפליקציה אצל הספקים, מגדירים כתובות redirect, scopes וכללי התאמת חשבונות בעת social login. מנסחים מדיניות של ביטול, אחסון וקישור של מספר דרכי כניסה למשתמש אחד.
-
בדיקת תרחישי כשל. מריצים את מה שבדרך כלל נשבר בפרודקשן: טוקן refresh שפג תוקפו, ביטול גישה בצד הספק, אי-סנכרון שעונים, callback חוזר, התנגשות חשבונות בעת כניסה דרך שירותים שונים. כל תרחיש חייב להסתיים במצב צפוי מראש, ולא בסשן תקוע.
-
מסירת ההגדרות. מוסרים את קונפיגורציות הספקים, תרשימי הזרימות, מדיניות הטוקנים ותיעוד התפעול — כדי שצוות הלקוח יוכל לשנות פרמטרים בלעדינו.
תארו את סכמת הכניסה הנוכחית שלכם — נחזור עם הערכה ותוכנית עבודה.
במה שונה קוד הרשאה עם PKCE מזרימה משתמעת?
הזרימה המשתמעת (implicit flow) הייתה פשרה של עידן שבו דפדפנים לא ידעו לאחסן סודות באופן אמין, ושרתי הרשאה לא תמכו בהחלפת קוד בטוקן ללא מעורבות צד אחורי. בה, אפליקציית הלקוח מקבלת access token ישירות בקטע של שורת הכתובת — תוך עקיפת שלב הביניים עם הקוד.
זה צמצם את מספר הבקשות, אך המחיר היה גבוה: הטוקן שקע בהיסטוריית הדפדפן, בלוגים של פרוקסי ובכותרות referer, ויירוט שלו באמצעות סקריפט מוזרק או תוסף מוחלף לא היה משימה קשה. בדיוק משום כך OAuth2 במהדורה המעודכנת אינו ממליץ במפורש על הזרימה המשתמעת עבור אפליקציות לקוח.
קוד הרשאה עם PKCE פותר את אותה משימה אחרת. האפליקציה מייצרת ערך אקראי (code_verifier), מחשבת ממנו נגזרת (code_challenge) ושולחת אותה יחד עם בקשת הכניסה. השרת מחזיר קוד חד-פעמי, והחלפת הקוד בטוקן אפשרית רק בהצגת ה-verifier המקורי.
גם אם תוקף יירט את הקוד — דרך הפניה מחדש, לוגים או אפליקציה זדונית במכשיר — ללא verifier הוא חסר תועלת. זה סוגר מחלקת תקיפות של יירוט קוד הרשאה, שבזרימה המשתמעת כלל לא היה קיים כשלב.
ההבדל המרכזי — היכן מגיע הטוקן. בזרימה המשתמעת הוא נוחת מיד בסביבת הלקוח וחי שם עד לפקיעת התוקף. בזרימה עם PKCE הטוקן מונפק רק לאחר אימות ה-verifier, והקוד עצמו חד-פעמי ומקושר לבקשה ספציפית.
עבור אפליקציות ניידות וחד-עמודיות, שבהן אין צד אחורי מהימן, זו האפשרות הנכונה היחידה: אין היכן לאחסן את סוד הלקוח, ו-PKCE מחליף אותו בקישור קריפטוגרפי.
קיימת גם קו הגנה שני — refresh rotation. refresh token בעל חיים ארוכים מוחלף בכל רענון בחדש, והישן מבוטל. אם הטוקן נגנב ושומש, הלקוח הלגיטימי יקבל סירוב ברענון הבא, וכל שרשרת הסשנים תבוטל.
בזרימה המשתמעת refresh token כלל לא הונפק — ניתן היה להאריך את הסשן רק בכניסה חוזרת, מה שדחף מפתחים להארכת תוקף ה-access token ולגידול חלון הפגיעות.
בפועל הבחירה נראית כך: עבור אפליקציות שרת עם צד אחורי מהימן — קוד הרשאה עם סוד לקוח; עבור חד-עמודיות וניידות — קוד הרשאה עם PKCE; הזרימה המשתמעת נותרת רק במערכות מורשת, שבהן החלפתה מחייבת בנייה מחדש של הלקוח.
אבטחת הכניסה נמדדת כאן לא בנוחות האינטגרציה, אלא בכמה מחלקות תקיפה נסגרו ברמת הפרוטוקול ולא ברמת המשמעת של המפתח.
מקרה בוחן: הכניסה הפסיקה ליפול לאחר רוטציית טוקנים
הלקוח — שירות עם אזור אישי ואפליקציה לנייד — הגיע עם תלונה שהתמיכה לא הצליחה לסגור: משתמשים התלוננו באופן קבוע ש"הכניסה נופלת". שיעור הכניסות הכושלות עמד על כ-8%: חלק מהבקשות לרענון טוקן הגיעו עם ערך שכבר נוצל, השרת השיב בסירוב, והאדם ראה טופס התחברות במקום האזור האישי.
כאב נפרד — שלילת גישה: לאחר חסימת עובד, הסשן שלו המשיך להתקיים עד לפקיעת הטוקן, כלומר הגישה נשמרה במשך שעות.
מה שינו
פירקנו את זרימת האימות לשלבים ומצאנו את השורש: האפליקציה רעננה את הטוקן במקביל מכמה לשוניות ומשימות רקע, והטוקן הישן לא בוטל באופן אטומי.
הכנסנו רוטציה של טוקני רענון: כל החלפה מנפיקה זוג חדש, הערך הקודם מסומן כנוצל, והצגה חוזרת שלו מתפרשת כסימן לדליפה — כל שרשרת הסשנים של המשתמש נשללת. המרוץ בין הלשוניות נסגר בצד הלקוח: ההחלפה עוברת דרך נקודה יחידה עם נעילה, שאר הבקשות ממתינות לתוצאה במקום ליזום בקשה משלהן.
עבור כניסה דרך ספקים חיצוניים חיברנו PKCE — קוד ההרשאה כבר לא עובר בגלוי, והפנייה מיורטת ללא מאמת היא חסרת תועלת. שלילת הגישה הועברה למרשם שרתי של סשנים פעילים: מנהל סוגר את הגישה, וכל הטוקנים הקשורים הופכים ללא תקפים ברגע השלילה, ולא עם פקיעת התוקף.
מה היה בסוף
| מדד | לפני | אחרי |
|---|---|---|
| שיעור הכניסות הכושלות | ~8% | פחות מ-0.5% |
| זמן שלילת גישה | עד תום חיי הטוקן | שניות לאחר השלילה |
| פניות לתמיכה לאיפוס סשנים | זרם קבוע | מקרים בודדים |
בנפרד סגרנו את תרחיש החלפת הסיסמה: הוא כעת מסיים את כל שאר הסשנים מלבד הנוכחי — קודם המשתמש החליף סיסמה ולא הבין מדוע הסשן הישן במכשיר אחר ממשיך לעבוד.
את הלוגיקה בנינו על פרוטוקול ההרשאה הסטנדרטי OAuth 2.0 עם הרחבות PKCE ורוטציה של טוקני רענון — בלי סכמות תוצרת בית שקשה אחר כך לתחזק ולבדוק מבחינת אבטחה.
התלבטויות לפני הטמעה: תאימות, מיגרציה, חזרה לאחור
הטמעת OAuth2 כמעט לעולם אינה מתחילה מאפס: ללקוח כבר יש בסיס משתמשים, סשנים משלו, תפקידים ושירותים חיצוניים שניגשים לאפליקציה לפי הכללים הישנים. לכן השאלה הראשונה אינה "כיצד להגדיר ספק", אלא "מה יקרה לכניסות הקיימות בזמן שאנחנו מעבירים את המערכת למתאר החדש".
אנחנו מתכננים את מיגרציית הכניסה כך שהמנגנון הישן והחדש יעבדו במקביל: המשתמש נכנס למערכת בכל דרך מוכרת, וחשבון המשתמש נקשר בהדרגה למזהה החדש. בנפרד אנחנו מתעדים תוכנית חזרה לאחור — אם בפרודקשן תתגלה אי-תאימות, החזרה לסכימה הקודמת אורכת דקות, לא שעות.
מה אם יש לנו ספק לא סטנדרטי או בסיס משתמשים משלנו? את OAuth2 אנחנו מרחיבים באמצעות ספק זהות משלנו: הוא מספק את אותם נתונים כמו הספקים החיצוניים, ומתחבר לבסיס הקיים ללא בנייה מחדש שלו. מה אם חלק מהשירותים לא יודעים לעבוד עם טוקנים?
עבורם אנחנו משאירים שכבת גישה תואמת, שמקבלת את הפורמט הקודם ובפנים בודקת את החדש — השירות אינו נכתב מחדש כולו.
האם אפשר לחזור לאחור אם הכניסה החדשה תשבור חלק מהתרחישים?
כן. אנחנו מחזיקים את שני המתארים פעילים ומעבירים תעבורה לפי חלק מהמשתמשים: בעת בעיה אנחנו מחזירים את הכניסה הקודמת בהגדרה אחת, בלי להחזיר את הריליס כולו לאחור. כל הטוקנים שהונפקו נשארים תקפים, והסשנים אינם נקטעים.
הלקוח מקבל לידיו לא "עובד ודי", אלא ערכה שלפיה ניתן לתחזק את המערכת ולהעביר אותה לצוות אחר:
- קונפיגורציות של ספקים — אילו פרמטרים הוגדרו ומדוע;
- תרשימי זרימות כניסה: היכן מונפק הטוקן, היכן נבדק, היכן מתחדש;
- מדיניות תוקף טוקנים וכללי ביטולם;
- מטריצת הרשאות: מי ולגישה לאילו נתונים זוכה לאחר הכניסה;
- תיעוד לחיבור שירותים חדשים וספקים חיצוניים;
- תוכנית חזרה לאחור עם נקודות מיתוג וקריטריונים להפעלה;
- הנחיות לתמיכה: כיצד לקרוא לוגים של כניסה ולמצוא את סיבת הכישלון.
תארו את סכימת הכניסה הנוכחית שלכם ואת רשימת השירותים המחוברים אליה — נחזור עם הערכה ותוכנית מעבר ללא עצירת העבודה.
לוחות זמנים של שלבי הטמעת OAuth2 ללא הצמדות לתאריכים
הטמעת OAuth2 מתחלקת לשלבים עם משך זמן שונה — מבדיקות מהירות ועד לעבודות שבהן הזמן העיקרי מוקדש לתיאום תרחישים ולבדיקות. להלן — לוחות זמנים משוערים לפרויקט טיפוסי: הם תלויים במספר הספקים החיצוניים, במספר התפקידים ובשאלה אם קיימת כבר תשתית אימות מוכנה.
האינטגרציה נמשכת הכי הרבה זמן — דווקא שם צצים אי-התאמות בין מודל המשתמשים אצל הספק לבין מבנה התפקידים הפנימי שלכם. הבדיקה תופסת בלוק נפרד: תרחישים עם פקיעת טוקן ורענון בו-זמני ממספר לשוניות נשברים לרוב, ומוטב לסגור אותם לפני המסירה.
האם אפשר להאיץ? כן, אם הספק החיצוני הוא אחד והתפקידים כבר מתוארים — שלב התכנון מתקצר בחצי. אם יש כמה ספקים ונדרש חשבון משתמש אחיד, לוחות הזמנים גדלים ביחס למספר האינטגרציות.
מה אם חלק מהעבודה כבר נעשה? הביקורת תראה מה נעשה בו שימוש חוזר ומה יצטרך להיכתב מחדש — מזה תלוי אם נתחיל מהתכנון או מאמצע ההגדרה.
תארו את המשימה שלכם: כמה נקודות כניסה, אילו ספקים, האם קיימת סכימת הפעלות נוכחית — נחזור עם הערכה ותוכנית לפי שלבים.







