Organizations ADR 005: マルチステップ認証フロー

コンテキスト

GitLabは現在、ユーザーがメール/ユーザー名とパスワードを同一ページで入力するシングルステップ認証プロセスを使用しています。このアプローチにはいくつかの制限があります。

  • Organization固有のブランドや認証ポリシーがない
  • Organization固有の認証方法(SAML、カスタムIdP)のサポートが限られている
  • ユーザーのOrganizationコンテキストに基づくルーティングがない
  • すべてのユーザーが同じ汎用エンドポイントを通じて認証される

Google Workspace、Slack、Microsoft 365などの現代的なエンタープライズアプリケーションは、まずユーザーのOrganizationコンテキストを識別してから、Organization固有の認証体験にルーティングするマルチステップ認証フローを使用しています。これにより、ブランドのサインインページ、Organization固有の認証方法、より良いユーザー体験が実現できます。

GitLabへのOrganizationsの導入に伴い、既存ユーザーとの後方互換性を維持しながらOrganization固有の認証をサポートする同様のパターンを実装する必要があります。

決定

ユーザーの識別と認証を分離するマルチステップ認証フローを実装します。

ステップ1: ユーザー識別

ユーザーはグローバルサインインページ(/users/sign_in)でサインインフローを通過し、loginクエリパラメーターを使用してTopology Serviceによって正しいCellにルーティングされます。将来、OrganizationsがVerified Domainsをサポートするようになったら、verified domainsを持つユーザーはブランドのOrganizationサインインページ(例: /o/<org-path>/users/sign_in)にリダイレクトされます。

Verified Domainsなしの場合

sequenceDiagram
  actor User as User
  participant Browser as Browser
  participant Router as Router
  participant Cell1 as Cell 1
  participant Topology as Topology Service
  participant Cell2 as Cell 2

  User ->> Browser: Navigate to gitlab.com/users/sign_in
  Browser ->> Router: GET /users/sign_in
  Router ->> Cell1: Route to Cell 1
  Cell1 ->> Browser: Render sign-in page
  User ->> Browser: Enter email/username
  User ->> Browser: Click "Continue" button
  Browser ->> Cell1: AJAX GET /users/[email protected]
  alt If user is found on Cell 1
    Cell1->>Browser: JSON response { "sign_in_path": null }
    Browser ->> Browser: Show password field with JavaScript (no reload)
    User ->> Browser: Enter password
    User ->> Browser: Click "Sign in" button
    Browser->>Cell1: POST /users/sign_in { "login": "[email protected]", "password": "foo" }
    Cell1 ->> Browser: Authenticate
  else Else
    Cell1->>Browser: JSON response { "sign_in_path": '/users/[email protected]' }
    Browser ->> Router: GET /users/[email protected]
    Router ->> Topology: Request cell using `login` param
    Topology ->> Router: Return cell ID
    Router ->> Cell2: Route to Cell 2
    Cell2 ->> Browser: Render sign-in page
    User ->> Browser: Enter password
    User ->> Browser: Click "Sign in" button
    Browser->>Cell2: POST /users/sign_in { "login": "[email protected]", "password": "foo" }
    Cell2 ->> Browser: Authenticate
  end

Verified Domainsありの場合

OrganizationsがVerified Domainsをサポートするようになったら、Verified Domainを持つユーザーをブランドのOrganizationサインインページ(例: /o/<org-path>/users/sign_in)にリダイレクトします。

sequenceDiagram
  actor User as User
  participant Browser as Browser
  participant Router as Router
  participant Cell1 as Cell 1
  participant Topology as Topology Service
  participant Cell2 as Cell 2

  User ->> Browser: Navigate to gitlab.com/users/sign_in
  Browser ->> Router: GET /users/sign_in
  Router ->> Cell1: Route to Cell 1
  Cell1 ->> Browser: Render sign-in page
  User ->> Browser: Enter email/username
  User ->> Browser: Click "Continue" button
  Browser ->> Cell1: AJAX GET /users/[email protected]

  alt If verified domain exists on this cell
    Cell1->>Browser: JSON response { "sign_in_path": '/o/<org-path>/users/[email protected]' }
    Browser ->> Router: GET /o/<org-path>/users/[email protected]
    Router ->> Topology: Request cell using login param
    Topology ->> Router: Return cell ID
    Router ->> Cell1: Route to Cell 1
    Cell1 ->> Browser: Render sign-in page (/o/<org-path>/users/[email protected])
    User ->> Browser: Enter password
    User ->> Browser: Click "Sign in" button
    Browser->>Cell1: POST /o/<org-path>/users/sign_in { "login": "[email protected]", "password": "foo" }
  else Else If user exists on this cell
    Cell1->>Browser: JSON response { "sign_in_path": null }
    Browser ->> Browser: Show password field with JavaScript (no reload)
    User ->> Browser: Enter password
    User ->> Browser: Click "Sign in" button
    Browser->>Cell1: POST /users/sign_in { "login": "[email protected]", "password": "foo" }
  else Else
    Cell1->>Browser: JSON response { "sign_in_path": '/users/[email protected]' }
    Browser ->> Router: GET /users/[email protected]
    Router ->> Topology: Request cell using login param
    Topology ->> Router: Return cell ID
    Router ->> Cell2: Route to Cell 2
    alt If verified domain exists on this cell
      Cell2 ->> Browser: Redirect to /o/<org-path>/users/[email protected]
    else Else
      Cell2 ->> Browser: Render sign-in page (/users/[email protected])
    end
    User ->> Browser: Enter password
    User ->> Browser: Click "Sign in" button
    Browser->>Cell2: POST current URL { "login": "[email protected]", "password": "foo" }
    Cell2 ->> Browser: Authenticate
  end

