最初はここが難しかった、Next.js (App Router + Middleware) + Auth.js で認証

# 概要
Next.jsによるWebアプリ開発の練習のため、本家チュートリアルに従ってAuth.jsを用いた認証機能の実装を試みましたが、想定外に難しかった部分をまとめます。
- **現在のCanaryバージョン(15)以上でないと、Next.jsチュートリアル通りの認証関連のコードがエラーになりました**
  → チュートリアルだしstable版だろうと思い込んでいました😂
  <br>
- **認証機能の要になる`auth.ts`, `auth.config.ts`, `middleware.ts`の置き場所を間違っていました**
  → `create-next-app`した時の設定にもよりますが、私の場合は`src/`直下でした
  <br>
- **上記を解決して認証機能は部分的に動きましたが、middlewareのmatcher設定を誤って画像類が表示されなくなりました**
  → Next.jsプロジェクトの`public/`フォルダに入れた画像がmatcherによって認証対象にならないように、`matcher: ['/']`という最小限の設定をしました
  <br>
- **Auth.jsのUserオブジェクトの扱いを誤って、`auth()`関数が空のuserオブジェクトを返していました**
  本来のUserオブジェクトに無いキーを持ったオブジェクトにすると、セッションにユーザ情報が保存されません......
https://github.com/nextauthjs/next-auth/discussions/2762

同じように悩んでいる方の参考になれば幸いです。

## もう少し詳しく
開発中のToDoアプリのソースコードを引っ張ってきます。

以下`src/auth.config.ts`には、ログイン用のページの指定や
ログイン後のリダイレクトの設定などがあります。
```TypeScript:src/auth.config.ts
import type { NextAuthConfig, User } from "next-auth";

export const authConfig = {
  pages: {
    // ログイン用に表示するページを
    // src/app/login/pages.tsx に置いています
    // ログインが必要な際にはリダイレクトされるのでなく
    // 表示内容がログイン用ページで置き換わるような動作をします。
    signIn: '/login',
  },
  callbacks: {
    // Next.jsチュートリアル通りの認証後のリダイレクトなどの設定
    async authorized({ auth, request: { nextUrl } }) {
      const isLoggedIn = !!auth?.user;
      const isOnRoot = nextUrl.pathname.startsWith('/')
      if (isOnRoot) {
        return isLoggedIn;
      } else if (isLoggedIn) {
        return Response.redirect(new URL('/', nextUrl));
      }
      return true;
    }
  },
  providers: [],
} satisfies NextAuthConfig;

```

以下`src/middleware.ts`には、認証を行うURLを指定するmatcherがあります。
認証機能が動いた後、画像類が一切表示されずに悩みました...
```TypeScript:src/middleware.ts
import NextAuth from "next-auth";
import { authConfig } from './auth.config';

export default NextAuth(authConfig).auth;

export const config = {
  // 元々はチュートリアルに従って matcher に
  // ['/((?!api|_next/static|_next/image|.*\\.png$).*)']
  // を設定していましたが、
  // public/*.svg に入れた画像がmatcherに引っかかって表示されなくなり、
  // 本当に必要な部分だけに絞ることにしました
  matcher: ['/'],
};
```

以下`src/auth.ts`内には、Auth.jsのUserオブジェクトの扱いについて注意するべきポイントがあります。
```TypeScript:src/auth.ts
import NextAuth, { DefaultSession, User } from "next-auth";
import Credentials from 'next-auth/providers/credentials';
import { authConfig } from './auth.config';
import { z } from 'zod';
import { db } from "@/db";
import { eq } from 'drizzle-orm';
import { users } from "@/db/schema";
import { hashWithSalt } from '@/lib/crypto';

// データベースから指定したユーザ名 id を持つユーザ情報を取得するための関数です
// Drizzle ORM を使用していますが、見たままなので詳細は省略します
async function getUser(id: string) {
  try {
    const user = await db.select().from(users).where(eq(users.id, id));
    return { username: user[0].id, passWithSalt: user[0].passWithSalt };
  } catch (error) {
    console.error('Failed to fetch user:', error);
    throw new Error('Failed to fetch user.');
  }
}

export const { handlers, signIn, signOut, auth } = NextAuth({
  ...authConfig,
  providers: [
    Credentials({
      credentials: {
        username: {},
        password: {},
      },
      // ほぼNext.jsチュートリアル通りの認証用関数
      async authorize(credentials): Promise<User|null> {
        const parsedCredentials = z
          .object({ username: z.string(), password: z.string() })
          .safeParse(credentials);

        if (!parsedCredentials.success)
          throw Error('Invalid credentials format.');

        const { username, password } = parsedCredentials.data;
        const hashedPassword = await hashWithSalt(
          password, process.env.HASH_SALT!
        );
        const user = await getUser(username);
        if (!user) return null;
        
        const passwordsMatch = (
          hashedPassword === user.passWithSalt
        );
        // !!
        // !! ここは注意が必要 !!
        // !! Auth.js内のUserの定義は
        // !! {
        // !!   id?: string;
        // !!   name?: string | null;
        // !!   email?: string | null;
        // !!   image?: string | null;
        // !! }
        // !! となっており、それ以外のフィールドがあると
        // !! セッション内にユーザ情報が記録されないです......
        // !! うっかりハッシュ化されたパスワードのデータ等を明らかにしないためでしょうか
        // !! これ以外のフィールドを足す方法もあるようです
        // !! https://authjs.dev/getting-started/typescript
        if (passwordsMatch) return { id: username };
        return null;
      },
    })
  ],
});
```

Auth.jsで定義されているUser型のキーに気を付けていれば、
https://authjs.dev/getting-started/session-management/get-session
の様にユーザ情報を取得できます。

## 試行錯誤中のリポジトリ
上記コードの元になっている開発中のWebアプリのリポジトリへのリンクを貼っておきます。

https://github.com/Daiius/sekirei-todo

この記事を書いている時点では、ユーザ情報を出力する不穏な`consoole.log`など残っているので順次削除して参ります...