最初に結論
Auth.js / NextAuth で、外部API用の accessToken を session() callback の返り値に載せてはいけません。
次のようなコードは危険です。
callbacks: {
async session({ session, token }) {
session.accessToken = token.accessToken;
return session;
},
}
なぜなら、session() callback の返り値はクライアントから見えるからです。
session はクライアントに見える
session() callback の返り値は、次の経路でブラウザ側に露出します。
useSession()/api/auth/session- クライアント側の
getSession()
たとえば、Client Componentで次のように読めます。
"use client";
import { useSession } from "next-auth/react";
export function DebugSession() {
const { data } = useSession();
console.log(data?.accessToken);
return null;
}
accessToken を session に入れると、ブラウザのJavaScriptから読める状態になります。これは、ブラウザにaccess tokenを渡さないというBFF構成の考え方と矛盾します。
注意: HttpOnly Cookie に保存しているつもりでも、session callback で accessToken を返すと、/api/auth/session 経由でブラウザに公開してしまいます。
jwt callback と session callback の違い
Auth.js / NextAuth では、jwt() callback と session() callback は役割が違います。
| callback | 役割 | クライアントから見えるか |
|---|---|---|
jwt() | トークン内部に情報を保存する | 直接は見えない |
session() | クライアントに返すsessionの形を作る | 見える |
jwt() の token に accessToken を保持することと、session() の session に accessToken を載せることは別です。
悪い例
次のコードは、外部API用の accessToken をクライアントに公開します。
callbacks: {
async jwt({ token, account }) {
if (account) {
token.accessToken = account.access_token;
}
return token;
},
async session({ session, token }) {
session.accessToken = token.accessToken;
return session;
},
}
jwt() で保持した値を、session() で公開コピーしているのが問題です。
よい例
session() には、UI表示に必要な最小限の情報だけを載せます。
callbacks: {
async jwt({ token, account }) {
if (account) {
token.accessToken = account.access_token;
token.refreshToken = account.refresh_token;
token.expiresAt = account.expires_at;
}
return token;
},
async session({ session, token }) {
if (token.sub) {
session.user.id = token.sub;
}
return session;
},
}
この構成では、accessToken はクライアントのsessionには出ません。
access token はサーバ専用ヘルパで取り出す
外部APIを呼ぶときは、Server Component、Server Action、Route Handlerなどのサーバ側から取り出します。
import "server-only";
import { getToken } from "next-auth/jwt";
import { headers } from "next/headers";
export async function getAccessToken() {
const reqHeaders = await headers();
const token = await getToken({
req: { headers: reqHeaders } as any,
secret: process.env.AUTH_SECRET,
});
return token?.accessToken;
}
import "server-only" を入れることで、クライアント側から誤ってimportした場合に検出しやすくなります。
Server Component で使う
import { auth } from "@/auth";
import { getAccessToken } from "@/lib/server/access-token";
import { redirect } from "next/navigation";
export default async function OrdersPage() {
const session = await auth();
if (!session) redirect("/login");
const accessToken = await getAccessToken();
const res = await fetch("https://api.example.com/orders", {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
const orders = await res.json();
return <OrderList orders={orders} />;
}
この accessToken はサーバ側のローカル変数です。Client Componentへpropsで渡さない限り、ブラウザには露出しません。
session に載せてよいもの
session() に載せてよいのは、UIに必要な最小限の情報です。
| 載せてもよい候補 | 理由 |
|---|---|
user.id | UIや権限制御に必要な場合がある |
user.name | 表示名 |
user.email | 表示や問い合わせに必要な場合 |
role | UI出し分けに必要な場合 |
載せてはいけないものは、外部APIを呼べるトークンや長期秘密です。
| 載せないもの | 理由 |
|---|---|
accessToken | APIを呼べる |
refreshToken | 長期的に強い権限を持つ |
| provider の秘密情報 | 漏洩リスクが高い |
まとめ
Auth.js / NextAuth では、jwt() と session() の違いを理解することが重要です。
jwt()の token はサーバ側で扱う内部情報session()の session はクライアントに返す情報session.accessToken = token.accessTokenは公開コピーになる- access token はサーバ側ヘルパで取り出す
- Client Componentや
/api/auth/sessionに秘密を出さない
参考リソース
- 公式ドキュメント - NextAuth / Auth.js で accessToken を session に載せてはいけない理由 を確認するための一次情報