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 provider_login: 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,
}
provider_login はログイン・連携のたびに ProviderUserInfo.username を小文字にして控え直す。
ログイン名は改名できるので本人の識別には provider_user_id を使い、こちらはコミット作者を Task ユーザーに
解決する用途(Git ホスティング↔タスク連携 §2「アクティビティ」)にだけ使う。
改名で同じ名前が別の人に移ることがあるので、控えるときに同じ provider / instance_url で同じ名前を持つ
別の接続からは外す。一意性はこの付け替えと、照合側の「ちょうど 1 件のときだけ採る」で保ち、UNIQUE 制約は
張らない(SeaORM の entity は 1 列に unique_key を 1 つしか持てず、provider は上の複合 UNIQUE に
入っているので表せない。表せない UNIQUE 制約は起動時の schema sync が黙って DROP する)。
ログインし直さない人の改名は追えない。
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 に統一する。
§4b. 本番(Dokploy)の設定
list_providers は is_provider_configured で client id と secret が両方入っている
プロバイダーだけ返し、OAuthButtons.vue は providers.length > 0 のときだけ描画する。
つまり未設定だとボタンも「または」の区切り線も出ない(エラーにはならない)。
Dokploy の Environment に入れただけでは効かない。 compose の backend.environment
に列挙したキーしかコンテナへ渡らないので、docker-compose.prod.yml 側にも書く。
Redirect URI には /api を含める。
{APP_ORIGIN}/api/v1/auth/oauth/{provider}/callback
backend は frontend の SSR サーバ(/api/* を転送し、redirect: 'manual' で
バックエンドの 302 をそのまま返す)越しにしか公開されていない。プロバイダーへ渡す
redirect_uri は APP_BASE_URL から組むので、compose は APP_BASE_URL に
${APP_ORIGIN}/api を渡す。ここを ${APP_ORIGIN} にすると、認可画面から
/v1/auth/... へ戻されて frontend のルーティングに落ち、コールバックが届かない。
コールバック後にユーザーを戻す先は別で、EMAIL_VERIFICATION_APP_URL(= APP_ORIGIN)
から組む(build_frontend_redirect)。/api は付かない。
OAUTH_ENCRYPTION_KEY は 32 文字以上。実際の鍵は auth_core::crypto の HKDF が
導出するので、文字列であればよい(openssl rand -base64 48)。一度決めたら変えない。
変えると保存済みのリフレッシュトークンが復号できず、連携済みのユーザーは
OAuth をやり直すことになる。
設定できているかは discovery を叩くのが早い。
curl -s https://<domain>/api/v1/auth/oauth/providers
# {"providers":[]} なら backend に渡っていない
5. OAuth フロー(Authorization Code + PKCE)
redirect_after は / で始まる相対パスのみ許可する(//・:・@・.. を拒否)。組み立て時はフロント基底 URL と url::Url::join で同一 origin を検証する。プロバイダーが error を返した場合は詳細をログに残し、フロントへ oauth_error=authorization_failed のみ付与してリダイレクトする。
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/security にまとめた。どちらも「最低 1 つは残す」という同じ制約を共有するため、
別々の画面にしていない。画面の仕様は
アカウント設定 §7 を正とする。
/settings/security
┌─────────────────────────────────────────────┐
│ 認証方法 │
├─────────────────────────────────────────────┤
│ パスワード 設定済み [パスワードを変更] │
│ GitHub user@example.com [解除] │
├─ 追加できる連携 ─────────────────────────────┤
│ Google — [連携する] │
└─────────────────────────────────────────────┘
連携の追加は既存の開始 URL(GET /v1/auth/oauth/{provider})をそのまま使い、
redirect_after にこの画面を指定して戻す。ログイン済みで開始した場合の連携追加は §6.3 の経路。