DELOGs
JWTとロールでAPIを守る ─ RBAC導入とGuard関数実装

JWTとロールでAPIを守る ─ RBAC導入とGuard関数実装

APIを安全にする鍵は「ロールベースの認可」。JWTのpayloadに含めたロール情報を活用し、Admin専用APIの実装を通じてRBACの基本を実践

初回公開日

最終更新日

0. はじめに ─ なぜAPIレベルでの認可が必要か?

過去記事「JWTで保護されたユーザ一覧を実装する ─ 認証・ロール・一覧表示まで」 では、JWTトークンを使った「ログイン済みユーザー」の保護と、role.code によるロール判定をもとに、管理者だけがアクセスできるUIページ(例:/users)を実装しました。
このアプローチでは、表示制御=UXレベルの認可体験が中心でした。つまり、「見せないようにする」ことで、一般ユーザーから管理者向けの機能を“隠す”というものです。
しかし、ここには大きな盲点があります。

見せない ≠ 守られている

クライアント側で「表示しない」ようにしても、URLやAPIのエンドポイントを知っていれば、リクエストそのものは送れてしまいます。 たとえば /api/secret-admin-route にfetchすれば、誰でもレスポンスを得られる状態なら、それは「認可」された状態とは言えません。
本当に守りたいのは「APIそのもの」 です。
つまり、ロールを使って「叩けないようにする」制御こそ、アプリケーションにおける本質的な認可(Authorization)といえます。

本記事のテーマ:APIをロールで守る

本記事では、JWTの中に含めたロール情報(role.code や role.priority)を活用して、
  • 管理者だけが使えるAPIの作成
  • Guard関数による共通化と保守性の向上
  • 「priority >= 100」などの実務的なロール判定
といった、より実用的な RBAC(ロールベースアクセス制御) を実装していきます。

Prisma連携からの続きで実装を進めます

なお本記事の実装は、以下の過去記事で構築したデータモデル・ロール構造をもとに進めます:
この時点で User モデルと Role モデルのリレーションは完成しており、JWT発行時にロール情報をトークンに含める処理も導入済みです。
次章では、このJWTの中に格納されたロール情報を再確認しながら、API保護にどのように活用していくかを詳しく見ていきます。

1. この記事の構成と対象スコープ

本記事では、「JWTトークンに含まれたロール情報を使ってAPIのアクセス制御を行う」という、RBAC(ロールベースアクセス制御)の導入に焦点を当てます。

本記事の前提

この記事は、以下の2つの記事の続きとして構成されています:
このような前提をもとに、「クライアント側での表示制御」から一歩進んで、API自体に対するアクセス制御(認可)を実装する段階に進んでいきます。

今回の記事で扱う範囲

項目内容
✅ 対象JWTトークンに含まれた role をもとにAPIアクセスを制御
✅ 実装管理者専用の /api/secret-admin-route を作成し、priority >= 100 のGuard関数で保護
✅ 解説role.codepriorityの使い分け/Guard関数の共通化/実行例とログ出力
✅ 補足Server Actionでも同様の制御が可能である点を後述で解説予定

対象外(次回以降に扱う予定)

