# התחברות חיצונית (SSO)

> התחברות משתמשים להתקנת Nodaro דרך ספק זהות מהימן, עם assertion חתום בפורמט JWT או עם OIDC ו-SAML, וקביעת האופן שבו חשבונות מקושרים.

Source: https://nodaro.ai/he/docs/developers/sso

**התחברות חיצונית (SSO)** מאפשרת למשתמשים להתחבר להתקנת Nodaro דרך ספק זהות (IdP) מהימן. באינטגרציה העיקרית, ספק הזהות שלכם חותם על assertion קצר־מועד בפורמט JWT, Nodaro מאמת אותו, והדפדפן מקבל סשן Nodaro רגיל. SSO כבוי עד שמנהל השרת של ההתקנה מגדיר לפחות ספק אחד.

החשבון שהמשתמש מגיע אליו הוא חשבון רגיל. SSO לא מוסיף פרטי גישה מיוחדים: הוא רק דרך להתחיל סשן.

## שתי דרכי אינטגרציה
| סוג | למי זה מתאים | איך זה עובד |
| --- | --- | --- |
| `assertion` | מנפיקי אסימונים שלא מדברים OIDC או SAML, כגון מארח שמטמיע את Nodaro ויכול רק להנפיק אסימון חתום קצר־מועד | ספק הזהות שלכם חותם על JWT, ו-Nodaro מאמת אותו ומחליף אותו בסשן. הדרך הזאת עובדת מקצה לקצה. |
| `oidc`, `saml` | ספקי זהות שמדברים OpenID Connect או SAML סטנדרטיים | דף ההתחברות מתחיל התחברות סטנדרטית עם Supabase Auth של ההתקנה עצמה, שמאמת את התשובה של ספק הזהות. לדרך הזאת יש מגבלות; ראו את הסעיף [ספקי OIDC ו-SAML](#oidc-and-saml-providers). |

## הגדרת ספקים
הגדירו בשרת את `EXTERNAL_SSO_PROVIDERS` למערך JSON של ספקים, ישירות בתוך המשתנה או כ-`@/path/to/providers.json` (תו `@` בהתחלה גורם לקריאת הקובץ שבנתיב הזה). ערך פגום עוצר את השרת באתחול, כך שטעות הקלדה לעולם לא תשמיט ספק בשקט ולא תשאיר את ההתחברות מוגדרת רק בחלקה.

```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](#oidc-and-saml-providers). |
| `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:

```ts

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())
```

## זרימת ההתחברות
1. המשתמש לוחץ על כפתור ה-SSO בדף ההתחברות של Nodaro, והדף קורא ל-`GET /v1/sso/:provider` בלי assertion.
2. Nodaro מפנה (`302`) אל ה-`initiateUrl` של הספק.
3. ספק הזהות מאמת את המשתמש ושולח את הדפדפן בחזרה אל `GET /v1/sso/:provider?assertion=<jwt>`, ואפשר להוסיף גם `&next=<path>`.
4. Nodaro מאמת את ה-assertion, דוחה שימוש חוזר ומחיל את [כללי קישור החשבונות](#how-accounts-are-linked).
5. Nodaro מפנה אל `/sso?sso_token=<one-time token>`. הדף הזה מחליף את האסימון החד־פעמי בסשן.
6. המשתמש מגיע אל `/projects`, או אל הנתיב שב-`next`.

ספק זהות, כגון מארח שמטמיע את Nodaro, יכול גם להתחיל בשלב 3: הוא מפנה את הדפדפן לנקודת הקצה של ההחלפה עם assertion חדש. הפרמטר `?redirect=` של דף ההתחברות עצמו לא עובר דרך ההפניות של SSO; עובר רק `next` שספק הזהות הוסיף.

## הצגת כפתור ה-SSO בדף ההתחברות
דף ההתחברות מציג כפתור SSO כששני התנאים מתקיימים:

- פרופיל הממשק של הפריסה כולל את `sso` ב-`auth.methods` ומגדיר את `auth.ssoLabel`. פרופיל בלי `ssoLabel` מסיר את `sso` מהשיטות. הטקסט של הכפתור הוא ה-`ssoLabel`.
- מוגדר לפחות ספק אחד. הדף בודק את `GET /v1/sso/providers`.

פרופילי ממשק זמינים במהדורות Business ו-Cloud. ראו את הדף [מהדורות ופרופילי ממשק](https://nodaro.ai/docs/self-hosting/editions-and-profiles).

פרופיל שה-`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` גם כשהכתובת תואמת יותר מחשבון אחד, כשהחיפוש נכשל וכששתי התחברויות מתחרות על יצירת אותו חשבון.

### בפריסה עם חשבון חיוב
פריסה יכולה להגדיר **חשבון חיוב** אחד שמשלם על כל המשתמשים של ההתקנה; ראו את הדף [ארנקים חיצוניים](https://nodaro.ai/docs/developers/external-wallet). הכללים לחשבון הזה מחמירים יותר, כי בו נמצאים הקרדיטים של הפריסה:

- **הוא מקושר בהתחברות המאומתת הראשונה שלו,** בלי קשר לערך של `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 היא הדרך שעובדת מקצה לקצה.
