ClerkでWebhookを利用する方法
はじめに
- Clerk の Webhook を利用し、サーバサイドで処理を実行する方法を紹介します。
- サーバサイドで処理できるようになることで、バックエンドのデータベースと Clerk の情報を同期できます。
- 下記が実装したコードです。
結論
- Clerk の Webhook を利用し、Clerk でのアクションをトリガーにバックエンドのデータベースの更新処理をします。
- Webhook は Clerk の管理画面から設定できます。
- ローカルで Clerk の Webhook を利用するには、ngrok を利用します。
Webhookについて
Clerk でサーバサイドのデータベースとデータを同期するには、Webhook を利用します。サーバサイドとの同期については Clerk のこちらのページに詳細があります。
処理は以下の図の通りです。
- ユーザーがアプリケーションのフロントエンドを介して Clerk に対してアクション(認証操作)を実行します。
- フロントエンドが、Clerk に対して、アクション(認証操作)をリクエストします。
- Clerk がアクション(認証操作)の結果をフロントエンドに返します。
- Clerk が Webhook のイベントを作成し、バックエンドに送信します。
- サーバサイドがイベントを実行し、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
@tailwind base;
@tailwind components;
@tailwind utilities;
export default function Home() {
return (
<main className="text-lg">
テストページ
</main>
)
}
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>
);
}
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
plugins: [],
};
{
"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 のアカウント作成はこちらを参照ください。
Clerkを設定
Next.js で Clerk が提供する SDK を利用するために、Clerk のパッケージをインストールします。
$ pnpm add @clerk/nextjs
環境変数を .env.local
に設定します。APIKEY はダッシュボードから取得します。
APIKEY を .env.local
に設定します。
$ touch .env.local
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_publishable_key
CLERK_SECRET_KEY=your_secret_key
どこからでもセッション、認証済みのユーザー情報などの認証情報にアクセスできるように、<ClerkProvider>
コンポーネントを設定します。layout.tsx
に <ClerkProvider>
を設定します。
+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
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 を利用し検証しています。どのように検証しているかは、こちらを参照ください。
ngrokを設定
Webhook が localhost にリクエストを送信するためには、ngrok を設定する必要があります。ngrok とは、ローカルサーバーを外部に公開するためのツールです。
ngrokのアカウントを作成
まず、ngrok を利用するために、ngrok のアカウントを作成します。
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 を有効化します。
- 「Add Endpoint」をクリックします。
- 各種情報を入力し、「Create」をクリックします。
- Endpoint には、
<先程コピーしたURL>/api/public/webhook
を入力します。 - Message Filtering は、
user.created
,user.deleted
,user.updated
を選択します。
3. これで Webhooks が有効化されました。
- Secret を取得します。
- Secret を環境変数に設定します。
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
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
を修正しておきます。
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://clerk.com/docs/users/sync-data-to-your-backend
- https://clerk.com/docs/integration/webhooks
- https://github.com/clerkinc/clerk-nextjs-examples/blob/main/examples/widget/pages/api/webhooks/user.ts
- https://docs.svix.com/receiving/verifying-payloads/how
- https://github.com/kavinvalli/clerk-auth/tree/dblocal
- https://www.youtube.com/watch?v=NgBxrIC1eHM
- https://nextjs.org/docs/app/building-your-application/routing/route-handlers
- https://ngrok.com/docs/integrations/clerk/webhooks/
- https://www.youtube.com/watch?v=NgBxrIC1eHM
- https://docs.svix.com/receiving/verifying-payloads/why
Discussion