Nodaro ドキュメント
ドキュメントノードリファレンスモデルAI エージェント(MCP)開発者向けセルフホスティングリサーチ

外部ログイン(SSO)

信頼できる ID プロバイダーが、署名付きの JWT アサーション、または OIDC や SAML のオプションで、Nodaro のインストール環境にユーザーをログインさせます。アカウントのリンク方法も制御できます。

外部ログイン(SSO)を使うと、信頼できる ID プロバイダー(IdP)が、ユーザーを Nodaro のインストール環境にログインさせられます。主な連携方法では、IdP が有効期間の短い JWT アサーションに署名し、Nodaro がそれを検証して、ブラウザーが通常の Nodaro セッションを受け取ります。インストール環境の管理者が少なくとも 1 つのプロバイダーを設定するまで、SSO はオフです。

ユーザーが最終的に使うアカウントは、通常のアカウントです。SSO は特別な認証情報を追加しません。セッションを開始する手段にすぎません。

2 つの連携方式

種類対象仕組み
assertionOIDC や SAML に対応していない発行元。たとえば、有効期間の短い署名付きトークンしか発行できない埋め込みホストIdP が JWT に署名し、Nodaro がそれを検証してセッションと交換します。この方式は、エンドツーエンドで動作します。
oidc, saml標準の OpenID Connect や SAML に対応した IdPログインページが、インストール環境自身の Supabase Auth で標準のログインを開始し、Supabase Auth が IdP の応答を検証します。この方式には制限があります。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すべてプロバイダーのスラッグです。ルート /v1/sso/:id で使われ、リンクされた各アカウントに保存されます。1〜63 文字で、先頭は小文字の英字か数字、その後は小文字の英字、数字、_、- です。ドットは使えません。一意である必要があります。
labelすべてプロバイダーの表示名です。
kindすべてassertion、oidc、saml のいずれかです。
secretassertion、必須アサーションの署名を検証する HS256 の鍵で、16 文字以上です。専用のシークレットを使い、IdP 自身のセッション署名用のシークレットは決して使わないでください。そうすれば、Nodaro が侵害されても、IdP のセッションを偽造されることはありません。
audienceassertion、必須アサーションの aud クレームと一致する必要がある値です。
claimMapassertion、任意メールアドレス、確認済みフラグ、サブジェクトを、それぞれどのクレームが持つかを指定します。デフォルトは email、email_verified、sub です。
initiateUrlassertion、任意SSO ボタンがユーザーを送る先です。指定しない場合、ボタンをクリックすると 400 no_assertion が返されます。
initiateUrlByHostassertion、任意ポートを含まない、小文字だけのホスト名から、そのホストで initiateUrl の代わりに使うアドレスへのマップです。複数のホスト名で応答するデプロイメント向けです。
maxLifetimeSecondsassertion、任意Nodaro が受け付ける最長の有効期間(exp から iat を引いた値)です。デフォルトは 300 秒で、最大は 3,600 秒です。
domainoidc、saml起動時に検証されますが、使われません。OIDC と SAML のプロバイダーを参照してください。
supabaseProvideroidc起動時に検証されますが、使われません。
  • initiateUrl は、IdP に何も転送しません。ログイン後にユーザーを特定のページに移動させたい場合は、IdP がリダイレクトで戻すときに &next=<path> を追加します。
  • initiateUrlByHost のキーは、起動時に検証されます。大文字やポートを含むキーがあると、サーバーは停止します。このマップが公開されることはなく、アカウントのリンクは、ユーザーがどのホストから来たかに左右されません。

アサーションの要件

IdP は JWT を発行し、ブラウザーを GET /v1/sso/:provider?assertion=<jwt> に送ります。Nodaro がアサーションを受け付けるのは、次のすべての条件を満たす場合だけです。

  • アルゴリズム:JWT が、HS256 とプロバイダーの secret で署名されていること。
  • オーディエンス:aud が、プロバイダーの audience と等しいこと。
  • 有効期限:exp があり、過去の時刻ではないこと。時計のずれは、5 秒まで許容されます。
  • 有効期間:exp から iat を引いた値が、maxLifetimeSeconds 以下であること。この値は、Nodaro の時計で計測されます。5 秒を超えて未来を指す iat は補正され、iat がない場合は現在時刻として扱われます。
  • 1 回限りの使用:jti があり、一意であること。Nodaro は、アサーション自体の有効期間が過ぎるまで各 jti を記憶し、2 回目は拒否します。
  • メールアドレス:メールアドレスのクレームがあること。
  • 確認済みのメールアドレス:アカウントを作成またはリンクするには、確認済みフラグが true であること。フラグがない場合は、未確認として扱われます。

