NextAuth v5では認証時に保存した値をRSC・Route Handler・クライアントで使用するコンポーネントなど、
様々な場面で利用することが出来ます。
import { NextResponse } from 'next/server'
import { auth } from '@/auth'
export async function GET() {
// auth() を使いRSC・Route HandlerなどでNextAuthで管理している情報を取得する
const session = await auth()
console.log(session?.user?.email)
return NextResponse.json({ session })
}
'use client'
import { SessionProvider, useSession } from 'next-auth/react'
export default function DashboardPage() {
// useSession() フックを使い、クライアントのコンポーネントでNextAuthで管理している情報を取得する
const session = useSession()
return (
<SessionProvider>
<div>{session.data?.user?.name}</div>
</SessionProvider>
)
}
最初のRSC・Route Handler内で使用したsessionという値はNextAuth.js内で以下のように定義されています
export interface User {
id?: string
name?: string | null
email?: string | null
image?: string | null
}
export interface Session extends DefaultSession {}
export interface DefaultSession {
user?: User
expires: ISODateString
}
type ISODateString = string
使用していく上で、デフォルトで定義された値以外を利用したいケースがあります。
もちろん何も設定しないとTypeScriptのエラーに引っ掛かかります
// Userで定義されていない companyId を利用したいが、
// 利用しようとすると TS2339: Property companyId does not exist on type となる
session?.user?.companyId
この記事ではNextAuth.jsでデフォルトで定義されていない値を保存し使えるようにしていきます!
検証した環境
1 | next-auth | 5.0.0-beta.19 |
2 | next | 14.1.3 |
先に結論
先に結論を書いておきます!
TypeScriptの型を拡張
import NextAuth from 'next-auth'
import { JWT } from 'next-auth/jwt'
// companyId を使用可能にする
declare module 'next-auth' {
interface User {
companyId: string
}
}
declare module 'next-auth/jwt' {
interface JWT {
companyId: string
}
}
useAuth()
・useSession()
で companyId を利用出来るように
callbacks > jwt ・ callbacks > session を定義
import NextAuth, { NextAuthConfig, User } from 'next-auth'
import Credentials from 'next-auth/providers/credentials'
export const authConfig = {
providers: [
Credentials({
async authorize() {
// 検証用としてログイン実行時に固定の値を返す
return { name: 'hoge', email: 'fuga@test.com', companyId: 'bar' }
},
}),
],
callbacks: {
async jwt({ token, user }) {
// userが存在する場合、保存するJWTにcompanyIdを追加する
if (user) {
token.companyId = user.companyId
}
return token
},
async session({ session, token }) {
session.user.companyId = token.companyId
return session
},
},
} satisfies NextAuthConfig
export const { signIn, auth } = NextAuth(authConfig)
ここから詳細を見ていきます
TypeScriptで使えるようにする
まず、tsでエラーにならないようにモジュール拡張を使います!
import NextAuth from 'next-auth'
declare module 'next-auth' {
interface User {
companyId: string
}
}
NextAuthでは、ジェネリクスではなくモジュール拡張を使用している理由について公式ページに記載があります
これでtsで「そんな値定義されていないよ」というエラーが起きなくなります
// エラーが起きなくなる
session?.user?.companyId
もちろん Session も同様に定義出来ます
import NextAuth from 'next-auth'
declare module 'next-auth' {
interface Session {
startAt: ISODateString
}
}
const session = await auth()
console.log(session?.startAt)
ただ、モジュール拡張をしたからといって
NextAuth.jsが自動的に値を保存してくれるようになるわけではありません。
ここからは拡張した値を保存し永続化出来るようにしていきます。
定義されていない値を保存する
NextAuth.jsは認証情報をcookieの authjs.session-token
に保存しています
cookieへの保存・取得の流れの理解がとても難解だったので
- cookieへの保存を管理するJWT callbackについて
- cookieにデフォルト以外の値を保存する
- 2.の値を利用する
の順に
メール/パスワード認証を例に見ていきます。
「ログインボタンを押下したら認証情報が保存される」という超シンプル構成です
コードは長くなるので折りたたんでおいておきます
NextAuthでメール/パスワード認証ログイン時に固定値を返す
import NextAuth, { NextAuthConfig } from 'next-auth'
import Credentials from 'next-auth/providers/credentials'
export const authConfig = {
providers: [
Credentials({
async authorize() {
// signIn('credentials') 実行時に呼ばれる
// 本来ここでログインのapiを呼んでその後の処理を分ける等を行うが、
// 固定値で値を返す
return { name: 'hoge', email: 'fuga@test.com' }
},
}),
],
} satisfies NextAuthConfig
export const { signIn, auth } = NextAuth(authConfig)
ブラウザからNextAuthのsignInを実行する
import { signIn } from '@/auth'
export default function LoginPage() {
async function login() {
'use server'
await signIn('credentials')
}
return (
<div>
<h1>ログイン</h1>
<form action={login}>
<button type="submit">ログイン</button>
</form>
</div>
)
}
認証情報を確認するためのRoute Handlers
import { NextResponse } from 'next/server'
import { auth } from '@/auth'
export async function GET() {
const session = await auth()
console.log(session?.user?.companyId)
return NextResponse.json({ session })
}
1. cookieへの保存を管理するJWT callbackについて
cookieへの認証情報を保存する処理の拡張には callbacks > jwt を用います
https://next-auth.js.org/configuration/callbacks#jwt-callback
export const authConfig = {
providers: [
Credentials({
async authorize() {
return { name: 'hoge', email: 'fuga@test.com' }
},
}),
],
callbacks: {
async jwt({ token, user }) {
// returnした値がcookieに保存される
return token
},
},
} satisfies NextAuthConfig
JWT callback は大きく3つの特徴があります
特徴1 returnした値をcookieに保存する
JWT callbackはreturnした値をcookieに保存します
そのため、returnで固定の値を返すと常に決まった値が保存されます
callbacks: {
async jwt({ token, user }) {
return {
...token,
name: 'Yamada Taro',
}
},
const session = await auth()
console.log(session?.user?.name)
// => `Yamada Taro`
特徴2 呼ばれるタイミング
- ログインのような認証情報を保存する時
auth()
・useSession()
のような認証情報を使う時
にJWT Callbackが呼ばれます
ログイン時の流れとしては
- ログイン処理
- JWT Callbackでcookieに保存する値を確定
- encodeで指定した方法でencodeしcookieに保存
となります。
以下の場合 ‘①authorize’ → ‘②jwt’ → ‘③encode’ の順で出力されます
export const authConfig = {
providers: [
Credentials({
async authorize() {
console.log('①authorize')
return { name: 'hoge', email: 'fuga@test.com', companyId: 'bar' }
},
}),
],
callbacks: {
async jwt({ token, user }) {
console.log('②jwt')
return token
},
},
jwt: {
encode: async ({ token, secret }) => {
console.log('③encode')
return encode({ token, secret, salt: 'your-custom-salt' })
},
},
} satisfies NextAuthConfig
特徴3 引数について
重要な引数 token
user
2つをピックアップします
export const authConfig = {
providers: [
Credentials({
async authorize() {
return { name: 'hoge', email: 'fuga@test.com', companyId: 'bar' }
},
}),
],
callbacks: {
async jwt({ token, user }: { token: JWT; user: User }) {
console.log(token)
console.log(user)
return token
},
},
} satisfies NextAuthConfig
token・・cookieに保存されている値(最初のログイン時は保存されるであろう値)
console.log(token)
{
name: 'hoge',
email: 'fuga@test.com',
picture: undefined,
sub: '7cf8c61b-cfc6-4879-b492-d3623c2c2781'
}
user・・ログイン処理でreturnした値
console.log(user)
{
name: 'hoge',
email: 'fuga@test.com',
companyId: 'bar',
id: '7cf8c61b-cfc6-4879-b492-d3623c2c2781'
}
その他の引数については
ライブラリのコード上の定義がかなり丁寧に解説されていて非常に分かりやすいです!
(私が見た時は431行目近辺が該当行でしたが jwt?: (params:
というワードで検索してもらえると表示されると思います)
2. cookieにデフォルト以外の値を保存する
お待たせしました!1番の本題部分です
JWT callbackの特徴で見てきた内容を使って値を保存します
まずUserの時と同じく、モジュール拡張をしてJWTでもcompanyIdを使えるようにします
import { JWT } from 'next-auth/jwt'
declare module 'next-auth/jwt' {
// JWTでcompanyIdを使用出来るようにする
interface JWT {
companyId: string
}
}
returnする前にcompanyIdを token
に含め、cookieに保存出来るようにします
export const authConfig = {
providers: [
Credentials({
async authorize() {
return { name: 'hoge', email: 'fuga@test.com', companyId: 'bar' }
},
}),
],
callbacks: {
async jwt({ token, user }) {
// userが存在する場合、保存するJWTにcompanyIdを追加する
if (user) {
token.companyId = user.companyId
}
return token
},
},
} satisfies NextAuthConfig
これでcookieにcompanyIdが保存出来ているのですが、利用する上での罠があります!!
3. cookieに保存した値を利用する
2.までの手順でcookieに保存はされるものの、
まだ auth()
や useSession()
を使っても表示されません。
const session = await auth()
console.log(session?.user?.companyId)
// => undefined になってしまう
最後のひと手間として callback > session
を定義します
export const authConfig = {
providers: [
Credentials({
async authorize() {
return { name: 'hoge', email: 'fuga@test.com', companyId: 'bar' }
},
}),
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.companyId = user.companyId
}
return token
},
async session({ session, token }) {
session.user.companyId = token.companyId
return session
},
},
} satisfies NextAuthConfig
Session callback は auth()
や useSession()
のような、戻り値に Session型 が返ってくる関数が実行される前に呼ばれる関数です。
const session = await auth()
console.log(session?.user?.companyId)
// => 'bar'
auth()
でも表示されるようになりました!🎉
sessionをあえて定義する理由
「セキュリティを高めるため」 と公式に書かれています
デフォルトでは、セキュリティを高めるためにトークンのサブセットのみが返されます。jwt()コールバックでトークンに追加したもの(上記のaccess_tokenやuser.idなど)を利用可能にしたい場合は、明示的にここに転送してクライアントが利用できるようにする必要があります。
Callbacks | NextAuth.js より引用(DeepL翻訳)
確かに、意図せず
<div>{session?.user?.accessToken}</div>
<div>{session?.user?.refreshToken}</div>
としてアクセストークンやリフレッシュトークンを画面に表示してしまった、という事も起こり得ますもんね。
コード全文
最後にコード全文を置いておきます!
import NextAuth from 'next-auth'
import { JWT } from 'next-auth/jwt'
// companyId を使用可能にする
declare module 'next-auth' {
interface User {
companyId: string
}
}
declare module 'next-auth/jwt' {
interface JWT {
companyId: string
}
}
import NextAuth, { NextAuthConfig, User } from 'next-auth'
import Credentials from 'next-auth/providers/credentials'
export const authConfig = {
providers: [
Credentials({
async authorize() {
// 検証用としてログイン実行時に固定の値を返す
return { name: 'hoge', email: 'fuga@test.com', companyId: 'bar' }
},
}),
],
callbacks: {
async jwt({ token, user }) {
// userが存在する場合、保存するJWTにcompanyIdを追加する
if (user) {
token.companyId = user.companyId
}
return token
},
async session({ session, token }) {
session.user.companyId = token.companyId
return session
},
},
} satisfies NextAuthConfig
export const { signIn, auth } = NextAuth(authConfig)