認証システム仕様書
1. システム概要
1.1 目的
本システムは、蒼翔祭(学祭)関連サービス全体で使用される統合認証基盤である。
- スタッフ認証: Discord OAuth2によるログイン
- ゲスト認証: LINE OAuth2によるログイン
- JWT管理: トークン発行・検証・BAN管理
1.2 アーキテクチャ構成
1.3 JWT利用サービス一覧
スタッフJWT(soshosai_staff_jwt):
| ドメイン | サービス名 | 用途 |
|---|---|---|
room.soshosai.com | Sho-Room | 学祭委員管理画面 |
fwd.soshosai.com | trackable-links | リンク管理 |
docs.soshosai.com | ドキュメント | Docusaurusドキュメント共有 |
apf.soshosai.com | 申請システム | 各種申請管理 |
v3.api.soshosai.com | 共通API | バックエンドAPI |
ゲストJWT(soshosai_customer_jwt):
| ドメイン | サービス名 | 用途 |
|---|---|---|
app.soshosai.com | 来場者アプリ | 来場者向けサービス |
v3.api.soshosai.com | 共通API | バックエンドAPI |
1.4 BFFによるJWT選択とService Binding
各フロントエンドのPages Functions(BFF)が、サイトの役割に応じて適切なCookieのみを選択し、Service Binding経由でAPIに転送する。
設計原則:
- クライアント(ブラウザ)は直接APIを呼ばない
- 全API呼び出しはBFF経由
- BFFはService BindingでWorkerを直接呼び出し(HTTP経由ではない)
- APIは
Authorization: Bearer xxxヘッダーのみで認証(Cookieは使用しない)
JWT選択ルール:
| サイト | BFFが選択するCookie | 用途 |
|---|---|---|
room.soshosai.com | soshosai_staff_jwt | スタッフ専用 |
fwd.soshosai.com | soshosai_staff_jwt | スタッフ専用 |
docs.soshosai.com | soshosai_staff_jwt | スタッフ専用 |
apf.soshosai.com | soshosai_staff_jwt | スタッフ専用 |
app.soshosai.com | soshosai_customer_jwt | ゲスト専用 |
Service Binding設定(wrangler.jsonc):
env 未指定でのデプロイ事故を防ぐため、binding は全て env.* 配下に置く(scripts/wrangler-lint.mjs の drift lint 方針)。以下は sho-room の production 環境の例。
{
"env": {
"production": {
"assets": {
"directory": "./dist",
"binding": "ASSETS",
"not_found_handling": "single-page-application",
// この2つだけ Worker を先に走らせる。他は静的アセット配信が先。
"run_worker_first": ["/api/*", "/fwd/*"],
},
"services": [
{ "binding": "GATEWAY", "service": "system-gateway" },
{ "binding": "TRACABLE_LINKS", "service": "soshosai-qr-forwarder-api" },
],
},
},
}
BFF実装パターン(Workers Static Assets + Service Binding):
スタッフサイトは Cloudflare Pages Functions ではなく Workers Static Assets で動く。エントリポイントは packages/web/sho-room/worker/index.ts、/api/* の転送は worker/proxies.ts にある。
interface Env {
ASSETS: Fetcher;
GATEWAY: Fetcher;
}
// packages/web/sho-room/worker/index.ts(スタッフサイト)
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const { pathname } = new URL(request.url);
// /api/* は Gateway Worker へ転送する
if (pathname.startsWith('/api/')) return handleGatewayProxy(request, env);
// 静的アセット / SPA フォールバック
// (run_worker_first の対象外なので通常ここには到達しない)
return env.ASSETS.fetch(request);
},
};
// packages/web/sho-room/worker/proxies.ts
export async function handleGatewayProxy(
request: Request,
env: Env,
): Promise<Response> {
// Cookie から staff JWT を取り出す
const staffJwt =
(request.headers.get('Cookie') ?? '').match(
/soshosai_staff_jwt=([^;]+)/,
)?.[1] ?? null;
if (!staffJwt) {
// BFFはログインページへリダイレクトしない。401を返し、
// ログインへの遷移はフロントエンド側に委ねる(後述)
return new Response(
JSON.stringify({ error: 'Unauthorized', message: '認証が必要です' }),
{ status: 401, headers: { 'Content-Type': 'application/json' } },
);
}
// ヘッダーを作り直す(Cookieは転送せず、Authorization に載せ替える)
const headers = new Headers();
headers.set('Authorization', `Bearer ${staffJwt}`);
const contentType = request.headers.get('Content-Type');
if (contentType) headers.set('Content-Type', contentType);
// Service Binding経由でGateway Workerを呼び出し
// /api/<rest> → https://gateway/<rest>
const url = new URL(request.url);
const restPath = url.pathname.replace(/^\/api\//, '');
const hasBody = request.method !== 'GET' && request.method !== 'HEAD';
return env.GATEWAY.fetch(`https://gateway/${restPath}${url.search}`, {
method: request.method,
headers,
body: hasBody ? request.body : undefined,
});
}
注: 上記は認証の流れを示すために要点だけを抜き出したもの。実際の
worker/index.tsには、BFF集約エンドポイント(/api/bff/*)やログイン不要な経路(/api/shifts/public/*)の振り分けも含まれる。正は実装を参照すること。
未認証時の扱い: BFFは /login のようなページへリダイレクトしない。スタッフサイトにそのようなルートは存在せず、BFFは上記のとおり401を返す。ログインへの遷移はフロントエンド側の責務で、@soshosai/auth-client の login() が login-api のOAuthエンドポイント(/discord-login)へ遷移させる。Sho-Roomでは ProtectedRoute がこれを担っている。
メリット:
- 低レイテンシ: ネットワークを経由しないWorker間直接通信
- シンプル: APIは
Authorizationヘッダーのみを確認 - セキュア: JWT混同問題が根本的に解決、内部通信なので外部からアクセス不可
- 明確: 各サイトの役割が明確
重要: APIは Cookieを一切確認しない。 BFFがService Binding経由でWorkerを直接呼び出し、適切なJWTのみを転送する。
guest-app の例外: gateway-worker が Cookie 境界を兼ねる(#419)
app.soshosai.com(来場者アプリ)は BFF Worker を持たない Static Assets 専用の SPA で、
API へはフロントから直接 gateway-worker を呼ぶ。そのため上表の「BFFが選択するCookie」に
相当する変換を gateway-worker が行う。
Authorizationヘッダーが既にある場合は何もしない(sho-room の BFF 経路を壊さない)Authorizationが無い場合のみsoshosai_customer_jwtをAuthorization: Bearerに変換するsoshosai_staff_jwtは読まない(JWT 混同問題の再導入になるため)
app.soshosai.com → v3.api.sys.soshosai.com は同一サイトのため、Domain=.soshosai.com; SameSite=Lax の Cookie が credentials: 'include' で届く。下流 Worker は従来どおり
Authorization: Bearer のみを確認するため、「APIはCookieを確認しない」原則は維持される。
CSRF 対策: SameSite=Lax はクロスサイトを遮断するが、*.soshosai.com の全サブドメイン
(プレビューデプロイを含む)は same-site 扱いで Cookie が届く。そのため Cookie で認証する
リクエストは、Origin が ENV_CONFIG[env].allowedOrigins に完全一致することを要求する。
ただし Origin が存在しない場合は許可する(ローカル開発の vite proxy 経由の同一オリジン
GET には Origin が付かないため)。
2. コンポーネント詳細
2.1 login-api
ドメイン: loginapi.soshosai.com
役割: 認証フロントエンド。OAuthフローの開始・コールバック処理・Cookie設定を担当。
主要エンドポイント:
| Method | Path | 説明 |
|---|---|---|
| GET | /discord-login | Discord OAuth2認証開始 |
| GET | /auth/discord-callback | Discord OAuthコールバック |
| GET | /line-login | LINE OAuth2認証開始 |
| GET | /login-callback | LINE OAuthコールバック |
| POST | /api/liff-login | LIFF IDトークンによるゲストログイン |
| GET | /api/me | 現在のユーザー情報取得(残存7日未満でセッション自動更新) |
| GET | /api/token | JWT取得(Authorization header用) |
| POST | /api/logout | ログアウト |
Service Binding:
[[services]]
binding = "DISCORD_AUTH_SERVICE"
service = "discord-auth-worker"
[[services]]
binding = "LINE_AUTH_SERVICE"
service = "line-auth-worker"
[[services]]
binding = "UNIFIED_AUTH_SERVICE"
service = "unified-auth-worker"
2.2 discord-auth-worker
役割: Discord OAuth2認証・ロール検証・スタッフ権限計算
主要エンドポイント:
| Method | Path | 説明 |
|---|---|---|
| POST | /staff/authenticate-discord | スタッフ認証(Discord ID直接) |
| POST | /token/verify | JWTトークン検証・BANチェック |
| GET | /internal/discord/members/:discordUserId | Discordメンバー情報取得 |
| GET | /internal/discord/guild/roles | ギルドロール一覧取得 |
| GET | /authority/info/:discordId | 権限情報取得 |
| POST | /authority/refresh/:discordId | 権限更新 |
認証フロー:
- Discord User IDを受け取り
- BANチェック(BannedUsersテーブル)
- Discord APIでギルドメンバー情報取得
- 必須ロール(
REQUIRED_ROLE_ID)の確認 - ロールから権限レベル計算(admin/staff)
- Staffsテーブルにユーザー作成/更新
- JWT発行(3日間有効)
JWT Payload(スタッフ):
interface StaffJwtPayload {
sub: string; // Discord User ID
userType: 'staff';
/**
* permission bits(整数)。各ビットが個別の権限を表す。
* Discordロールから計算され、ORで合成される。
* 例: BASE_STAFF_PERMISSIONS = 5(TRACKABLE_LINKS_VIEW | TRACKABLE_LINKS_ANALYTICS)
*/
permissions?: number;
discordUser?: {
id: string;
username: string;
global_name?: string;
avatar?: string;
};
metadata?: {
authoritySource: 'discord_roles' | 'database' | 'fallback';
departments: string[];
lastUpdated: string;
};
exp: number;
iat: number;
}
権限ビット一覧:
| 定数名 | ビット値 | 説明 |
|---|---|---|
TRACKABLE_LINKS_VIEW | 1 | リンク・QRコード閲覧 |
TRACKABLE_LINKS_EDIT | 2 | リンク・QRコード作成・編集 |
TRACKABLE_LINKS_ANALYTICS | 4 | アクセス統計閲覧 |
TRACKABLE_LINKS_DELETE | 8 | QRコード(全件)・プロジェクト削除 |
STAFF_VIEW | 16 | スタッフ一覧閲覧 |
STAFF_MANAGE | 32 | スタッフ編集・BAN |
SYSTEM_MANAGE | 64 | システム設定・全体管理 |
EQUIPMENT_VIEW | 128 | 備品一覧・詳細閲覧 |
EQUIPMENT_EDIT | 256 | 備品登録・編集 |
EQUIPMENT_DELETE | 512 | 備品削除 |
EQUIPMENT_INTEGRATION | 1024 | 備品連携(import/export) |
EQUIPMENT_RESET | 2048 | 備品リセット |
EQUIPMENT_MANAGE_ORGANIZATION | 4096 | 団体管理 |
EQUIPMENT_MANAGE_LOCATION | 8192 | 場所管理 |
EQUIPMENT_MANAGE_TYPE | 16384 | 備品種別管理 |
EQUIPMENT_BULK_OPERATION | 32768 | 備品一括操作 |
FORM_VIEW | 65536 | フォーム一覧閲覧 |
FORM_SUBMISSION_APPROVE | 131072 | フォーム回答承認/却下 |
EVENT_VIEW | 262144 | イベント一覧閲覧 |
EVENT_EDIT | 524288 | イベント作成・編集・削除 |
複数の権限はビットORで合成される。ALL_PERMISSIONS(全ビットOR)を持つユーザーはすべての操作が可能。
BASE_STAFF_PERMISSIONS = TRACKABLE_LINKS_VIEW | TRACKABLE_LINKS_ANALYTICS = 5(全スタッフの最低権限)。
注:
TRACKABLE_LINKS_EDITを持つユーザーは自身が作成したQRコード(creator_id一致)のみ削除可。TRACKABLE_LINKS_DELETEを持つユーザーは全QRコード・全プロジェクトの削除が可能。
2.3 line-auth-worker
役割: LINE OAuth2認証・ゲスト登録
主要エンドポイント:
| Method | Path | 説明 |
|---|---|---|
| POST | /guest/authenticate | ゲスト認証(LINE ID直接、Service Binding用) |
認証フロー:
- LINE User IDまたは認証コードを受け取り
- 認証コードの場合、LINE APIでアクセストークン取得
- LINEプロフィール取得
- BANチェック
- Guestsテーブルにユーザー作成(新規の場合はGroupも作成)
- JWT発行(30日間有効。TBD-003)
JWT Payload(ゲスト):
interface GuestJwtPayload {
sub: string; // Guest ID (guest-xxxxxxx)
userType: 'guest';
lineUserId: string;
groupId: string;
exp: number;
iat: number;
}
2.4 unified-auth-worker
役割: JWT検証・BAN管理・レート制限
主要エンドポイント:
| Method | Path | 説明 |
|---|---|---|
| POST | /token/verify | トークン検証 |
| POST | /admin/ban | ユーザーBAN |
| POST | /admin/ban-user-global | グローバルBAN |
| POST | /admin/ban-user-by-type | タイプ別BAN |
| POST | /admin/unban | BAN解除 |
| POST | /admin/unban-advanced | BAN ID指定解除 |
| GET | /admin/user-info/:discordUserId | ユーザー情報取得 |
| GET | /admin/bans | BAN一覧取得 |
| POST | /rate-limit/login-api/check | レートリミットチェック |
| POST | /rate-limit/login-api/record-violation | 違反記録 |
トークン検証フロー:
Authorization: Bearer xxxヘッダーからトークン取得- JWT署名検証(HMAC-SHA256)
- 有効期限チェック
- BANチェック(BannedUsersテーブル)
- 検証結果を返却
3. 認証フロー詳細
3.1 スタッフ認証フロー(Discord)
3.2 ゲスト認証フロー(LINE)
4. セキュリティ仕様
4.1 Cookie設定
CookieはDomain=.soshosai.comで設定され、全サブドメインで共有される。
スタッフJWT Cookie:
Set-Cookie: soshosai_staff_jwt=eyJhbGc...;
Domain=.soshosai.com; ← 全サブドメインで共有
Path=/;
HttpOnly;
Secure;
SameSite=Lax;
Max-Age=259200 (3日間)
ゲストJWT Cookie:
Set-Cookie: soshosai_customer_jwt=eyJhbGc...;
Domain=.soshosai.com; ← 全サブドメインで共有
Path=/;
HttpOnly;
Secure;
SameSite=Lax;
Max-Age=172800 (2日間)
Cookie共有の仕組み:
| 設定 | 共有範囲 |
|---|---|
Domain=.soshosai.com | 全サブドメイン(room, fwd, docs, apf, app等) |
Domain=room.soshosai.com | room.soshosai.comのみ |
注意: スタッフCookie(
soshosai_staff_jwt)とゲストCookie(soshosai_customer_jwt)は別名のため混同しない。各サービスは必要なCookieのみを使用する。
4.2 JWT仕様
- アルゴリズム: HS256 (HMAC-SHA256)
- シークレット: 環境変数
JWT_SECRET(Secrets Store経由) - 有効期限: スタッフ3日、ゲスト2日
4.3 レート制限
| 種別 | 制限 | 対象 |
|---|---|---|
| auth | 厳格 | /login, /auth, /callback |
| read | 緩和 | GET/HEAD/OPTIONS |
| write | 標準 | POST/PUT/PATCH/DELETE |
| api_me | 非常に緩和 | /api/me |
4.4 Service Binding
auth-workersは直接公開せず、Service Binding経由でのみアクセス可能。
login-api → discord-auth-worker (DISCORD_AUTH_SERVICE)
login-api → line-auth-worker (LINE_AUTH_SERVICE)
login-api → unified-auth-worker (UNIFIED_AUTH_SERVICE)
API → unified-auth-worker (JWT検証)
5. BAN管理
5.1 BANタイプ
| タイプ | 説明 |
|---|---|
staff | スタッフとしてのBAN |
guest | ゲストとしてのBAN |
global | 全サービスからのBAN |
5.2 BANテーブル
CREATE TABLE BannedUsers (
ban_id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
user_type TEXT NOT NULL, -- 'staff', 'guest', 'global'
reason TEXT,
banned_by TEXT NOT NULL,
banned_at TEXT NOT NULL,
expires_at TEXT -- NULL = 永久BAN
);
5.3 BAN判定タイミング
- ログイン時: discord-auth-worker / line-auth-worker でチェック
- API呼び出し時: unified-auth-worker の
/token/verifyでチェック
6. 環境変数
6.1 login-api
| 変数名 | 説明 |
|---|---|
JWT_SECRET | JWT署名シークレット |
DISCORD_CLIENT_ID | Discord OAuth Client ID |
DISCORD_CLIENT_SECRET | Discord OAuth Client Secret |
CHANNEL_ID | LINE Channel ID |
CHANNEL_SECRET | LINE Channel Secret |
LOGIN_API_URL | 開発環境URL(設定時は開発モード) |
6.2 discord-auth-worker
| 変数名 | 説明 |
|---|---|
JWT_SECRET | JWT署名シークレット(Secrets Store) |
AUTH_DISCORD_TOKEN | Discord Bot Token(Secrets Store) |
REQUIRED_GUILD_ID | 必須ギルドID(Secrets Store) |
REQUIRED_ROLE_ID | スタッフ必須ロールID(Secrets Store) |
ADMIN_ROLE_ID | 管理者ロールID |
DB | D1 Database Binding |
6.3 line-auth-worker
| 変数名 | 説明 |
|---|---|
JWT_SECRET | JWT署名シークレット(Secrets Store) |
LINE_CLIENT_ID | LINE Channel ID |
LINE_CLIENT_SECRET | LINE Channel Secret |
LINE_REDIRECT_URI | コールバックURL |
DB | D1 Database Binding |
6.4 unified-auth-worker
| 変数名 | 説明 |
|---|---|
JWT_SECRET | JWT署名シークレット(Secrets Store) |
RATE_LIMITER | レートリミッターBinding |
DB | D1 Database Binding |
7. 開発・デプロイ
7.1 ローカル開発
# login-api
cd packages/api/login-api
wrangler dev
# auth-workers(個別)
cd packages/api/auth-workers/discord-auth-worker
wrangler dev
cd packages/api/auth-workers/line-auth-worker
wrangler dev
cd packages/api/auth-workers/unified-auth-worker
wrangler dev
7.2 デプロイ順序
Service Bindingの依存関係上、以下の順序でデプロイ:
- unified-auth-worker(依存なし)
- discord-auth-worker(依存なし)
- line-auth-worker(依存なし)
- login-api(上記3つに依存)
# 一括デプロイスクリプト例
cd packages/api/auth-workers/unified-auth-worker && wrangler deploy
cd packages/api/auth-workers/discord-auth-worker && wrangler deploy
cd packages/api/auth-workers/line-auth-worker && wrangler deploy
cd packages/api/login-api && wrangler deploy