アサーションが要件を満たさない場合は、401 とともに、invalid_signature、invalid_claims、expired、too_long_lived、missing_jti、missing_email、assertion_replayed のいずれかが返されます。このエンドポイントが受け付けるのは、1 つの IP アドレスから 60 秒あたり 20 リクエストまでです。それを超えると、Retry-After ヘッダーとともに 429 rate_limit_exceeded が返されます。

次の例は IdP 側のコードで、Node.js 用の jose ライブラリを使って、有効なアサーションを発行します。

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

ログインの流れ

  1. ユーザーが、Nodaro のログインページで SSO ボタンをクリックします。ボタンは、アサーションなしで GET /v1/sso/:provider を呼び出します。
  2. Nodaro が、プロバイダーの initiateUrl にリダイレクト(302)します。
  3. IdP がユーザーを認証し、ブラウザーを GET /v1/sso/:provider?assertion=<jwt> に戻します。任意で &next=<path> も付けます。
  4. Nodaro がアサーションを検証し、再利用(リプレイ)を拒否して、アカウントのリンクのルールを適用します。
  5. Nodaro が /sso?sso_token=<one-time token> にリダイレクトします。このページが、ワンタイムトークンをセッションと交換します。
  6. ユーザーは、/projects か、next のパスに移動します。

埋め込みホストなどの IdP は、手順 3 から始めることもできます。その場合、IdP は新しいアサーションを付けて、ブラウザーを交換エンドポイントにリダイレクトします。ログインページ自身の ?redirect= パラメーターは、SSO のリダイレクトでは引き継がれません。引き継がれるのは、IdP が追加した next だけです。

ログインページに SSO ボタンを表示する

ログインページに SSO ボタンが表示されるのは、次の両方の条件を満たす場合です。

  • デプロイメントのサーフェスプロファイルが、auth.methods に sso を含み、auth.ssoLabel を設定していること。ssoLabel のないプロファイルでは、メソッドから sso が外されます。ボタンのテキストは、ssoLabel です。
  • 少なくとも 1 つのプロバイダーが設定されていること。ページは、GET /v1/sso/providers で確認します。

サーフェスプロファイルは、Business エディションと Cloud エディションの機能です。エディションとサーフェスプロファイルを参照してください。

auth.methods に sso だけを含むプロファイルでは、サーバー側の制限もオンになります。ログインするすべてのアカウントは、SSO 経由で作成またはリンクされている必要があり、それ以外のセッションは、最初の API 呼び出しで 403 sso_required によって拒否されます。唯一の例外は、後述するデプロイメントの請求アカウントです。メソッドに email を追加すると、インストール環境全体でこの制限がオフになります。

アカウントのリンク方法

アサーションが有効な場合、Nodaro はルールに従って、アカウントを探すか作成します。このルールは、メールアドレスが同じというだけの既存のアカウントを、アサーションが決して乗っ取れないように作られています。

状況結果
そのメールアドレスのアカウントがなく、メールアドレスが確認済み新しいアカウントが作成され、プロバイダーにリンクされます。
そのメールアドレスのアカウントがなく、メールアドレスが未確認403 email_unverified で拒否されます。そのため、未確認のクレームで実在のアドレスを占有することはできません。
アカウントがすでにこのプロバイダーにリンクされているユーザーはログインします。
アカウントが別のプロバイダーにリンクされているEXTERNAL_SSO_LINK_EXISTING がオンでも、403 account_linked_other_provider で拒否されます。
SSO なしのローカルアカウントがあり、EXTERNAL_SSO_LINK_EXISTING がオンで、メールアドレスが確認済みアカウントがプロバイダーにリンクされ、ユーザーはログインします。
SSO なしのローカルアカウントがあり、フラグがオフか、メールアドレスが未確認403 account_exists で拒否されます。

EXTERNAL_SSO_LINK_EXISTING はデフォルトでオフで、これが乗っ取りに対して安全な設定です。オンになるのは、大文字小文字を問わず、値が true または 1 の場合だけです。既存のアカウントに結び付けてもよいと思えるほど、IdP の確認済みメールアドレスのクレームを信頼できる場合にだけ、オンにしてください。

account_exists と email_unverified による拒否は、どのアカウントでも同じ文言を使います。そのため、ログインフォームを使って、どのアドレスが特別なロールを持っているかを調べることはできません。account_exists は、アドレスが複数のアカウントに一致した場合、検索が失敗した場合、2 つのログインが同じアカウントを同時に作成しようとした場合にも返されます。

請求アカウントがあるデプロイメントの場合

