2017-05-16

כשאתה אומר "Event-Driven Architecture", למה אתה מתכוון?

אחד הכנסים האהובים עלי, שאני נוהג לעקוב אחרי התוכן שמוצג בו הוא כנס ;goto

ה Keynote ב Goto Chicago שהתקיים לפני מספר ימים היה של מרטין פאוולר, בו הוא ביצע סיווג של סגנונות של "Event-Driven Architecture" (או בקיצור: EDA).


יש לי באופן אישי בעיה עם ההגדרה "Event-Driven Architecture".
  • האם הארכיטקטורה עצמה מונעת על ידי אירועים?
  • האם באמת הפרט החשוב ביותר בארכיטקטורה הוא האירועים או כיצד הם מטופלים?

לומר ש "יש לנו ארכיטקטורה שהיא Event-Driven" זה כמעט כמו לומר "יש לנו ארכיטקטורה שהיא Database Driven" או "ארכיטקטורה שהיא Java Driven". כלומר:

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

בכלל, ככל שמערכת גדלה, ובמיוחד כאשר היא נעשית מבוזרת - יותר סביר שנשתמש באירועים איפשהו במערכת. האם זה הופך את המערכת שלנו ל Event-Driven?

ובכן... מדוע אנשים מציינים שיש להם "Event Driven Architecture"? כנראה כי:
א. זה נשמע "טוב". באזז מרשים.
ב. כי אותם אנשים מרוצים מהשימוש ב events - ונתונים לתכונה הזו של המערכת דגש מיוחד.

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

הייתי קורא למערכת כזו "Event Based", ומנסה להבין מה מניע את המערכת והארכיטקטורה שלה. האם מדובר ב Scalability? האם חלוקה של תהליך עיבוד ליחידות קטנות ופשוטות יותר?


אז למה באמת מתכוונים ב "Event-Driven Architecture"?


אם נחפש קצת בספרות מקצועית, נוכל למצוא הגדרות כמו זו:

מתוך הספר Event-Driven Architecture: How SOA Enables the Real-Time Enterprise

אני לא רוצה להיות מרושע, אבל באמת זו חתיכת פסקה ארוכה שלא אומרת הרבה...




בספרון של מארק ריצ'רדס בשם Software Architecture Patterns הוא מתאר שני סוגים עיקריים של EDA:
  • Mediator Topology - בה יש "מח" מרכזי שיודע איזה שלבים יש לכל event, אלו ניתן למקבל וכו'- והוא זה שמנהל אותם
  • Broker Topology - בה החוכמה היא "מבוזרת" וכל רכיב שמטפל ב event יודע מה היעד הבא של ה Event / או אילו events חדשים יש לשלוח.
הוא בוחן את נקודות החוזק והחולשה בכל גישה - וזה נחמד, אך הוא עדיין לא ממש מספק תמונה שמכסה את השימושיים העיקריים של events במערכות תוכנה.


קצת עזרה??


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

פאוולר מגדיר ארבעה צורות עיקריות לשימוש ב Events במערכת. אם נשתמש במונחים הללו, ולא במונח הכללי "Event-Driven Architecture" - הרי נוכל להבין טוב יותר אחד את השני.


Event Notification


זה המקרה הפשוט בו אני רוצה להעביר הודעה בין מודול א' למודול ב' בצורה אסינכרונית.
למשל: 
  1. אפליקציית הנהג מודיעה שהסיעה הסתיימה.
  2. המערכת לניהול נסיעות סוגרת את הנסיעה ושולחת הודעה שהנסיעה הסתיימה.
    הקריאה היא אסינכרונית בפועל: אין צורך להמתין לחיוב שהתבצע - אלא רק ל Queue שהוא אכן קיבל את ההודעה. אם מערכת החיוב פונה לחברות האשראי ייתכן שייקח לה מספר שניות לבצע את החיוב, בעוד Queue יקבל את ההודעה ב מילישניות בודדות.
  3. מערכת החיוב בודקת את ה Queue כל הזמן אם יש הודעת. כאשר היא שולפת הודעות - היא מטפלת בהן ב"זמנה החופשי".

המודל של Event Notification הוא פשוט ושימושי.
  • הוא מתאים כאשר מישהו רוצה לשלוח הודעה והוא לא מצפה לתשובה.
  • הוא מתאים כאשר הצד מרוחק יבצע את הפעולה בקצב שונה. למשל: בשל תקלה בחברת כרטיסי האשראי ייתכן שהחיוב יתבצע רק לאחר שעה (דוגמה קיצונית).
  • הוא מאפשר למערכת ניהול הנסיעות להיות בלתי תלויה במערכת החיוב: אם יום אחד רכיב אחר יטפל בחיוב / הטיפול יחולק בין כמה רכיבים - מערכת ניהול הנסיעות לא תשתנה כתוצאה מכך.
    • חוסר התלות הזו היא לא מושלמת: מה קורה כאשר מערכת החיוב זקוקה לנתון נוסף לצורך החיוב? (על כך בהמשך)
    • כלל טוב לצמצום התלות הוא לשגר אירוע "כללי" שמתאר את מה שקרה במערכת "נסיעה הסתיימה" ולא פקודה כמו "חייב נסיעה!". שימוש במינוחים של פקודה גורם לנו לקבל בצורה עמוקה יותר את התלות בין המודולים - ולהעצים אותה לאורך הזמן.
  • כאשר יש ריבוי של Events Notifications במערכת - קשה יותר לעקוב אחרי ה flow, במיוחד כאשר events מסוימים מתרחשים רק לפעמים ו/או במקביל.
    Mitigation אפשרי הוא מערכת לוגים מרכזית ופעפוע "request id" (ואולי גם hop counter) על גבי ה events. כל כתיבה ללוג תציין את ה request id - וכך יהיה אפשר לפלטר את כל מה שהתרחש במערכת במערכת הלוגים ולראות תמונה שלמה. בערך.

