🌚

ClerkでWebhookを利用する方法

2023/08/10に公開

はじめに

  • Clerk の Webhook を利用し、サーバサイドで処理を実行する方法を紹介します。
  • サーバサイドで処理できるようになることで、バックエンドのデータベースと Clerk の情報を同期できます。
  • 下記が実装したコードです。

https://github.com/hayato94087/nextjs-clerk-webhook-sample

結論

  • Clerk の Webhook を利用し、Clerk でのアクションをトリガーにバックエンドのデータベースの更新処理をします。
  • Webhook は Clerk の管理画面から設定できます。
  • ローカルで Clerk の Webhook を利用するには、ngrok を利用します。

Webhookについて

Clerk でサーバサイドのデータベースとデータを同期するには、Webhook を利用します。サーバサイドとの同期については Clerk のこちらのページに詳細があります。

処理は以下の図の通りです。

  1. ユーザーがアプリケーションのフロントエンドを介して Clerk に対してアクション(認証操作)を実行します。
  2. フロントエンドが、Clerk に対して、アクション(認証操作)をリクエストします。
  3. Clerk がアクション(認証操作)の結果をフロントエンドに返します。
  4. Clerk が Webhook のイベントを作成し、バックエンドに送信します。
  5. サーバサイドがイベントを実行し、Clerk から送信されたデータをもとにデータベースの更新など、サーバサイドの処理を実行します。

今回は、データベースの更新を除き、一連の作業を実装します。

Next.jsプロジェクトの新規作成

作業するプロジェクトを新規に作成していきます。

長いので、折り畳んでおきます。

新規プロジェクト作成と初期環境構築の手順詳細
$ pnpm create next-app@latest nextjs-clerk-webhook-sample --typescript --eslint --import-alias "@/*" --src-dir --use-pnpm --tailwind --app
$ cd nextjs-clerk-webhook-sample

以下の通り不要な設定を削除し、プロジェクトの初期環境を構築します。

$ mkdir src/styles
$ mv src/app/globals.css src/styles/globals.css
src/styles/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;
src/app/page.tsx
export default function Home() {
  return (
    <main className="text-lg">
      テストページ
    </main>
  )
}
src/app/layout.tsx
import '@/styles/globals.css'

export const metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="ja">
      <body className="">{children}</body>
    </html>
  );
}
tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  plugins: [],
};
tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
+   "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

コミットします。

$ pnpm build
$ git add .
$ git commit -m "新規にプロジェクトを作成し, 作業環境を構築"

Clerkのアカウント作成

Clerk のアカウント作成はこちらを参照ください。

https://zenn.dev/hayato94087/articles/effd7e94ae8302

Clerkを設定

Next.js で Clerk が提供する SDK を利用するために、Clerk のパッケージをインストールします。

$ pnpm add @clerk/nextjs

環境変数を .env.local に設定します。APIKEY はダッシュボードから取得します。

APIKEY を .env.local に設定します。

$ touch .env.local
.env.local
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_publishable_key
CLERK_SECRET_KEY=your_secret_key

どこからでもセッション、認証済みのユーザー情報などの認証情報にアクセスできるように、<ClerkProvider> コンポーネントを設定します。layout.tsx<ClerkProvider> を設定します。

src/app/layout.tsx
+import { ClerkProvider } from "@clerk/nextjs";
import "./globals.css";

export const metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
+   <ClerkProvider>
      <html lang="ja">
        <body className="bg-white">{children}</body>
      </html>
+   </ClerkProvider>
  );
}

アプリケーション全体を保護するため、middleware.ts を更新します。

$ touch src/middleware.ts
src/middleware.ts
import { authMiddleware } from "@clerk/nextjs";

export default authMiddleware();

export const config = {
  matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
};

ローカルで実行します。

$ pnpm dev

コミットします。

$ pnpm build
$ git add .
$ git commit -m "clerkを設定"

いつWebhookを使うのか

  • Webhook は、Clerk とアプリケーションのバックエンドの間でデータを同期するための推奨される方法です。
  • Clerk で Webhook を有効化すると、Clerk インスタンスでイベントが発生するたびに、Clerk が指定した URL にリクエストを送信します。

Webhook対象のイベント

2023 年 8 月 7 日現在、Webhook 対象のイベントは以下です。

  • email.created
  • organization.created
  • organization.deleted
  • organization.updated
  • organizationInvitation.accepted
  • organizationInvitation.created
  • organizationInvitation.revoked
  • organizationMembership.created
  • organizationMembership.deleted
  • organizationMembership.updated
  • session.created
  • session.ended
  • session.removed
  • session.revoked
  • sms.created
  • user.created
  • user.deleted
  • user.updated

Webhookにおけるセキュリティ

Clerk は Svix を使用し Webhook におけるセキュリティを強化しています。具体的には、Webhook リクエストが有効であることを Svix を利用し検証しています。どのように検証しているかは、こちらを参照ください。

https://docs.svix.com/receiving/verifying-payloads/why

ngrokを設定

Webhook が localhost にリクエストを送信するためには、ngrok を設定する必要があります。ngrok とは、ローカルサーバーを外部に公開するためのツールです。

ngrokのアカウントを作成

まず、ngrok を利用するために、ngrok のアカウントを作成します。

https://dashboard.ngrok.com/signup にアクセスします。

https://dashboard.ngrok.com/signup

「Sign up with GitHub」をクリックします。

GitHub アカウントにログインします。

「Authorize ngrok-private」をクリックします。

アカウントを作成します。

2 段階認証を設定します。

MFA のコードを入力します。

2 段階認証のリカバリーコードが表示されるので大事にメモしておきます。

ngrok のアカウント作成が完了しました。

ngrokをローカルにインストール

