OAuth 認証拡張 仕様書
ステータス: Draft / 作成日: 2026-05-27 依存: なし(既存の
users+sessions基盤を拡張)
1. 概要
既存のメール + パスワード認証(Argon2id)に加え、GitHub・GitLab・汎用 OIDC(Google 等)でのログイン・新規登録を可能にする。
設計方針:
- Authorization Code + PKCE のみ(Implicit Flow 禁止)
- OAuth ユーザーもセッション管理は既存の Redis セッションと共通
- メール/パスワードユーザーは後から OAuth を連携できる(逆も可)
- パスワードなしの OAuth 専用ユーザーも許容する
2. データモデル変更
2.1 users テーブル変更
OAuth ユーザーはパスワードを持たないため password_hash を NULL 許容にする。
ALTER TABLE users ALTER COLUMN password_hash DROP NOT NULL;
既存の全ユーザーはパスワードハッシュを持つため、NULL 許容化はデータ上の破壊的変更ではない。
ログイン時にパスワードが NULL のユーザーへのパスワード認証は401 invalid-credentialsを返す。
2.2 oauth_connections(新規)
pub struct Model {
pub id: Uuid,
pub user_id: Uuid,
pub provider: String, // "github" | "gitlab" | "google" | "oidc:{issuer}"
pub provider_user_id: String, // プロバイダー側のユーザー ID
pub provider_email: Option<String>, // プロバイダーが返したメール(参照用)
pub access_token_enc: Option<String>, // AES-256-GCM 暗号化
pub refresh_token_enc: Option<String>, // AES-256-GCM 暗号化
pub token_expires_at: Option<DateTimeUtc>,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
}
3. マイグレーション
ALTER TABLE users ALTER COLUMN password_hash DROP NOT NULL;
CREATE TABLE oauth_connections (
id UUID PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
provider VARCHAR NOT NULL,
provider_user_id VARCHAR NOT NULL,
provider_email VARCHAR,
instance_url VARCHAR,
access_token_enc TEXT,
refresh_token_enc TEXT,
token_expires_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (provider, provider_user_id, instance_url)
);
CREATE INDEX idx_oauth_connections_user ON oauth_connections(user_id);
4. 対応プロバイダー
プロバイダー設定は環境変数で管理する(テナント単位の設定は本仕様の対象外):
OAUTH_GITHUB_CLIENT_ID=...
OAUTH_GITHUB_CLIENT_SECRET=...
OAUTH_GITLAB_CLIENT_ID=...
OAUTH_GITLAB_CLIENT_SECRET=...
OAUTH_GITLAB_SELFHOSTED_CLIENT_ID=...
OAUTH_GITLAB_SELFHOSTED_CLIENT_SECRET=...
OAUTH_GOOGLE_CLIENT_ID=...
OAUTH_GOOGLE_CLIENT_SECRET=...
# 汎用 OIDC(複数設定可)
OAUTH_OIDC_ISSUER_URL=https://accounts.example.com
OAUTH_OIDC_CLIENT_ID=...
OAUTH_OIDC_CLIENT_SECRET=...
§4a. GitLab self-hosted フロー
GitLab.com と異なり、OAuth エンドポイントがインスタンスごとに異なる。
1. ユーザーがログイン画面で「GitLab self-hosted でログイン」を選択し、
インスタンス URL(例: https://gitlab.example.com)を入力
2. GET /v1/auth/oauth/gitlab_selfhosted?instance_url={url} をリクエスト
→ バックエンドが instance_url を Redis の state に紐付けて保存
→ 認可 URL: {instance_url}/oauth/authorize?...
3. コールバック: GET /v1/auth/oauth/gitlab_selfhosted/callback
→ state から instance_url を復元
→ トークンエンドポイント: {instance_url}/oauth/token
→ ユーザー情報: {instance_url}/api/v4/user
→ oauth_connections.instance_url に保存
4. 同一ユーザーが複数の self-hosted インスタンスを接続可能
(UNIQUE(provider, provider_user_id, instance_url) で識別)
OAuth アプリの事前登録: self-hosted 利用には、対象インスタンスに管理者がシステムの OAuth アプリ(Client ID / Secret)を登録しておく必要がある。callback URL は {APP_BASE_URL}/v1/auth/oauth/gitlab_selfhosted/callback に統一する。
5. OAuth フロー(Authorization Code + PKCE)
6. ユーザー照合ロジック
コールバック受信時、プロバイダーから provider_user_id と provider_email を受け取る。
A) oauth_connections に (provider, provider_user_id) が存在する
→ 既存ユーザーとしてログイン。access_token を更新。
B) 存在しない かつ provider_email が users.email と一致するユーザーがいる
→ "メール競合" → §6.1 参照
C) 存在しない かつ メール一致なし
→ 新規ユーザーを作成し oauth_connections も INSERT(§6.2 参照)
6.1 メール競合(ケース B)
既存のメール/パスワードユーザーと同じメールアドレスで OAuth ログインを試みた場合:
- 既存セッションあり(ログイン済み): oauth_connections を追加で INSERT → 連携完了
- 既存セッションなし:
409 Conflictを返し、フロントエンドが「このメールアドレスは既に登録されています。ログインして連携してください。」を表示
プロバイダーが返すメールアドレスは不変ではなく、なりすまし防止のため自動マージはしない。
6.2 新規ユーザー作成(ケース C)
users::ActiveModel {
id: Set(Uuid::new_v4()),
username: Set(derive_username_from_provider(provider_info)),
email: Set(provider_email),
email_verified: Set(provider_info.email_verified.unwrap_or(false)), // プロバイダーの検証フラグを反映
password_hash: Set(None), // OAuth ユーザーはパスワードなし
bio: Set(Some(String::new())),
avatar_url: Set(provider_info.avatar_url),
}
ユーザー名は {provider_username} をベースに、重複時は {name}_2, {name}_3 ... とする。
7. アカウント連携・解除
連携(ログイン済みユーザーが追加 OAuth を接続する)
- ログイン済みで
GET /v1/auth/oauth/{provider}をリクエスト(セッションあり) - コールバック受信後、現在のセッションユーザーに oauth_connections を追加
- 同じプロバイダーが既に連携済みなら
409
解除
DELETE /v1/auth/oauth/connections/{provider}
ガード: 最後の認証手段を解除できない:
(oauth_connections が 1 件のみ) AND (password_hash IS NULL)
→ 403: パスワードを設定してから解除してください
8. API
GET /v1/auth/oauth/connections レスポンス:
{
"connections": [
{
"provider": "github",
"provider_email": "user@example.com",
"connected_at": "2026-05-27T10:00:00Z"
}
]
}
POST /v1/auth/password(OAuth ユーザーのパスワード初回設定):
{ "password": "new-password-min-8" }
既にパスワードが設定されている場合は
409。変更は/v1/auth/password/changeを使用(パスワードリセット仕様書 §4 参照)。
9. セキュリティ
10. フロントエンド(Phase B)
ログイン画面
┌─────────────────────────────────────────────┐
│ ログイン │
├─────────────────────────────────────────────┤
│ メールアドレス [___________________] │
│ パスワード [___________________] │
│ [ログイン] │
│ │
│ ─────────── または ──────────── │
│ │
│ [ GitHub でログイン ] │
│ [ GitLab でログイン ] │
│ [ Google でログイン ] │
└─────────────────────────────────────────────┘
アカウント設定「連携済みサービス」
/settings/account
┌─────────────────────────────────────────────┐
│ 連携済みサービス │
├─────────────────────────────────────────────┤
│ GitHub user@example.com [解除] │
│ GitLab — [連携する] │
│ Google — [連携する] │
│ │
│ パスワード: 未設定 [パスワードを設定] │
└─────────────────────────────────────────────┘