עד כאן טוב ויפה. 
מה קורה כאשר מערכת החיוב דורשת עוד נתון ממערכת הנסיעות? למשל: האם הנסיעה התחילה בשדה תעופה או לא?


בגישת קיצון אחת, להלן הגישה העצלה - ניתן לשלוח ב event רק את id של הנסיעה שהסתיימה. מערכת החיוב תשלים את הנתונים החסרים ממערכת הנסיעות / מערכות אחרות.
בגישת קיצון שנייה, להלן הגישה הנלהבת (eager) - אנחנו מעדכנים את ה event לכלול את כל הנתונים שמערכת החיוב זקוקה להם.

בשני המקרים, יש לנו תלות מסוימת במערכת החיוב: שינוי במערכת החיוב עלול לדרוש שינוי במערכת ניהול הנסיעות - בגרסה אחת להוסיף שדה ב API של מערכת ניהול הנסיעות, בגרסה שניה - נתון על ה event.

איזו גישה עדיפה? - אין לי תשובה חד משמעית.

בד"כ אני מעדיף את הגישה הנלהבת, כי:
  • יותר קל לעקוב מה מתרחש - כי אפשר להסתכל על ה event ולהבין אלו נתונים נשלחו - ובמקום אחד.
  • הערכים שעברו על ה event הם קונסיסטנטים ל event - לא יקרה מצב שבחצי שנייה (או דקה) שלוקח למערכת החיוב לבצע קריאה - אחד הערכים השתנה.
  • יש פחות קריאות ברשת, ומעט פחות דברים שעשויים להשתבש.
כמובן שאם יש נתונים שצריכים להיות fresh, כדאי לערבב בין הגישות - ולקרוא אותם ברגע האמת בעזרת API.



Event-Carried State Transfer

גישה זו היא וריאציה של גישת ה Event Notification, אבל שינוי אחד בה - משנה בצורה משמעותית את כללי המשחק:

  1. אפליקציית הנהג מודיעה שהנסיעה הסתיימה.
  2. מערכת ניהול הנסיעות שולחת את כל הנתונים שיש לה על ה event.
    איך שולחים את כל הנתונים? בד"כ לוקחים את אובייקט המודל של ה ORM - ועושים לו serialization. 
  3. מערכת החיוב בודקת כל הזמן אחר הודעות. כאשר יש הודעה - היא קוראת ממנה רק את הנתונים שהיא זקוקה להם. היא עשויה לשמור עותק מקומי שלהם.
בעצם במקום להעביר הודעה, אנחנו מעבירים את ה state השלם של האובייקט בין המערכות השונות.

לגישה זו כמה יתרונות:
  • השגנו isolation גדול יותר (במימד אחד): שינוי במערכת החיוב לא ידרוש שינוי במערכת ניהול הנסיעות.
  • Availability - אם מערכת ניהול הנסיעות קרסה, ניתן להמשיך לבצע חיובים, כי למערכת החיוב יש את כל הנתונים שהיא צריכה.

לגישה יש גם כמה חסרונות:
  • שברנו את ה encapsulation: מערכת החיוב מכירה את מבנה הנתונים הפנימי של מערכת ניהול הנסיעות. מעכשיו יהיה קשה הרבה יותר לבצע שינויים במבנה הנתונים, ויש גם סכנה שהמתכנת של מערכת החיוב לא יבין את השדות נכון - ויפעל ע"פ נתונים מוטעים.
  • העברנו הרבה נתונים מיותרים ברשת - בד"כ זו בעיה משנית.
  • יצרנו עותקים שונים של הנתונים ברשת, מה שפוטנציאלית יוצר בעיה של Consistency בין הנתונים. נתונים שכן צריכים להיות up-to-date לצורך הפעולה - יהיו לא מעודכנים ויתכן שיהיה צורך בליישם מנגנון של eventual consistency.

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


מה קורה כאשר מידע נוסף נמצא על מערכת שלישית? למשל, את הפרט אם נסיעה התחילה בשדה תעופה (ואז יש לתת 30% הנחה ;-)) ניתן להסיק רק כאשר יש נתונים נוספים ממערכת האזורים?

גם כאן יש בחירה: אפשר לתת את המשימה למערכת ששולחת את ה event, או לזו שמקבלת אותו. כנראה הבחירה תעשה ע"פ איזו תלות בין המערכות - היא סבירה יותר.




Event Sourcing


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



אם מישהו רוצה לקרוא את האובייקט - הוא צריך להרכיב את שרשרת השינויים בכדי לקבל את האובייקט העדכני.

מה התועלת בגישה הזו?
  • אם אנחנו מקבלים עדכוני state מכמה מקורות - אין לנו race condition בו מקור א' מוחק את השינוי של מקור ב'.
    זה יכול להתרחש אם מקור א' קרא עותק ישן של האובייקט, לפני השינוי שמקור ב' ביצוע - אבל ההודעה ממנו הגיעה אחריו.
  • אנחנו שומרים היסטוריה של האובייקט ושינויי המצב שלו. אם נרצה - נוכל להרכיב את מצב האובייקט בכל רגע נתון (בהנחה ששמרנו את הזמן שבו קיבלנו את ההודעה).
    • זה יכול להיות שימושי לצורך debugging / הסבר התנהגות המערכת.
  • ההודעות שנשלחות הן קטנות (רלוונטי כאשר האובייקט השלם הוא גדול)