AJAX GET /users/sign_in_path

リクエスト
GET /users/sign_in_path
Host: gitlab.com
Content-Type: application/json

{
  "login": "[email protected]",
}
レスポンス
ユーザーが現在のCellに存在する場合

ステータス: 200 OK

JSONボディ:

{
  "sign_in_path": null
}
ユーザーが現在のCellに存在しない場合

ステータス: 200 OK

JSONボディ:

{
  "sign_in_path": "/users/[email protected]"
}
ユーザーがVerified Domainを持つOrganizationに所属する場合

ステータス: 200 OK

JSONボディ:

{
  "sign_in_path": "/o/<organization>/users/[email protected]"
}

OmniAuth

現在のCellにいないユーザーは、OmniAuthオプション(Google、SAMLなど)が機能する前に、まずメール/ユーザー名を入力する必要があります。メール/ユーザー名を入力する前にOmniAuthオプションを使用しようとすると、エラーメッセージが表示されます。エラーメッセージを調整して、まずメール/ユーザー名を入力する必要があることを説明します。

これはOpenID/OAuth Clientに置き換えられます。

パスキーでのサインイン

パスキーはサインインしているCellに保存されるため、まずユーザーをサインインしているCellにルーティングする必要があります。「パスキーでサインイン」ボタンは、ユーザーがメール/ユーザー名を入力し、サインインしているCellにルーティングされた後に表示されます。

Self-ManagedとDedicatedでは、メール/ユーザー名を入力せずにパスキーが使用できます。

ステップ2: Organization固有の認証

  • ユーザーはOrganizationで設定された方法(パスワード、SAMLなど)を使用して認証します
  • Organizationのブランドと特定の認証ポリシーが適用されます
  • 2FAが必要な場合は、一次認証後の別画面として表示されます

後方互換性

  • ユーザー名ベースのログインはレガシーの/users/sign_inページで引き続き機能します
  • 既存のOAuthおよびSAMLコールバックパスはすべて保持されます
  • gitlab.com/o/<org-path>/users/sign_in経由の直接Organizationアクセスがサポートされます

メールの一意性

  • 各メールアドレスはすべてのGitLabインスタンスにわたって正確に1つのOrganizationに属します
  • メールドメインを特定のOrganizationsに制限できます
  • Topology Serviceはメールドメインマッピングに基づいて決定論的ルーティングを提供します

代替アクセス

  • ユーザーはgitlab.com/o/<org-path>/users/sign_in経由でOrganizationのサインインページに直接アクセスできます
  • プライベートOrganizationsは匿名ユーザーをサインインページにリダイレクトします
  • パブリックOrganizationsはサインインオプションとともにすぐにページを表示します

結果

ポジティブな結果

  • Organizationのブランディング: Organizationsはカスタムロゴとスタイリングでブランドのサインイン体験を提供できます
  • 柔軟な認証: OrganizationsはOrganization固有の認証方法(SAMLのみ、パスワード+2FAなど)を設定できます
  • スケーラブルなアーキテクチャ: Organization固有の認証ポリシーを持つ分散Cellアーキテクチャをサポートします

技術的な結果

  • Legacy Cell互換性: /users/sign_inページはLegacy Cellまたはそれ以降のCellによって引き続き提供されます
  • Topology Service統合: メール分類とOrganizationルーティングのためにRails統合が必要です
  • コールバックの保持: 既存のOAuth(/oauth/callback)とSAML(/groups/my-group/-/saml/callback)のすべてのコールバックパスは変更されません
  • ユーザー名サポート: ユーザー名ベースの認証は後方互換性のために引き続き機能します
  • パスワードマネージャー: 現在のCellにいないOrganizationにサインインする場合、メール/ユーザー名を入力した後でサインインページを再読み込みする必要があります。これにより、一部のパスワードマネージャーは第2ステップでパスワードを入力するのに追加クリックが必要になる場合があります。現在のCellにいるユーザーはページの再読み込みを必要としません。

代替案

サブドメインベースのルーティング

Organization固有のサインインにacme.gitlab.comのようなサブドメインの使用を評価しました。これは以下の理由で却下されました。

  • 複雑なDNS管理とSSL証明書の要件
  • すべてのGitLabサービス(SSH、APIエンドポイント)と互換性がない
  • 既存の統合やブックマークを壊す可能性がある
  • すべてのデプロイメントモデル(SaaS、Self-Managed、Dedicated)にわたって実装が困難

実装注記

URLパターン

  • Organizationサインイン: gitlab.com/o/<org-path>/users/sign_in
  • レガシーサインイン: gitlab.com/users/sign_in(変更なし)
  • Organizationページ: gitlab.com/o/<org-path>(プライベートOrganizationsはサインインにリダイレクト)

将来の機能強化

  • カスタムエイリアスドメイン: Organizationページへのルーティングを行うgitlab.company.com
  • OrganizationスコープのSAMLコールバック: gitlab.com/o/org-path/-/saml/callback
  • 強化されたOrganizationのブランディングとカスタマイズオプション