認証ガイド
概要
SwallowKit の Authentication(認証) 機能は、プロジェクトに完全な認証基盤を追加します — ユーザーログイン、JWT トークン管理、ロールベース認可、React 認証コンテキスト — すべてが設定から単一の CLI コマンドで生成されます。
現時点の実装状況は次のとおりです:
| モード | 説明 | ステータス |
|---|---|---|
custom-jwt | 外部 RDB ユーザーデータベース + JWT トークン | ✅ 利用可能(v1) |
swa | Static Web Apps 組み込み認証 | 🔜 計画中 |
swa-custom | ハイブリッド(SWA 認証 + カスタム拡張) | 🔜 計画中 |
現在の挙動: 現在実装されているのは
custom-jwtのみです。CLI には将来向けの provider 名も残っていますが、現行の生成結果はcustom-jwtベースです。
💡 ポイント: custom-jwt は JWT(セッションではなく)を使用します。これは Azure Functions がステートレスであることが原則だからです。BFF レイヤーが Cookie 管理(トランスポート層の責務)を担い、Functions がトークンの発行と検証(セキュリティの責務)を担います。
⚠️ SWA ルートルール(staticwebapp.config.json の allowedRoles)は SWA 組み込み認証プロバイダーでのみ機能し、custom-jwt では効きません。
アーキテクチャ
ログインフロー(custom-jwt)
ブラウザ
│
├─ POST /api/auth/login ──→ BFF(Next.js API Route)
│ │
│ └──→ Azure Functions(auth-login)
│ │
│ ├─ RDB ユーザーテーブルを検索
│ ├─ パスワード検証(bcrypt)
│ └─ JWT 生成
│ │
│ ◄──────────────┘
│ httpOnly Cookie を設定
◄──────────────────────────────┘認証済みリクエストフロー
ブラウザ(Cookie 付き)
│
├──→ Next.js Middleware
│ │
│ ├─ Cookie の存在確認
│ ├─ Base64 デコード → 有効期限チェックのみ(暗号化処理なし)
│ ├─ 期限切れ/未設定 → /login にリダイレクト
│ └─ Authorization ヘッダーをリクエストに追加
│ │
│ ▼
│ BFF(Next.js API Route)
│ │
│ ▼
│ Azure Functions
│ │
│ ├─ 完全な JWT 署名検証
│ ├─ ロールベースアクセスチェック(authPolicy)
│ └─ ビジネスロジックの実行
│ │
◄──────────────┘💡 Defense in Depth(多層防御): Middleware は軽量な base64 有効期限チェックのみを行います。これは Edge Runtime にネイティブの暗号化 API がないためです。完全な JWT 署名検証は Azure Functions で実行されます。
はじめに
1. ユーザーデータベース用の Connector を追加
ユーザーデータベースは RDB Connector として登録する必要があります。既に設定済みの場合、このステップはスキップしてください。
# npx
npx swallowkit add-connector userdb --type rdb --provider postgres
# pnpm
pnpm dlx swallowkit add-connector userdb --type rdb --provider postgresこれにより swallowkit.config.js に Connector エントリが追加されます。詳しくは Connector ガイド をご覧ください。
2. swallowkit.config.js で認証を設定
ユーザーデータベースの Connector を指す auth セクションを追加します:
// swallowkit.config.js
module.exports = {
auth: {
provider: 'custom-jwt',
customJwt: {
userConnector: 'userdb',
userTable: 'users',
loginIdColumn: 'login_id',
passwordHashColumn: 'password_hash',
rolesColumn: 'roles',
jwtSecretEnv: 'JWT_SECRET',
tokenExpiry: '24h',
},
authorization: {
defaultPolicy: 'authenticated',
policies: {
'estimate': { roles: ['admin', 'estimator'] },
'team': { roles: ['admin'] },
},
},
},
connectors: {
userdb: {
type: 'rdb',
provider: 'postgres',
connectionEnvVar: 'USERDB_CONNECTION_STRING',
},
},
};3. add-auth を実行
すべての認証基盤ファイルを生成します:
# npx
npx swallowkit add-auth
# pnpm
pnpm dlx swallowkit add-authこれにより login/logout/me エンドポイント、BFF ルート、ミドルウェア、ログインページ、React 認証コンテキストが作成されます。生成されるファイルの一覧は生成されるファイルをご覧ください。
4. モデルに authPolicy を追加
ロールベースアクセスが必要なモデルに authPolicy をエクスポートします:
// shared/models/estimate.ts
export const authPolicy = { roles: ['admin', 'estimator'] };5. 認証ポリシー付きモデルを再 Scaffold
# npx
npx swallowkit scaffold shared/models/estimate.ts
# pnpm
pnpm dlx swallowkit scaffold shared/models/estimate.tsScaffold は authPolicy エクスポートを検出し、生成される Functions にロールガードを注入します。詳しくは Scaffold 連携をご覧ください。
6. モックコネクタで開発サーバーを起動
# npx
npx swallowkit dev --mock-connectors --seed-env local
# pnpm
pnpm dlx swallowkit dev --mock-connectors --seed-env local--mock-connectors はすべての RDB コネクタデータをインメモリでモック化します — auth.customJwt.userTable で参照されるユーザーテーブルも含まれます。つまり、実際のデータベースなしでもモックのユーザーデータに対してログインが動作します。他のコネクタモデルと同様に、dev-seeds/<env>/user.json でユーザーのシードデータを定義してください:
[
{
"id": "1",
"login_id": "admin",
"password_hash": "password123",
"name": "管理者",
"email": "admin@example.com",
"roles": ["admin", "estimator"]
}
]フィールド名は auth.customJwt で設定されたカラム名(loginIdColumn、passwordHashColumn、rolesColumn)と一致する必要があります。
⚠️ シードファイルのパスワードはプレーンテキストです — これらのファイルはローカル開発専用です。実際の認証情報をコミットしないでください。
設定リファレンス
auth.provider
| プロパティ | 型 | 必須 | 説明 |
|---|---|---|---|
provider | 'custom-jwt' | ✅ | v1 で現在実装されている認証プロバイダーモード |
auth.customJwt
| プロパティ | 型 | 必須 | 説明 |
|---|---|---|---|
userConnector | string | ✅ | connectors セクションの Connector 名(RDB Connector である必要あり) |
userTable | string | ✅ | ユーザーレコードを格納するデータベーステーブル |
loginIdColumn | string | ✅ | ログイン識別子として使用するカラム(例:ユーザー名、メールアドレス) |
passwordHashColumn | string | ✅ | bcrypt ハッシュ化されたパスワードを格納するカラム |
rolesColumn | string | ✅ | ユーザーロールを格納するカラム(JSON 配列またはカンマ区切り文字列) |
jwtSecretEnv | string | ✅ | JWT 署名シークレットを保持する環境変数名 |
tokenExpiry | string | ✅ | トークンの有効期限(例:'1h'、'24h'、'7d') |
auth.authorization
| プロパティ | 型 | 必須 | 説明 |
|---|---|---|---|
defaultPolicy | 'authenticated' | 'public' | ❌ | 明示的な authPolicy のないモデルに適用されるデフォルトアクセスポリシー。デフォルト値は 'authenticated' |
policies | Record<string, { roles: string[] }> | ❌ | モデル名から必要なロールへの名前付きポリシーマッピング |
authorization: {
defaultPolicy: 'authenticated',
policies: {
'estimate': { roles: ['admin', 'estimator'] },
'team': { roles: ['admin'] },
},
},💡 ヒント: defaultPolicy: 'authenticated' は、特定のポリシーを持たないモデルにログイン済みの任意のユーザーがアクセスできることを意味します。ほとんどのエンドポイントが未認証の場合にのみ 'public' に設定してください。
モデル認証ポリシー
モデルは authPolicy をエクスポートすることで、モデルレベルでのロールベースアクセス制御を設定できます。scaffold がこのエクスポートを検出すると、生成される Functions にロールガードが注入されます。
基本的な使い方
モデルのすべてのオペレーションに特定のロールを要求します:
// shared/models/estimate.ts
import { z } from 'zod/v4';
export const Estimate = z.object({
id: z.string(),
title: z.string(),
amount: z.number(),
});
export type Estimate = z.infer<typeof Estimate>;
export const displayName = 'Estimate';
export const authPolicy = { roles: ['admin', 'estimator'] };読み取り・書き込みの分離
読み取りと書き込みで異なるロールを適用します:
// shared/models/report.ts
export const authPolicy = {
read: ['admin', 'estimator', 'viewer'],
write: ['admin'],
};| 形式 | 読み取りオペレーション(GET) | 書き込みオペレーション(POST/PUT/DELETE) |
|---|---|---|
{ roles: [...] } | 一覧に含まれるすべてのロール | 一覧に含まれるすべてのロール |
{ read: [...], write: [...] } | read のロールのみ | write のロールのみ |
設定ポリシーとの関係
認証ポリシーは 2 つの場所で定義できます:
- モデルファイル内 —
export const authPolicy = { ... } - swallowkit.config.js 内 —
auth.authorization.policies
同一モデルに両方が存在する場合、モデルレベルのエクスポートが優先されます。設定レベルのポリシーは、一元的な概要の確認や、直接変更したくないモデルに対して有用です。
生成されるファイル
add-auth コマンドは以下のファイルを生成します:
| ファイル | 説明 |
|---|---|
shared/models/auth.ts | LoginRequest、AuthUser、LoginResponse の Zod スキーマ |
app/api/auth/login/route.ts | BFF ルート — 認証情報を Functions に転送し、成功時に httpOnly Cookie を設定 |
app/api/auth/logout/route.ts | BFF ルート — 認証 Cookie をクリア |
app/api/auth/me/route.ts | BFF ルート — JWT から現在のユーザー情報を返却 |
proxy.ts | Next.js プロキシミドルウェア — Cookie チェック、有効期限検証、Authorization ヘッダー追加、未認証時は /login にリダイレクト |
app/login/page.tsx | フォーム UI 付きログインページ |
lib/auth/auth-context.tsx | React コンテキストプロバイダーと useAuth フック |
バックエンド言語別ファイル
Functions ファイルはバックエンド言語によって異なります:
| バックエンド | 認証エンドポイント | JWT ヘルパー |
|---|---|---|
| TypeScript | functions/src/auth.ts | functions/src/auth/jwt-helper.ts |
| C# | functions/Auth/AuthFunctions.cs | functions/Auth/JwtHelper.cs |
| Python | functions/auth_functions.py | functions/auth/jwt_helper.py |
変更されるファイル
| ファイル | 変更内容 |
|---|---|
lib/api/call-function.ts | BFF ルートから Azure Functions への Authorization ヘッダー転送を追加 |
Scaffold 連携
scaffold がモデルを処理する際、authPolicy(モデルファイルからエクスポートされたもの、または auth.authorization.policies で定義されたもの)が存在すると、以下が自動的に行われます:
- ロールガードを注入 — 生成される Azure Functions コード(バックエンド)
- ロール対応 UI を生成 — 書き込みアクションの条件付きレンダリング(フロントエンド)
- 認証対応
callFunctionを選択 — Middleware から Functions への Authorization ヘッダー転送
バックエンドガード
authPolicy = { roles: ['admin', 'estimator'] } を持つモデルの場合:
- すべての生成エンドポイントにビジネスロジック実行前の JWT 検証とロールチェックが含まれる
- 認可されていないリクエストには
403 Forbiddenレスポンスが返される
読み取り・書き込み分離のモデルの場合:
- GET エンドポイントは
readロールに対してチェックされる - POST / PUT / DELETE エンドポイントは
writeロールに対してチェックされる
Cosmos DB モデルとコネクタ(RDB / API)モデルの両方に適用されます。
フロントエンドロール制御
認証が設定されており、モデルの authPolicy に write ロールがある場合、scaffold はロール対応のレンダリングを含む UI ページを生成します:
| ページ | 動作 |
|---|---|
| 一覧ページ | 「Create New」ボタンと「Edit」/「Delete」アクションは、write ロールを持たないユーザーには非表示 |
| 詳細ページ | 「Edit」と「Delete」ボタンは、write ロールを持たないユーザーには非表示 |
| 新規作成・編集ページ | write ロールがない場合、一覧ページにリダイレクト |
生成コードは認証コンテキストの useAuth() フックと hasAnyRole() を使用します:
// scaffold が生成するコード(一覧ページの例)
const { hasAnyRole } = useAuth();
const canWrite = hasAnyRole(["admin"]);
// canWrite が true の場合のみ "Create New" ボタンをレンダリング
{canWrite && <Link href="/employee/new">Create New</Link>}💡 注意: フロントエンドのロールチェックは UX の利便性であり、セキュリティ境界ではありません。実際の認可は Azure Functions レイヤーで行われます。ユーザーが UI をバイパスしても、バックエンドは 401/403 で不正なリクエストを拒否します。
--mock-connectors 時の認証制御
--mock-connectors で実行する場合、モックサーバーは本番環境と同じ認証ルールをすべてのコネクタモデルのルートに適用します:
- 有効な JWT トークンのないリクエストには
401 Unauthorizedが返される - ロールが不足しているリクエストには
403 Forbiddenが返される - 認証制御は各モデルの
authPolicyとauth.authorization.defaultPolicyを尊重する
ユーザーテーブルも通常の RDB データとしてモック化されるため、シードデータのユーザーでログインし、実際の JWT を受け取ることができます。これにより開発時の動作が本番環境と一致します — デプロイ時に驚くことはありません。
デフォルトポリシーの動作
明示的な authPolicy を持たないモデルは、auth.authorization.defaultPolicy に従います:
defaultPolicy | 動作 |
|---|---|
'authenticated' | 有効な JWT が必須(特定のロールチェックなし) |
'public' | 認証ガードは注入されない — エンドポイントは公開アクセス可能 |
💡 ヒント: デフォルトは 'authenticated' にして、公開エンドポイントを明示的にマークしましょう。これは最小権限の原則に従います。
セキュリティに関する考慮事項
JWT 設計
- トークンは
JWT_SECRET環境変数に格納されたシークレットで署名される - トークンにはユーザー ID、ログイン ID、ロールが含まれる — API キー、パスワード、その他の機密データは JWT ペイロードに絶対に格納しないこと
- トークンの有効期限は
tokenExpiryで設定可能(デフォルト:'24h')
Cookie 設定
- BFF は
httpOnly、secure、sameSite: 'strict'フラグ付きで Cookie を設定する httpOnlyは JavaScript からのアクセスを防止(XSS 対策)secureは HTTPS 経由でのみ Cookie が送信されることを保証(localhost を除く)sameSite: 'strict'は CSRF 攻撃を防止
Defense in Depth(多層防御)
認証フローは二層検証戦略を採用しています:
| レイヤー | チェック内容 | 理由 |
|---|---|---|
| Next.js Middleware(Edge Runtime) | Base64 デコードした有効期限タイムスタンプのみ | Edge Runtime にはネイティブの暗号化 API がなく、JWT 署名を検証できない |
| Azure Functions | 完全な JWT 署名検証 + ロールベースアクセスチェック | Functions は完全な Node.js/C#/Python ランタイムを持ち、暗号化をサポート |
これにより、有効期限切れのトークンはエッジで早期に拒否(高速・低コスト)され、改ざんされたトークンは Functions レイヤーで検出(完全検証)されます。
JWT に含めてはいけないもの
- ❌ API キーやサードパーティトークン
- ❌ パスワードやパスワードハッシュ
- ❌ 認証に必要な範囲を超える個人識別情報
- ❌ 大きなデータペイロード(JWT はすべてのリクエストで送信される)
✅ 含めて良いもの: ユーザー ID、ログイン ID、ロール、トークン有効期限。
ベストプラクティス
プロバイダーモードの選択
- ✅ 既存のユーザーデータベースがあり、認証フローを完全に制御したい場合は
custom-jwtを使用 - ⏳ Azure AD / GitHub / ソーシャルログインで十分なシンプルなプロジェクト向けの
swaは計画中です - ⏳ SWA の利便性にカスタム拡張を加える
swa-customは計画中です
シークレット管理
JWT_SECRETは Azure App Settings(本番環境)と.env.local(開発環境)に保存する- 強力なランダムシークレットを使用 — 最低 256 ビット(32 文字以上)
- 定期的にシークレットをローテーションし再デプロイする
- シークレットをソースコントロールにコミットしないこと
# .env.local(ローカル開発用)
JWT_SECRET=your-strong-random-secret-at-least-32-characters
USERDB_CONNECTION_STRING=postgres://user:pass@localhost:5432/mydbロール命名規則
- 小文字の説明的なロール名を使用:
admin、estimator、viewer - ロール数は少なく保つ — 細かい権限を作成するよりもロールの組み合わせを推奨
- 各ロールがアクセスを許可する範囲をドキュメント化する
認証コンテキストの使い方
アプリを認証プロバイダーでラップし、コンポーネントで useAuth フックを使用します:
// app/layout.tsx
import { AuthProvider } from '@/lib/auth/auth-context';
export default function RootLayout({ children }) {
return <AuthProvider>{children}</AuthProvider>;
}// 任意のコンポーネント内
import { useAuth } from '@/lib/auth/auth-context';
function Dashboard() {
const { user, logout, isAuthenticated } = useAuth();
if (!isAuthenticated) return null;
return <div>Welcome, {user.name}</div>;
}制限事項
認証機能の v1 における現時点での制限事項は以下のとおりです:
- リフレッシュトークンなし: トークンは設定した
tokenExpiryの期間で失効します。トークンが切れた場合、ユーザーは再ログインする必要があります swaおよびswa-customモードなし:custom-jwtのみが実装されています。SWA ベースの認証プロバイダーは今後のリリースで予定されています- トークン失効(リボケーション)なし: 発行された JWT を有効期限前に無効化することはできません。即座にアクセスを取り消すには、
JWT_SECRETをローテーション(全トークン無効化)してください - Edge Runtime に暗号化 API なし: Next.js Middleware(Edge Runtime)では JWT 署名を検証できません — Middleware レイヤーでは有効期限チェックのみが行われます
- パスワードリセットフローなし:
add-authコマンドはパスワードリセットやアカウント復旧のエンドポイントを生成しません - 多要素認証(MFA)なし: 生成される認証フローでは MFA はサポートされていません
- セッション管理 UI なし: アクティブなセッション/トークンを表示・管理するための管理画面はありません
💡 参考情報: CLI コマンドの詳細は CLI リファレンス を、Connector の設定については Connector ガイド を、モデルの Scaffold については Scaffold ガイド をご参照ください。
