
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つの記事の続きとして構成されています:
- 「JWTで保護されたユーザ一覧を実装する ─ 認証・ロール・一覧表示まで」
- UIレベルでの「認証済み + adminのみ閲覧」の経験済み
- 「Prisma × PostgreSQLで始めるユーザー・ロール管理」
- DB上に
Role
モデルがあり、JWTのpayloadにもrole.code / priority
が含まれている状態
- DB上に
このような前提をもとに、「クライアント側での表示制御」から一歩進んで、API自体に対するアクセス制御(認可)を実装する段階に進んでいきます。
今回の記事で扱う範囲
項目 | 内容 |
---|---|
✅ 対象 | JWTトークンに含まれた role をもとにAPIアクセスを制御 |
✅ 実装 | 管理者専用の /api/secret-admin-route を作成し、priority >= 100 のGuard関数で保護 |
✅ 解説 | role.code とpriority の使い分け/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.code | string | "ADMIN" | UI表示や分岐の明示的な判定に使いやすい |
role.priority | number | 100 | ロールの強さを数値で比較する用 |
これらを用いて、次のような制御が可能になります:
role.code === "ADMIN"
→ 管理者だけにボタンを表示(UI)role.priority >= 100
→ 管理者レベルのAPI実行を許可(API)
なぜpriorityを導入したのか?
role.code
だけでも一見十分に見えますが、priority
を導入することで以下のような柔軟な制御が可能になります。例:ロールごとの柔軟な制御
ロール | code | priority |
---|---|---|
管理者 | "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.code
やrole.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の場合の手順:
Chromeの場合の手順:
- ブラウザの開発者ツールを開きます(Mac:Cmd + Option + I、Windows:F12 または Ctrl + Shift + I)
- [アプリケーション] タブを選択
- 左側メニューから [ローカル ストレージ] → http://localhost:3000 を選択(ポート番号はみなさんの実行環境に合わせてください)
- 保存されているキー(例:token)を確認
- 例:
Key | Value(←JWT) |
---|---|
token | eyJhbGciOiJIUzI1NiIsInR5cCI6... (続く) |
🔐 注意点
- 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)
- Method:GET
- URL:http://localhost:3000/api/secret-admin-route
- Header:
- Authorization: Bearer
この章で得られたこと
- 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.ts | APIごとに役割別の認可処理 |
- 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.code
と priority
を使って、APIレベルでの認可(RBAC)を実装する方法を紹介してきました。今回のまとめ
- JWTのペイロードに
role
情報を含めることで、サーバ側でユーザー権限を確認できる - API単位でのガード処理(
requireMinPriority
やrequireExactRole
)により、安全で明示的な認可制御が実現できる - 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だけでなくページ単位の認可」もシンプルに実現できるようになります。
参考文献
-
- JWTの公式仕様。トークンの構造や標準クレーム(
iat
,exp
など)の定義が記載されています。
- JWTの公式仕様。トークンの構造や標準クレーム(
-
- App Routerベースの構成におけるAPIルートやServer Action、middlewareの使い方などを確認できます。
-
- middleware.tsの設計や、リクエストベースでの処理分岐(認可チェックなど)について記載。
-
- 本記事でJWTの署名・検証に使用しているライブラリ。
SignJWT
,jwtVerify
などの詳細実装も確認可能。
- 本記事でJWTの署名・検証に使用しているライブラリ。
-
- ロールベースアクセス制御の概要と、基本設計方針を理解するための入門資料。
-
- HTTPヘッダーの仕様と、Bearerトークンの送信方法について。
-
- 今回の実装のベースとなった前回記事。
AuthenticatedArea.tsx
などの構成も登場します。
- 今回の実装のベースとなった前回記事。
-
- JWTのペイロードに含める
role.code
やpriority
の背景となるDB設計を解説した前提記事。
- JWTのペイロードに含める
この記事の執筆・編集担当
DE
松本 孝太郎
DELOGs編集部/中年新米プログラマー
ここ数年はReact&MUIのフロントエンドエンジニアって感じでしたが、Next.jsを学んで少しずつできることが広がりつつあります。その実践記録をできるだけ共有していければと思っています。
▼ 関連記事
Prisma × PostgreSQLで始めるユーザー・ロール管理
スキーマ設計とDB連携の基礎構築を通じて、認可の土台となるユーザー・ロール情報の管理を実践
2025/8/3公開

JWTで保護されたユーザ一覧を実装する ─ 認証・ロール・一覧表示まで
ログイン済みのadminユーザーだけにユーザー一覧を表示します。JWT認証の保護ルートとRBAC導入の第一歩となる実装
2025/7/30公開

JWTログインAPIをNext.jsで実装する
Shadcn/uiとつなぐ認証基盤の第一歩
2025/7/29公開

JWTって何? Next.js での認証方式とトークンの仕組みを徹底解説(超入門)
JWTについて、Next.jsでのログイン認証に使えるトークンの仕組みと活用方法を初心者向けに丁寧に解説
2025/7/23公開