יש גם כמה חסרונות:
  • הרכבה של האובייקט, במיוחד אם קיבל רשימה ארוכה של עדכונים - היא פעולה יקרה יחסית.
    • החיסרון הזה הוא בעייתי במיוחד אם הרכבה של האובייקט דורשת נתונים נוספים ממערכות אחרות.
    • החיסרון הזה מתמתן אם אנחנו מחזיקים עותק של הנתונים בזיכרון.
  • מה קורה כאשר הסכמה משתנה? כלומר: מבנה הנתונים?

פה עשויה לעלות שאלה פילוסופית: אם אני מקבל את העדכונים כ delta, אבל אז בכל עדכון עושה merge עם האובייקט שאני מחזיק אצלי - האם זה עדיין Event Sourcing?

אפשר לומר שכן - ההצהרה הזו עדיין תעזור לאנשים לקבל במשפט אחד תובנה על המערכת. מניעת racing condition הוא ערך גדול בפני עצמו. יש פה tradeoff כמובן: ביטול חסרונות משמעותיים - מול איבוד כמה מהיתרונות האחרים.  זהו tradeoff סביר לחלוטין.



Command Query Responsibility Segregation (בקיצור: CQRS)

מספרים שאם הייתם מחפשים בגוגל "CQRS" לפני עשור, הוא היה שואל: "`?did you mean `Cars"
הרעיון הוא דיי ישן, ומקורו בשנות השמונים, אבל רק בשנים האחרונות הוא הפך למאוד-מוכר.

אני מניח שהרוב הגדול של הקוראים מכיר את השם, אבל לא בהכרח מכיר את הרעיון מאחוריו. לרוב האנשים CQRS מתקשר ל "high performance".

האמת שהרעיון של CQRS אינו קשור קשר ישיר ל events, אבל פעמים רבות - משתמשים בו כך.

הרעיון בגדול אומר:
קיים מודל אחד לכתיבה של אובייקט, ומודל נפרד לקריאה של אובייקט.




מתי זה שימושי?

כאשר דפוס הקריאה ודפוס הכתיבה שונים זה מזה.

למשל: נניח בטוויטר (אני מספר מהזיכרון - ייתכן שזה לא מה שקורה שם) כתיבה של טוויט (להלן: Command) הולכת למודל הכתיבה. שחררתי את הטוויט מהר והמשכתי לאינסטגרם.

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

במקום זה, לאחר שכתבתי את ה tweet יש Background Processor שמעתיק את הטוויט שלי לפיד של כל העוקבים.
כלומר:
  • מודל "הכתיבה" הוא רשימה של טוויטים ע"פ מחבר.
  • מודל "הקריאה" הוא הפיד של כל משתמש בנפרד.