項目対応方針
🔜 middleware.ts によるルート全体の保護記事Cで詳しく扱う予定(Cookie認証に移行し、 /admin/* 保護なども実現)
🔜 Context APIによるロール共有・UI制御記事D以降でヘッダUIやページ分岐のUX強化に使う予定
🔜 JWT保存方式の変更(localStorage → httpOnly Cookie)記事Cにて実装予定(middleware対応と同時に実施)
この章の目的は、APIレベルでの「守るべきポイント」がどこにあるのかを明確にすることです。 次章からは、JWTのpayloadに含まれたロール情報を再確認しつつ、実際のAPI設計に踏み込んでいきます。

2. トークンの構造を再確認(role.codeとpriority)

本格的にRBAC(ロールベースアクセス制御)を扱う前に、まずはJWTトークンの中身を再確認しておきましょう。
現在のDELOGsプロジェクトでは、ログイン成功時に以下のようなトークンを発行しています:
json
1{ 2 "email": "admin@example.com", 3 "role": { 4 "code": "ADMIN", 5 "priority": 100 6 }, 7 "iat": 1720000000, 8 "exp": 1720003600 9}

含まれている認可情報

このペイロードには、以下のような認可に関する情報が含まれています:
キー用途
role.codestring"ADMIN"UI表示や分岐の明示的な判定に使いやすい
role.prioritynumber100ロールの強さを数値で比較する用
これらを用いて、次のような制御が可能になります:
  • role.code === "ADMIN" → 管理者だけにボタンを表示(UI)
  • role.priority >= 100 → 管理者レベルのAPI実行を許可(API)

なぜpriorityを導入したのか?

role.codeだけでも一見十分に見えますが、priorityを導入することで以下のような柔軟な制御が可能になります。
例:ロールごとの柔軟な制御
ロールcodepriority
管理者"ADMIN"100
サポート"STAFF"50
一般ユーザー"USER"10
これにより、たとえば以下のような条件が書けます:
  • priority >= 50 → 管理者またはスタッフに許可
  • priority >= 100 → 管理者のみ許可
文字列の比較では難しい「範囲的な認可」が実現できるのが priority の最大のメリットです。

注意:トークンの改ざん対策は前提として必要

ここで確認しているJWTの内容は、 認証サーバで署名されたもの を前提としています。 クライアントが内容を自由に書き換えられる構造になっていてはいけません。
  • ✅ トークンは signToken() 関数で署名付きで生成
  • ✅ APIサイドでは verifyToken() によって署名検証とpayload復元を実施
この前提が守られていれば、トークンの role 情報を信用してアクセス制御に使うことができます。 次章では、このトークン情報を使って、実際に管理者だけが使えるAPIエンドポイントを構築してみます。

3. Admin専用APIを作ってみる(/api/secret-admin-route)

ここでは、「管理者ロール(priorityが100以上)だけがアクセスできるAPI」を実装してみます。 JWTトークンに含まれるロール情報(role.coderole.priority)を使って、APIの実行前に認可チェックを行います。

目的

  • 認証(Authentication)だけでなく、 認可(Authorization) も必要な状況を体験する
  • 管理者ユーザーと一般ユーザーで、APIレスポンスの違いを明確にする
  • Guard関数の導入に向けた前段階として、認可処理の構造を理解する

ファイル:src/app/api/secret-admin-route/route.ts

まずはシンプルに、verifyToken() を直接使ってトークンを検証し、role.priority で判定する構成にしてみましょう。
src/app/api/secret-admin-route/route.tsを新規作成して、下記の内容を記述します:
ts
1import { NextResponse } from "next/server"; 2import { verifyToken } from "@/lib/jwt"; 3 4export async function GET(req: Request) { 5 const authHeader = req.headers.get("Authorization"); 6 7 // トークンがない or フォーマットが不正な場合 8 if (!authHeader || !authHeader.startsWith("Bearer ")) { 9 return NextResponse.json({ error: "トークンが必要です" }, { status: 401 }); 10 } 11 12 const token = authHeader.split(" ")[1]; 13 const payload = await verifyToken(token); 14 15 // トークンの検証に失敗した場合 16 if (!payload) { 17 return NextResponse.json({ error: "無効なトークンです" }, { status: 401 }); 18 } 19 20 // priority でロールチェック(100以上が admin 扱い) 21 if (!payload.role || payload.role.priority < 100) { 22 return NextResponse.json({ error: "許可されていません" }, { status: 403 }); 23 } 24 25 // 管理者だけに見せる情報を返す 26 return NextResponse.json({ 27 message: "管理者のみが見られる秘密の情報です", 28 secret: "これはRBACによって保護されています", 29 }); 30}

動作確認

npm run devでプロジェクトを実行します。その後、管理者権限のユーザでログインします。
管理者権限のユーザ情報
前回記事で設定した内容は下記です。
  • アカウントID:testAccount0123
  • メールアドレス:admin@example.com
  • パスワード:adminPass0123456
JWTを確認する方法(localStorage版)
Chromeの場合の手順:
  • ブラウザの開発者ツールを開きます(Mac:Cmd + Option + I、Windows:F12 または Ctrl + Shift + I)
  • [アプリケーション] タブを選択
  • 左側メニューから [ローカル ストレージ] → http://localhost:3000 を選択(ポート番号はみなさんの実行環境に合わせてください)
  • 保存されているキー(例:token)を確認
  • 例:
KeyValue(←JWT)
tokeneyJhbGciOiJIUzI1NiIsInR5cCI6...(続く)
🔐 注意点
  • JWTはBase64でエンコードされているだけで暗号化されていません。中身は誰でも見られます。
  • JWTの中身を確認したい場合は、「jwt.io」 に貼り付ければペイロードの中身を確認できます。
JWTトークンが確認できたら、エディタ等のターミナルを別タブで開き、取得したJWTトークンを使って下記のようにリクエストを送ります。
zsh
1curl -X GET http://localhost:3000/api/secret-admin-route \ 2 -H "Authorization: Bearer <adminのJWT>"
  • http://localhost:3000のところは、ご自分の実行環境の値に変更してください。
  • <adminのJWT>のところを実際のJWTトークンに置き換えてください。

💎成功すると以下のようなJSONが返ります:

txt
1{"message":"管理者のみが見られる秘密の情報です","secret":"これはRBACによって保護されています"}

❗️一般ユーザーのJWTで実行すると?

参照権限のユーザ情報
前回記事で設定した内容は下記です。
  • アカウントID:testAccount0123
  • メールアドレス:viewer@example.com
  • パスワード:viewerPass1234567
403 Forbidden が返り、次のようなレスポンスになります:
txt
1{"error":"許可されていません"}
💡 他のツールでも確認可能(Postman / Thunder Client)

この章で得られたこと

  • API単位で 「誰がアクセスできるか」 を制御するのは、RBACの中心的な役割
  • verifyToken()payload.role.priority という流れで、実行前にロールを検査
  • JWTトークンに含まれる情報だけで、シンプルに認可チェックが可能
次の章では、このロール判定処理をGuard関数として切り出して再利用可能にしていきます。

4. Guard関数で認可処理を共通化

前章では、管理者専用APIを /api/secret-admin-route に実装し、verifyToken() を使って JWT トークンの role.priority を直接チェックする方法を取りました。
しかし、このままでは API ごとに同じようなコードが重複してしまいます。

毎回書くとどうなるか?

たとえば、下記のように毎回 Authorization ヘッダを確認 → トークン抽出 → 検証 → priority 判定という流れを繰り返す必要があります:
tsx
1const authHeader = req.headers.get("Authorization"); 2const token = authHeader?.split(" ")[1]; 3const payload = await verifyToken(token); 4if (!payload?.role || payload.role.priority < 100) { 5 return NextResponse.json({ error: "許可されていません" }, { status: 403 }); 6}
APIが3つ、5つと増えるたびに、これを毎回書くのは冗長でエラーの元にもなります。

Guard関数とは?

このような 認可ロジックを関数として共通化 したものを 「Guard関数」 と呼びます。 Guard(ガード)とは「守るもの」の意味で、アクセス制御に特化した処理です。
目的は次の通りです:
目的内容
共通化同じ認可処理を1箇所にまとめて再利用
保守性ロジックを1箇所変更すれば全体に反映される
安全性コピペによるミスや漏れを防止できる

実装例:hasAdminRole() 関数

以下は、「priorityが100以上」の管理者かどうかを判定する Guard関数です。※ここでは例を示します。概要だけ掴んでください。実際の組み込みは次章で行います。
/lib/authorization.tsの例:
ts
1import { verifyToken } from "@/lib/jwt"; 2 3/** 4 * Authorizationヘッダから Bearer トークンを抽出 5 */ 6function extractBearerToken(req: Request): string | null { 7 const auth = req.headers.get("Authorization"); 8 if (!auth || !auth.startsWith("Bearer ")) return null; 9 return auth.split(" ")[1]; 10} 11 12/** 13 * 管理者ロールかどうかを判定する Guard関数 14 */ 15export async function hasAdminRole(req: Request): Promise<boolean> { 16 const token = extractBearerToken(req); 17 if (!token) return false; 18 19 const payload = await verifyToken(token); 20 if (!payload?.role || payload.role.priority < 100) return false; 21 22 return true; 23}

利用方法の例(API側での呼び出し)

※ここでは例を示します。概要だけ掴んでください。実際の組み込みは次章で行います。
Guard関数を使えば、 API側 では次のようにシンプルに呼び出すだけで済みます:
ts
1import { hasAdminRole } from "@/lib/authorization"; 2 3export async function GET(req: Request) { 4 const authorized = await hasAdminRole(req); 5 if (!authorized) { 6 return NextResponse.json({ error: "許可されていません" }, { status: 403 }); 7 } 8 9 return NextResponse.json({ message: "管理者専用の情報です" }); 10}

コード全体がすっきりする

before(手動で認可処理):
ts
1// トークン抽出 2// 検証 3// ロールチェック 4// 403 or 処理
after(Guard関数利用):
ts
1if (!await hasAdminRole(req)) return 403;

今後の拡張性にもつながる

Guard関数として切り出しておけば、以下のようなバリエーションにも柔軟に対応できます:
  • hasEditorRole()(priorityが50以上)
  • hasExactRole("ADMIN")
  • hasAnyRole(["ADMIN", "EDITOR"])
  • hasMinPriority(80)
長々と書きましたが「APIの記述を短くできる」ってだけの話ではあります。
次の章では、こうしたRBACロジックの共通化をさらに進め、複数のロールパターン(code / priority)に対応する拡張可能な設計を実装します。

5. RBACロジックを共通化 ─ 複数パターンに対応

※前章では、Guard関数の導入目的と簡易な実装例を紹介しました。 本章ではそれをより柔軟かつ拡張可能な構成へと発展させ、実用的なRBACの骨格として整えていきます。
これまでの例では、hasAdminRole() のように 「priorityが100以上」かどうか に固定された認可チェックを行っていました。 しかし、現実的なシステムでは、次のような複数パターンの条件を使い分ける必要があります:
  • priority >= 100 のユーザーだけを許可したい(管理者)
  • role.code === 'EDITOR' のユーザーだけに許可したい
  • role.code'ADMIN' | 'EDITOR' のいずれかなら許可したい
  • role を持たないユーザーは一律NGにしたい
こうしたパターンに対応するため、柔軟なGuard関数群として共通ロジックを整理していきます。

共通ロジックの基本構成

まずは、共通化のための土台を src/lib/authorization.ts にまとめていきます。
src/lib/authorization.tsを下記内容で作成します:
ts
1import { verifyToken } from "@/lib/jwt"; 2 3/** 4 * Authorizationヘッダから Bearer トークンを抽出 5 */ 6export function extractBearerToken(req: Request): string | null { 7 const auth = req.headers.get("Authorization"); 8 if (!auth || !auth.startsWith("Bearer ")) return null; 9 return auth.split(" ")[1]; 10} 11 12/** 13 * priority が指定値以上かどうかチェック 14 */ 15export async function requireMinPriority( 16 req: Request, 17 min: number, 18): Promise<boolean> { 19 const token = extractBearerToken(req); 20 if (!token) return false; 21 22 const payload = await verifyToken(token); 23 return ( 24 typeof payload?.role?.priority === "number" && payload.role.priority >= min 25 ); 26} 27 28/** 29 * role.code が完全一致しているかチェック 30 */ 31export async function requireExactRole( 32 req: Request, 33 roleCode: string, 34): Promise<boolean> { 35 const token = extractBearerToken(req); 36 if (!token) return false; 37 38 const payload = await verifyToken(token); 39 return payload?.role?.code === roleCode; 40} 41 42/** 43 * role.code が複数の候補のいずれかに一致するかチェック 44 */ 45export async function requireAnyRole( 46 req: Request, 47 roles: string[], 48): Promise<boolean> { 49 const token = extractBearerToken(req); 50 if (!token) return false; 51 52 const payload = await verifyToken(token); 53 return roles.includes(payload?.role?.code || ""); 54}

実際に認可処理を行うAPIの場所と使い方

すべて src/app/api/***/route.ts 内で使います。
例えば、「3. Admin専用APIを作ってみる(/api/secret-admin-route)」で作成した。src/app/api/secret-admin-route/route.tsは下記のように書き直すことができます。

① priorityでチェックするAPI(src/app/api/secret-admin-route/route.ts

ts
1import { NextResponse } from "next/server"; 2import { requireMinPriority } from "@/lib/authorization"; 3 4export async function GET(req: Request) { 5 const ok = await requireMinPriority(req, 100); 6 if (!ok) { 7 return NextResponse.json({ error: "許可されていません" }, { status: 403 }); 8 } 9 10 return NextResponse.json({ 11 message: "管理者のみが見られる秘密の情報です", 12 secret: "これはRBACによって保護されています", 13 }); 14}
だいぶ、見やすくなります。 以下同様に、用途に応じたAPIを作成してみます。

② role.code でピンポイントチェックするAPI

src/app/api/editor-only/route.tsを下記内容で作成します。
ts
1import { NextResponse } from "next/server"; 2import { requireExactRole } from "@/lib/authorization"; 3 4export async function GET(req: Request) { 5 const ok = await requireExactRole(req, "EDITOR"); 6 if (!ok) { 7 return NextResponse.json({ error: "許可されていません" }, { status: 403 }); 8 } 9 10 return NextResponse.json({ data: "エディター専用の情報です" }); 11}
編集権限のユーザ情報
前回記事で設定した内容は下記です。
  • アカウントID:testAccount0123
  • メールアドレス:editor@example.com
  • パスワード:editorPass1234567
◯テストする場合は、下記のようにAPIを指定します
zsh
1curl -X GET http://localhost:3000/api/editor-only \ 2 -H "Authorization: Bearer <editorのJWT>"

③ role.code が複数一致するかチェックするAPI

src/app/api/admin-or-editor/route.tsを下記内容で作成します。
ts
1import { NextResponse } from "next/server"; 2import { requireAnyRole } from "@/lib/authorization"; 3 4export async function GET(req: Request) { 5 const ok = await requireAnyRole(req, ["ADMIN", "EDITOR"]); 6 if (!ok) { 7 return NextResponse.json({ error: "許可されていません" }, { status: 403 }); 8 } 9 10 return NextResponse.json({ 11 data: "ADMIN または EDITOR に許可された情報です", 12 }); 13}

ポイント整理

内容実装場所備考
Guard関数本体src/lib/authorization.ts再利用できるように関数化
APIでの利用src/app/api/***/route.tsAPIごとに役割別の認可処理
  • Guard関数は「booleanで返す」形にしてあるので、今後は throw にする構成なども可能です
  • 今回はシンプルさを重視して、 読みやすく書ける最小の構成 にしました。

補足:priorityとcode、どちらで認可すべきか?

RBACの設計では、「このユーザーは何ができるか?」をチェックする際に priority(数値)か code(ラベル)か、どちらを使うべきか? という判断が出てきます。(まあ、私がこの2つの要素をつかうようにしているだけではありますが。。)
それぞれの特徴を整理してみましょう。
判定方式特徴適した用途
priority(数値)上下関係を表現できる(並べ替え・大小比較に強い)管理者/一般/ゲストなど「階層的なアクセス制御」priority >= 100 → 管理者
code(文字列)意味が明確・可読性が高い「編集者のみOK」など明示的に制御したいときcode === 'EDITOR'
code in ['ADMIN', 'EDITOR']柔軟性が高い・複数ロール許可に向く管理+編集どちらも可などの複数パターン['ADMIN', 'EDITOR'].includes(code)

🔍選び方のガイド

  • 明確な上下関係がある操作(例:削除は管理者のみ) → priorityで判断
  • 役割ベースで判断したいとき(例:編集者だけが下書きを変更できる)→ codeで判断
  • 特定の複数ロールに許可したいとき → codeの配列で柔軟に対応
このように設計しておくと、ロールが増えたときに、priorityを調整すれば柔軟に制御できるというメリットはあると思います。

6. まとめ + API以外でもRBACは使える?

ここまで、JWTに含めた role.codepriority を使って、APIレベルでの認可(RBAC)を実装する方法を紹介してきました。

今回のまとめ

  • JWTのペイロードに role 情報を含めることで、サーバ側でユーザー権限を確認できる
  • API単位でのガード処理(requireMinPriorityrequireExactRole)により、安全で明示的な認可制御が実現できる
  • Guard関数を共通化することで、保守性・拡張性を高めることができる
  • priorityは「階層的な判定」、codeは「ラベル的な判定」に使い分けると柔軟な設計が可能

よくある質問:「UI側の表示制御はどうするの?」

本記事で扱った認可ロジックはあくまで **「APIレベルでのアクセス制御」 ** です。 クライアント側でのUI制御(ボタンの表示切り替えなど)は別の仕組み(例:Context APIなど)で扱うのが一般的です。
このあたりは後述する「共通レイアウト編」で詳しく解説予定です。

補足:API以外でもRBACは使える? ─ Server Actionとの併用へ

今回はAPIでRBACの実装を行いましたが、Next.js App Routerの最大の強みは「Server Action」によるサーバ処理の統合です。 特にフォーム処理や一覧取得など、クライアントから明示的にfetchする必要がないものは、今後Server Actionに置き換えていくこともできます。 実際の運用では 「トークン検証」と「RBACチェック」さえ共有できれば、処理の場所(API or Server Action) は柔軟に選べます。
次回以降、よりセキュアで保守しやすい構成に向けて、こうした選択肢も取り入れていきます。

次回予告:middlewareでルート単位のGuard制御

次回は middleware.ts を使って /admin/** のルートを一括で保護する構成を導入していきます。
これにより「APIだけでなくページ単位の認可」もシンプルに実現できるようになります。

参考文献

この記事の執筆・編集担当
DE

松本 孝太郎

DELOGs編集部/中年新米プログラマー

ここ数年はReact&MUIのフロントエンドエンジニアって感じでしたが、Next.jsを学んで少しずつできることが広がりつつあります。その実践記録をできるだけ共有していければと思っています。