Skip to content

【Next.js】Auth.jsとKeycloakで実装する認証基盤 その3 ~RBAC編~

はじめに

Keycloak の RBAC を Next.js アプリケーションに実装する方法を学びます。この記事はシリーズの3回目です。

今回は、最終的に以下の画像のように、ユーザーごとにアクセスできるページをロールで制御します。そのために Terraform で Keycloak の設定を行い、Next.js でのアクセス制御の実装まで、具体的なコード例を交えて学習した内容をアウトプットします。

user-group-role

成果物

https://github.com/kntks/blog-code/tree/main/2025/01/nextjs-keycloak-rbac

環境

バージョン
MacSequoia 15.2
Keycloak26.0.7
Docker26.0.0
Docker Composev2.24.5
Terraform1.10.3
Node.jsv22.12.0

環境構築

【Next.js】Auth.jsとKeycloakで実装する認証基盤”で使用した環境を利用します。

Keycloakの設定

Keycloakの設定はすべて Terraform で作成しました。

クライアントロール

  • admin: 管理者権限
  • editor: 編集権限
  • read-only: 読み取り専用権限

グループ

  • Admin
  • Engineering
  • Sales

ユーザー

  • user1
  • user2
  • user3

ユーザー、グループ、ロールの関連付け

ユーザーグループ紐づけられるクライアントロール
user1Adminadmin
user2Engineeringeditor
user3Salesread-only

参考:

アクセストークンへのロール追加

Keycloak では、ユーザーが特定のクライアントロールを持っている場合、そのロール情報が自動的にアクセストークンに含まれます。

アクセストークンにロールが追加される背後には、クライアントスコープ(Client Scope) の役割があります。Keycloak では、各クライアントにはデフォルトで roles というクライアントスコープが関連付けられています。この roles スコープが、クライアントロールをアクセストークンに含める処理を担っています。

This scope has mappers, which are used to add the roles of the user to the access token and add audiences for clients that have at least one client role.

訳)このスコープにはマッパーがあり、アクセストークンにユーザーのロールを追加し、少なくとも1つのクライアントロールを持つクライアントのオーディエンスを追加するために使用されます。

引用:Client scopes - Keycloak Documentation

ユーザーへのロール割り当て

今回、ユーザーへ直接ロールの割り当てる代わりに、グループにロールを割り当て、ユーザーをグループに所属させる方法を採用します。

グループに所属しているユーザーは、そのグループに割り当てられたクライアントロールを自動的に持つことになります。

参考:Groups - Keycloak Documentation

Next.jsでのロールチェック実装

Auth.jsを使ってKeycloakから取得したロール情報を扱う方法を説明します。

セッションへのロール情報の追加

Keycloak からロールの情報を取得しようと考えた場合、方法が2つあります。アクセストークンから取得する方法と、ID トークンから取得する方法です。今回は ID トークンから取得する方法を実施します。

まずはじめに、Keycloak から取得できる ID トークンにはロール情報が存在しないため、ロール情報を追加する設定を行います。

ID トークンにロールの情報を入れる

Keycloak はクライアントスコープ roles を使って、アクセストークンにロール情報を追加します。

client roles をクリックする client-scopes-roles-mappers

Add to ID tokenOn に設定する client-scopes-roles-mapper-details

参考:ID token is not including roles #14617 - GitHub

ロール情報の取得とロールチェックの実装

src/auth.ts にロール情報を取得するためのコードを追加します。

Auth.js では Keycloak から受け取った IDトークンを利用してロールを取得します。
この例では jwt コールバックで profile.resource_access を参照し、token に roles の値を渡します。
その後、session コールバックで token.roles をセッションに上書きし、session.user.roles として使用可能にします。

このとき、TypeScript で型定義を拡張すると、any 型を避けることができます。

canAccessPath 関数は、アクセス可能なパスをロールごとに設定し、そのパスにアクセスできるかどうかを判定します。