זה אומר שיש הרבה שכפול נתונים במערכת, ושטח האחסון הנדרש הוא אדיר. אם יש למישהו מיליון עוקבים - כל טוויט ישוכפל מיליון פעמים.
מצד שני, זה גם אומר שגם אם אני עוקב אחרי 1000 פרופילים ויותר - הפיד שלי ייטען ב (O(1.
במקרה של טוויטר סביר שמודל הכתיבה ומודל הקריאה הן בכלל מערכות שונות - כאשר כל הוספה של טוויט למודל הכתיבה - שולחת אירוע של עדכון (event notification) למודל הקריאה - שם נמצא ה Background processor.

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

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


שימוש אחר של CQRS הוא לא דווקא מבחינת ביצועים גבוהים, אלא פשטות של הקוד. אם המודלים סבוכים למדי - החלוקה ל"מודל כתיבה" ו"מודל קריאה" יקטינו את ה code base בחצי, בערך.

כאן שווה לציין ש Event Sourcing ו CQRS הולכים יד ביד זה עם זה:
מודל הכתיבה הוא ה State Log - אבל יש מודל קריאה שהוא המצב העדכני. זה יכול להיות בסיס נתונים או טבלה אחרת בה שומרים את המצב העדכני, וזה יכול להיות מודל שעובד מעל אותם נתונים - ורק מכיל את הקוד של "השטחת" העדכונים בזמן ה Query.




סיכום


התלהבתי מהסדר שפאוולר עשה בנושא ה Event-Driven, ולכן כתבתי את הפוסט והוספתי עוד כמה תובנות משלי.
השארתי קצת אפור בהגדרות של הגישות השונות. תמיד ניתן לערבב ולייצר וריאציות. למשל:

הייתי מעורב בבניית מערכת שמבצעים בה רפליקציה של נתונים לשירותים אחרים, כמו בגישת ה Event-Carried State Transfer - בכדי להשיג High Availability. מצד שני, כמות הנתונים שמועתקת היא קטנה ומדודה מאוד, והנתונים הם ברמת הפשטה של ממשק ולא מבנה נתונים פנימי - כך שאין פגיעה בהכמסה של המערכת.

אם הייתי מנסה לתעד כל וריאציה שימושית בפני עצמה - כנראה שהיינו גומרים עם 16 צורות לשימוש ב Events ולא ארבעה, מה שהיה מאריך את הפוסט אבל יותר גרוע: מקשה על יצירת "שפה משותפת" שאנשים זוכרים וחולקים.

אז מה הופך מערכת להיות Event-Driven? - לא תצאו עם תשובה חד משמעית מהפוסט.
יצא לי לעבוד על מערכת בה השתמשנו בכל ארבעת הגישות, ולרגע לא חשבתי לקרוא למערכת "Event Driven".... מוזר.


שיהיה בהצלחה!


---

לינקים רלוונטיים

ההרצאה של פאוולר ב Goto; Chicago
פוסט של פאוולר בנושא

2017-05-02

על אימות זהות בווב, וקצת על OAuth 2.0 ו OpenID Connect

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

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

אנחנו ניגשים לאנשי השרת - וגם הם לא ממש בטוחים מה לעשות:
  • האם יש משהו לא טוב ב Basic Authentication? הרי "Basic is beautiful" (ע"פ פוקס).
  • אולי עדיף להוסיף כל מיני "טריקים שמקשים"? אולי לחפש ב StackOverflow?

הנה מדריך יפה שמצאתי בגוגל, "The Ultimate Guide Of Mobile Security" של חברת StormPath (לארונה נרכשה ע"י OKTA).
בטוח שיש בחברה הזו מומחי אבטחה גדולים ממני. אין ספק!
מצד שני... זה המדריך האולטימטיבי? האם הוא באמת משרת את צורכי האבטחה של הקורא, או בעיקר עושה On-Boarding מהיר ספציפית לפתרון של StormPath?

גם כשאתם ניגשים למדריך מוכן, כדאי שיהיה קצת מושג - ועל כן הפוסט הבא.
אדבר על אימות שרת-לשרת, מובייל ועל אפליקציה שרצה בדפדפן, על Basic Authentication אבל גם על OAuth 2.0 וקצת על OpenID Connect.


נתחיל מההתחלה: Basic Authentication (בקיצור: BA)


נתחיל במקרה הפשוט ביותר (מבחינת האבטחה): תקשורת שרת-לשרת.

שיטת אימות הזהות (Authentication) הבסיסית ביותר בווב נקראת "Basic Authentication".
נניח שיש לנו תסריט בו סוכן-נסיעות (להלן "שוקה") רוצה לתקשר בצורה מאבטחת עם חברת תעופה מסוימת (להלן "שחקים").

הנה אופן הפעולה:

1. המתכנת של שחקים מייצר מפתח-זיהוי ייחודי וקשה מאוד לניחוש, שיזהה את הלקוח הספציפי - שוקה:
jsa7arpZ8sPZ60YZyZwfD97gf5cHbEBj77VF6nF4
מחרוזת אקראית באורך 40 תווים נחשבת כיום למפתח מספיק חזק.

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

3. כאשר השרת של שוקה פונה לשרת של שחקים, על גבי HTTPS, הוא מוסיף על הבקשות את ה header הבא:
Authorization: Basic anNhN2FycFo4c1BaNjBZWnlad2ZEOTdnZjVjSGJFQmo3N1ZGNm5GNA==
הפרמטר הראשון אומר איזו סוג זיהוי (Authentication) מדובר. במקרה הזה: BA.
הפרמטר השני הוא ה credentials ("אישור ההרשאות"), במקרה הזה: מפתח הזיהוי מקודד ב Base64.

Base64 הוא קידוד של binary-to-text הנדרש על מנת להעביר את המידע על פרוטוקול HTTP בצורה תקינה. הפרוטוקול מצפה לטקסט, ותווים מסוימים יכולים להתפרש אחרת לחלוטין (למשל: שורה ריקה משמעה שהסתיימו ה headers ומתחיל ה body של בקשת ה HTTP).

4. השרת של שחקים מזהה שמדובר ב BA, ומקודד חזרה מ Base64 את מפתח-הזיהוי. הוא משווה אותו לערך שנשמר אצלו (בצורה מאובטחת). יש התאמה? הזהות אומתה - ואכן ניתן להניח בביטחון גבוה שהבקשה הגיעה משוקה.


חוזקות:
  • פרוטוקול פשוט שקל ליישום.

חולשות:
  • מפתח-הזיהוי מועבר על כל בקשה, ועל כן תוקף פוטנציאלי יכול לנסות ולקרוא אותו. מי שיש לו את המפתח - יכול להזדהות בשם הלקוח (״שוקה״).


סה״כ, עבור תקשורת שרת-לשרת, בסביבה שאיננה מרובת איומים - ה BA הוא סביר.
נקודת מפתח היא הקושי ליירט תקשורת שרת לשרת (בהנחה שהשרת לא יושב במקום ציבורי ומשתמש ב Wifi / תא סלולארי)



גרסאת ה Web Client

כאשר התקשורת היא בין דפדפן לשרת, ה flow עובד מעט אחרת:
  • אין תקשורת מוקדמת עם המשתמש, ולא שלוחים לו מפתח-זיהוי.
  • המשתמש פונה ל URL הרצוי.
  • השרת שאינו מזהה את המשתמש, מחזיר HTTP Status 401, כלומר: Unauthorized - "אני לא יודע מי אתה". כמו כן הוא שולח header המסביר באיזו סוג authentication הוא תומך:
WWW-Authenticate: Basic
  • הדפדפן יקפיץ למשתמש חלון להקלדת שם-משתמש וסיסמה.
  • הדפדפן ייצור מחרוזת credentials כ ״שם משתמש:סיסמה״ - יקודד אותה ב Base64 ויעביר אותה על ה Authorization Header בקריאה הבאה:

Authorization: Basic <credentials>
  • השרת יפתח את הקידוד ויזהה אם יש לו משתמש עם סיסמה שכזו. אם כן - הוא יניח שהבקשה הגיעה מהמשתמש.
  • שם המשתמש והסיסמה ישארו ב cache של הדפדפן לזמן מוגדר (15 דקות?). בכל בקשה לשרת יישלח ה Authorization Header עם הסיסמה.


חוזקות:
  • פרוטוקול פשוט שקל ליישום.

חולשות:
  • שם המשתמש והסיסמה עוברים כ clear text על כל בקשה (Base64 שקול ל clear text). תקשורת בין endpoint (לצורך פוסט זה: ״תחנת קצה״ -כמו מחשבים ניידים או סמארטפונים) לשרת - הרבה יותר קל ליירט מאשר תקשורת שרת לשרת. למשל: התוקף ותחנת הקצה נמצאים על אותה רשת Wifi.
    • אם התקשורת נעשית על גבי HTTP (רחמנא ליצלן!) - אזי גניבת הסיסמה היא פעולת sniffing פשוטה.
  • שם המשתמש והסיסמה נשארים cached בדפדפן. תוקף יכול לעשות שימוש גם הוא ב credentials מבלי שאף אחד יידע (התקפת CSRF).

סה״כ, BA בדפדפן היא שיטה שנחשבת כחלשה למדי לזיהוי מאובטח של המשתמש, והיא איננה מומלצת לשימוש.



גרסאת המובייל

במובייל יש כמה אפשרויות:
  • אפשר לנסות ולהיצמד לגרסת השרת של ה BA: ולשלוח מפתח-זיהוי לכל משתמש - אך זה אפשרי רק עם התקנה ידנית של Certificate (תהליך סזיפי למשתמש) או מערכות ששותלות Agent במכשיר ועושות זאת - בעיקרון מערכות Enterprise שלא ישימות בסביבת Consumers.
  • אפשר לנסות ולהיצמד לגרסת הדפדפן של ה BA: ולשמור את שם המשתמש והסיסמה באחסון המוגן של המכשיר (shared preferences / key chain) - ואז לשלוח אותם כ credentials בכל בקשה.
    שליחת ה credentials, בכל בקשה, ובמיוחד מתוך endpoints היא חולשה משמעותית - ולכן לא ניתן להמליץ על הגישה הזו.


ל HTTP יש עוד שיטת Authentication שנקראת HTTP Digest Access Authentication, שמנסה להקשות על גילוי הסיסמה - אך בפועל כיום היא נחשבת שיטה פחות בטוחה מ BA.



הבו לנו OAuth 2.0!


רוב הסיכויים שאתם שמעתם על פרוטוקול 2.0 OAuth ל Authentication. יש לו שם של פרוטוקול מכובד "Enterprise Level", אולי הנפוץ ביותר לשימוש היום - ולכן אפשר לסמוך עליו!

המחשבה ש"אני משתמש ב OAuth 2.0 - ולכן אני מאובטח", היא אולי נעימה - אך לא ממש נכונה. ל OAuth 2.0 יש יתרונות - אך גם חסרונות.

ראשית כל OAuth 2.0 הוא פרוטוקול ל Delegated Access ("ייפוי כח") ולא ל Authentication ("אימות זהות). הפרוטוקול נבנה על מנת לאפשר "ייפוי כח" למישהו אחר לגשת למידע שבבעלותי אך נשמר על ידי צד שלישי. בתוך ה Protocol מוגדרת "מסגרת ל Authentication" שהיא כללית, ואיננה נחשבת כחזקה במיוחד.

החלק הזה "מפיל" אנשים, ולכן אני הולך לתת חיזוק לטענה:

מתוך האתר הרשמי של OAuth, קישור: https://oauth.net/articles/authentication 

והנה הגדרה פורמאלית מה OAuth כן עושה:

The OAuth 2.0 authorization framework enables a third-party application to obtain limited
access to an HTTP service, either on behalf of a resource owner by orchestrating an
approval interaction between the resource owner and the HTTP service, or by allowing the
third-party application to obtain access on its own behalf.

שימו לב לקשר החזק ל HTTP.

מכיוון Authentication ב OAuth היא רק Framework ולא פרוטוקול - אין הגדרה חד-משמעית ומדויקת כיצד ה Authentication אמורה להתבצע. זה אומר ש:
  • ה Framework משאיר נושאים מסוימים לבחירת המימוש: איך להשתמש ב scopes, איך לבצע endpoint discovery, רישום דינאמי של לקוחות, וכו'.
  • אין בהכרח תאימות בין מימוש א' למימוש ב'. אם גוגל ופייסבוק מאפשרים חיבור ב OAuth - לא בטוח שאותו מימוש (של Client) יוכל לעבוד מול שניהם.
  • יש מימושים מאובטחים יותר, ומימושים מאובטחים פחות. תו התקן "OAuth" מציין בעיקר את סדר ההתקשרות - אבל מבטיח אבטחה רק במידה מסוימת.
    • למשל: לא נאמר כיצד להעביר את מפתח ההזדהות. ב URL או כ Header? (עדיף Header כי פחות סביר שהמידע הרגיש הזה יצוץ אח"כ בלוגים)
    • כיצד להעביר את המידע בין השחקנים השונים? לכאורה עדיף להצפין את נתוני ה token, אך יש כאלו שמשאירים אותם גלויים כי נוח שה Client יכול לקרוא גם הוא נתונים על המשתמש....
    • הנה רשימת עשרת החולשות הנפוצות ביותר במימושים של OAuth 2.0.


כיצד OAuth 2.0 עובד (בקצרה)

OAuth בעצם מגדיר ארבע אופנים שונים (נקראים Grants) לקבל Token ולהשתמש בו.

ה Token הוא תחליף ל Password, מכיוון שיצירת password במערכות רבות היא סכנת אבטחה ממשית: אנשים נוטים לעשות שימוש חוזר ב passwords שלהם במערכות שונות, ומערכת אחת שנפרצת - מאפשרת גישה למערכות נוספות, לעתים חשובות יותר.

כדי להבין את OAuth כדאי להשקיע דקה ולהכיר את ה"שחקנים" (Roles) המדוברים:

Resource Owner 
זה בעצם המשתמש, וכך גם אקרא לו בפוסט.

Resource Server (נקרא גם בשם המבלבל "protected resource")
אקרא לו בפוסט גם "שירות B" - השרת שמאכסן את הנתונים של המשתמש, למשל פייסבוק.

Authentication Server 
השרת שמולו המשתמש מזדהה, כלומר שם מתבצע תהליך ה Authentication והוא מנפיק Token שמאמת את זהות המשתמש. ברוב המקרים זהו יהיה ה Resource Server עצמו (או שירות B) - אך התקן לא מחייב זאת.

Client
אקרא לו בפוסט גם בשם "שירות A" - השירות שאליו בעצם פונה המשתמש בבקשה לשירות.
שירות זה לקוח של ה Resource Server והוא מבקש ממנו גישה למשאבים השייכים למשתמש, להלן "ייפוי הכח".


Authorization code grant

זהו כנראה אופן השימוש הנפוץ ביותר ב OAuth. אתחיל בלתאר אותו בצורה "סיפורית":

עכשיו המשתמש רוצה להתחבר לשירות A (למשל: ל Comeet), שם מופיעה לו אופציה להתחבר בעזרת החשבון הקיים שלו בשירות B, למשל: "Connect with Linkedin".


המשתמש מקיש על הלינק של "חיבור בעזרת...", וקופצת חלונית חדשה בדפדפן (יש הנחה שמדובר בדפדפן) בה מתבצע תהליך ה Authentication מול ה Auth. Server, שהוא לרוב בעצם שירות B עצמו (למשל: לינק-אין).

שירות B מציג אלו פריטי מידע (resource) הוא התבקש לחשוף (לעתים זהו רק ה email, לעתים רשימת חברים ויותר) וכאשר המשתמש מאשר - הוא חוזר לאתר המקורי (שירות A) ומקבל אליו גישה.

התוצאה: המשתמש קיבל גישה לשירות A מבלי שהיה צריך להזין בו שם משתמש / סיסמה. לעתים חושבים שלא נוצר שם חשבון - אך לרוב הוא נוצר על בסיס מזהה ייחודי שסיפק שירות B.


עכשיו נחזור על ה flow בצורה יותר טכנית:

לפני שהכל מתחיל, ה Client (למשל "Site") נרשם אצל Resource Server (למשל: Facebook). כתוצאה מתהליך הרישום ה Client מקבל client_secret ("מפתח זיהוי") - אותו הוא שומר בצורה מאובטחת.


כעת המשתמש רוצה להתחבר לשירות A:
  • שירות A מבקש מהמשתמש להזדהות, ומאפשר לו לעשות את זה בעזרת שירות B.
  • המשתמש בוחר בשירות B, ומופנה ל Authentication Server המתאים.
    • שירות A סומך על ה Authentication Server לבצע אימות מאובטח מספיק, ולנפק access-token  מתאים.
    • בד"כ מוצג למשתמש אילו נתונים הוא הולך למסור, ולעתים הוא יכול להחליט לא למסור חלק מהנתונים. כל קבוצת נתונים מוגדרת כ scope. על ה redirect URL ה Client ציין אלו scopes הוא מבקש.
    • המשתמש מזדהה בעזרת שם משתמש וסיסמה (או כל אימות אחר) ומאשר את ה scopes.
  • ישנו Redirect חזרה ל Client עליו מופיעים פרטים של הבקשה המקורית ל Authentication - שבהם ישתמש ה Client על מנת לאמת שזו אכן תשובה לבקשה שנשלחה, authorization code.
  • ה Client עכשיו פונה ל Auth. Server בעצמו,
    •  ושולח לו:
      • את ה authorization code שקיבל
      • client_id - זיהוי של ה client (לא של המשתמש), אינו סודי.
      • client_secret - קוד סודי שרק ה client מכיר (אותו הוא קיבל ברישום). 
    • בתמורה הוא מקבל אובייקט JSON המכיל:
      • תאריך תפוגה - ה token טוב לזמן מוגבל (באיזור השעה, בד"כ).
      • access_token - איתו הוא יכול לגשת לנתונים.
      • refresh_token - המאפשר לבקש גישה נוספת, במידה וה access_token פג תוקף.
    • הערה: התקן של OAuth לא מציין מה מבנה ה token, אך מקובל להשתמש ב JWT (קיצור של: JSON Web Token), פורמט הכולל 3 חלקים: Claims, Header, וחתימה.

העיקרון מאחורי ה tokens נקרא TOFU (קיצור של Trust On First Use), והוא אומר שבמקום לשלוח את מפתח הזיהוי בכל בקשה - תוך כדי שאנחנו חשופים ל sniffing, נצמצם את החשיפה לתקשורת הראשונה. בפעולה הראשונה אנו סומכים (יותר) על הצד השני, תחת ההנחה שפחות סביר להיות חשופים ל sniffing ברגע ההתקשרות הראשונית. אם ה access token, למשל, נחשף ב sniffing - הפגיעה תהיה מוגבלת לזמן הנתון, ועדיין ה refresh token וה client_secret לא נחשפו והם יכולים להמשיך לנפק בצורה מאובטחת יחסית access_tokens אחרים.

  • בשלב האחרון ה Client ניגש לשירות B בעזרת ה access_token - ושולף ממנו את הנתונים שביקש.
    לכאורה בתרשים זו קריאה בודדת, אך בעצם יכולות להיות עוד קריאות לאורך זמן.
    • אם פג התוקף של ה access token, ה Client לא יורשה לקבל את הנתונים. הוא יצטרך לפנות עם ה refresh_token ל Auth. Server ולקבל access_token ו refresh_token חדשים.


ציינו שיש 4 סוגים שונים של Grant בתקן. הנה תרשים החלטה מקובל שעוזר להחליט באיזה Grant כדאי להשתמש (מבוסס על תרשים של Alex Bilbie):



בקצרה:

  • Authorization Code Grant - הוא ה flow שעברנו עליו. אם המשאב הוא זיהוי של המשתמש (למשל: email), אזי יישמנו סוג של מנגנון Authentication על גבי OAuth.
    • ה Flow מתבסס על המצאות דפדפן והיכולת לבצע Re-direct (כלומר: Multi-page app, לפחות לשלב ה login).
    • ה Client הוא צד-השרת של האפליקציה, בו ניתן לשמור את ה client_secret בצורה מאובטחת.
  • Client Credentials Grant - הוגדר עבור חלקים שונים של אותה האפליקציה (הגדרה: "ה Client עצמו הוא ה Resource Owner"), שיש בין החלקים אמון גבוה. האימות הוא שרת-לשרת, ולא דורש התערבות של המשתמש. זהו בעצם מן גרסה משופרת של Basic Authentication בין שרת לשרת שמיישם את עקרון ה TOFU. מיותר לציין שנעשה שימוש נרחב ב Flow זה גם בין אפליקציות זרות, וזה לא דבר רע בהכרח (יותר טוב מ Basic Auth).
    • Flow זה גם נקרא two-legged OAuth (כי יש בו רק שני שחקנים מעורבים), בניגוד לכל שאר ה flows הנקראים three-legged OAuth.
  • Implicit Grant - הוגדר עבור אפליקציות ווב או מובייל שבהן לא ניתן לסמוך ברמה גבוהה על ה client_secret. לאפליקציית mobile ניתן לעשות reverse engineering ולשלוף את הקוד, בווב - ...זה אפילו יותר קל.
    • יש כאן פשרה של אבטחה על מנת לאפשר כן סוג של Delegation סביר של הרשאות.
    • ב Flow הזה אין refresh Token - ולאחר שפג תוקפו של ה access_token יש לבצע Authentication מחדש.
    • לא מעבירים ל Client את ה Auth. Code (כי לא סומכים עליו - הוא לא מספק אבטחה מוגברת), אלא ישר שולחים לו את ה access-token.
  • Password Credentials Grant (נקרא גם Resource Owner Credentials Grant) - הוגדר עבור אפליקציות בעלות Trust גבוה. ב Flow הזה המשתמש מספק את שם המשתמש וההרשאות שלו לאפליקציה (שלא שומרת אותן!), והיא משתמשת בהן על מנת לקבל access_token בשמו. את ה access_token אפשר לשמור בצורה מאובטחת(!) באפליקציה. 
    • אם ה access_token נגנב אז אפשר לגשת איתו רק לנתונים מסוימים של משתמש יחיד - נזק מוגבל.
    • את ה access_token יש לחדש מדי פעם, על ידי זיהוי מחדש של המשתמש. באפליקציות מובייל לא מקובל לבקש מהמשתמש סיסמה מחדש יותר מפעם בזמן מה.... חודש, נניח?


באיזה Flow כדאי להשתמש באפליקציית מובייל?

ב Spec המקורי ההגדרה הייתה להשתמש ב Implicit Grant: מעט אמון, ומעט אבטחה.
מאז יש מעבר לשימוש ב Authorization Code Grant, כאשר משתמשים להרשאות ב Native Browser ולא WebView. למשל: SFSafariViewController ולא WKWebView.

האם אפליקציית מובייל יכולה לשמור Client Secret ב Source Code שלה בצורה אמינה? באנדרואיד, למשל, קל מאוד להוריד apk ולבצע לו Reverse Engineering. ההמלצה אם כן היא להחזיק את הקוד ויישום ה OAuth בקוד ++C (קרי שימוש ב NDK) ולא ב Java - שם ה reverse engineering הוא קשה משמעותית.

Stormpath (שנרכשו לאחרונה ע"י OKTA) בכלל לא דנים בשאלה - ושולחים את המתכנתים ליישם Password Grant. אני לא בטוח שלא מעורבים פה שיקולים עסקיים (לספק Flow שקל ללקוחות ליישם, ולא להטריד אותם ב"זוטות").

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


לסיכום:

בתקשורת שרת לשרת

חוזקות:
  • עדיין פשוט ליישום.
  • TOFU עדיף על העברה של המפתח בכל פעם.

חולשות:
  • לא תוכנן במקור לשימוש בין שרתים עם Trust מוגבל. 

סה"כ טוב מספיק עבור מגוון שימושים, ועדיף על פני Basic Authentication.



בתקשורת Web 

חוזקות:
  • עדיף על Basic Authentication, בכמה מובנים (בעיקר Authorization Code Grant).
  • הפך כמעט לקונצנזוס - יש מעט מאוד מתחרים, לכל אחד את הבעיות שלו.

חולשות:
  • לא תוכנן כפרוטוקול Authentication.
  • מימוש יחסית מורכב בצד-השרת (בצד-הלקוח המימוש טריוויאלי)
  • Room For Error: ה Specification שלו מבלבל, ומגוון האפשרויות - מובילים למימושים / יישומים רבים שאינם באמת מאובטחים דיים. מספק "תחושת אבטחה מוגזמת".


בתקשורת מובייל

חוזקות:
  • עדיף על Basic Authentication.

חולשות:
  • Mobile App נחשבת לעתים קרובות כ Trusted - למרות שזה שיקוף לא נכון של המציאות.
  • חלק ממנגנוני האבטחה של OAuth מסתמכים על המצאות דפדפן שאפשר לסמוך עליו: אין יכולת ל redirect. שימוש ב WebView פותח פתח ל Phishing / Clickjacking.
  • Implicit Grant טוב מ Basic Authentication רק במעט.



OpenID Connect 


מעט היסטוריה שתעזור להבין את השתלשלות האירועים:
  • 2006- יצא תקן לאימות זהות בשם OpenID 1.0.  הוא עובד בערך כך: "אני ליאור", שואלים צד שלישי שסומכים עליו: "האם זה ליאור?", התשובה היא "זה ליאור" - ואז מאשרים את זהותי.
  • 2007 - יצא OpenID 2.0 עם כמה שיפורים.
  • 2007 - יצא 1.0 OpenID Attribute Exchange (בקיצור OIDAE) - הרחבה של OpenID 2.0 המאפשרת גם למשוך פרטים נוספים על המשתמש "זה ליאור, הוא בן 40, ויש לו 3 ילדים".
  • 2010 - יצא OAuth 1.0
  • 2012 - יצא OAuth 2.0
  • 2014 - יצא פורטוקול OpenID Connect (בקיצור: OIDC) שמשלב בין OpenID 2.0 + OIDAE על גבי ה Framework של OAuth 2.0 בצירוף של JWT (וחברים). 

לא נכון להשוות בין OpenID ל OpenID Connect. זה כמו להשוות בין Ext2 ללינוקס (בהגזמה).
OIDC אינו תואם ל OpenID בגרסאותיו השונות - הוא רק שואב מהם רעיונות.



OIDC הוא פרוטוקול, והוא סוגר הרבה מהפינות הפתוחות של OAuth 2.0:
  • הוא מחייב כללים רבים שהם בגדר המלצה ב OAuth 2.0.
  • הוא מגדיר את מבנה ה tokens (על בסיס JWT) - שב OAuth 2.0 פתוחים לפרשנות.
  • הוא מגדיר scopes סטנדרטיים (email, phone, וכו') - המאפשרים התנהגות סטנדרטית מסביב למידע בסיסי של המשתמש. הפרוטוקול מגדיר endpoint חדש הנקרא userInfo Endpoint ממנו ניתן לשאוב מידע בסיסי על המשתמש.
  • הוא גם פותח בתקופה בה אפליקציות מובייל הן מאוד משמעותיות, ויש התייחסות רחבה יותר למובייל בהגדרת הפרוטוקול.
  • החל מ 2015 יש הסמכה של OIDC, ויש רשימה של מימושים שהם Certified, אפשר כבר לצפות ל Compatibility.
  • OIDC מגדיר סוג נוסף של token הנקרא ID Token המאפשר ל Client לדעת כמה פרטים על המשתמש ועל מצב ב Authentication שלו. עיוות נפוץ של מימושי OAuth 2.0 הוא לאפשר ל Client לקרוא את ה Access Token - מה שפוגע ב Segregation of duties.
  • הפרוטוקול מוסיף הגנה בפני התקפת replay: עשיתי sniffing לתקשורת ולא הבנתי מה נאמר - אבל אבקש מהשרת לעשות זאת עוד פעם (מה שלעתים יגרום נזק).

הנה ה Flow הבסיסי של OIDC, שהוא בעצם התאמה של ה Flow המקביל שתואר עבור OAuth 2.0:

ה Flow מעט פשוט יותר, לא מתבסס בהכרח על דפדפן, והיעד הוא להשיג מידע על המשתמש - ולא "משאב כללי".



האם OpenID Connect הוא הטוב מכל העולמות?

אם אתם מעוניינים ב Flow של authentication - אז הוא אופציה מבטיחה. קשה לי לחשוב על מקרה שבו עדיף לעשות Authentication על גבי OAuth 2.0 ולא על גבי OCID.

OIDC הוא פרוטוקול צעיר יחסית, שעדיין לא זוכה לאימוץ דומה לזה של OAuth 2.0. יש אולי עשר מימושים ויותר של OAuth 2.0 על כל מימוש OIDC.

בשנים הראשונות, מימושים של OIDC זכו לכמה "פאדיחות". במימוש של Facebook התגלה באג חמור, שזיכה את המגלה שלו ב Bug Bounty הגדול ביותר בהיסטוריה של פייסבוק. מימוש ה reference של ארגון ה OpenID עצמו סבל מכמה בעיות, וגם מוזילה נכוותה.

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

OpenID מסומן היום כמחליף הטבעי של SAML 2.0 (פרוטוקול אימות זהות / SSO) מסורבל ועם תיעוד לקוי - אבל שמילא את מטרתו במשך תקופה נכבדת.

אם אתם רוצים גישה למשאב (ולא זהות) - OAuth 2.0 הוא עדיין רלוונטי (אם כי אתם יכולים לתפור הרחבה תקנית על גבי OIDC ולהימנע מסכנות רבות).

בתחום המובייל (כלומר: native app) פרוטוקול ה OIDC עדיין לא נחשב טוב מספיק. שתי תקנים מרכזיים Proof Of Possession ו PKCE בפיתוח - ואמורים להעלות מדרגה בתחום.

OpenID Connect הוא לא מושלם. למשל: קיימת עדיין הדילמה של החזקת גישה למערכות רבות ע"י Identity Provider אחד. יש סכנה שפריצה למערכת הזו תאפשר גישה לתוכן רב - אך היא ככל הנראה פחותה מהסכנה שבפיזור ססמאות. מבין האופציות הקיימות, OpenID Connect היא האופציה העדיפה.


השוואה מהירה בין SAML 2.0 ל OCID


סיכום


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

שיהיה בהצלחה!


----

קישורים רלוונטיים

ה OAuth 2.0 Threat Model המגדיר את הסיכונים השונים ביישום OAuth.
ה OAuth Grants מוסברים על ידי Alex Bilbie.
עוד קישור טוב על OAuth 2.0
"OAuth has ruined everything"
מדריך טוב ל OpenID Connect