Remixで認証するならremix-auth便利ですよね!

そんなremix-auth、使っていると困っていることがありました。
それがエラーメッセージってどうやって返却するんだ?ってことです。

認証においてエラーメッセージを返却するというのはよくあるパターンだと思いますが、その実装方法に少しハマってしまいました。
今回はremix-authを使って独自にエラーハンドリングしてメッセージを返却する方法を紹介します。

前提条件

  • Remixの基礎的な使い方を理解している
  • 認証部分の処理は具体的に紹介しない

今回の記事ではRemixの基礎的な使い方は理解した上で、認証の具体的な実装方法は紹介しません。

Remix Authの公式は下記なので、もしこの記事で触れない不明点がある場合は参考にしてください。

[Remix Auth \| Remix Resources](https://remix.run/resources/remix-auth)

Remix Authの認証部分の設定

まずはremix-authで認証をするための定義を作ります。
ここは独自のエラーレスポンスを返すために特別な設定はしておらず、remix-authを基本的な形で使うためのコードのみです。

import { Authenticator } from 'remix-auth'
import { FormStrategy } from 'remix-auth-form'

export const authenticator = new Authenticator<number>(sessionStorage) // sessionStorageは下で記述する公式のコードを想定

authenticator.use(
  new FormStrategy(async ({ form }) => {
    const email = form.get('email')
    const password = form.get('password')

    // 認証処理

    // 認証失敗時にはエラーをthrow
    throw new Error('メールアドレスとパスワードが一致しません') // このメッセージをactionから返却する

    // 認証成功時にはユーザーIDを返却
    return userId
  }),
  'user-pass',
)

上のauthenticatorのインスタンスを作るときのsessionStoreは公式で紹介されているようなコードを想定しています。

https://remix.run/resources/remix-auth#usage

下記コードは公式から引用したコードです。

import { createCookieSessionStorage } from '@remix-run/node'

// export the whole sessionStorage object
export let sessionStorage = createCookieSessionStorage({
  cookie: {
    name: '_session', // use any name you want here
    sameSite: 'lax', // this helps with CSRF
    path: '/', // remember to add this so the cookie will work in all routes
    httpOnly: true, // for security reasons, make this cookie http only
    secrets: ['s3cr3t'], // replace this with an actual secret
    secure: process.env.NODE_ENV === 'production', // enable this in prod only
  },
})

// you can also export the methods individually for your own usage
export let { getSession, commitSession, destroySession } = sessionStorage

authencatorの設定ができたら、次はエラーレスポンスを返却するための実装を行います。

remix-authにエラーハンドリングを任せる場合

独自のエラーの前にremix-authを素直に使った場合の実装を見ておきます。

上で設定したuser-passを呼び出すために、authencateメソッドを下記のようにするだけ認証成功、失敗の処理が完結します。

export async function action({ request }: ActionFunctionArgs) {
  const result = await authenticator.authenticate('user-pass', request, {
    successRedirect: '/', // 認証成功時のリダイレクト先
    failureRedirect: '/login', // 認証失敗時のリダイレクト先
  })
}

とても短くて簡潔ですが、困るのが認証失敗時のエラーメッセージの表示です。
たとえば認証に失敗したら「メールアドレスとパスワードが一致しません」というようなメッセージを表示するケースはよくあると思いますが、remix-authを素直に使うとそのようなエラーメッセージを表示することができません。

remix-authでは認証の成功でも失敗でも設定したリダイレクト先にリダイレクトされてしまいます。

独自のエラーレスポンスを定義する

remix-authを素直に使うと任意のエラーメッセージを表示することができないので、独自のエラーレスポンスを返却するための実装を行います。
まずはremix-authのauthenticateメソッドを使って認証処理を実装。

import { redirect, json } from '@remix-run/node'

export async function action({ request }: ActionFunctionArgs) {
  try {
    return await authenticator.authenticate('user-pass', request, {
      successRedirect: '/',
    })
  } catch (e) {
    // この記述がないと成功時にリダイレクトができない
    // 認証成功時はResponseのstatusが302になる
    if (e instanceof Response) {
      return e
    }

    // 認証失敗時にthrowしたエラー
    if (e instanceof Error) {
      return json({ message: e.message }, { status: 401 })
    }

    return json({ message: '認証に失敗しました' }, { status: 401 })
  }
}

これで

  • 成功時: catchでstatusが302のResponseをハンドリング
  • 失敗時: authenticateのuser-pass内部でthrowしたErrorをハンドリング

authenticateのuser-passで呼び出している内部でエラーをthrowすることで、失敗時に@remix-run/nodeのjsonメソッドを使ってエラーレスポンスを返却することができます。
注意点として公式にも明記されていますが、リダイレクトはResponseを返却することで動作しているので成功時においても、catchでハンドリグが必要です。

actionで返却したデータを使ってエラーメッセージを表示する

後はactionで返却したデータを使ってエラーメッセージを表示するだけです。

import { useActionData } from '@remix-run/react'

export default function Index() {
  const data = useActionData<typeof action>() as {
    message?: string
  }

  return (
    <Content>
      <Container size="sm">
        <div>
          <h1 className="text-xl mb-8 text-primary-600">ログイン</h1>
          {data?.message && <div>{data.message}</div>}
          <form>ログインフォーム</form>
        </div>
      </Container>
    </Content>
  )
}

今回紹介しているコードでは@remix-run/nodeのjsonメソッドを使ってuseActionDataで取得するとdataはanyになります。
そのためasを使って型を指定しています。

型の情報を取得しやすいremix-typedjsonを使ってもResponse型であれば取得できますが、`{ message: string}`の型は得られなかったので今回は純粋に@remix-run/nodeのjsonメソッドを使っています。

useActionDataでの型については「<PostLink slug=”remix-jsonifyobject” />」で書いたようにremix-typedjsonが使いやすいので使用を検討するのも良いと思います。

まとめ

Remix Authは面倒な認証処理がシンプルに行えますが、どうやって独自にエラーハンドリングするのが良いか悩んだので紹介してみました。

認証に失敗したらいきなりリダイレクトするという仕様はあまりないように思うので、もしremix-authでエラーメッセージを表示する方法を探している方の参考になれば幸いです。