Next.jsでNextAuth.jsを使用したJWT認証の実装方法

Next.jsにはSupabaseやFirebase、Userbaseなど多くの認証手段がありますが、ここではNextAuth.jsと既存のDjangoバックエンドを連携させたJWTセッション管理に焦点を当てて説明します。特定のユースケースに集中するため、不要な機能は省略していきます。

NextAuth.jsを選択する理由

このフルスタックライブラリは主要OAuthプロバイダーとの統合をサポートし、メール認証にも対応しています(セキュリティ向上のため推奨)。必要に応じてカスタムOAuthや認証プロバイダーを作成することも可能です。このライブラリの魅力は、標準的なフローとデフォルトハンドラーを提供しつつ、各ステップを柔軟にカスタマイズできる点です。

最小限の設定構成

yarn add next-authまたはnpm install next-authでパッケージをインストール後、APIルート/api/auth/[...nextauth].tsに設定ファイル[...nextauth].tsを作成します。これにより、/api/auth/*へのすべてのリクエストがNextAuth.jsによって処理されます。

// pages/api/auth/[...nextauth].ts

import NextAuth from 'next-auth';

export default async function authenticationHandler(
  request: NextApiRequest,
  response: NextApiResponse
) {
  return await NextAuth(request, response, {
    providers: [ /* プロバイダー設定 */ ],
    session: {
      strategy: "jwt",
    },
    cookies: cookieConfig,
    callbacks: { /* コールバック関数 */ },
  });
}

JWTセッションを利用する際は、session.strategyjwtに設定し、トークン暗号化キーを指定する必要があります。シークレットは環境変数NEXTAUTH_SECRETで設定することを推奨します。Vercel以外へのデプロイ時は、サイトの正式URLをNEXTAUTH_URLとして設定してください。

今回の実装ではaccess-tokenredirect-urlcsrf-tokenを使用します。これらはそれぞれJWTトークン格納、ログイン・ログアウト後のリダイレクト先、CSRFトークンとして機能します。すべてのサブドメインでトークンを利用可能にするには、Cookieのドメインオプションを有効なドメインに設定する必要があります。例えば、account.example.comとexample.comの場合は、ドメインオプションをexample.comに設定してください。

// pages/api/auth/[...nextauth].ts

const cookieConfig: Partial<CookiesOptions> = {
  accessToken: {
    name: `custom-auth.access-token`,
    options: {
      httpOnly: true,
      sameSite: "none",
      path: "/",
      domain: process.env.NEXT_PUBLIC_ROOT_DOMAIN,
      secure: true,
    },
  },
  redirectUrl: {
    name: `custom-auth.redirect-url`,
    options: {
      /* その他のオプション */
    },
  },
  csrfToken: {
    name: "custom-auth.csrf-token",
    options: {
      /* その他のオプション */
    },
  },
};

型安全性の確保

NextAuth.jsは組み込み型を提供しますが、セッションオブジェクトに追加情報を保存するため、SessionUserJWTオブジェクトの型定義を上書きする必要があります。

// types/next-auth.d.ts

declare module "next-auth" {
  interface Session {
    refreshExpiry?: number;
    accessExpiry?: string;
    refreshToken?: string;
    authToken?: string;
    errorMessage?: string;
    userInfo?: User;
  }

  interface User {
    firstName?: string;
    lastName?: string;
    email?: string | null;
    userId?: string;
    addressInfo?: {
      addressId?: string;
    };
  }
}

declare module "next-auth/jwt" {
  interface JWT {
    refreshExpiry?: number;
    accessExpiry?: number;
    refreshToken?: string;
    authToken: string;
    exp?: number;
    iat?: number;
    jti?: string;
  }
}

認証プロセスの実装

認証プロバイダーの設定

認証プロバイダーを設定に追加します。nameidは異なるプロバイダーを識別するために必要です。credentialsオブジェクトはログインフォームのフィールドを表し、このオブジェクト構造がauthorize関数の最初の引数として渡されます。

// pages/api/auth/[...nextauth].ts

providers: [
  Providers.Credentials({
    name: 'auth-provider',
    id: 'auth-provider',
    credentials: {
      email: { },
      password: { }
    },
    async authorize(credentials, req) {
      // 認証ロジック
    }
  })
],

authorize関数はカスタムバックエンド実装からユーザー情報を取得し、JWTトークンを提供する役割を担います。

// pages/api/auth/[...nextauth].ts

async authorize(credentials) {
  const result = await fetch('https://your-api-endpoint.com/auth', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      email: credentials?.email,
      password: credentials?.password,
    }),
  });
  const responseData = await result.json();
  if (result.ok && responseData?.authToken) {
    return responseData;
  }
  return Promise.reject(new Error(responseData?.message));
};

コールバック処理

最初のコールバックはjwtです。JSON Web Tokenの作成またはアクセス時に呼び出されます。トークンの更新処理を実装する適切な場所です。updateAuthToken関数はtokenオブジェクトに格納されたリフレッシュトークンを使用し、更新された有効期限を持つ新しいアクセストークンを取得します。バックエンドは有効期限を秒単位で提供し、Date.now()はミリ秒単位のため、1000で除算する必要があります。

