# シングルサインオン

> 信頼できる ID プロバイダーで、セルフホスティングの Nodaro にユーザーをログインさせます。EXTERNAL_SSO_PROVIDERS の設定、アカウントのリンク規則、SSO のみの許可について説明します。

Source: https://nodaro.ai/ja/docs/self-hosting/sso

**シングルサインオン（SSO）**を使うと、信頼できる外部の ID プロバイダーを通じて、ユーザーがセルフホスティングの Nodaro にログインできます。各プロバイダーを `EXTERNAL_SSO_PROVIDERS` 変数に記述すると、ログインページに SSO ボタンが表示されます。ユーザーが最終的に使うアカウントは、環境の通常のユーザーです。SSO は、セッションを開始する方法の 1 つにすぎません。このページでは、環境を運用する側の設定を説明します。プロトコルについては、[開発者向けのシングルサインオン](https://nodaro.ai/docs/developers/sso)を参照してください。

## 使うプロバイダーの種類
| 種類 | 使う場面 | 状態 |
| --- | --- | --- |
| `assertion` | 発行元が OpenID Connect や SAML に対応していなくても、有効期間の短い JWT に署名できる場合。たとえば、Nodaro を埋め込むアプリです。Nodaro はトークンを検証し、通常のセッションと交換します。 | 一連の流れがすべて動作します。 |
| `oidc` | ID プロバイダーが OpenID Connect に対応していて、環境の Supabase Auth で設定されている場合。ブラウザーは通常の Supabase のログインリダイレクトを行い、Supabase が結果を検証します。 | プロバイダーの `id` は、`keycloak` や `azure` など、Supabase Auth での OAuth プロバイダーの名前と同じにする必要があります。 |
| `saml` | — | エントリーは検証を通過しますが、ログインページが SAML のドメインを送信できないため、誰もログインさせられません。`assertion` か `oidc` を使ってください。 |

SSO は、デフォルトでは無効です。`EXTERNAL_SSO_PROVIDERS` が未設定の場合、ログインページに SSO ボタンは表示されず、`GET /v1/sso/providers` は空のリストを返し、すべてのプロバイダーのルートが `404 unknown_provider` を返します。

## プロバイダーを設定する
`EXTERNAL_SSO_PROVIDERS` に、JSON 配列を設定します。値を直接書くか、`@/path/to/providers.json` の形式でファイルから読み込みます。

```json
[
{
"id": "librechat",
"label": "LibreChat",
"kind": "assertion",
"secret": "a-dedicated-32+char-hmac-key-not-the-idp-session-secret",
"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"
}
]
```

値の形式が正しくないと、**起動が停止します**。シークレットを含むエントリーに入力ミスがあっても、プロバイダーが黙って外されたり（SSO が `404` で失敗しているように見えます）、ログインが中途半端に設定されたりすることがあってはならないためです。

Compose スタックでは、`EXTERNAL_SSO_PROVIDERS` と `EXTERNAL_SSO_LINK_EXISTING` を、`nodaro` サービスの `environment:` に追加してください。Compose ファイルは、`.env` のこれらの変数をコンテナに渡さないためです。ファイルを使う場合は、ファイルをコンテナにマウントし、コンテナ内でのパスを指定します。

### プロバイダーのフィールド
| フィールド | 対象 | 内容 |
| --- | --- | --- |
| `id` | すべて | URL で使える名前です。先頭は英小文字か数字で、以降は英小文字、数字、`_`、`-` を使えます。全体で 1〜63 文字で、ドットは使えません。ルート `/v1/sso/<id>` に使われ、ユーザーに保存されます。一意である必要があります。 |
| `label` | すべて | プロバイダーの名前です。ログインボタンに表示されるのは、プロバイダーが 2 つ以上あり、サーフェスプロファイルで `auth.ssoLabel` が設定されていない場合だけです。 |
| `kind` | すべて | `assertion`、`oidc`、`saml` のいずれかです。 |
| `secret` | `assertion`（必須） | アサーションの署名を検証する HS256 のキーで、16 文字以上です。ID プロバイダー自身のセッションシークレットは使わず、専用のキーを使ってください。そうすれば、Nodaro 側でキーが漏れても、プロバイダーのセッションを偽造されることはありません。 |
| `audience` | `assertion`（必須） | アサーションの `aud` クレームと一致する必要がある値です。 |
| `claimMap` | `assertion` | メールアドレス、確認済みフラグ、サブジェクトを、それぞれどのクレームで受け取るかを指定します。デフォルトは `{ "email": "email", "emailVerified": "email_verified", "subject": "sub" }` です。 |
| `initiateUrl` | `assertion` | 絶対 URL です。ユーザーがログインボタンをクリックすると、Nodaro はこの URL にリダイレクトします。プロバイダーはユーザーをログインさせ、アサーションを持って戻ってきます。このフィールドがない場合、クリックすると `400 no_assertion` が返ります。 |
| `initiateUrlByHost` | `assertion` | ポートを含まない小文字のホスト名から、絶対 URL への対応表です。そのホストでクリックされた場合は、`initiateUrl` の代わりに対応する URL に移動します。複数のホスト名で応答する環境向けです。キーが正しくないと起動が停止し、この対応表が公開されることはありません。 |
| `maxLifetimeSeconds` | `assertion` | アサーションに許される最長の有効期間（`exp` から `iat` を引いた値）です。デフォルトは `300`（5 分）で、最大は `3600` です。 |
| `supabaseProvider` | `oidc` | 検証はされますが、使われません。ログインページは、プロバイダーの `id` を Supabase に渡します。 |
| `domain` | `oidc`、`saml` | 検証はされますが、使われません。`oidc` と `saml` のエントリーには、`domain` か `supabaseProvider` が必要です。 |

公開されるのは `id`、`label`、`kind` だけで、`GET /v1/sso/providers` を通じて公開されます。シークレットがサーバーの外に出ることはありません。

## Nodaro がアサーションで確認すること
`assertion` プロバイダーの ID サーバーは JWT に署名し、ブラウザーを `GET /v1/sso/<id>?assertion=<jwt>` に送ります。Nodaro は、次の条件をすべて満たす場合にだけ、このアサーションを受け入れます。

- **HS256** とプロバイダーの `secret` で署名されていること。
- **`aud`** が、プロバイダーの `audience` と一致すること。
- **`exp`** があり、期限が過ぎていないこと（許容誤差は 5 秒）。また、有効期間が `maxLifetimeSeconds` 以内であること。
- **`jti`** があり、一度も使われていないこと。各アサーションは 1 回しか使えません。
- **メールアドレス**のクレームがあること。アカウントを作成またはリンクするには、**確認済み**を示すクレームが `true` である必要があります。

確認に失敗すると、`401` が返ります。このルートは、1 つの IP アドレスから 60 秒あたり 20 回までのリクエストを受け付け、それを超えると `429` を返します。仕様の全体、エラーコード、リダイレクトの流れは、[開発者向けのシングルサインオン](https://nodaro.ai/docs/developers/sso)にあります。

## アカウントのリンク方法
アサーションが確認を通過すると、Nodaro は該当するユーザーを探すか、新しく作成します。次のルールにより、メールアドレスが同じというだけで、アサーションが既存のアカウントを乗っ取ることはできません。

| 状況 | 結果 |
| --- | --- |
| そのメールアドレスのアカウントがなく、メールアドレスが確認済み | 新しいユーザーが作成され、プロバイダーにリンクされます。 |
| そのメールアドレスのアカウントがなく、メールアドレスが未確認 | 拒否されます：`403 email_unverified`。 |
| アカウントがすでにこのプロバイダーにリンクされている | ログインします。 |
| アカウントが別のプロバイダーにリンクされている | `EXTERNAL_SSO_LINK_EXISTING=true` の場合でも拒否されます：`403 account_linked_other_provider`。 |
| SSO を使わないローカルアカウントがあり、`EXTERNAL_SSO_LINK_EXISTING=true` で、メールアドレスが確認済み | アカウントがプロバイダーにリンクされ、ログインします。 |
| SSO を使わないローカルアカウントがあり、フラグが `false` か、メールアドレスが未確認 | 拒否されます：`403 account_exists`。 |

`EXTERNAL_SSO_LINK_EXISTING` のデフォルトは `false` で、乗っ取りに対して安全です。有効になるのは `true` か `1` の場合だけです（大文字と小文字は区別しません）。有効にするのは、プロバイダーの「メールアドレス確認済み」のクレームを、既存のアカウントに結び付けてよいと言えるほど信頼できる場合だけにしてください。

`account_exists` と `email_unverified` は、どのアカウントでも同じ文言を使います。そのため、ログインフォームを使って、どのアドレスが存在するかを探ることはできません。どのアカウントが対象になったかは、サーバーのログに記録されます。

## ログインボタン
プロバイダーが 1 つ以上設定されていると、ログインページに SSO ボタンが表示されます。

- **プロバイダーが 1 つの場合**：ボタンには**シングルサインオン**、またはサーフェスプロファイルの `auth.ssoLabel` が表示されます。
- **プロバイダーが複数の場合**：サーフェスプロファイルで `auth.ssoLabel` が設定されていなければ、各ボタンにはそのプロバイダーの `label` が表示されます。

ログイン後、ユーザーは `/projects` に移動します。プロバイダーは、リダイレクトで戻すときに `&next=<path>` を付けて、別のページを指定できます。Nodaro がこの指定に従うのは、同じオリジン上の相対パスで、`/` で始まり、`//` で始まらない場合だけです。それ以外の場合は `/projects` に移動します。`CORS_ORIGIN` に列挙されたホストでは、ログイン後のリダイレクトは相対パスのままで、ユーザーがアクセスしてきたホストにとどまります。

## SSO によるログインだけを許可する
Business エディションでは、サーフェスプロファイルで、ほかのログイン方法を無効にできます。

```bash
NODARO_SURFACE_PROFILE={"auth":{"methods":["sso"],"ssoLabel":"Sign in with Acme"}}
```

リストに `sso` だけを指定すると、サーバー側のルールも有効になります。ログインするすべてのアカウントは、SSO を通じて作成またはリンクされている必要があります。それ以外のセッションは、最初の API 呼び出しで `403 sso_required` によって拒否されます。そのため、ログインサービスに直接登録したアカウントは、この環境を使えません。リストに `email` を加えると、このルールは無効になります。[エディションとサーフェスプロファイル](https://nodaro.ai/docs/self-hosting/editions-and-profiles#sign-in-methods)を参照してください。

## セキュリティのまとめ
- **専用のシークレット**：プロバイダーごとに、プロバイダー自身のセッションシークレットとは別のシークレットを使います。
- **1 回限りのアサーション**：有効期間は短く、サーバー側で強制されます。
- **同一オリジンへのリダイレクトのみ**：ログイン後のリダイレクト先は、同じオリジンに限られます。
- **シークレットはサーバーの外に出ません**。アサーションとワンタイムトークンは、リクエストログから削除されます。
- **レート制限**：交換用のルートには、IP アドレスごとのレート制限があります。
- **通常のセッション**：交換のあと、ユーザーは特別な認証情報を持たない通常のユーザーになります。

## Frequently asked questions

### シングルサインオンは、デフォルトで有効ですか？

いいえ。EXTERNAL_SSO_PROVIDERS が未設定の場合、ログインページに SSO ボタンは表示されず、SSO のすべてのルートが 404 を返します。有効にするには、プロバイダーを 1 つ以上設定してください。

### どの種類のプロバイダーを設定すればよいですか？

Nodaro を埋め込むアプリなど、有効期間の短い署名付きトークンを発行できる発行元には、assertion を使います。環境の Supabase Auth で設定した ID プロバイダーには、oidc を使います。種類が saml のエントリーは検証を通過しますが、誰もログインさせられません。

### SSO で、同じメールアドレスを持つ既存のアカウントを引き継げますか？

デフォルトではできません。EXTERNAL_SSO_LINK_EXISTING は false のため、ローカルアカウントがすでにあるメールアドレスのアサーションは、account_exists で拒否されます。true に設定するのは、プロバイダーの「メールアドレス確認済み」のクレームを信頼できる場合だけにしてください。

### SSO だけでログインできるようにするには、どうすればよいですか？

Business エディションで、サーフェスプロファイルの auth.methods を ["sso"] に設定し、auth.ssoLabel も指定します。以降、すべてのアカウントは SSO を経由する必要があり、それ以外のセッションは 403 sso_required で拒否されます。

### EXTERNAL_SSO_PROVIDERS に誤りがあると、どうなりますか？

環境が起動しなくなります。値の形式が正しくないときに、プロバイダーが黙って外されたり、ログインが中途半端に設定されたりすることはありません。
