Next.jsで認証を実装したことがなかったのでいちばん有名そうな Auth.js を使って実装してみる。
※ 実装してから気づいたのですが、Better Authが今後のスタンダードになるそう、、、orz
実装したソースコード
1. パッケージのインストール
pnpm add next-auth@beta
2. 環境変数の設定
issureを調べる
REALM_NAME=AppCommon
curl -sSkL "https://keycloak.prd.baseport.net/realms/$REALM_NAME/.well-known/openid-configuration" | jq -r ".issuer"
# https://keycloak.prd.baseport.net/realms/AppCommon
Auth.jsに必要な環境変数を設定します。
AUTH_SECRET=your-generated-secret-key # openssl rand -base64 32 で生成
AUTH_KEYCLOAK_ID=your-client-id
AUTH_KEYCLOAK_SECRET=your-client-secret
AUTH_KEYCLOAK_ISSUER=https://keycloak.prd.baseport.net/realms/AppCommon
AUTH_TRUST_HOST=true
-
AUTH_TRUST_HOST
リバースプロキシ経由でアプリケーションを公開する場合、AUTH_TRUST_HOST=trueに設定するとAuth.js はリバースプロキシから送信される X-Forwarded-Host ヘッダーを信頼するようになります。
3. Auth.js設定ファイルの作成
プロジェクトのルートにauth.tsを作成してAuth.jsの設定を作成します。
KeycloakをIDプロバイダとする場合の設定はこちらのドキュメントにのっていました。
import NextAuth from "next-auth"
import type { NextAuthConfig } from "next-auth"
import Keycloak from "next-auth/providers/keycloak"
// NextAuthConfig: https://authjs.dev/reference/nextjs#nextauthconfig
const config = {
// https://authjs.dev/reference/nextjs#pages
pages: {
signIn: "/login",
},
providers: [
// Keycloakプロバイダーの設定
// https://authjs.dev/getting-started/providers/keycloak#configuration
Keycloak({
clientId: process.env.AUTH_KEYCLOAK_ID!,
clientSecret: process.env.AUTH_KEYCLOAK_SECRET!,
issuer: process.env.AUTH_KEYCLOAK_ISSUER!,
}),
]
} satisfies NextAuthConfig
// 認証関数(`auth`, `signIn`, `signOut`)とAPIハンドラー(`handlers`)をエクスポート
// NextAuthResult: https://authjs.dev/reference/nextjs#nextauthresult
export const { handlers, signIn, signOut, auth } = NextAuth(config)
4. APIルートハンドラーの作成
Auth.jsが必要とする認証APIエンドポイントを提供するためのルートハンドラを作成します。
以下のエンドポイントが自動的に提供されます
-
/api/auth/signin/keycloak- ログイン開始 -
/api/auth/callback/keycloak- Keycloakからのコールバック(必須) -
/api/auth/signout- ログアウト -
/api/auth/session- セッション情報取得 -
/api/auth/csrf- CSRFトークン取得
※ Keycloakとの通信やセッション管理はすべてこのエンドポイントを経由するため、このファイルがないと認証が動作しません。
import { handlers } from "@/auth"
export const { GET, POST } = handlers
5. プロキシ(ミドルウェア)の作成
ページごとに認証チェックを実装するのでは非効率ですし、抜け漏れも発生しやすいのでミドルウェアを作成して認証の状態を一括チェックします。
役割:
- すべてのページアクセス前に認証状態をチェック
- 未認証ユーザーを
/loginにリダイレクト - 認証済みユーザーがログインページにアクセスした場合、
/dashboardにリダイレクト
import { auth } from "@/auth"
import { NextResponse } from "next/server"
// ミドルウェアで認証を強制する
export default auth((req) => {
// ユーザーがログインしているかどうか
const isLoggedIn = !!req.auth
const { pathname } = req.nextUrl
// 公開ページリスト
const publicPages = ["/", "/login"]
const isPublicPage = publicPages.includes(pathname)
// 未ログインで、公開ページ以外にアクセスしようとした場合
if (!isLoggedIn && !isPublicPage) {
return NextResponse.redirect(new URL("/login", req.url))
}
// ログイン済みで、ログインページにアクセスしようとした場合
if (isLoggedIn && pathname === "/login") {
return NextResponse.redirect(new URL("/dashboard", req.url))
}
// それ以外はそのまま通す
return NextResponse.next()
})
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
}
コード解説:
-
req.auth: ミドルウェア内で使える、既に取得済みのセッション情報 -
new URL("/login", req.url): 現在のドメイン情報を使って絶対URLを生成(環境に依存しない) -
matcher: ミドルウェアを実行するパスを指定(API、静的ファイル、画像は除外)
6. / ページの作成
/ ページではアクセス時にログイン状態に応じてリダイレクトを行います
- ログイン済み:
/dashboardにリダイレクト - 未ログイン:
/loginにリダイレクト
import { auth } from "@/auth"
import { redirect } from "next/navigation"
export default async function Home() {
const session = await auth()
if (session) {
redirect("/dashboard")
} else {
redirect("/login")
}
}
7. 認証が不要なページの作成 (/login)
/login ページはミドルウェアで保護されていないページなので、未ログイン状態でアクセス可能です。
このページではログインボタンを表示します。
import { signIn } from "@/auth"
export default function LoginPage() {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="w-full max-w-md space-y-4 p-8">
<h1 className="text-2xl font-bold text-center">ログイン</h1>
<form
action={async () => {
"use server"
await signIn("keycloak", { redirectTo: "/dashboard" })
}}
>
<button
type="submit"
className="w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600"
>
Keycloakでログイン
</button>
</form>
</div>
</div>
)
}
ポイント:
-
"use server": Server Actionとして実行 -
signIn("keycloak", { redirectTo: "/dashboard" }): Keycloakログイン後に/dashboardへリダイレクト
8. 認証が必要なページの作成 (/dashboard)
/dashboard はプロキシ(ミドルウェア)によって保護されたページで、ログイン済みの状態でしかアクセスできません。
認証オブジェクト( auth() )からユーザー名やメールアドレスを取得して表示してみます。
import { auth } from "@/auth"
import { SignOutButton } from "@/components/ui/SignOutButton"
export default async function DashboardPage() {
const session = await auth() // サーバーコンポーネント
return (
<div className="flex min-h-screen items-center justify-center">
<div className="w-full max-w-md space-y-4 p-8">
<h1 className="text-2xl font-bold text-center">ダッシュボード</h1>
<p>ようこそ、{session?.user?.name}さん</p>
<p>メール: {session?.user?.email}</p>
<SignOutButton />
</div>
</div>
)
}
ログアウトボタン
import { signOut } from "@/auth"
export function SignOutButton() {
return (
<form
action={async () => {
"use server"
await signOut({ redirectTo: "/login" })
}}
>
<button
type="submit"
className="bg-red-500 text-white py-2 px-4 rounded hover:bg-red-600"
>
ログアウト
</button>
</form>
)
}
9. 動作確認
pnpm dev
http://localhost:3000 にアクセス
http://localhost:3000/login
http://localhost:3000/dashboard
動作フロー
1. ユーザーが /dashboard にアクセス
↓
2. ミドルウェアが認証状態をチェック(req.auth)
↓
3. 未認証の場合 → /login にリダイレクト
↓
4. ログインボタンをクリック
↓
5. signIn("keycloak") が /api/auth/signin/keycloak を呼び出し
↓
6. Keycloakログインページへリダイレクト
↓
7. ユーザーがKeycloakでログイン
↓
8. /api/auth/callback/keycloak にコールバック
↓
9. セッション作成
↓
10. /dashboard にリダイレクト