次に、ngrok をローカルにインストールします。Mac の場合、Homebrew でインストールできます。その他のインストール方法はこちらを参照ください。

$ brew install ngrok/ngrok/ngrok

これでインストールが完了しました。

ngrokを起動

ngrok を起動します。localhost の 3000 番ポートを指定します。

$ ngrok http 3000
ngrok                                                                                        (Ctrl+C to quit)

🤯 Try the ngrok Kubernetes Ingress Controller: https://ngrok.com/s/k8s-ingress

Session Status                online
Session Expires               1 hour, 52 minutes
Terms of Service              https://ngrok.com/tos
Version                       3.3.2
Region                        Japan (jp)
Latency                       21ms
Web Interface                 http://127.0.0.1:4040
Forwarding                    https://1087-240b-10-d3e1-xxxx-xxxx-xxx-xxx-xxx.ngrok.io -> http://localhost

Connections                   ttl     opn     rt1     rt5     p50     p90
                              0       0       0.00    0.00    0.00    0.00

起動すると、locaohost:3000 が公開される先の URL が表示されます。これをコピーしておきます。

ClerkのWebhooksを有効化

Clerk の管理画面で Webhooks を有効化します。

  1. 「Add Endpoint」をクリックします。

  1. 各種情報を入力し、「Create」をクリックします。
  • Endpoint には、<先程コピーしたURL>/api/public/webhook を入力します。
  • Message Filtering は、user.created, user.deleted, user.updated を選択します。


3. これで Webhooks が有効化されました。

  1. Secret を取得します。

  1. Secret を環境変数に設定します。
.env.local
NGROK_WEBHOOK_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Next.jsでWebhookの処理を実装

それでは、Next.js で Webhook を受け取る処理を実装します。

パッケージをインストールします。

$ pnpm add svix
$ pnpm add encoding

実装するファイルを作成します。

$ mkdir -p src/app/api/public/webhook
$ touch    src/app/api/public/webhook/route.ts
src/app/api/public/webhook/route.ts
import { Webhook } from "svix";
import { headers } from "next/headers";
import { NextRequest } from "next/server";

const webhookSecret: string = process.env.NGROK_WEBHOOK_SECRET || "";

export async function POST(req: NextRequest) {
  const payload = await req.json();
  const payloadString = JSON.stringify(payload);
  const headerPayload = headers();
  const svixId = headerPayload.get("svix-id");
  const svixIdTimeStamp = headerPayload.get("svix-timestamp");
  const svixSignature = headerPayload.get("svix-signature");
  if (!svixId || !svixIdTimeStamp || !svixSignature) {
    console.log("svixId", svixId);
    console.log("svixIdTimeStamp", svixIdTimeStamp);
    console.log("svixSignature", svixSignature);
    return new Response("Error occured", {
      status: 400,
    });
  }
  const svixHeaders = {
    "svix-id": svixId,
    "svix-timestamp": svixIdTimeStamp,
    "svix-signature": svixSignature,
  };
  const wh = new Webhook(webhookSecret);
  let evt: Event | null = null;
  try {
    evt = wh.verify(payloadString, svixHeaders) as Event;
  } catch (_) {
    console.log("error");
    return new Response("Error occured", {
      status: 400,
    });
  }

  // Handle the webhook
  const eventType: EventType = evt.type;
  if (eventType === "user.created") {
    const { id, ...attributes } = evt.data;
    console.log("user.created: ", id);
    // console.log(attributes);

    //todo : do something

    return new Response("", {
      status: 201, // 201 Created
    });
  } else if (eventType === "user.deleted") {
    const { id, ...attributes } = evt.data;
    console.log("user.deleted: ", id);
    // console.log(attributes);

    //todo : do something

    return new Response("", {
      status: 200, // 200 OK
    });
  } else if (eventType === "user.updated") {
    const { id, ...attributes } = evt.data;
    console.log("user.updated: ", id);
    // console.log(attributes);

    //todo : do something

    return new Response("", {
      status: 200, // 200 OK
    });
  }
  return new Response("bad request", {
    status: 400, // 400 Bad Request
  });
}

type Event = {
  data: Record<string, string | number>;
  object: "event";
  type: EventType;
};

type EventType = "user.created" | "user.deleted" | "user.updated";

middleware.ts を修正しておきます。

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

export default authMiddleware({
  beforeAuth(req) {
    // todo : do something
  },
  ignoredRoutes: ["/api/public/webhook"],
});

export const config = {
  matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
};

コミットします。

$ pnpm build
$ git add .
$ git commit -m "Webhookの処理を実装"

動作確認

開発サーバを起動し確認します。

$ pnpm dev

まず、ユーザーが存在しないことを確認します。存在する場合は削除します。

localhost:3000 にアクセスし、「Continue with Google」でサインインします。これで、ユーザーが作成されます。

サーバのログを確認すると、以下のように webhook によってイベントが発生していることがわかります。

user.created:  user_2TjmoiuonKeJC9ccc7e7SycQLIf
user.updated:  user_2TjmoiuonKeJC9ccc7e7SycQLIf

Clerk の管理画面からユーザーを削除します。

サーバのログを確認すると、以下のように webhook によってイベントが発生していることがわかります。

user.deleted:  user_2TjmoiuonKeJC9ccc7e7SycQLIf

まとめ

  • Clerk の Webhook をトリガーに、サーバサイドで処理を実行する方法を紹介しました。
  • サーバサイドで処理できるようになることで、サーバサイドのデータベースと Clerk の情報を連携できます。
  • 今回は、データベースの更新を除き、一連の作業を実装しました。

下記が実装したコードです。

https://github.com/hayato94087/nextjs-clerk-webhook-sample

参考

Discussion