Security & Lifecycle — Auth / Access Control / Hooks
この章のゴール
- Collection に
auth: trueを付けるだけで何が用意され、何が手に入るかを掴む - 関数で書く Access Control の基本シグネチャと、Where 返却による 行レベル制御を理解する
- Hooks の 4 階層 (Root / Collection / Global / Field) と主要な実行タイミングを押さえる
- 監査ログ / slug 自動生成 / 派生フィールド計算など典型パターンを書ける
Authentication
Collection に auth を付けると何が起こるか
公式 doc (Authentication Overview) によれば、Collection に auth: true を付けると、その Collection が 「ユーザー」を表す Collection になり、ログイン / ログアウト / パスワードリセットを含む 完全な認証ワークフローが自動生成されます。Admin UI への管理 UI も追加されます。
最小例:
import type { CollectionConfig } from 'payload'
export const Users: CollectionConfig = {
slug: 'users',
auth: true,
fields: [
{ name: 'role', type: 'select', options: ['admin', 'editor', 'viewer'], defaultValue: 'viewer' },
],
}
自動生成されるエンドポイント
auth-enabled Collection には以下の REST エンドポイントが自動生成されます (REST API Overview):
| メソッド | パス | 用途 |
|---|---|---|
POST | /api/users/login | ログイン (JWT 発行) |
POST | /api/users/logout | ログアウト |
GET | /api/users/me | 現在のユーザー情報取得 |
POST | /api/users/refresh-token | トークン更新 |
POST | /api/users/forgot-password | パスワード再設定リクエスト |
users は slug の例で、Collection 名が違えばエンドポイント名もそれに従います。
主要オプション
| オプション | 役割 |
|---|---|
tokenExpiration | ログイン状態の維持期間 (秒) |
maxLoginAttempts / lockTime | 失敗ロックアウト制御 |
verify | メール検証必須化 |
loginWithUsername | username 認証許可 (デフォルトは email) |
forgotPassword | パスワード再設定方式のカスタマイズ |
useSessions | session ベース認証 (デフォルト true) ↔ stateless JWT (false) の切り替え |
useAPIKey | API Keys 有効化 |
strategies | カスタム認証戦略の追加 (外部 IdP / Auth.js 連携の入り口) |
disableLocalStrategy | 内蔵の email / password 認証を無効化 (外部認証へ全面委譲する場合) |
sessions vs stateless JWT
useSessions が切り替えるのは「session ベース (stateful、デフォルト true) か、stateless JWT か」です。公式 doc は "True by default. Set to false to use stateless JWTs for authentication instead of sessions" と説明しています。どちらの方式でもブラウザへの送達には HTTP-only Cookie が使われ、公式は XSS 攻撃から保護される点を強調しています ("HTTP-only cookies are totally protected from XSS attacks")。フロントが SPA + 別オリジンで Cookie を運べないときは Authorization ヘッダで token を送り、Next.js 同居なら Cookie のまま、というのが実務の分かれ目です。
API Keys
useAPIKey: true を付けると、サードパーティ統合用の API Key 発行が有効になります。サーバーサイド処理 (Webhook 受け側など) で JWT ライフサイクルを持たない統合先から PayloadCMS を叩く用途に向きます。
Access Control
基本シグネチャ
Access Control は 関数で書くのが特徴です (Access Control Overview)。引数は { req }、req.user でリクエスト元ユーザーに触れます。返り値は boolean または Where クエリです。
const isAuthenticated = ({ req: { user } }) => Boolean(user)
これが公式の最小デフォルトに近い形で、認証済みなら通す、それ以外は拒否、という意味です。
3 階層の Access Control
| 階層 | 設定先 | 役割 |
|---|---|---|
| Collection | Collection の access プロパティ | Collection 全体の CRUD 制御 |
| Global | Global の access プロパティ | Global の read/update 制御 |
| Field | Field の access プロパティ | フィールド単位で read/create/update を制御 |
制御できる操作
公式 doc が明示する Collection-level の操作:
createreadupdatedelete
Versions / Drafts を使う場合は readVersions / publishVersion 等も追加されます (各機能の doc 側に詳述)。
Where 返却 = 行レベルフィルタ
boolean の代わりに Where 条件を返すと、PayloadCMS は その条件に合致するドキュメントだけを読ませる/操作させる挙動になります。
// Posts Collection の access
const access = {
read: ({ req: { user } }) => {
if (user?.role === 'admin') return true
// 一般ユーザーは published のものだけ読める
return {
_status: { equals: 'published' },
}
},
}
true を返せばその操作は全行に対して許可、false を返せば一切拒否、Where を返せば「条件に合う行だけ」という 3 段階で書けます。
Field-level Access の例
{
name: 'secretNote',
type: 'text',
access: {
read: ({ req: { user } }) => user?.role === 'admin',
update: ({ req: { user } }) => user?.role === 'admin',
},
}
このフィールドは admin だけが読み書きできます。Collection-level の access を通過したあとに 重ねて適用されます。
Hooks
4 階層
公式 doc (Hooks Overview) は Hooks を 4 階層に分けます:
| 階層 | 設定先 | 役割 |
|---|---|---|
| Root Hooks | Config の hooks | エラー処理など全体に効くフック |
| Collection Hooks | Collection の hooks | Collection ごとのライフサイクル |
| Global Hooks | Global の hooks | Global ごとのライフサイクル |
| Field Hooks | Field の hooks | 個別フィールドの読み書き処理 |
Collection Hooks の主要タイミング
| Hook | 実行タイミング |
|---|---|
beforeOperation | 操作開始直後 |
beforeValidate | バリデーション直前 |
beforeChange | バリデーション後、DB 書き込み前 |
afterChange | DB 書き込み後 |
beforeRead | 読み出し前 |
afterRead | 読み出し後、レスポンス返却前 |
beforeDelete | 削除前 |
afterDelete | 削除後 |
afterOperation | 操作完了後 |
Hook の引数と戻り値
引数オブジェクトは Hook の種類で違いますが、共通して req (PayloadRequest)、operation ('create' | 'update' | 'delete' など)、対象 hook によって data (書き込もうとしている値) / doc (DB 上の現在値) / originalDoc (変更前) などが渡されます。
data 系 Hook で値を return すると、それが上書きとして後続のフローに使われます。Promise を返すと PayloadCMS が await して次のステップに進みます。
よくある実装パターン
slug 自動生成 (beforeChange)
{
slug: 'posts',
fields: [
{ name: 'title', type: 'text' },
{ name: 'slug', type: 'text', admin: { readOnly: true } },
],
hooks: {
beforeChange: [
({ data }) => {
if (data.title && !data.slug) {
data.slug = data.title.toLowerCase().replace(/\s+/g, '-')
}
return data
},
],
},
}
監査ログ (afterChange)
hooks: {
afterChange: [
async ({ doc, req, operation }) => {
// payload.logger は pino ベース: 第 1 引数 object + 第 2 引数 message の形式
req.payload.logger.info(
{ postId: doc.id, userId: req.user?.id },
`Post ${operation}`,
)
},
],
}
Cache 失効 (afterChange, Next.js 同居時の典型)
import { revalidateTag } from 'next/cache'
hooks: {
afterChange: [
async ({ doc }) => {
revalidateTag('posts')
revalidateTag(`post-${doc.slug}`)
},
],
}
これは 06 Next.js Integration で扱う Next.js cache 統合の心臓部です。
Field レベル Hook (派生値計算)
{
name: 'wordCount',
type: 'number',
admin: { readOnly: true },
hooks: {
beforeChange: [
({ siblingData }) => {
const text = siblingData.body ?? ''
return text.split(/\s+/).filter(Boolean).length
},
],
},
}
siblingData で同じドキュメント内の他フィールドにアクセスし、その派生値を計算して保存できます。
Auth × Access × Hooks を組み合わせる典型例
「投稿は author 本人と admin だけが編集できる、公開済みは誰でも読める、書き込み時に slug を自動生成、公開時にキャッシュ失効」を 1 つの Collection で書くとこうなります:
import type { CollectionConfig } from 'payload'
import { revalidateTag } from 'next/cache'
export const Posts: CollectionConfig = {
slug: 'posts',
access: {
read: ({ req: { user } }) => {
if (user?.role === 'admin') return true
return { _status: { equals: 'published' } }
},
update: ({ req: { user } }) => {
if (!user) return false
if (user.role === 'admin') return true
return { author: { equals: user.id } }
},
delete: ({ req: { user } }) => user?.role === 'admin',
},
versions: { drafts: true },
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'slug', type: 'text', admin: { readOnly: true } },
{ name: 'body', type: 'richText' },
{ name: 'author', type: 'relationship', relationTo: 'users', required: true },
],
hooks: {
beforeChange: [
({ data }) => {
if (data.title && !data.slug) {
data.slug = data.title.toLowerCase().replace(/\s+/g, '-')
}
return data
},
],
afterChange: [
({ doc }) => {
revalidateTag('posts')
},
],
},
}
ここまで来ると、「データモデルがそのまま動く CMS になる」という Config-First の思想が体感できるはずです。
実装例 — 非同期 Access Control で他 Collection の状態を見る
Access Control は async で書けます。req.payload を使って他の Collection を query し、その結果から判定する、という実用パターンです (Access Control doc)。
公式 doc が示す典型例: 「契約が紐づいている顧客は削除できない」 — Customer Collection の delete access function 内で contracts Collection を覗き、関連レコードが 0 件のときだけ削除を許可します。
import type { Access } from 'payload'
import type { Customer } from '@/payload-types'
export const canDeleteCustomer: Access<Customer> = async ({ req, id }) => {
const result = await req.payload.find({
collection: 'contracts',
limit: 0,
depth: 0,
where: {
customer: { equals: id },
},
})
return result.totalDocs === 0
}
// Customers Collection の access に組み込む
export const Customers: CollectionConfig = {
slug: 'customers',
access: {
delete: canDeleteCustomer,
},
fields: [/* ... */],
}
ポイント:
limit: 0/depth: 0で 件数判定のみに必要な最小コストでクエリreq.payloadで 3 層 API の Local API を使う (05 章)- 関連レコードがあれば
falseを返して削除拒否 → Admin UI でも REST でも GraphQL でも同じく拒否される
公式 doc が引用形式で示しているのは delete 操作の非同期パターンです。update などへの同型の応用は公式が明示していないため、適用時は実際に動作を確認することを推奨します (実装上は同じく async 関数として書けます)。
この章のまとめ
auth: trueで Users 化 + 認証ワークフロー全部が揃う (login / me / refresh-token / forgot-password)- Access Control は 関数で書く: 返り値
true/false/Whereの 3 通り。Where を返すと 行レベルフィルタ - 階層は Collection / Global / Field の 3 段、Hooks は Root / Collection / Global / Field の 4 段
- Hooks の代表 4 タイミング (
beforeValidate/beforeChange/afterChange/afterRead) を押さえれば多くのケースが書ける - Next.js 同居時は
afterChangeでrevalidateTagを呼ぶのが定番パターン (詳細は 06 章) - Access Control は async で書ける —
req.payload.find()で他 Collection を見て判定するパターンが公式に明示
次に読む
次章 05 APIs — Local / REST / GraphQL 3 層 では、3 つの API レイヤをどう使い分け、どこから何を呼ぶのが正解か、自動生成型と合わせて整理します。