🔐

React Hook FormとClerkを使用して認証処理を実装してみた

2024/10/14に公開

はじめに

こんばんは🌙
都内のSaaS企業でクラウドエンジニアをしている、Atsushiです。
今回、個人で開発しているAIチャットボットの認証機能を実装する際に、Clerkという認証サービスを使用しました。そこで得た知見をシェアしたいと思います。

そもそもClerkとは?

Clerkは、組み込み可能なUIコンポーネント、API、およびユーザー認証・管理のための管理ダッシュボードを提供しているサービスです。
Next.js、ReactやRemixといったフロントエンドフレームワークに対応しています。

認証機能以外にも、提供されているUIコンポーネントを使用することで、簡単にサインイン、ログイン画面を実装できます。また、SupabaseやFirebaseとのインテグレーションも可能です。

アカウント作成

アカウント作成手順は以下です。

サインアップ

https://clerk.com/のページの右上にある「Get started」ボタンを押してアカウント作成ページに移動し、任意の方法でアカウントを作成してください。

アプリケーション設定

アカウント作成後、以下のようなページに移動します。
アプリケーション名を設定し、「Create application」を押下してください。

作成後は以下のページに飛ばされるはずです。

認証画面の実装

Clerkは前述の通り、UIコンポーネントを提供しているので、それを使用すれば簡単に認証画面の実装が可能です。
なお今回は、Next.jsでの実装かつApp Routerを使用する前提で説明します。

事前準備

Clerkを導入するために環境変数とProviderを設定する必要があります。

環境変数の設定

以下のようにパブリックキーとシークレットキーを.env.localへ記載してあげます。

NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=xxx
CLERK_SECRET_KEY=xxx

これらの値は、Configure > API Keysから取得できます。

Providerの設定

<ClerkProvider>を使用して囲ってあげるだけで大丈夫です。

app/layout.tsx
import React from 'react'
import { ClerkProvider } from '@clerk/nextjs'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <title>Next.js 13 with Clerk</title>
      </head>
      <ClerkProvider>
        <body>{children}</body>
      </ClerkProvider>
    </html>
  )
}

デフォルトだと英語表記になるため、Localizationを指定して日本語表記に変換することも可能です。

Localizationを指定する場合は、以下のように <ClerkProvider> へpropsとして渡す実装となります。

app/layout.tsx
import { ClerkProvider } from "@clerk/nextjs";

import { jaJP } from "@clerk/localizations";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <ClerkProvider localization={jaJP}>
      <html>
        <body>
          <main>{children}</main>
        </body>
      </html>
    </ClerkProvider>
  );
}

Middlewareの実装

最後にMiddlewareの実装を行います。
ドキュメントを参考に、以下のコードをmiddleware.tsへ追記することで完了です。
あくまでサンプルなので、必要に応じてmatcherの値は変更が必要です。

middleware.ts
import { clerkMiddleware } from "@clerk/nextjs/server";

export default clerkMiddleware();

export const config = {
  matcher: [
    // Skip Next.js internals and all static files, unless found in search params
    "/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
    // Always run for API routes
    "/(api|trpc)(.*)",
  ],
};

提供されたコンポーネントを使用した実装例

SignIn

提供されているSignInコンポーネントを使用することで、以下のようなサインイン表示を簡単に実装できます。

import { SignIn } from "@clerk/nextjs";
export default function Page() {
  return <SignIn />;
}

提供されているコンポーネントを使用することで、簡単にUIの実装が可能です。ここまででも使い勝手の良さがわかりますね。

しかし、ここからが本番です。
フォームの管理はReact Hook Formに任せたいといったケースもあるかと思いますので、React Hook Formを使用した実装例を説明していきます。

React Hook Formを使用した実装例

今回はSignInの実装のみを説明していきます。

client-side-helpers

今回SignIn処理の実装には、Clerkが提供しているヘルパーの一つである、useSignIn()を使用しています。

ほかにも色々なヘルパーが用意されているので、興味があれば覗いてみてください。

  • useUser()
  • useClerk()
  • useAuth()
  • useSignIn()
  • useSignUp()
  • useSession()
  • useSessionList()
  • useOrganization()
  • useOrganizationList()

SignIn

Hooks

カスタムフック内で、Clerkが提供するuseSignInを呼び出します。

ここで、formの定義とform経由で受け取った値をClerk側へ送信する処理を実装しています。
formへ入力された値のバリデーション用途にZodを使用しています。

src/hooks/useSignIn.ts
import { useSignIn } from '@clerk/nextjs'
import { zodResolver } from '@hookform/resolvers/zod'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { z, ZodType } from "zod";

type UserLoginProps = {
  email: string;
  password: string;
};

// ZodTypeを使用することで、既存の型情報と整合性が保たれない場合に型エラーを吐く
const UserLoginSchema: ZodType<UserLoginProps> = z.object({
  email: z
    .string()
    .email({ message: "メールアドレスの形式が正しくありません。" }),
  password: z.string(),
});

