NextAuth.jsでデフォルト定義されていない値を保存する方法

投稿日
NextAuth.jsでデフォルト定義されていない値を保存する方法

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の型を拡張

type.d.ts
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 を定義

src/auth.ts
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でエラーにならないようにモジュール拡張を使います!

type.d.ts
import NextAuth from 'next-auth'
 
declare module 'next-auth' {
  interface User {
    companyId: string
  }
}

NextAuthでは、ジェネリクスではなくモジュール拡張を使用している理由について公式ページに記載があります

Auth.js | Typescript



これでtsで「そんな値定義されていないよ」というエラーが起きなくなります

// エラーが起きなくなる
session?.user?.companyId


もちろん Session も同様に定義出来ます

type.d.ts
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のauthjs.session-tokenに認証情報を保存する


cookieへの保存・取得の流れの理解がとても難解だったので

  1. cookieへの保存を管理するJWT callbackについて
  2. cookieにデフォルト以外の値を保存する
  3. 2.の値を利用する

の順に

メール/パスワード認証を例に見ていきます。



「ログインボタンを押下したら認証情報が保存される」という超シンプル構成です



コードは長くなるので折りたたんでおいておきます

NextAuthでメール/パスワード認証ログイン時に固定値を返す

src/auth.ts
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を実行する

app/login/page.tsx
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

app/api/session-confirm/route.ts
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

src/auth.ts
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で固定の値を返すと常に決まった値が保存されます

src/auth.ts
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が呼ばれます


ログイン時の流れとしては

  1. ログイン処理
  2. JWT Callbackでcookieに保存する値を確定
  3. 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つをピックアップします

src/auth.ts
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'
}

その他の引数については

ライブラリのコード上の定義がかなり丁寧に解説されていて非常に分かりやすいです!

https://github.com/nextauthjs/next-auth/blob/f278079581aaf40c27e31f78ed2aab2f08bc6a2b/packages/core/src/index.ts#L431


(私が見た時は431行目近辺が該当行でしたが jwt?: (params: というワードで検索してもらえると表示されると思います)

2. cookieにデフォルト以外の値を保存する

お待たせしました!1番の本題部分です


JWT callbackの特徴で見てきた内容を使って値を保存します




まずUserの時と同じく、モジュール拡張をしてJWTでもcompanyIdを使えるようにします

type.d.ts
import { JWT } from 'next-auth/jwt'
 
declare module 'next-auth/jwt' {
  // JWTでcompanyIdを使用出来るようにする
  interface JWT {
    companyId: string
  }
}

returnする前にcompanyIdを token に含め、cookieに保存出来るようにします

src/auth.ts
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 を定義します

src/auth.ts
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>

としてアクセストークンやリフレッシュトークンを画面に表示してしまった、という事も起こり得ますもんね。



コード全文

最後にコード全文を置いておきます!


type.d.ts
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
  }
}

src/auth.ts
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)
プロフィール画像
Yuki Takara
都内でフリーランスのエンジニアをやってます。フロントとアプリ開発メインに幅広くやってます。