src/auth.ts
import NextAuth, { type DefaultSession } from "next-auth";
import Keycloak from "next-auth/providers/keycloak";
import { NextResponse } from "next/server";
const KEYCLOAK_ROLE = {
admin: "admin",
editor: "editor",
readOnly: "read-only",
};
const publicPaths = ["/", "/forbidden"];
const pathPermissions: Array<{
pattern: RegExp;
roles: string[];
}> = [
{ pattern: /^\/admin(?:\/.*)?$/, roles: [KEYCLOAK_ROLE.admin] },
{
pattern: /^\/edit(?:\/.*)?$/,
roles: [KEYCLOAK_ROLE.admin, KEYCLOAK_ROLE.editor],
},
{
pattern: /^\/home(?:\/.*)?$/,
roles: [KEYCLOAK_ROLE.admin, KEYCLOAK_ROLE.editor, KEYCLOAK_ROLE.readOnly],
},
];
function canAccessPath(path: string, userRoles: string[]): boolean {
const matched = pathPermissions.find(({ pattern }) => pattern.test(path));
if (!matched) return false;
return userRoles.some((role) => matched.roles.includes(role));
}
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
Keycloak({
checks: ["pkce", "nonce"],
}),
],
callbacks: {
authorized: async ({ request: { nextUrl }, auth }) => {
if (publicPaths.includes(nextUrl.pathname)) return true;
if (!auth) return false;
if (canAccessPath(nextUrl.pathname, auth.user.roles)) return true;
// 認可エラーの場合、forbiddenページにリダイレクト
return NextResponse.redirect(new URL("/forbidden", nextUrl.origin));
},
jwt: async ({ token, profile, trigger }) => {
if (!(trigger === "signIn" && profile)) return token;
// signIn時にKeycloakから取得したロール情報をトークンに追加
token.roles = profile.resource_access?.myapp?.roles ?? [];
return token;
},
session: async ({ session, token }) => {
session.user.roles = token.roles;
return session;
},
},
// authorized コールバックでfalseを返すと、リダイレクトされるページ
pages: {
signIn: "/",
},
});
// Profileの型定義を拡張する
declare module "next-auth" {
interface Profile {
resource_access?: {
myapp?: {
roles?: string[];
};
};
}
}
// JWTの型定義を拡張する
import { JWT } from "next-auth/jwt";
declare module "next-auth/jwt" {
interface JWT {
roles: string[];
}
}
// Sessionの型定義を拡張する
declare module "next-auth" {
interface Session {
user: {
roles: string[];
/**
* By default, TypeScript merges new interface properties and overwrites existing ones.
* In this case, the default session user properties will be overwritten,
* with the new ones defined above. To keep the default session user properties,
* you need to add them back into the newly declared interface.
*/
} & DefaultSession["user"];
}
}

参考:TypeScript - Auth.js

コンポーネントでのロール判定

クライアントサイドでのロールチェック

クライアントコンポーネントでロールチェックを行うには、まず SessionProvider を作成し、アプリケーションのルートで使用する必要があります。これにより、クライアントコンポーネント内で useSession フックを使用できるようになります。

SessionProvider を作成し、以下のように実装します。このコンポーネントは “use client” ディレクティブを使用して、クライアントサイドでのみ実行されることを明示します。

src/providers.tsx
"use client";
import { SessionProvider } from "next-auth/react";
const Providers: React.FC<React.PropsWithChildren> = ({ children }) => {
return <SessionProvider>{children}</SessionProvider>;
};
export default Providers;

次に、クライアントコンポーネントで useSession フックを使用してセッション情報を取得します。セッションには先ほど追加したロール情報が含まれており、session?.user.roles で参照できます。

実装例として、以下のようなクライアントコンポーネントを作成します。このコンポーネントは現在のユーザーのロール情報を表示します。

src/app/home/client/page.tsx
"use client";
import { useSession } from "next-auth/react";
const Page: React.FC = () => {
const { data: session } = useSession();
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<div>クライアントコンポーネント</div>
<div>あなたはのロールは{session?.user.roles}です。</div>
</main>
</div>
);
};
export default Page;

このように、クライアントサイドでのロールチェックは useSession フックを使用することで簡単に実装できます。ただし、機密性の高い処理や重要な認可判定はサーバーサイドで行うことをオススメします。

Get Session - Auth.js

サーバーコンポーネントでの実装

NextAuth 関数から取得できる auth 関数を使用することで、セッションを取得することができ、サーバーサイドでのロールチェックを行うことができます。

src/components/home.tsx
// ...
export const Home: React.FC = async () => {
const session = await auth();
if (!session) redirect("/");
const { user } = session;
// ...

Protecting Resources - Auth.js

まとめ

この記事では、Next.js アプリケーションに Keycloak の RBAC を実装する方法について説明しました。

実装のポイントは以下の通りです:

  1. Keycloak でのロール設定

    • クライアントロールを作成し、グループ経由でユーザーに割り当て
    • IDトークンにロール情報を含めるためのマッパー設定
  2. Next.js での実装

    • Auth.js のコールバック関数を使用したロール情報の取得
    • TypeScript による型定義の拡張
    • クライアント/サーバーそれぞれでのロールチェック実装
  3. セキュリティ考慮事項

    • パス単位のアクセス制御
    • 適切なエラーハンドリングとリダイレクト