export const useSignInForm = () => {
  const { isLoaded, setActive, signIn } = useSignIn()
  const [loading, setLoading] = useState<boolean>(false)
  const methods = useForm<UserLoginProps>({
    resolver: zodResolver(UserLoginSchema),
    mode: 'onChange',
  })
  const onHandleSubmit = methods.handleSubmit(
    async (values: UserLoginProps) => {
      if (!isLoaded) return

      try {
        setLoading(true)
        // ここでclerk側へデータを送る
        const authenticated = await signIn.create({
          identifier: values.email,
          password: values.password,
        })

        // clerk側の処理が完了したら、トーストを表示してsessionに値を詰める
        if (authenticated.status === 'complete') {
          await setActive({ session: authenticated.createdSessionId })
        }
      } catch (error: any) {
        setLoading(false)
      }
    },
  )

  return {
    methods,
    onHandleSubmit,
    loading,
  }
}

Form Provider

Form Providerの定義を行います。
作成したhooksから必要な値を取得し、それぞれセットしていきます。

今回useFormContextを使用して、値の受け渡しを行いたいので<FormProvider {...methods}>を定義してあげます。

またonHandleSubmitはフォームからの送信をトリガーに処理を行わせたいので、formに以下のようにセットします。
<form onSubmit={onHandleSubmit} />

components/forms/sign-in/form-provider.tsx
'use client'
import { Loader } from '@/components/loader'
import { useSignInForm } from '@/hooks/sign-in/use-sign-in'
import React from 'react'
import { FormProvider } from 'react-hook-form'

type Props = {
  children: React.ReactNode
}

const SignInFormProvider = ({ children }: Props) => {
  const { methods, onHandleSubmit, loading } = useSignInForm()

  return (
      <FormProvider {...methods}>
        <form onSubmit={onHandleSubmit}>
          <div>
            {children}
          </div>
        </form>
      </FormProvider>
  )
}

export default SignInFormProvider

Formコンポーネント

次にFormコンポーネントを作成します。
useFormContext()を使用して、先ほど定義したFormの内容へアクセスします。

components/forms/sign-in/login-form.tsx
"use client";
import React from "react";
import { useFormContext } from "react-hook-form";

type Props = {};

const LoginForm = (props: Props) => {
  const {
    register,
    formState: { errors },
  } = useFormContext();
  return (
    <>
      <h2>ログイン</h2>
      <input
        id="email"
        type="input"
        placeholder="email"
        {...register("email", { required: "必須です。" })}
      />
      <input
        id="password"
        type="input"
        placeholder="password"
        {...register("password", { required: "必須です。" })}
      />
    </>
  );
};

export default LoginForm;

ログインページ

最後はログインページの作成です。
先ほど定義したProviderで囲み、作成したフォームコンポーネント<LoginForm />を呼び出せば完成です。

app/auth/sign-in/page.tsx
import SignInFormProvider from '@/components/forms/sign-in/form-provider'
import LoginForm from '@/components/forms/sign-in/login-form'
import Link from 'next/link'
import React from 'react'

const SignInPage = () => {
  return (
    <div>
      <div>
        <SignInFormProvider>
          <div>
            <LoginForm />
            <div>
              <button type="submit">
                ログイン
              </button>
              <p>
                アカウントをお持ちでないですか?
                <Link href="/sign-up">
                  アカウントの発行
                </Link>
              </p>
            </div>
          </div>
        </SignInFormProvider>
      </div>
    </div>
  )
}

export default SignInPage

この実装により、Clerk + React Hook Formを使用した認証機能が実現できました。
ただし、この段階ではCSSを適用していないため、UIは以下のような基本的なHTML要素の外観になります。実際のプロジェクトでは、必要に応じてスタイリングを追加してデザインを整えることになります。

認証情報の取り出し(おまけ)

初回サインアップしたユーザーに対して、createdUserIdが紐付けされます。
これをユーザー作成時にDBに保存しておくことで、Clerk側のユーザーデータとアプリケーション側のユーザーデータを紐付けることができます。

アプリケーション側でIDの取得を行いたい場合は、以下のようにして可能です:

import { currentUser } from '@clerk/nextjs/server'
const user = await currentUser()

const id = user.id

まとめ

今回Clerkを導入してみて、認証周りの実装を容易にしてくれて、とても助かるな〜という印象を持ちました。
また、提供されたUIを使わずに、useSignInといったヘルパーを使用することで、よく使用するReact Hook FormZodとも共存できるので、とても使い勝手が良いのではないでしょうか?

とはいえ、Next.jsを使うのであれば、Auth.jsの使用や、そもそもSupabaseをDBとして使用するのであれば、認証も任せることは可能なので、ユースケースに合わせて今後使用を検討していきたいなと思いました!

英語版はこちらから

参考情報

https://clerk.com/
https://clerk.com/docs/customization/localization
https://clerk.com/docs/references/nextjs/clerk-middleware
https://clerk.com/docs/components/authentication/sign-up
https://clerk.com/docs/components/authentication/sign-in
https://clerk.com/docs/references/nextjs/overview#client-side-helpers
https://react-hook-form.com/docs/useformcontext
https://authjs.dev/

Discussion