🗝️

E2EテストでNextAuth認証(OAuthなど)を突破する方法

2024/03/07に公開1

NextAuth (Auth.js) で認証させているWebアプリをPlaywrightなどでE2Eテストする際に、認証をどうやってさせるか、あるいは回避するかが悩ましい部分です。

もし採用している認証方式が、単純なID/パスワード認証であればテストユーザを作成し、Playwrightにパスワードを入力させれば認証できるので問題はありません。

しかし、Google認証などの外部のプロバイダを経由するような場合は、E2Eテストをすることが難しくなります。そこでこの記事では、NextAuthの認証済み状態をPlaywrightで再現させる方法を紹介します。

やり方は大きく2つ

NextAuthの設定に依存してやり方は大きく2つあります。

  • セッションデータを database で管理している場合
  • セッションデータを jwt で管理している場合

データベースの場合

セッションデータをデータベースに保持している場合は、対処法はメチャクチャ簡単です。

セッションキーを事前に作って保存する

もう答えそのものですが、NextAuthでセッションをデータベースに保持する方式にすると、Session テーブルの sessionToken カラムにセッションキーを保存します。そしてアクセスしたブラウザが持つCookie authjs.session-token の値と突き合せて一致すれば Session.userId のユーザとして認識する仕組みです。

ここを理解できればもう簡単ですね。

E2Eでテストする前に、テスト用シードデータで適当な値を sessionToken カラムに保存し、それとリレーションされたテストユーザを作成しておきます。あとはPlaywrightのCookieに生成したセッションキーをもたせることで認証済みのリクエストをすることができるようになります。

test("ログイン状態でアクセスすると、ユーザ情報が表示される", async ({
  browser,
}) => {
  // あらかじめSessionテーブルにテストユーザと紐づくレコードを作成しておく
  await prisma.session.create({
    data: {
      sessionToken: "dummy",
      userId: 1,
      expires: new Date(new Date().getTime() + 86400),
    },
  });

  // ブラウザにセッションのCookieを保存する
  const context = await browser.newContext();
  await context.addCookies([
    {
      name: "authjs.session-token",
      value: "dummy",
      domain: "localhost:3000",
      path: "/",
    },
  ]);

  // ログインしていないとアクセスできないページのテストをする
  const page = await context.newPage();
  await page.goto("http://localhost:3000/protect");
});

JWTの場合

セッションデータをJWTでもつ場合、当然ですがセッション情報はブラウザのCookieにJWT方式で保持されます。このJWTデータをテスト用に組み立ててブラウザのCookieに保存してあげればログイン済み状態を再現することができます。

ただし、NextAuthではデフォルトでJWTを暗号化したうえでCookieに保存するため単にJWTデータをCookieに保存しても意味はありません。いくつかやり方はあるのですが、今回はシンプルにJWTのエンコード・デコードをテスト用にカスタマイズするやり方を取ります。

JWTエンコード・デコード処理をカスタマイズする

NextAuthでは独自のエンコード・デコード処理に切り替えられるようになっています。NextAuthの設定時に以下のように処理をカスタマイズします。この処理は、authjs.session-token のCookieさえ持っていれば、どんな値だろうと問答無用に固定のデータを返す処理になっています。要は固定テストユーザとして強制ログインさせる形です。

const jwtTestEnv = {
  async encode(params: JWTEncodeParams<JWT>): Promise<string> {
    return "dummy";
  },
  async decode(params: JWTDecodeParams): Promise<JWT | null> {
    return {
      name: "user",
      email: "user@example.com",
      picture: "https://avatars.githubusercontent.com/u/000000",
      sub: "dummy",
    };
  },
};

export const config = {
  session: {
    strategy: "jwt",
  },
  ...(process.env.APP_ENV === "test" ? { jwt: jwtTestEnv } : {}),
  // その他設定は省略
} satisfies NextAuthConfig;

export const { handlers, auth, signIn, signOut } = NextAuth(config);

このカスタマイズを環境変数 APP_ENVtest の時だけ採用するようにします。[2] これで先程のデータベース方式と同じ様にPlaywrightにCookieをセットしてアクセスをするとログイン状態を再現することができます。

テストユーザを動的にしたい

もし、テストユーザを動的にして切り替えたい場合は以下のような対応が可能です。

const jwtBase64 = {
  async encode(params: JWTEncodeParams<JWT>): Promise<string> {
    return btoa(JSON.stringify(params.token));
  },
  async decode(params: JWTDecodeParams): Promise<JWT | null> {
    if (!params.token) return {};
    return JSON.parse(atob(params.token));
  },
};

これは、JWTフォーマットを単にBase64しているだけです。これであれば、PlaywrightでJWTデータを自由に組み立てて、Cookieにセットすることが可能になります。

test("ログイン状態でアクセスすると、ユーザ情報が表示される", async ({
  browser,
}) => {
  // テストユーザのJWTを自由に組み立てる
  const jwt = {
    name: "user",
    email: "user@example.com",
    picture: "https://avatars.githubusercontent.com/u/000000",
    sub: "dummy",
  };

  // Base64してCookieにセットする
  const context = await browser.newContext();
  await context.addCookies([
    {
      name: "authjs.session-token",
      value: btoa(JSON.stringify(jwt)), 
      domain: "localhost:3000",
      path: "/",
    },
  ]);

  // ログインしていないとアクセスできないページのテストをする
  const page = await context.newPage();
  await page.goto("http://localhost:3000/protect");
});

という感じで、認証そのものはテストせず、認証済みユーザとしてアプリケーションのE2Eテストを実施したい場合には上記のような手法を取ることで実現することができます。もしもっと手軽に安全に実現する方法があればコメントで教えて下さい。

脚注
  1. 正確にはセルフホストしたり、Prisma AcceleratorなどのHTTPベースでクエリ実行できるサービスを使えば利用できます。 ↩︎

  2. Next.jsでは NODE_ENVdevelopmentproduction に限定されます。test 環境で分離させたい場合は APP_ENV など別の環境変数で分岐させましょう。 APP_ENV=test npm run dev ↩︎

ムーザルちゃんねる

Discussion

daiki / きちくりすdaiki / きちくりす

公式では、development環境でだけユーザ名パスワードの認証を許可するみたいな実装が紹介されてたりします
https://authjs.dev/guides/testing

but you also must be extremely careful that you do not leave insecure authentication methods available in production.

注意は必要ですが