App Router x Auth.js 5 (Beta)でハマったことメモ

公式ドキュメントに記載がない部分でハマった箇所について備忘録として残しておく。
特に断りがない限り、React Server Components は RSC と呼ぶ。

## Server Actions や RSC を使用する場合、`/api/auth/[...nextauth]/route.ts`は不要

これいらなくね？となった。公式ドキュメントには、まるで必須かのように `/api/auth/[...nextauth]/route.ts` を準備せよと書いてあるが、実は不要である。App Router の新機能、つまり Server Actions や RSC を使う場合、Auth.js の`auth`関数や`signIn`関数を直接呼べばよく、API エンドポイントをコールする必要がないからである。

### `GET /api/auth`

App Router では、`GET /api/auth` 相当のものは、`auth`関数で代用可能であり、準備する必要はない。
例えば、Server Actions の場合は、以下のようにしてセッション情報を取り出すことができる。

```ts
"use server";
import { signOut } from "@/auth";
export async function action() {
  const session = await auth();
  // 任意の処理
}
```

一方、RSC の場合も同様に取り出すことができる。

```tsx
import "server-only";
import { auth } from "@/auth";
export async function RSC() {
  const session = await auth();
  return <Component session={session} />;
}
```

### `POST /api/auth`

また、`POST /api/auth` についても、以下のように Server Actions を用意し、フォームの Action に紐付けてやれば OK。

```ts
"use server";
import { signIn } from "@/auth";
export async function signIn(formData: FormData) {
  await signIn(formData);
}
```

```tsx
"use client";
import { signIn } from "./signIn";
export function Button() {
  return <form action={signIn}>{/* 任意のコード */}</form>;
}
```

## リダイレクトがうまく動かない

`signIn` 関数を Server Actions 経由で呼び出しログインする場合で、try-catch でのエラーハンドリングを使う場合に、ログイン後にうまくリダイレクトされない問題が起きた。これは Auth.js の問題というよりは App Router で redirect をしようとする場合に throw されるためである。再現コードは以下。

```ts
"use server";
import { isRedirectError } from "next/dist/client/components/redirect";

import { signIn } from "@/auth";
export async function signIn(formData: FormData) {
  try {
    await signIn(formData);
  } catch (e) {
    // NEXT_REDIRECT が e.messageとして throw されてしまう
    return { errorMessage: e.message };
  }
}
```

この問題の解決策の 1 つは、Next.js の `isRedirectError` を使用し、 `Redirect` の場合はそのまま throw しなおす方法で解決できる。

```ts
"use server";
import { isRedirectError } from "next/dist/client/components/redirect";

import { signIn } from "@/auth";
export async function signIn(formData: FormData) {
  try {
    await signIn(formData);
  } catch (e) {
    if (isRedirectError(e)) {
      throw e;
    }
    return { errorMessage: e.message }; // Client側の useFormState で使いたい値を返す
  }
}
```

もしくは、Auth.js の redirect の処理を 無効にしてしまって、 App Router 側でやってしまう方法も取れる。

```ts
"use server";
import { isRedirectError } from "next/dist/client/components/redirect";

import { signIn } from "@/auth";
export async function signIn(formData: FormData) {
  try {
    await signIn({
      redirect: false, // 追加
      name: formData.get("name").toString(),
      password: formData.get("password").toString(),
    });
  } catch (e) {
    // throw NEXT_REDIRECT されなくなるので、re-throwしなくて良い
    // ...
  }

  // 追加
  redirect("/path/to/redirect");
}
```

## 複数の Middleware が使用できない

公式ページには `middleware.ts` に以下を書け、と書いてあるが、これでは他に使いたい関数がある場合に使うことができない。

```ts
// middleware.ts
export { auth as middleware } from "@/auth";
```

同ページには以下のようなサンプルもあるが、これでもやっぱり多少辛いものがある。

```ts
// middleware.ts
export default auth((req) => {
  // req.auth
});
```

自分は以下のようにすることでこの問題を解決した。これであれば Middleware を追加できるはず。

```ts
export async function middleware() {
  const session = await auth();
  if (!sesison) {
    return NextResponse.json({}, { status: 401 });
  }
  // 他に使いたいものがあればこの中に。
}
```

もっと増えてきたら、`applyMiddlewares` のように、チェインする関数を作って middleware を複数適用できるようにしてもいいかもしれない。

## auth middleware は実は何もしていない

以下の auth middleware であるが、実は何もしていない。認可も認証もしていない。大事なことなので二度言いました。

```ts
// middleware.ts
export { auth as middleware } from "@/auth";
```

認可処理が必要なら、以下のように拡張する必要がある。middleware の config に正規表現を使ってパスを指定する方法もあるようだが、あれは可読性が低いのでおすすめしない。

```ts
export async function middleware(req: NextRequest) {
  const session = await auth();

  // /auth 以下は認証を不要
  if (req.url.pathname.startWith("/auth")) {
    return NextResponse.next();
  }

  // それ以降は認証が必要
  if (!sesison) {
    return NextResponse.json({}, { status: 401 });
  }

  // /admin以下は権限が必要
  if (req.url.pathname.startWith("/admin")) {
    if (session.grant === "admin") {
      return NextResponse.json({}, { status: 401 });
    }
  }
}
```