2025-10-20

NextAuth.jsで代理ログインを実装する方法

認証があるアプリケーションを実装しているとき、動作確認を行うため他のユーザになりすましてログインする「代理ログイン」をしたくなることがある。 NextAuth.js を認証基盤として利用している場合は、カスタム認証プロバイダとして代理ログイン用の処理を実装することで実現できる。

PrismaをORMとして使っている場合は以下のようなカスタム認証プロバイダで実装すれば良い。

import CredentialsProvider from "next-auth/providers/credentials";
import { prisma } from "@/lib/prisma";
import { logger } from "@/lib/logger";

// IDを指定するだけでログインできる認証プロバイダ
export const impersonateCredentialsProvider = CredentialsProvider({
  id: "impersonation",
  name: "Impersonation",
  credentials: {
    targetUserId: { label: "Target User ID", type: "text" },
    developerUserId: { label: "Developer User ID", type: "text" },
  },
  async authorize(credentials) {
    // 必要に応じて認証チェック  
    // if (process.env.NEXT_PUBLIC_APP_ENV === "production") {
    //   return null;
    // }
      
    // バリデーション 
    if (!credentials?.targetUserId) {
      return null;
    }

    try {
      const targetUser = await prisma.user.findUnique({
        where: { id: credentials.targetUserId },
      });

      logger.info(
        `Developer ${credentials.developerUserId} impersonated user ${targetUser.email}`,
      );

      return {
        id: targetUser.id,
        email: targetUser.email,
        name: targetUser.name,
        image: targetUser.image,
        impersonatedBy: credentials.developerUserId,
        impersonatedAt: Math.floor(Date.now() / 1000),
      };
    } catch (error) {
      logger.error("Impersonation authorization error:", { error });
      return null;
    }
  },
});

認証プロバイダの設定はこんな感じで環境変数を見て切り替える。以下では本番環境では代理ログインプロバイダを無効化している。

export const authOptions: NextAuthOptions = {
  providers: [
    // ...(snip)...
    
    ...(process.env.NEXT_PUBLIC_APP_ENV !== "production"
      ? [impersonateCredentialsProvider]
      : []),
  ],
  
  callbacks: {
    // ...(snip)...  
    async session({ session, token, user }) {
      if (session?.user) {
        // 代理ログイン情報を追加
        if (token.impersonatedBy) {
          session.user.impersonatedBy = token.impersonatedBy as string;
          session.user.impersonatedAt = token.impersonatedAt as number;
        }
      }
      return session;
    },

    async jwt({ token, user, account }) {
      // 代理ログイン時
      if (user && account?.provider === "impersonation") {
        token.impersonatedBy = user.impersonatedBy;
        token.impersonatedAt = user.impersonatedAt;
      }

      return token;
    },
  },
  // ...(snip)...
}

callbacksの session(), jwt() の実装も必要でそれぞれ

となっている。一見すると async session({ session, token, user }) でuserから直接 impersonatedBy などを取得できそうなのだが、strategy: "database"ときだけuserに値が入るようなので、tokenの情報を使うしか無いみたい。

これで以下のような感じで呼びだせば良い。

signIn("impersonation", {
  targetUserId: "{target-user-id}",
  developerUserId: session.user.id,
});

権限チェックやなりすましログイン時の表示変更など(帯を出すなど)を実装すればOK。 もし本番にも適用したい場合は代理ログインを区別できるようなaudit logを適切に入れる必要がある。