// pages/api/auth/[...nextauth].ts

export const jwtCallback = async ({ token, user }: { token: JWT; user?: User }) => {
  if (user?.email) {
    return { ...token, ...user };
  }

  if (token?.accessExpiry) {
    if (Date.now() / 1000 < token?.accessExpiry) return { ...token, ...user };
  } else if (token?.refreshToken) return updateAuthToken(token);

  return { ...token, ...user };
};

jwtの出力はsessionコールバックのtokenとして渡されます。これは追加データをsessionオブジェクトに渡す適切な場所です。バックエンドから提供されるtoken内のすべてのデータを解析し、WebクライアントのuserInfoオブジェクトとして渡します。また、アクセストークンとリフレッシュトークンの有効期限チェックとエラー発生も可能です。

// pages/api/auth/[...nextauth].ts

export const sessionCallback = ({ session, token }: { session: Session; token: JWT })
 : Promise<Session> => {

  if (Date.now() / 1000 > token?.accessExpiry && 
      token?.refreshExpiry && Date.now() / 1000 > token?.refreshExpiry) {
    return Promise.reject({
      error: new Error("リフレッシュトークンの有効期限が切れました。再度ログインして新しいリフレッシュトークンを取得してください。"),
    });
  }

  const tokenPayload = JSON.parse(atob(token.authToken.split(".")?.at(1)));
  session.userInfo = tokenPayload;
  token.accessExpiry = tokenPayload.exp;

  session.authToken = token?.authToken;

  return Promise.resolve(session);
};

クライアントサイドでの利用

カスタムログインページを使用しているため、クライアント側で検証後にsignInを呼び出すことができます。redirectfalseに設定することで、エラーと成功を手動で処理できます。

// pages/login.tsx

signIn("auth-provider", {
  email: formData?.email,
  password: formData?.password,
  redirect: false,
}).then((result) => {
  if (result?.error) {
    // エラーメッセージ表示
  } else {
    // 目的のページにリダイレクト
  }
});

レスポンス検証が不要な場合、redirecttrueに設定し、コールバックURLを指定することで、ユーザーがログアウト後に指定ページにリダイレクトされるようにできます。

// pages/logout.tsx

signOut({
  redirect: true,
  callbackUrl: `${process?.env.NEXT_PUBLIC_AUTH_PAGE}/login`,
});

sessionデータやクライアント側のtokenにアクセスする際は、useSession()フックを使用できます。今回の例では、カスタムプロパティを持つsession型を使用します。

ミドルウェアの活用

Next.js 12以上を使用している場合、ミドルウェアでNextAuth.jsを利用できます。基本的な使い方では、保護したいパス名の配列を含むmatcherオブジェクトをエクスポートします。

// middleware.ts

export { default } from "next-auth/middleware";

export const config = { matcher: [ /* パス配列 */ ] };

高度なロジックが必要な場合は、カスタムmiddleware実装を利用できます。getToken()を通じてミドルウェア内のトークンデータにアクセスおよびデコードできます。管理者アクセス権がないユーザーをリダイレクトするなどの処理が可能です。

// middleware.ts

export async function middleware(request: NextRequest) {
  const tokenData = await getToken({
    req: request,
    secret: process?.env?.NEXTAUTH_SECRET,
    cookieName: AUTH_TOKEN_COOKIE, // custom-auth.access-token
  });

  if (!tokenData?.authToken || Date.now() / 1000 >= tokenData?.accessExpiry) {
    return NextResponse.redirect("/login");
  }

  if (!tokenData?.isAdmin) {
    return NextResponse.redirect("/admin-access");
  }

  return NextResponse.next();
}

マルチドメイン環境の設定

単一ドメインの場合、これまでの設定で正常に動作します。複数サブドメイン間での認証を実装する場合、NextAuth.jsの公式ドキュメントには現時点では正式な解決策がありません。カスタムCookieポリシーを設定する必要があります。

authorize機能以外のすべてをサブドメイン間で共有する必要があります。これは、サブドメインからsignInを呼び出すことはないため理にかなっています。

// pages/api/auth/[...nextauth].ts

export default NextAuth({
  providers: [
    CredentialsProvider({
      id: "multi-domain",
      name: "credentials",
      credentials: {},
      async authorize(_credentials, _req) {
        return null;
      },
    }),
  ],
  session: {
    strategy: "jwt",
  },
  cookies: sharedCookieConfig,
  callbacks: {
    session: sessionCallback,
    jwt: jwtCallback,
  },
});

cookiesessionjwtコールバックの設定を共有し、トークンのアクセスと更新を同じ方法で行えるようにする必要があります。これらの機能を共有モジュールに移動し、両方のサイトからアクセスできるようにすることをお勧めします。同じsecretを使用しないと、tokenの復号化に失敗することを忘れないでください。

タグ: nextjs next-auth JWT authentication TypeScript

7月18日 19:31 投稿