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

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メール検証必須化
loginWithUsernameusername 認証許可 (デフォルトは email)
forgotPasswordパスワード再設定方式のカスタマイズ
useSessionssession ベース認証 (デフォルト true) ↔ stateless JWT (false) の切り替え
useAPIKeyAPI 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

階層設定先役割
CollectionCollection の access プロパティCollection 全体の CRUD 制御
GlobalGlobal の access プロパティGlobal の read/update 制御
FieldField の access プロパティフィールド単位で read/create/update を制御

制御できる操作

公式 doc が明示する Collection-level の操作:

  • create
  • read
  • update
  • delete

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 HooksConfig の hooksエラー処理など全体に効くフック
Collection HooksCollection の hooksCollection ごとのライフサイクル
Global HooksGlobal の hooksGlobal ごとのライフサイクル
Field HooksField の hooks個別フィールドの読み書き処理

Collection Hooks の主要タイミング

Hook実行タイミング
beforeOperation操作開始直後
beforeValidateバリデーション直前
beforeChangeバリデーション後、DB 書き込み前
afterChangeDB 書き込み後
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 例

公式 doc が引用形式で示しているのは delete 操作の非同期パターンです。update などへの同型の応用は公式が明示していないため、適用時は実際に動作を確認することを推奨します (実装上は同じく async 関数として書けます)。

出典: Access Control - Collections (公式 doc)

この章のまとめ

  • auth: trueUsers 化 + 認証ワークフロー全部が揃う (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 同居時は afterChangerevalidateTag を呼ぶのが定番パターン (詳細は 06 章)
  • Access Control は async で書ける — req.payload.find() で他 Collection を見て判定するパターンが公式に明示

次に読む

次章 05 APIs — Local / REST / GraphQL 3 層 では、3 つの API レイヤをどう使い分け、どこから何を呼ぶのが正解か、自動生成型と合わせて整理します。

参考