はじめに
仕事で Next.js を使っているプロジェクトがあり、コーディングを担当している方からコードレビューの依頼を受けました。私自身 Next.js はあまり触ったことがなく、Auth.js と組み合わせてログイン機能を実装しているコードを見てレビューが難しい状況でした。
そこで今回は、Next.js と Auth.js を使ってログイン機能を実装する方法を学ぶことにしました。
成果物
https://github.com/kntks/blog-code/tree/main/2025/01/nextjs-keycloak-login
環境
バージョン
バージョン Mac Ventura 13.2.1 Keycloak 26.0.7 Docker 26.0.0 Docker Compose v2.24.5 Terraform 1.10.3 Node.js v22.12.0
環境構築
Keycloakのセットアップ
Keycloak を docker compose で起動します。
image : quay.io/keycloak/keycloak:26.0.7
- KEYCLOAK_ADMIN_PASSWORD=admin
- ./keycloak:/opt/keycloak/data
Terraform を使用するために Keycloak側のセットアップ が必要です。
client_secret = "client secretをコピペ"
url = "http://localhost:8080"
terraform apply で Keycloak の設定を行います。
この実行により、Keycloak にレルム、クライアント、ユーザーを作成します。
terraform apply -auto-approve
プロジェクト作成
create-next-app を使ってプロジェクトを作成します。
pnpm dlx create-next-app nextjs-keycloak-login --ts --app --turbopack --import-alias "@/*" --tailwind --src-dir --no-eslint --use-pnpm
使わない画像データを削除します。
rm -f src/app/favicon.ico public/ *
eslint の代わりに、biome を使用します。
pnpm add --save-dev --save-exact @biomejs/biome
pnpm dlx @biomejs/biome init
package.json に biome のコード整形用スクリプトを追加します。
"dev" : "next dev --turbopack" ,
"fmt" : "biome check --write ./src"
Auth.jsのインストール
pnpm add -E next-auth@beta
環境変数を設定します。
AUTH_KEYCLOAK_SECRET=client secretをコピペ
AUTH_KEYCLOAK_ISSUER=http://localhost:8080/realms/myrealm
shadcn/uiのインストール
pnpm dlx shadcn@latest add card button
実装
前のセクションで環境構築が済んだので、実装を進めます。
Auth.jsの設定
import NextAuth from "next-auth" ;
import Keycloak from "next-auth/providers/keycloak" ;
export const { handlers , signIn , signOut , auth } = NextAuth ({
import { handlers } from "@/auth" ;
export const { GET , POST } = handlers ;
参考:
ログインページの作成
今回はログイン完了後、/hello
にリダイレクトさせます。
先にログインボタンを作成します。
import { signIn , signOut } from "@/auth" ;
import { Button } from "@/components/ui/button" ;
export const LoginButton : React . FC = () => {
await signIn ( "keycloak" , { redirectTo : "/hello" });
export const LogoutButton : React . FC = () => {
await signOut ({ redirectTo : "/" });
トップページはログインボタンのみ配置します。
import { LoginButton } from "@/components/login" ;
const Page : React . FC = () => {
< 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" >
見た目は以下のようになります。
ボタンをクリックすると Keycloak のログインページに遷移します。
今回は、Terraform でユーザーとパスワードともに myuser
として作成しています。
ログイン後のページの作成
Keycloak でログイン後、/hello
にリダイレクトされます。
/hello
にはログイン情報を表示するコンポーネントを配置します。
import { Hello } from "@/components/hello" ;
const Page : React . FC = () => {
< 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" >
import { auth } from "@/auth" ;
} from "@/components/ui/card" ;
import { LogoutButton } from "./button" ;
export const Hello : React . FC = async () => {
const session = await auth ();
< Card className = "w-[500px]" >
< CardTitle >ようこそ </ CardTitle >
{ session ?. user ?. name }さん、ログインに成功しました!
< p >メールアドレス : { session ?. user ?. email }</ p >
< pre className = "mt-2 w-[450px] rounded-md bg-slate-950 p-4" >
< code className = "text-white" >
{ JSON . stringify ( session ?. user , null , 2)}
< CardFooter className = "flex justify-between" >
Keycloak でログイン後、/hello
にリダイレクトされます。
SessionProvider の設定
今回はログインページと Hello ページ共に Server Components を使用したため、Auth.js の useSession
を必要としませんでした。
しかし、Client Components (ファイルの先頭に "use client"
を書く React コンポーネント)でセッション情報を取り扱う場合、SessionProvider
を設定しておく必要があります。
今後追加で実装することを想定して、SessionProvider
を設定しておきます。
import { SessionProvider } from "next-auth/react" ;
const Providers : React . FC < React . PropsWithChildren > = ({ children }) => {
return < SessionProvider >{ children } </ SessionProvider > ;
export default Providers ;
import Providers from "@/providers" ;
export default function RootLayout ({
children : React . ReactNode ;
className = { ` ${ geistSans . variable } ${ geistMono . variable } antialiased` }
< Providers >{ children } </ Providers >
SessionProvider
でラップされたコンポーネント内で useSession
フックを使用できるようにします。以下は公式のサンプルコードです。
import { useSession } from "next-auth/react"
export default function Dashboard () {
const { data : session } = useSession ()
if ( session ?. user ?. role === "admin" ) {
return < p > You are an admin , welcome !</ p >
return < p > You are not authorized to view this page !</ p >
引用:https://authjs.dev/getting-started/session-management/get-session
ミドルウェアの設定
これまでの設定により、Keycloak を使用したログイン機能が実装できました。
ただし、現状ではブラウザから /hello
に直接アクセスすると、認証なしで hello コンポーネントが表示されてしまいます。
そこで、/hello
などの認証が必要なページへのアクセスについて、未ログインの場合はログインページへ自動的にリダイレクトする設定を行いたいと思います。
この要件は、Next.js のミドルウェアを使用することで実現でき、Next.js の公式ドキュメントでも Middleware のユースケースとして紹介されています。
認証と認可: ユーザーの身元を確認し、特定のページやAPIルートへのアクセスを許可する前にセッションCookieを確認します。
サーバーサイドリダイレクト: 特定の条件(例:ロケール、ユーザーロール)に基づいて、サーバーレベルでユーザーをリダイレクトします。
パスの書き換え: A/Bテスト、機能展開、またはレガシーパスのサポートのために、リクエストプロパティに基づいてAPIルートやページへのパスを動的に書き換えます。
ボット検出: ボットトラフィックを検出しブロックすることで、リソースを保護します。
ログと分析: ページやAPIがリクエストを処理する前に、リクエストデータを収集し分析してインサイトを得ます。
-フィーチャーフラグ: 機能展開やテストをスムーズに行うために、機能を動的に有効化または無効化します。
引用:Middleware - Next.js
ミドルウェアを設定するには、プロジェクトのルートに middleware.ts
というファイルを作成する必要があります。
Use the file middleware.ts (or .js) in the root of your project to define Middleware. For example, at the same level as pages or app, or inside src if applicable.
訳:ミドルウェアを定義するには、プロジェクトのルートにあるmiddleware.ts(または.js)ファイルを使用します。 例えば、pagesやappと同じレベルか、該当する場合はsrcの中です。
引用:Convention - Next.js
どこに認可ロジックを書くか
ロジックを書く場所は2通り存在し、middleware.ts
か auth.ts
のどちらかです。
今回は、auth.ts
に書く方法を採用します。理由は、認可以外のミドルウェアを設定する場合、上手に分離できるためです。
ミドルウェアで認可のみ行う場合は、middleware.ts
でも auth.ts
でもどちらでも良いと思います。
middleware.ts に書く場合
auth メソッドにロジックを追加することで拡張できます。
import { auth } from "./auth"
export default auth (( req ) => {
auth.ts に書く場合
callbacks.authorized にロジックを追加することで拡張できます。このコールバックは、ミドルウェアを設定した場合に実行されます。
ミドルウェアで認可のみ行う場合、middleware.ts には auth メソッドをそのまま export します。
export { auth as middleware } from "@/auth" ;
しかし、他のミドルウェアを設定することを考慮して以下のように compose
関数を実装します。
import { auth } from "@/auth" ;
type NextAuthMiddleware = typeof auth ;
// ref: https://github.com/nextauthjs/next-auth/discussions/8961#discussioncomment-9625060
... fns : ( NextAuthMiddleware | NextMiddleware )[]
const validMiddlewares = fns . filter (( fn ) => typeof fn === "function" );
return async ( req : NextRequest , event : NextFetchEvent ) => {
for ( const fn of validMiddlewares ) {
const response = await fn ( req , event );
if ( response instanceof NextResponse || response instanceof Response )
return NextResponse . next ();
function log ( req : NextRequest , _event : NextFetchEvent ) {
console . log ( "request path" , req . nextUrl . pathname );
const middlewares : NextMiddleware [] = [ log , auth ];
export default compose ( ... middlewares );
"/((?!api/auth|_next|[^?]* \\ .(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)" ,
} satisfies MiddlewareConfig ;
middleware.ts に書いた config の mathcer では、トップページである “http://localhost:3000 ” にリクエストした場合でもミドルウェアの処理が実行されてしまいます。そのため、トップページではミドルウェアを実行しないようにするために、if (request.nextUrl.pathname === "/") return true;
という条件を入れています。
それ以外のページにアクセスした場合は、auth
の存在を確認し、auth
が存在しない場合は、リダイレクトします。
import NextAuth from "next-auth" ;
import Keycloak from "next-auth/providers/keycloak" ;
export const { handlers , signIn , signOut , auth } = NextAuth ({
authorized : async ({ request , auth }) => {
if ( request . nextUrl . pathname === "/" ) return true ;
return NextResponse . redirect ( new URL ( "/" , request . nextUrl . origin ));
ログイン後、Cookie に保存されたセッショントークンを削除して、再度 /hello
にアクセスしてみます。
問題なくログインページにリダイレクトされました。
参考:
まとめ
今回は、Next.js と Auth.js を使ってログイン機能を実装する方法を学びました。具体的には、ミドルウェアを使用して認証を行い、認証されていないユーザーをサインインページにリダイレクトする方法を実装しました。また、複数のミドルウェアを組み合わせて使用する方法についても学びました。
この実装により、認証が必要なページへのアクセスを制御し、セキュリティを強化できます。今回の実装の途中で、Auth.js のコードリーディングをして分かったこともありました。
今後は、さらに詳細な認可制御や、セッションの有効期限の管理など、より高度な機能を追加して記事を書いていきたいと思います。