AstroでAuth.jsを使ったパスワード認証と、セッション情報のカスタマイズ【auth-astro】

## 背景

- Astroでメールアドレスとパスワードでの認証をして、セッション情報をcookieに保存するような機能を実現したかった
- [Astroで公式に紹介されている](https://docs.astro.build/ja/guides/authentication/#authjs)、[auth-astro](https://github.com/nowaythatworked/auth-astro)でできそうだったので試した
  - auth-astroは、Auth.js(旧NextAuth.js)をAstroで使いやすくするためのラッパーライブラリ
- 以下の2点を実現するためのまとまった情報が見つからなかったので整理した
  - パスワード認証の設定方法
  - cookieに保存される認証情報のカスタマイズ

<font color="red">**注意事項**</font>

今回試してみてAuth.jsは使いづらい部分もあると感じたため、実際に利用するかどうかは事前に検討した方が良さそう。
具体的には[所感](#所感)欄を参照してください。

## 動作確認環境

- Node.js：20.11
- `astro`：4.15.2
- `auth-astro`：**4.1.2**
- `@auth/core`：**0.35.3**

※ auth-astro 4.1.2では`"@auth/core": "^0.32.0",`がdependenciesに指定されているが、`0.32.x`だとCredentialsSigninに対するエラーハンドリングで500エラーになる問題があり、動作確認時点で最新バージョンを別途指定した

## 最終コード

Astroで作成したプロジェクトに、auth-astroの導入までは終わっている前提。
auth.config.mjsを以下のように設定することで、パスワード認証とセッション情報のカスタマイズができる。
以下は、ログインしたユーザーIDをセッショントークンとしてcookieに保存する設定の例になる。

```js
// auth.config.mjs
import { CredentialsSignin } from "@auth/core/errors";
import Credentials from "@auth/core/providers/credentials";
import { defineConfig } from "auth-astro";
 
export default defineConfig({
  providers: [
    Credentials({
      name: "Email",
      credentials: {
        email: { type: "email", required: true, label: "Email" },
        password: { type: "password", required: true, label: "Password" },
      },
      authorize: async (credentials) => {
        const { email, password } = credentials;
        const user = await authorize(email, password); // your logic here
        if (!user) throw new CredentialsSignin();
        return { id: user.id };
      },
    }),
  ],
  callbacks: {
    async jwt({ user, token }) {
      if (user) {
        return { id: user.id };
      }
      return token;
    },
    session({ session, token }) {
      return {
        ...session,
        user: { id: token.id },
      };
    },
  },
});
```

## 画面の見た目
Auth.jsで用意されているログイン画面を利用すると、以下のような挙動になる
### ログイン画面( /api/auth/signin )
[![Image](https://gyazo.com/fad9b60f4cc4af064bffd75845e8aef0/max_size/1000)](https://gyazo.com/fad9b60f4cc4af064bffd75845e8aef0)

### 認証失敗時( /api/auth/signin?error=CredentialsSignin&code=credentials )
[![Image](https://gyazo.com/7d623e37f4c6db5a45c3ca3c2588f158/max_size/1000)](https://gyazo.com/7d623e37f4c6db5a45c3ca3c2588f158)

### 認証成功時
認証が成功すると別ページへリダイレクトされる
getSessionを使ってセッション情報を取り出すことができる
```js
// pages/example.astro
---
import { getSession } from 'auth-astro/server';
 
const session = await getSession(Astro.request)
console.log(session)
// {
//   user: { id: 1 },
//   expireds: '2024-10-01T00:00:00.000Z'
// }
---
{session?.user ? (
  <p>Logged in as User {session.user.id}</p>
) : (
  <p>Not logged in</p>
)}
```
認証に成功した場合は`authjs.session-token`というCookieが保存され、中身はAuth.jsによりJWTの形式で暗号化されている。

**セッショントークンの例**
```
eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwia2lkIjoiTjEzYW5aS3hzMmpXT0pDUmNYaTRCYjZCMkdUNmxZdmxaa3lUVEdfbHRYZG14UEJVYkVkNkJxaVlRRnhNdTFuZjRBa3BGRmxhTlI1dW11ZENRc3JaQ0EifQ..IAkEwYkG1M9Y-J5rTju63g.haLgQeSxUEqctr2Gjn23sMaNEAaGb9y1ott-dZnws-oo7Sdcz1fnPRRIvpLpBe6LkJN2JMELAHbElbAl-41BrgAzUDTeWnGlNTeFVa3Em6iWObNtfwX_H7nOtnddB3OZ.aOZQ_jzdVkNvdepksoAFc9I8Qz7fuFlcAtJcN-Tp-ng
```

## セッショントークンのカスタマイズについて

### デフォルトの挙動

Credentialsを利用した場合、デフォルトでは`email`がユニークなキーとしてcookieに保存される。
emailのままで問題無い場合は、callbacksを指定したカスタマイズは不要。以下の設定だけで問題ない。

```js
// auth.config.mjs
import { CredentialsSignin } from "@auth/core/errors";
import Credentials from "@auth/core/providers/credentials";
import { defineConfig } from "auth-astro";
 
export default defineConfig({
  providers: [
    Credentials({
      name: "Email",
      credentials: {
        email: { type: "email", required: true, label: "Email" },
        password: { type: "password", required: true, label: "Password" },
      },
      authorize: async (credentials) => {
        const { email, password } = credentials;
        const user = await authorize(email, password); // your logic here
        if (!user) throw new CredentialsSignin();
        return { email: user.email };
      },
    }),
  ],
});
```

**sessionの中身**

```js
const session = await getSession(Astro.request)
console.log(session)
// {
//   user: { email: 'test@example.com', name: undefined, image: undefined },
//   expireds: '2024-10-01T00:00:00.000Z'
// }
```

### カスタマイズの詳細

email以外の情報をセッショントークンに保存したい場合は、callbacksの指定が必要になる。
以下は、emailの代わりにidをトークンに残す例。これを反映したコードが、記事の「最終コード」で記載したものになる。
```js
authorize: async (credentials) => {
  // ...
  return { id: user.id }; // 返り値の形式を変更
},
```

```js
callbacks: {
  // cookieに保存するトークンの形式を変更
  async jwt({ user, token }) {
    if (user) {
      return { id: user.id };
    }
    return token;
  },
  // トークンから読み取る情報の形式を変更
  session({ session, token }) {
    return {
      ...session,
      user: { id: token.id },
    };
  },
},
```
この場合、sessionからは以下のような形式で取れる
```js
const session = await getSession(Astro.request)
console.log(session)
// {
//   user: { id: 1 },
//   expireds: '2024-10-01T00:00:00.000Z'
// }
```

## 所感 

やりたかった「Astroでメールアドレスとパスワードでの認証をして、セッション情報をcookieに保存するような機能」は上記のような設定で実現できた。

ただ、今回Auth.jsを触ってみて、挙動のカスタマイズが難しそうに感じた。

例えば以下の場合には公式で機能が用意されていないため、カスタムページを用意した上で自前で実装する必要がありそう。
- エラー時のメッセージを変更する場合(多言語対応など)
- ログインページの見た目を変更したい場合

また、認証失敗時にメールアドレスをフィールドに入力されたままに残すのは、現状のAuth.jsの仕組み上かなり難しい気がした。
認証実行時にリダイレクトが挟まれるため、その際にフォームに入力された情報が失われてしまう。(ここが簡単になってくれると嬉しいが…そもそもAuth.jsはパスワードでの認証を推奨していないようなのでどうだろうか)

調べてみると、以下のような意見も見つかった。

- [You're not a bad engineer, NextAuth is a bad library. - Reddit](https://www.reddit.com/r/nextjs/comments/19a86sa/youre_not_a_bad_engineer_nextauth_is_a_bad_library/)
  - > NextAuthは認証の複雑さを避けることを目的として書かれているように感じますが、それがうまくいっていません。すぐに何かを動かすことはできますが、後でカスタマイズしようとすると大変な苦痛を伴います。 (日本語訳)

- [Why is next-auth (or Auth.js) so popular? - Reddit](https://www.reddit.com/r/nextjs/comments/1crn50l/why_is_nextauth_or_authjs_so_popular/)
  - >認証機能を持つ新しいウェブサイトを作ろうとしたところ、Auth.js（v5）での経験はまさに災難でした。ドキュメントはひどく、カスタマイズ性はほとんどなく、設定がうまくいきませんでした。もし私がプロジェクトリーダーだったら、この不安定なものを推奨することはありません。 (日本語訳)

Auth.jsのデフォルトの挙動をそのまま利用する場合は問題ないかもしれないが、少しでもカスタマイズを考えている場合は事前に検討した方が良さそうに感じた。

## 参考

Authentication | Astro
https://docs.astro.build/en/guides/authentication/#authjs

nowaythatworked/auth-astro: Community maintained Astro integration of @auth/core
https://github.com/nowaythatworked/auth-astro

Credentials | Auth.js
https://authjs.dev/getting-started/authentication/credentials

Callbacks | NextAuth.js
https://next-auth.js.org/configuration/callbacks

NextAuth(Auth.js)でのemail, passwordログイン時の処理を追ってみる
https://zenn.dev/hid3/articles/369179cd48c918

NextAuth.jsでログイン画面をカスタマイズしてみる - wheatandcatの開発ブログ
https://www.wheatandcat.me/entry/2023/11/22/230556

NextAuth.jsの認証失敗時にエラーメッセージを取得するための工夫
https://zenn.dev/harper/articles/3c727ea5774735