メインコンテンツまでスキップ

認証システム仕様書

1. システム概要​

1.1 目的​

本システムは、蒼翔祭(学祭)関連サービス全体で使用される統合認証基盤である。

  • スタッフ認証: Discord OAuth2によるログイン
  • ゲスト認証: LINE OAuth2によるログイン
  • JWT管理: トークン発行・検証・BAN管理

1.2 アーキテクチャ構成​

1.3 JWT利用サービス一覧​

スタッフJWT(soshosai_staff_jwt):

ドメインサービス名用途
room.soshosai.comSho-Room学祭委員管理画面
fwd.soshosai.comtrackable-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.comsoshosai_staff_jwtスタッフ専用
fwd.soshosai.comsoshosai_staff_jwtスタッフ専用
docs.soshosai.comsoshosai_staff_jwtスタッフ専用
apf.soshosai.comsoshosai_staff_jwtスタッフ専用
app.soshosai.comsoshosai_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のみを転送する。

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設定を担当。

主要エンドポイント:

MethodPath説明
GET/discord-loginDiscord OAuth2認証開始
GET/auth/discord-callbackDiscord OAuthコールバック
GET/line-loginLINE OAuth2認証開始
GET/login-callbackLINE OAuthコールバック
POST/api/liff-loginLIFF IDトークンによるゲストログイン
GET/api/me現在のユーザー情報取得(残存7日未満でセッション自動更新)
GET/api/tokenJWT取得(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認証・ロール検証・スタッフ権限計算

主要エンドポイント:

MethodPath説明
POST/staff/authenticate-discordスタッフ認証(Discord ID直接)
POST/token/verifyJWTトークン検証・BANチェック
GET/internal/discord/members/:discordUserIdDiscordメンバー情報取得
GET/internal/discord/guild/rolesギルドロール一覧取得
GET/authority/info/:discordId権限情報取得
POST/authority/refresh/:discordId権限更新

認証フロー:

  1. Discord User IDを受け取り
  2. BANチェック(BannedUsersテーブル)
  3. Discord APIでギルドメンバー情報取得
  4. 必須ロール(REQUIRED_ROLE_ID)の確認
  5. ロールから権限レベル計算(admin/staff)
  6. Staffsテーブルにユーザー作成/更新
  7. 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_VIEW1リンク・QRコード閲覧
TRACKABLE_LINKS_EDIT2リンク・QRコード作成・編集
TRACKABLE_LINKS_ANALYTICS4アクセス統計閲覧
TRACKABLE_LINKS_DELETE8QRコード(全件)・プロジェクト削除
STAFF_VIEW16スタッフ一覧閲覧
STAFF_MANAGE32スタッフ編集・BAN
SYSTEM_MANAGE64システム設定・全体管理
EQUIPMENT_VIEW128備品一覧・詳細閲覧
EQUIPMENT_EDIT256備品登録・編集
EQUIPMENT_DELETE512備品削除
EQUIPMENT_INTEGRATION1024備品連携(import/export)
EQUIPMENT_RESET2048備品リセット
EQUIPMENT_MANAGE_ORGANIZATION4096団体管理
EQUIPMENT_MANAGE_LOCATION8192場所管理
EQUIPMENT_MANAGE_TYPE16384備品種別管理
EQUIPMENT_BULK_OPERATION32768備品一括操作
FORM_VIEW65536フォーム一覧閲覧
FORM_SUBMISSION_APPROVE131072フォーム回答承認/却下
EVENT_VIEW262144イベント一覧閲覧
EVENT_EDIT524288イベント作成・編集・削除

複数の権限はビット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認証・ゲスト登録

主要エンドポイント:

MethodPath説明
POST/guest/authenticateゲスト認証(LINE ID直接、Service Binding用)

認証フロー:

  1. LINE User IDまたは認証コードを受け取り
  2. 認証コードの場合、LINE APIでアクセストークン取得
  3. LINEプロフィール取得
  4. BANチェック
  5. Guestsテーブルにユーザー作成(新規の場合はGroupも作成)
  6. 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管理・レート制限

主要エンドポイント:

MethodPath説明
POST/token/verifyトークン検証
POST/admin/banユーザーBAN
POST/admin/ban-user-globalグローバルBAN
POST/admin/ban-user-by-typeタイプ別BAN
POST/admin/unbanBAN解除
POST/admin/unban-advancedBAN ID指定解除
GET/admin/user-info/:discordUserIdユーザー情報取得
GET/admin/bansBAN一覧取得
POST/rate-limit/login-api/checkレートリミットチェック
POST/rate-limit/login-api/record-violation違反記録

トークン検証フロー:

  1. Authorization: Bearer xxx ヘッダーからトークン取得
  2. JWT署名検証(HMAC-SHA256)
  3. 有効期限チェック
  4. BANチェック(BannedUsersテーブル)
  5. 検証結果を返却

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.comroom.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判定タイミング​

  1. ログイン時: discord-auth-worker / line-auth-worker でチェック
  2. API呼び出し時: unified-auth-worker の /token/verify でチェック

6. 環境変数​

6.1 login-api​

変数名説明
JWT_SECRETJWT署名シークレット
DISCORD_CLIENT_IDDiscord OAuth Client ID
DISCORD_CLIENT_SECRETDiscord OAuth Client Secret
CHANNEL_IDLINE Channel ID
CHANNEL_SECRETLINE Channel Secret
LOGIN_API_URL開発環境URL(設定時は開発モード)

6.2 discord-auth-worker​

変数名説明
JWT_SECRETJWT署名シークレット(Secrets Store)
AUTH_DISCORD_TOKENDiscord Bot Token(Secrets Store)
REQUIRED_GUILD_ID必須ギルドID(Secrets Store)
REQUIRED_ROLE_IDスタッフ必須ロールID(Secrets Store)
ADMIN_ROLE_ID管理者ロールID
DBD1 Database Binding

6.3 line-auth-worker​

変数名説明
JWT_SECRETJWT署名シークレット(Secrets Store)
LINE_CLIENT_IDLINE Channel ID
LINE_CLIENT_SECRETLINE Channel Secret
LINE_REDIRECT_URIコールバックURL
DBD1 Database Binding

6.4 unified-auth-worker​

変数名説明
JWT_SECRETJWT署名シークレット(Secrets Store)
RATE_LIMITERレートリミッターBinding
DBD1 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の依存関係上、以下の順序でデプロイ:

  1. unified-auth-worker(依存なし)
  2. discord-auth-worker(依存なし)
  3. line-auth-worker(依存なし)
  4. 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