認証とアクセス制御

アプリがユーザー認証を行い、ロールでアクションを制御する方法。

このモノレポでは認証は中央集約されています。アプリ自身はBetter Authを組み込んだり、パスワードを保存したりしません。共有Auth UIがサインインを処理し、アプリはミドルウェア経由でセッションを確認するだけです。ロールベースの制限(例: 「マネージャーだけがレシピを編集できる」)は共有データベーステーブルを使って上に重ねます。

アーキテクチャ

3つのピースが連携します:

ピース場所役割
Auth APIshared/auth/auth-api/Better Auth + Postgres + Resend(マジックリンク)。システムサービス。編集しない。
Auth UIshared/auth/auth-ui/サインインページ、管理コンソール、Auth APIへのプロキシ。
あなたのアプリapps/<your-app>/ミドルウェア経由でAuth UIを呼び出すだけ。Better Authコードは不要。

templates/web/テンプレートから作成されたアプリには、認証対応済みのmiddleware.tsがすでに含まれています。通常は触る必要がありません。

セッションチェックの流れ

テンプレートのミドルウェアはリクエストごとに:

  1. ブラウザのcookieを転送しつつGET ${AUTH_UI_URL}/api/auth/get-sessionを呼び出す。
  2. 未認証でAUTH_REQUIRED=trueなら、${AUTH_UI_URL}/?returnTo=<current-url>へリダイレクト。
  3. サインイン後、Auth UIがユーザーを戻し、.up.railway.appにセッションcookieが設定されて永続化される。

環境変数

変数設定場所目的
AUTH_UI_URLデプロイ時に自動注入Auth UIサービスのベースURL。
AUTH_REQUIRED環境ごとの設定本番/ステージングはデフォルトtrue、必要に応じて環境ごとに上書き。

AUTH_UI_URLを自分で設定する必要はありません — デプロイワークフローが非認証サービスすべてに自動注入します。詳しくは環境変数を参照。

プレビューでの認証テスト

  1. PRを開き、プレビューURLが投稿されるコメントを待つ。
  2. プレビューURLにアクセス → ステージングのAuth UIへリダイレクトされる。
  3. サインイン(マジックリンク)。
  4. プレビューアプリに戻され、セッションcookieが設定される。

カスタムドメインは不要です — プレビューはすべて*.up.railway.appで動きます。

ロールベースのアクセス制御(きめ細かい制御)

「ロールXだけがアクションYを実行できる」(例: マネージャーだけがレシピを作成・編集できる)が必要なときは、セッションの上に権限チェックを重ねます。

ロールは共有のauthテーブルに入っていて、アプリ自身がロールenumを定義することはありません:

テーブル目的
allowed_userサインインできるユーザー。roleadmin/user)とオプションのrole_idを持つ。
auth_role"manager", "chef"のような名前付きロール。管理者が定義。
auth_role_appどのロールがどのアプリを開けるか。

権限ヘルパー

メールアドレスで検索してbooleanを返すヘルパーをapp/_lib/auth.tsに1つ置きます:

app/_lib/auth.tstypescript
import { getDb } from '@/app/_lib/db';

export async function checkIsManager(email: string): Promise<boolean> {
const db = getDb();
const result = await db.query<{ base_role: string; role_name: string | null }>(
  `SELECT au.role AS base_role, ar.name AS role_name
   FROM allowed_user au
   LEFT JOIN auth_role ar ON au.role_id = ar.id
   WHERE au.email = $1`,
  [email]
);
if (!result.rowCount) return false;
const row = result.rows[0];
return row.base_role === 'admin' || row.role_name === 'manager';
}

フラグの使い方

  • Server Component: const isManager = await checkIsManager(session.user.email)でフラグを取得し、クライアントコンポーネントにpropsで渡す — ブラウザ側でチェックを再実装しない。
  • API route(POST/PUT/DELETE): ロールがない場合は403 Forbiddenを返す。
  • UI: 条件付きレンダリング {isManager && <Link href="/recipes/new">新しいレシピ</Link>}

リファレンスアプリ

apps/nani-cooking/がエンドツーエンドで実装しています。コピーして参考にできるファイル:

  • app/_lib/auth.tscheckIsManagerヘルパー
  • app/api/recipes/route.ts — マネージャー限定API
  • app/recipes/new/page.tsx — マネージャー限定ページ
  • components/RecipeCard.tsxisManagerをpropsで受け取る

Claudeに聞く

アクションにロール制限を追加
Claudeプロンプト
meal-plannerアプリで、「manager」ロールを持つユーザーだけがレシピを作成・削除できるようにしてください。app/_lib/auth.tsに権限ヘルパーを追加、APIルートを保護、マネージャーでない場合はUIの「新しいレシピ」ボタンを非表示にしてください。nani-cookingのパターンに従ってください。

クイズ

Quiz

`manager`や`chef`のようなカスタムロール名はどこに存在しますか?