kk-web

NextAuthのv4でNext.jsのApp Routerに対応する方法

2024-04-30

next-auth の v4 では App Router への対応がめちゃくちゃ中途半端だったりします。

現在 v5 を開発中 とのことで、これでようやく公式の対応が入るのですが。

今更 Pages Router を使うのは個人的にイヤすぎるので、結構強引に App Router にアップデートしてしまいました。


で、現在 v4 で把握している問題点ですが。

どうやら api 側で Session Token の更新に失敗するらしく、そこを middleware でカバーする必要があるみたいです。

今回は 公式の Issue にほぼ答えが書かれていたんですが、一応実装をば。

ちなみに実装感は 100 自分ではないので、あしからず。

import { withAuth } from "next-auth/middleware";
import {
  CognitoRefreshToken,
  CognitoUser,
  CognitoUserPool,
  CognitoUserSession,
} from "amazon-cognito-identity-js";
import { encode } from "next-auth/jwt";
import { NextResponse } from "next/server";

export default withAuth(async function middleware(request) {
  const response = NextResponse.next();

  if (request.url.includes(".")) {
    return response;
  }

  const {
    nextauth: {
      token: { name, refreshToken, expirationTime },
    },
  } = request;

  if (
    typeof expirationTime === "number" &&
    expirationTime > Date.now() / 1000
  ) {
    return response;
  }

  const cognitoUserPool = new CognitoUserPool({
    UserPoolId: process.env.AWS_USER_POOLS_ID,
    ClientId: process.env.AWS_USER_POOLS_CLIENT_ID,
  });
  const cognitoUser = new CognitoUser({
    Username: name,
    Pool: cognitoUserPool,
  });
  const cognitoRefreshToken = new CognitoRefreshToken({
    RefreshToken: refreshToken,
  });
  const cognitoUserSession = await new Promise<CognitoUserSession>(
    (resolve) => {
      cognitoUser.refreshSession(
        cognitoRefreshToken,
        async (_, session: CognitoUserSession) => {
          resolve(session);
        },
      );
    },
  );
  const accessToken = cognitoUserSession.getAccessToken();
  const jwtToken = accessToken.getJwtToken();
  const {
    payload: { exp },
  } = accessToken;
  const nextCognitoRefreshToken = cognitoUserSession.getRefreshToken();
  const token = nextCognitoRefreshToken.getToken();
  const cognitoIdToken = cognitoUserSession.getIdToken();
  const { sub } = cognitoIdToken.decodePayload();
  const sessionToken = await encode({
    secret: process.env.NEXTAUTH_SECRET,
    token: {
      name: sub,
      accessToken: jwtToken,
      refreshToken: token,
      expirationTime: parseInt(exp, 10),
    },
    maxAge: 30 * 24 * 60 * 60,
  });
  // maximum size of each chunk
  const size = 3933;
  const regex = new RegExp(".{1," + size + "}", "g");
  const tokenChunks = sessionToken.match(regex);

  if (tokenChunks) {
    const sessionCookie = process.env.NEXTAUTH_URL?.startsWith("https://")
      ? "__Secure-next-auth.session-token"
      : "next-auth.session-token";

    tokenChunks.forEach((tokenChunk, index) => {
      response.cookies.set(`${sessionCookie}.${index}`, tokenChunk, {
        httpOnly: true,
        // 7 days
        maxAge: 604800,
        secure: process.env.NEXTAUTH_URL?.startsWith("https://"),
        sameSite: "lax",
      });
    });
  }

  return response;
});

export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     */
    "/((?!api|_next/static|_next/image|favicon.ico|login).*)",
  ],
};

動作感も結構甘めなので、参考程度にしていただければと。

ちなみに余談ですが、next-auth × Cognito なら無難に Amplify を使うべきだと思います、強く。


そんな感じです。

ちなみに個人的には next-auth があまり好きではないです、それなら middleware や layout で実直に書けば良いよなー派です。

とはいえ使用している現場もちょこちょこ存在すると思いますので、参考になりましたら。

© 2018 kk-web