התחברות חיצונית (SSO)
התחברות משתמשים להתקנת Nodaro דרך ספק זהות מהימן, עם assertion חתום בפורמט JWT או עם OIDC ו-SAML, וקביעת האופן שבו חשבונות מקושרים.
התחברות חיצונית (SSO) מאפשרת למשתמשים להתחבר להתקנת Nodaro דרך ספק זהות (IdP) מהימן. באינטגרציה העיקרית, ספק הזהות שלכם חותם על assertion קצר־מועד בפורמט JWT, Nodaro מאמת אותו, והדפדפן מקבל סשן Nodaro רגיל. SSO כבוי עד שמנהל השרת של ההתקנה מגדיר לפחות ספק אחד.
החשבון שהמשתמש מגיע אליו הוא חשבון רגיל. SSO לא מוסיף פרטי גישה מיוחדים: הוא רק דרך להתחיל סשן.
שתי דרכי אינטגרציה
| סוג | למי זה מתאים | איך זה עובד |
|---|---|---|
assertion | מנפיקי אסימונים שלא מדברים OIDC או SAML, כגון מארח שמטמיע את Nodaro ויכול רק להנפיק אסימון חתום קצר־מועד | ספק הזהות שלכם חותם על JWT, ו-Nodaro מאמת אותו ומחליף אותו בסשן. הדרך הזאת עובדת מקצה לקצה. |
oidc, saml | ספקי זהות שמדברים OpenID Connect או SAML סטנדרטיים | דף ההתחברות מתחיל התחברות סטנדרטית עם Supabase Auth של ההתקנה עצמה, שמאמת את התשובה של ספק הזהות. לדרך הזאת יש מגבלות; ראו את הסעיף ספקי OIDC ו-SAML. |
הגדרת ספקים
הגדירו בשרת את EXTERNAL_SSO_PROVIDERS למערך JSON של ספקים, ישירות בתוך המשתנה או כ-@/path/to/providers.json (תו @ בהתחלה גורם לקריאת הקובץ שבנתיב הזה). ערך פגום עוצר את השרת באתחול, כך שטעות הקלדה לעולם לא תשמיט ספק בשקט ולא תשאיר את ההתחברות מוגדרת רק בחלקה.
[
{
"id": "acme-chat",
"label": "Acme Chat",
"kind": "assertion",
"secret": "a-dedicated-32-character-hmac-key-for-nodaro-only",
"audience": "nodaro",
"claimMap": { "email": "email", "emailVerified": "email_verified", "subject": "sub" },
"initiateUrl": "https://chat.example.com/oauth/nodaro",
"maxLifetimeSeconds": 300
},
{
"id": "keycloak",
"label": "Acme (Keycloak)",
"kind": "oidc",
"supabaseProvider": "keycloak"
}
]| שדה | חל על | משמעות |
|---|---|---|
id | כולם | ה-slug של הספק, שמשמש בנתיב /v1/sso/:id ונשמר בכל חשבון מקושר. 1–63 תווים: אות קטנה או ספרה בהתחלה, ואחריה אותיות קטנות, ספרות, _ ו--. בלי נקודות. חייב להיות ייחודי. |
label | כולם | השם המוצג של הספק. |
kind | כולם | assertion, oidc או saml. |
secret | assertion, נדרש | מפתח ה-HS256 שמאמת את החתימה של ה-assertion, באורך של 16 תווים לפחות. השתמשו בסוד ייעודי, אף פעם לא בסוד שבו ספק הזהות עצמו חותם על סשנים, כך שפריצה ל-Nodaro לא תאפשר לזייף סשנים של ספק הזהות. |
audience | assertion, נדרש | הערך שה-claim aud של ה-assertion חייב להיות שווה לו. |
claimMap | assertion, אופציונלי | באילו claims נמצאים האימייל, דגל האימות וה-subject. ברירת המחדל היא email, email_verified ו-sub. |
initiateUrl | assertion, אופציונלי | לאן כפתור ה-SSO שולח את המשתמש. בלי השדה הזה, לחיצה על הכפתור מחזירה 400 no_assertion. |
initiateUrlByHost | assertion, אופציונלי | מיפוי משם מארח בלבד, באותיות קטנות ובלי פורט, לכתובת שמחליפה את initiateUrl עבור המארח הזה. מיועד לפריסה שעונה בכמה שמות מארח. |
maxLifetimeSeconds | assertion, אופציונלי | אורך החיים הארוך ביותר (exp פחות iat) ש-Nodaro מקבל. ברירת המחדל היא 300 שניות, והמקסימום הוא 3,600. |
domain | oidc, saml | נבדק באתחול, אבל לא בשימוש. ראו את הסעיף ספקי OIDC ו-SAML. |
supabaseProvider | oidc | נבדק באתחול, אבל לא בשימוש. |
- השדה
initiateUrlלא מעביר שום דבר לספק הזהות. אם אתם רוצים שהמשתמש יגיע לדף מסוים אחרי ההתחברות, ספק הזהות שלכם מוסיף&next=<path>כשהוא מפנה אותו בחזרה. - המפתחות של
initiateUrlByHostנבדקים באתחול: מפתח עם אותיות גדולות או עם פורט עוצר את השרת. המיפוי לעולם לא מתפרסם, וקישור החשבונות לא מושפע מהמארח שדרכו המשתמש הגיע.
החוזה של ה-assertion
ספק הזהות שלכם מנפיק JWT ושולח את הדפדפן אל GET /v1/sso/:provider?assertion=<jwt>. Nodaro מקבל את ה-assertion רק כשכל הכללים מתקיימים:
- אלגוריתם. ה-JWT חתום עם
HS256ועם ה-secretשל הספק. - קהל יעד. ה-claim
audשווה ל-audienceשל הספק. - תפוגה. ה-claim
expקיים והוא לא בעבר, עם סובלנות של 5 שניות להפרשי שעונים. - אורך חיים. ההפרש
expפחותiatהוא לכל היותרmaxLifetimeSeconds, לפי השעון של Nodaro. ערךiatשנמצא יותר מ-5 שניות בעתיד מוגבל לטווח המותר, ו-iatחסר נחשב לרגע הנוכחי. - שימוש חד־פעמי. ה-claim
jtiקיים וייחודי. Nodaro זוכר כלjtiעד שחלון התוקף של ה-assertion עצמו עובר, ודוחה אותו בפעם השנייה. - אימייל. ה-claim של האימייל קיים.
- אימייל מאומת. כדי שחשבון ייווצר או יקושר, דגל האימות חייב להיות
true. דגל חסר נחשב לאימייל לא מאומת.
כש-assertion נכשל, התגובה היא 401 עם invalid_signature, invalid_claims, expired, too_long_lived, missing_jti, missing_email או assertion_replayed. נקודת הקצה מקבלת עד 20 בקשות ב-60 שניות מכל כתובת IP; מעבר לזה היא מחזירה 429 rate_limit_exceeded עם כותרת Retry-After.
הדוגמה הזאת, מהצד של ספק הזהות, מנפיקה assertion תקף עם הספרייה jose ל-Node.js:
import { SignJWT } from "jose"
import { randomUUID } from "node:crypto"
const secret = new TextEncoder().encode(process.env.NODARO_SSO_SECRET)
const assertion = await new SignJWT({ email: user.email, email_verified: true })
.setProtectedHeader({ alg: "HS256" })
.setSubject(user.id)
.setAudience("nodaro")
.setIssuedAt()
.setExpirationTime("2m")
.setJti(randomUUID())
.sign(secret)
const url = new URL("https://nodaro.example.com/v1/sso/acme-chat")
url.searchParams.set("assertion", assertion)
url.searchParams.set("next", "/projects")
res.redirect(url.toString())זרימת ההתחברות
- המשתמש לוחץ על כפתור ה-SSO בדף ההתחברות של Nodaro, והדף קורא ל-
GET /v1/sso/:providerבלי assertion. - Nodaro מפנה (
302) אל ה-initiateUrlשל הספק. - ספק הזהות מאמת את המשתמש ושולח את הדפדפן בחזרה אל
GET /v1/sso/:provider?assertion=<jwt>, ואפשר להוסיף גם&next=<path>. - Nodaro מאמת את ה-assertion, דוחה שימוש חוזר ומחיל את כללי קישור החשבונות.
- Nodaro מפנה אל
/sso?sso_token=<one-time token>. הדף הזה מחליף את האסימון החד־פעמי בסשן. - המשתמש מגיע אל
/projects, או אל הנתיב שב-next.
ספק זהות, כגון מארח שמטמיע את Nodaro, יכול גם להתחיל בשלב 3: הוא מפנה את הדפדפן לנקודת הקצה של ההחלפה עם assertion חדש. הפרמטר ?redirect= של דף ההתחברות עצמו לא עובר דרך ההפניות של SSO; עובר רק next שספק הזהות הוסיף.
הצגת כפתור ה-SSO בדף ההתחברות
דף ההתחברות מציג כפתור SSO כששני התנאים מתקיימים:
- פרופיל הממשק של הפריסה כולל את
ssoב-auth.methodsומגדיר אתauth.ssoLabel. פרופיל בליssoLabelמסיר אתssoמהשיטות. הטקסט של הכפתור הוא ה-ssoLabel. - מוגדר לפחות ספק אחד. הדף בודק את
GET /v1/sso/providers.
פרופילי ממשק זמינים במהדורות Business ו-Cloud. ראו את הדף מהדורות ופרופילי ממשק.
פרופיל שה-auth.methods שלו כולל רק את sso מפעיל גם מחסום בצד השרת. כל חשבון מחובר חייב להיות חשבון שנוצר או קושר דרך SSO, וכל סשן אחר נדחה בקריאת ה-API הראשונה שלו עם 403 sso_required. החריג היחיד הוא חשבון החיוב של הפריסה, שמתואר בהמשך. הוספת email לשיטות מכבה את המחסום בכל ההתקנה.
איך חשבונות מקושרים
כש-assertion תקף, Nodaro מוצא את החשבון או יוצר אותו לפי כללים שנבנו כך ש-assertion לעולם לא יכול להשתלט על חשבון קיים רק כי יש לו אותה כתובת אימייל.
| מצב | תוצאה |
|---|---|
| אין חשבון עם האימייל, והאימייל מאומת | נוצר חשבון חדש, והוא מקושר לספק. |
| אין חשבון עם האימייל, והאימייל לא מאומת | ההתחברות נדחית עם 403 email_unverified, כדי ש-claim לא מאומת לא יוכל לתפוס כתובת אמיתית. |
| החשבון כבר מקושר לספק הזה | המשתמש מתחבר. |
| החשבון מקושר לספק אחר | ההתחברות נדחית עם 403 account_linked_other_provider, גם כש-EXTERNAL_SSO_LINK_EXISTING מופעל. |
קיים חשבון מקומי בלי SSO, EXTERNAL_SSO_LINK_EXISTING מופעל, והאימייל מאומת | החשבון מקושר לספק, והמשתמש מתחבר. |
| קיים חשבון מקומי בלי SSO, והדגל כבוי או שהאימייל לא מאומת | ההתחברות נדחית עם 403 account_exists. |
המשתנה EXTERNAL_SSO_LINK_EXISTING כבוי כברירת מחדל, וזו ההגדרה שמגינה מפני השתלטות. רק הערכים true או 1, בלי הבדל בין אותיות גדולות לקטנות, מפעילים אותו. הפעילו אותו רק כשאתם בוטחים ב-claims של אימייל מאומת שמגיעים מספקי הזהות שלכם, עד כדי כך שאתם מוכנים לקשר אותם לחשבונות שכבר קיימים.
הדחיות account_exists ו-email_unverified מנוסחות באותו אופן לכל חשבון, כך שאי אפשר להשתמש בטופס ההתחברות כדי לגלות לאיזו כתובת יש תפקיד מיוחד. התגובה היא account_exists גם כשהכתובת תואמת יותר מחשבון אחד, כשהחיפוש נכשל וכששתי התחברויות מתחרות על יצירת אותו חשבון.
בפריסה עם חשבון חיוב
פריסה יכולה להגדיר חשבון חיוב אחד שמשלם על כל המשתמשים של ההתקנה; ראו את הדף ארנקים חיצוניים. הכללים לחשבון הזה מחמירים יותר, כי בו נמצאים הקרדיטים של הפריסה:
- הוא מקושר בהתחברות המאומתת הראשונה שלו, בלי קשר לערך של
EXTERNAL_SSO_LINK_EXISTING. assertion לא מאומת עבורו נדחה עםemail_unverified, ו-assertion מספק אחר נדחה עםaccount_linked_other_provider. - כל התחברות מאוחרת יותר נבדקת שוב. האימייל חייב להיות מאומת, וה-subject חייב להיות זה שקישר את החשבון בפעם הראשונה. אחרת ההתחברות נדחית עם
403 account_linked_other_subject. - הוא שומר סיסמה כגישת חירום, ליום שבו ספק הזהות לא יכול להנפיק עבורו assertion. בפריסה שמאפשרת רק SSO, טופס הסיסמה נמצא ב-
/login?billing=1, ורק חשבון החיוב יכול להתחבר דרכו. - מנהל של ההתקנה לא יכול לחסום או למחוק אותו (
403 payer_account_protected), כך שתפקיד מנהל שספק הזהות של הפריסה עצמה מחלק לא יכול למנוע מההתקנה גישה לקרדיטים שלה.
אם ה-subject שספק הזהות נותן לחשבון החיוב משתנה אי פעם, ההתחברות של החשבון דרך SSO ממשיכה להיכשל עם account_linked_other_subject. החשבון עדיין יכול להתחבר עם הסיסמה שלו.
בפריסה עם חשבון חיוב, הכתובות של מנהלי הפלטפורמה (PLATFORM_OPERATOR_EMAILS, או PLATFORM_OWNER_EMAIL כשהמשתנה הראשון ריק) לעולם לא מקבלות קישור חדש. הן נדחות עם account_exists, וכך החשבונות של מנהלי הפלטפורמה נשארים מחוץ ל-SSO. חשבון של מנהל פלטפורמה שכבר מקושר ממשיך להתחבר.
ספקי OIDC ו-SAML
הסוגים oidc ו-saml מעבירים את ההתחברות ל-Supabase Auth של ההתקנה עצמה, שמאמת את התשובה של ספק הזהות. לשניהם יש מגבלות:
oidc: דף ההתחברות מעביר את ה-idשל הספק כשם של ספק ה-OAuth. לכן ה-idחייב להיות בעצמו השם של ספק OAuth שמוגדר ב-Supabase Auth של ההתקנה, כגוןkeycloakאוazure.saml: דף ההתחברות מעביר את ה-idשל הספק כדומיין ה-SAML. ה-idלא יכול להכיל נקודה, ולכן אי אפשר לבטא דומיין כגוןacme.com, וספק מסוגsamlלא עובד מקצה לקצה.- assertion שנשלח לספק מסוג
oidcאוsamlנדחה עם400 not_assertion_provider.
נקודות קצה
שתי נקודות הקצה ציבוריות ולא דורשות אסימון. אלה הנתיבים היחידים תחת /v1/sso/.
| שיטה | נתיב | מה היא עושה |
|---|---|---|
GET | /v1/sso/providers | מחזירה את רשימת הספקים לדף ההתחברות, עם id, label ו-kind בלבד, אף פעם לא סוד. מחזירה רשימה ריקה כש-SSO כבוי. |
GET | /v1/sso/:provider | נקודת הקצה של ההחלפה. זו נקודת קצה של הפניות בדפדפן, לא JSON API. |
| סטטוס | קוד | מתי |
|---|---|---|
400 | no_assertion | הכפתור נלחץ, ולספק אין initiateUrl. |
400 | not_assertion_provider | assertion נשלח לספק מסוג oidc או saml. |
401 | invalid_signature, invalid_claims, expired, too_long_lived, missing_jti, missing_email, assertion_replayed | ה-assertion הפר כלל של החוזה. |
403 | email_unverified, account_exists, account_linked_other_provider, account_linked_other_subject | כללי קישור החשבונות דחו את ההתחברות. |
403 | sso_required | פריסה שמאפשרת רק SSO דחתה סשן שלא הגיע דרך SSO. |
404 | unknown_provider | אין ספק עם ה-id הזה, או ש-SSO כבוי. |
429 | rate_limit_exceeded | יותר מדי בקשות מכתובת IP אחת. |
הערות אבטחה
- השתמשו בסוד ייעודי. ה-
secretהוא מפתח אימות של Nodaro בלבד, נפרד מסוד הסשנים של ספק הזהות עצמו. - כל assertion עובד פעם אחת. בדיקת ה-
jtiדוחה assertion שנשלח בפעם השנייה, ומגבלת אורך החיים שומרת על חלון זמן קצר. - ההפניות נשארות בתוך ההתקנה. Nodaro מקבל את
nextרק כנתיב יחסי באותה התקנה, שמתחיל ב-/אבל לא ב-//. כל ערך אחר מוביל אל/projects. - שום סוד לא יוצא מהשרת. הנתיב
GET /v1/sso/providersמחזיר רקid,labelו-kind. ה-assertion והאסימון החד־פעמי מושמטים מיומני הבקשות. - על ההחלפה חלה מגבלת קצב לכל כתובת IP.
שאלות נפוצות
האם SSO מופעל כברירת מחדל ב-Nodaro?
לא. עד ש-EXTERNAL_SSO_PROVIDERS מציין לפחות ספק אחד, דף ההתחברות לא מציג כפתור SSO, נקודת הקצה GET /v1/sso/providers מחזירה רשימה ריקה וכל נתיב של ספק מחזיר 404.
איך צריך לחתום על ה-assertion של SSO?
עם HS256 ועם סוד שאתם מגדירים לספק ולא משתמשים בו לשום דבר אחר. ה-assertion חייב לכלול גם את aud, את exp, מזהה jti ייחודי ואת האימייל, ו-Nodaro מקבל כל assertion פעם אחת בלבד.
האם assertion של SSO יכול להשתלט על חשבון קיים עם אותו אימייל?
לא כברירת מחדל. חשבון מקומי קיים מקושר רק כש-EXTERNAL_SSO_LINK_EXISTING הוא true וה-assertion מציין שהאימייל מאומת. חשבון שכבר מקושר לספק אחר לעולם לא מקושר שוב.
האם משתמש SSO הוא סוג מיוחד של חשבון Nodaro?
לא. אחרי שה-assertion מוחלף, יש למשתמש סשן רגיל. SSO הוא רק דרך להתחיל את הסשן הזה.
האם Nodaro תומך בספקי זהות של OIDC ושל SAML?
חלקית. הסוגים oidc ו-saml מעבירים את ההתחברות לשירות האימות של ההתקנה עצמה, עם מגבלות שמתוארות בדף הזה. החלפת ה-assertion היא הדרך שעובדת מקצה לקצה.
דפים קשורים
כניסה מאוחדת (SSO)
מהדורות ופרופילי ממשק
תצורה
ארנקים חיצוניים
אימות
עודכן לאחרונה