ymmr
Recent Articles
    公開日: 2024/01/20

    NextAuth.js のセッションにユーザー ID を含めたい

    NextAuth.js の API で取得したセッションですが、デフォルトではユーザー ID を含んでいません。セッションのオブジェクトをカスタマイズして、型推論まで効かせる方法を備忘録としてまとめます。

    やりたいこと

    POST で API を叩くことで、ログイン中のユーザー ID を外部キーとして持つデータを永続化したい場面がありました。NextAuth.js の認証プロバイダーには Email (マジックリンク方式) を用いており、DB アダプターである Prisma と連携しています。

    /lib/next-auth/index.ts
    import { PrismaAdapter } from '@next-auth/prisma-adapter';
    import { PrismaClient } from '@prisma/client';
    import EmailProvider from 'next-auth/providers/email';
    
    import type { AuthOptions } from 'next-auth';
    
    const prisma = new PrismaClient();
    
    export const authOptions: AuthOptions = {
      adapter: PrismaAdapter(prisma),
      providers: [
        EmailProvider({
          server: process.env.EMAIL_SERVER,
          from: process.env.EMAIL_FROM,
        }),
      ],
    };
    
    /pages/api/posts/index.ts
    import { authOptions } from '@/lib/next-auth';
    import { PrismaClient } from '@prisma/client';
    import { getServerSession } from 'next-auth';
    
    import type { Prisma } from '@prisma/client';
    import type { NextApiRequest, NextApiResponse } from 'next';
    
    const prisma = new PrismaClient();
    
    const handler = async (req: NextApiRequest, res: NextApiResponse) => {
      if (req.method === 'POST') {
        // セッションオブジェクトを取得
        const session = await getServerSession(req, res, authOptions);
    
        if (!session || !session.user) {
          res.status(401).send({ error: 'Not signed in' });
    
          return;
        }
    
        const { title } = req.body as { title: string };
        // ユーザーオブジェクトから ID を取得
        // session.user に id プロパティは存在しないためエラーとなる
        const { id: authorId } = session.user;
        //     ^^^^^ Property 'id' does not exist on type'{ name?: string | null | undefined; email?: string | null | undefined; image?: string | null | undefined; } | undefined'
        const data = {
          title,
          authorId,
        } satisfies Prisma.PostUncheckedCreateInput;
    
        // DB にデータを保存
        await prisma.post.create({ data });
    
        res.status(204).end();
      }
    };
    
    export default handler;
    

    セッションの型定義を見ると、次のようになっていました。これを拡張する必要がありそうです。

    export interface DefaultSession {
      user?: {
        name?: string | null
        email?: string | null
        image?: string | null
      }
      expires: ISODateString
    }
    

    セッションオブジェクトにユーザー ID を含める

    調査の結果、セッションオブジェクトを拡張するには 2 つの手順が必要でした。

    1. Session callback を追加して、セッション取得のための API (getServerSession など) が返すオブジェクトを変更する
    2. セッションオブジェクトの型定義を上書きする

    Session callback を追加する

    認証プロバイダー等を設定している authOptions オブジェクトに callbacks.session を追加します。関数内では spread syntax で作り直したセッションオブジェクトを返すことで、session.userid プロパティを持つようにしています。

    /lib/next-auth/index.ts
     export const authOptions: AuthOptions = {
       adapter: PrismaAdapter(prisma),
    +  callbacks: {
    +    session: ({ session, user }) => ({
    +      ...session,
    +      user: {
    +        ...user,
    +        id: user.id,
    +      },
    +    }),
    +  },
       providers: [
         EmailProvider({
           server: process.env.EMAIL_SERVER,
    
    

    今回の様に認証プロバイダーと DB を連動している場合は、session callback の引数で受け取った user オブジェクトは User 型になるため id を取得できます。

    セッションオブジェクトの型定義を上書きする

    types/next-auth.d.ts を追加して、セッションオブジェクトの型定義を上書きします。DefaultSession を拡張する形で型をマージしているため、既存の情報を損ねることもありません。

    /types/next-auth.d.ts
    import type { DefaultSession } from 'next-auth';
    
    declare module 'next-auth' {
      interface Session {
        user: {
          id: string;
        } & DefaultSession['user'];
      }
    }
    
    

    これで API Route のハンドラー内で発生していたエラーも消えて、きちんと動作するようになりました。

    おわりに

    NextAuth.js はドキュメントもしっかり整備されており、今回の様なケースまで網羅されていました。この記事が何かの参考になれば幸いです。

    参考

    Callbacks | NextAuth.jsCallbacks are asynchronous functions you can use to control what happens when an action is performed.next-auth.js.org
    TypeScript | NextAuth.jsNextAuth.js has its own type definitions to use in your TypeScript projects safely. Even if you don't use TypeScript, IDEs like VSCode will pick this up to provide you with a better developer experience. While you are typing, you will get suggestions about what certain objects/functions look like, and sometimes links to documentation, examples, and other valuable resources.next-auth.js.org