デプロイメントでは、インストール環境のすべてのユーザーの料金を支払う請求アカウントを 1 つ指定できます。外部ウォレットを参照してください。このアカウントはデプロイメントのクレジットを保持しているため、適用されるルールがより厳しくなっています。

  • 最初の確認済みのログインでリンクされます。EXTERNAL_SSO_LINK_EXISTING の設定は関係ありません。このアカウントに対する未確認のアサーションは email_unverified で、別のプロバイダーからのアサーションは account_linked_other_provider で拒否されます。
  • 2 回目以降のログインも、毎回確認されます。メールアドレスが確認済みで、サブジェクトが最初にアカウントをリンクしたものと同じである必要があります。そうでない場合、ログインは 403 account_linked_other_subject で拒否されます。
  • 緊急用の入口として、パスワードを保持します。IdP がこのアカウントをアサートできなくなった場合に備えるものです。SSO のみのデプロイメントでは、そのフォームは /login?billing=1 にあり、請求アカウントしか入れません。
  • インストール環境の管理者でも、利用停止や削除はできません(403 payer_account_protected)。そのため、デプロイメント自身の IdP が管理者ロールを付与しても、そのロールでインストール環境をクレジットから締め出すことはできません。

IdP における請求アカウントのサブジェクトが変わった場合、そのアカウントの SSO ログインは、account_linked_other_subject で失敗し続けます。その場合でも、このアカウントはパスワードでログインできます。

プラットフォームのオペレーターのアドレス(PLATFORM_OPERATOR_EMAILS、それが空の場合は PLATFORM_OWNER_EMAIL)は、請求アカウントがあるデプロイメントでは、新たにリンクされることはありません。これらのアドレスは account_exists で拒否され、オペレーターのアカウントは SSO の対象外に保たれます。すでにリンクされているオペレーターのアカウントは、引き続きログインできます。

OIDC と SAML のプロバイダー

oidc と saml の種類は、ログインをインストール環境自身の Supabase Auth に引き渡し、Supabase Auth が IdP の応答を検証します。どちらにも制限があります。

  • oidc:ログインページは、プロバイダーの id を OAuth プロバイダー名として渡します。そのため、id 自体が、インストール環境の Supabase Auth に設定された OAuth プロバイダーの名前(keycloak や azure など)である必要があります。
  • saml:ログインページは、プロバイダーの id を SAML ドメインとして渡します。id にはドットを含められないため、acme.com のようなドメインを表せません。そのため、saml はエンドツーエンドでは動作しません。
  • oidc または saml のプロバイダーにアサーションを送ると、400 not_assertion_provider で拒否されます。

エンドポイント

どちらのエンドポイントも公開されていて、トークンは不要です。/v1/sso/ の下にあるルートは、この 2 つだけです。

メソッドパス内容
GET/v1/sso/providersログインページ用に、プロバイダーを一覧表示します。返すのは id、label、kind だけで、シークレットは決して返しません。SSO がオフの場合は、空のリストを返します。
GET/v1/sso/:provider交換エンドポイントです。JSON API ではなく、ブラウザーのリダイレクト用のエンドポイントです。
ステータスコード発生する状況
400no_assertionボタンがクリックされましたが、プロバイダーに initiateUrl がありません。
400not_assertion_provideroidc または saml のプロバイダーに、アサーションが送られました。
401invalid_signature, invalid_claims, expired, too_long_lived, missing_jti, missing_email, assertion_replayedアサーションが、要件のいずれかを満たしていません。
403email_unverified, account_exists, account_linked_other_provider, account_linked_other_subjectアカウントのリンクのルールによって、ログインが拒否されました。
403sso_requiredSSO のみのデプロイメントが、SSO 経由ではないセッションを拒否しました。
404unknown_providerその id のプロバイダーがないか、SSO がオフです。
429rate_limit_exceeded1 つの IP アドレスからのリクエストが多すぎます。

セキュリティに関する注意

  • 専用のシークレットを使う:secret は、Nodaro 専用の検証用の鍵です。IdP 自身のセッション用のシークレットとは分けてください。
  • アサーションは 1 回だけ有効:jti のチェックにより、一度送信されたアサーションは 2 回目には拒否されます。また、有効期間の上限によって、有効な時間帯は短く保たれます。
  • リダイレクトはインストール環境内にとどまる:next が有効なのは、同じインストール環境内の相対パスで、/ で始まり、// で始まらない場合だけです。それ以外の場合、ユーザーは /projects に移動します。
  • シークレットがサーバーの外に出ることはない:GET /v1/sso/providers が返すのは、id、label、kind だけです。アサーションとワンタイムトークンは、リクエストログから削除されます。
  • 交換にはレート制限がある:IP アドレスごとに制限されます。

よくある質問

最終更新

目次