Next.js 13 App Router での Auth.js の使い方
本ブログは、App Router
方式でのAuth.js
の使い方の説明になります。公式ページはPage Router
方式での手順のため、ご参考までに執筆しました。また、Next.jsで絶賛開発中のサーバ・アクションでの動きも調べました。ソースコードはこちらにあります。
yarn add next-auth
openssl rand -base64 32
.env
に環境変数を設定します。
NEXTAUTH_SECRET="vhXFoNfK (生成した文字列) N2hvFXaRKI="
NEXTAUTH_URL=http://localhost:3000
Route Handler
に認証処理のエンドポイントを作成します。Page Router
方式とはパスが違うので注意してください。
import { options } from "@/app/options";
import NextAuth from "next-auth";
const handler = NextAuth(options);
export {handler as GET,handler as POST}
options.ts
にサポートする認証方式を設定します。ここでは、メルアド認証とOAuth(Github,Google等)を設定します。
コンストラクターに渡すオプションです。Page Router方式と特に違いはありませんので、さらっと流します。各種OAuthの設定も同じなので説明は省きます。
import type {NextAuthOptions} from "next-auth";
import GitHubProvider from "next-auth/providers/github";
import GoogleProvider from "next-auth/providers/google";
import CredentialsProvider from "next-auth/providers/credentials";
export const options: NextAuthOptions = {
debug: true,
session: {strategy: "jwt"},
providers: [
GitHubProvider({
clientId: process.env.GITHUB_ID!,
clientSecret: process.env.GITHUB_SECRET!,
}),
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!
}),
CredentialsProvider({
name: "Sign in",
credentials: {
email: {
label: "Email",
type: "email",
placeholder: "example@example.com",
},
password: {label: "Password", type: "password"},
},
// メルアド認証処理
async authorize(credentials) {
const users = [
{id: "1", email: "user1@example.com", password: "password1"},
{id: "2", email: "user2@example.com", password: "password2"},
{id: "3", email: "abc@abc", password: "123"},
];
const user = users.find(user => user.email === credentials?.email);
if (user && user?.password === credentials?.password) {
return {id: user.id, name: user.email, email: user.email, role: "admin"};
} else {
return null;
}
}
}
),
],
callbacks: {
jwt: async ({token, user, account, profile, isNewUser}) => {
// 注意: トークンをログ出力してはダメです。
console.log('in jwt', {user, token, account, profile})
if (user) {
token.user = user;
const u = user as any
token.role = u.role;
}
if (account) {
token.accessToken = account.access_token
}
return token;
},
session: ({session, token}) => {
console.log("in session", {session, token});
token.accessToken
return {
...session,
user: {
...session.user,
role: token.role,
},
};
},
}
}
;
ログイン、ログアウトのボタンを用意します。使いまわせるようにコンポーネント化します。
"use client";
import {signIn, signOut} from "next-auth/react";
// ログインボタン
export const LoginButton = () => {
return (
<button style={{marginRight: 10}} onClick={() => signIn()}>
Sign in
</button>
);
};
// ログアウトボタン
export const LogoutButton = () => {
return (
<button style={{marginRight: 10}} onClick={() => signOut()}>
Sign Out
</button>
);
};
ログイン画面を作成します。(あとでもう少し真面目に実装します。)
import {
LoginButton,
LogoutButton,
} from "@/app/components/buttons";
export default async function Home() {
return (
<main
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "70vh",
}}
>
<div>
<LoginButton/>
<LogoutButton/>
</div>
</main>
);
}
ここまでで一旦完了です。とりあえず、何か動きます。
つぎは認可処理
認証(Authentication)処理が通過したら、つぎは認可(Authorization)処理に進みます。
実際のWebサービスでは次のようなことをする必要があるかと思います。
ログインユーザがゲストか管理者によって、
- エンドポイントへのアクセス権を変える。403Forbiddenエラー
- ボタンやメニューなどのUIを変える
- JSONレスポンスのフィールドの出しわけ
1、3番目はmiddleware
やRouter Handler
に仕掛けます。2番目はReactコンポーネントのレンダリング処理で分岐処理します。
それを踏まえて、ミドルウェアー、ラウター・ハンドラー、サーバー・コンポーネント、サーバー・アクション、クライアント・コンポーネントの順に見ていきたいと思います。
ミドルウェア
next-auth
が提供するnext-auth/middleware
をインポートすると簡単に実装できます。config.matcher
にプロテクションを除外するURLパターンを設定します。ここでは、/loginと/apiを認可チェックから外します。
export { default } from "next-auth/middleware"; // defaultをママ使う。
export const config = {
matcher: ["/((?!register|api|login).*)"], // ?!で否定です。
};
Webブラウザから http://localhost:3000/hogehoge
とタイプしてみてください。hogehogeは適当になんでも良いです。
すると以下のように、/api/auth/signin
にリダイレクトされます。GETパラメータcallbackUrl
に最初にアクセスを試みたページhogehogeがコールバック先として設定されます。
http://localhost:3000/api/auth/signin?callbackUrl=%2Fhogehoge
カスタマイズしたいときは、withAuth
でラップします。第2引数のオプションのcallbacks.authorized
で認可処理をカスタマイズしています。デフォではJWTトークンがあれば許可し、middleware
に進みます。ここでは、role
がadmin
の場合だけ許可しています。この場合、adminしか利用できないWebサービスになります。
import {withAuth} from "next-auth/middleware"
export default withAuth(
function middleware(req) {
// callbacks.authorizedがtrueの場合のみ進入できる
console.log('in middleware: ', req.nextauth.token)
},
{
callbacks: {
// 認可に関する処理。ロールが `admin` ならOK
authorized: ({token}) => {
console.log('in authorized: ', token)
return token?.role === "admin"
// if(token) return true // デフォ
},
},
}
)
yarn dev コンソール出力
in jwt { id: '3', name: 'abc@abc', email: 'abc@abc', role: 'admin' }
in middleware: {
name: 'abc@abc',
email: 'abc@abc',
sub: '3',
user: { id: '3', name: 'abc@abc', email: 'abc@abc', role: 'admin' },
role: 'admin',
iat: 1686210417,
exp: 1688802417,
jti: '88afc9a2-0203-4db6-8593-6000000000a'
}
Route Handlers (API)
Route Handlers
に実装します。
app/my-api-auth/route.ts
という名前で以下のようなRoute Handler
を作成します。
import {NextResponse} from 'next/server';
import {getServerSession} from "next-auth/next"
import {options} from "@/app/options";
export async function GET() {
const session = await getServerSession(options) // セッション情報を取得
console.log(session?.user) // ユーザ情報を取得
return NextResponse.json({message: "ok"});
}
cURLでエンドポイントを叩いてみましょう。
curl -v http://localhost:3000/my-api-auth
すると、以下のようにmiddlewareのJWTトークンの存在チェックcallbacks.authorized
がfalse
になり、signin
ページに307リダイレクトされます。なぜなら、cURLコマンドでクッキーを飛ばしていないからです。
では今度はブラウザからアクセスして見てください。ログインページに転送され、ログインが成功するとクッキーにJWTトークンが保存されmiddlewareをパスし/my-api-auth
にアクセスできます。
この仕込まれたクッキー(JWT)をコピペして、cURLで-H 'Cookie: next-auth.session-token=...
で飛ばしてあげればアクセスできます。
curl -v http://localhost:3000/my-api-auth -H 'Cookie: next-auth.session-token=eyJhbGc...'
さて、ここでsession.user
の中身をデバッガーで見てみると、JWTトークンの内容とは少し違うようですね。
どうやらセキュリティ上の理由でトークンのプロパティの一部だけがuser
オブジェクトにコピーされるそうです。そこで、Session Callbackでtokenの値をuserに追加でコピーしてあげます。ここの例ではroleをコピーしています。
session: ({ session, token }) => {
console.log("in session", { session, token });
return {
...session,
user: {
...session.user,
role: token.role, // tokenの値をコピーしとく
},
};
},
するとめでたく、roleがuserオブジェクトにも設定されたのが確認できました。
{ name: 'abc@abc', email: 'abc@abc', image: undefined, role: 'admin' }
サーバー・コンポーネント
以前作成したログイン画面を改造します。getServerSession()
をawaitで呼ぶとsession
が取得できます。先ほどsession callback
で仕込んだオブジェクトです。userオブジェクトの存在チェックでログイン/ログアウト状態を判定します。それ応じてボタンを出し分けます。
(説明に不要な箇所は削っています)
import {getServerSession} from "next-auth/next";
import {options} from "@/app/options";
export default async function Home() {
const session = await getServerSession(options)
const user = session?.user // ログインしていなければnullになる。
return (
<main
style={{...}}
>
<div>
<div>{`${JSON.stringify(user)}`}</div>
{user ? <div>Logged in</div> : <div>Not logged in</div>}
{user ? <LogoutButton/> : <LoginButton/>}
</div>
</main>
);
}
他にも、ロールで分岐処理させたり、GitHubのトークンを取り出してGitHubのAPIを叩くなどのユースケースが考えられます。
サーバ・アクション
サーバ・アクションとはRPC風にクライアントからサーバーの関数を叩く仕組みです。APIの代替となる方式になります。この際に、middlewareやauthは機能するのか気になるところですので調査しました。
login-server-action/page.tsx
を作成します。login*
にマッチするのでmiddlewareはバイパスします。サーバ・アクションはRPC的なものですがエンドポイントはこのフォームを表示したパスになります。
import {getServerSession} from "next-auth/next";
import {options} from "@/app/options";
export default function ActionForm() {
// サーバ・アクション
async function addItem(formData: FormData) {
'use server'; // This is required for server actions.
const session = await getServerSession(options)
const user = session?.user
console.log('user:', user)
}
return (
<div>
<form action={addItem}>
<input type="text" name="id" defaultValue="A001" className="bg-gray-500"/>
<button type="submit">カートに追加</button>
</form>
</div>
)
}
正常にuserが取得できました。
user: {
name: 'TF',
email: 'tf@gmail.com',
image: 'https://lh3.googleusercontent.com/...',
role: undefined,
accessToken: 'ya29.a0A...' // session callbackでセットしてます。
}
次に、ミドルウェアは呼ばれるのかを検証します。フォルダ名をserver-action
に変更し、middleware
を通るようにします。
デバッガーでmiddleware.tsにブレーク・ポイントを設置して確認します。ご覧のようにリクエストが通過したのが分かります。
クライアント・コンポーネント
SessionProvider
とuseSession()
を使用します。使い回せるように、以下のようにNextAuthProvider
を作ります。
"use client";
import {SessionProvider} from "next-auth/react";
type Props = {
children?: React.ReactNode;
};
export const NextAuthProvider = ({children}: Props) => {
return <SessionProvider>{children}</SessionProvider>;
};
前回作成したログイン画面をクライアント・コンポーネントに改造してみましょう。
先ほど作成したNextAuthProvider
でラップします。そうすると、ログイン画面・コンポーネントから、useSession()
でセッション情報を取得できます。
'use client';
import {NextAuthProvider} from "@/app/providers";
import {useSession} from "next-auth/react";
export default async function Home() {
return (
<NextAuthProvider>
<ClientHome/> // ここにログイン画面部品を挟みます。
</NextAuthProvider>
)
}
function ClientHome() {
const {data: session} = useSession();
const user = session?.user
useEffect(() => {
console.log("session.user", session?.user)
}
, [session])
return (
<main
style={{...}}
>
<div>
<div>{`${JSON.stringify(user)}`}</div>
{user ? <div>Logged in</div> : <div>Not logged in</div>}
{user ? <LogoutButton/> : <LoginButton/>}
</div>
</main>
);
}
Chrome Dev Tool
で確認すると、HTTPリクエストを投げてサーバー側で認証処理をしているのが分かります。
Vercelへのデプロイ
環境変数をVercelのダッシュボード> Project Settings
> Environment Variables
から設定する必要があります。
NEXTAUTH_SECRET
GITHUB_ID
GITHUB_SECRET
GOOGLE_CLIENT_ID
GOOGLE_CLIENT_SECRET
OAuthのコールバックのURLを本番環境のエンドポイントに変更してください。
ex. ) http://localhost:3000/api/auth/callback/github Vercelのドメインに向ける。
参考
Discussion
端折りすぎてて初心者には難しいです><
SessionProviderってsessionを引数に渡す必要ないんですか?