Webhook

Webhookによるユーザー同期

AuthonでユーザーをBANや削除しても、アプリのデータベースは自動的に知ることができません。Webhookはリアルタイムイベントをバックエンドに配信することでこの問題を解決します。

問題

Webhookがないと、Authonのユーザー状態が変化したときにアプリが知る方法がありません。削除されたユーザーが表示され続けたり、BANされたユーザーが機能にアクセスし続けるなど、古いデータが残ります。

1
管理者がAuthonダッシュボードでユーザーを削除
2
Authonのユーザーレコードが削除される
3
アプリのデータベースには古いユーザー行がそのまま残る — 同期なし

解決策

user.*イベントを購読します。イベントが発火したらデータベースをAuthonの状態に合わせて更新します。

1
AuthonダッシュボードでWebhookエンドポイントを登録
2
user.created、user.updated、user.deleted、user.banned、user.unbannedを購読
3
ハンドラーでデータベースの対応する行をupsertまたは削除

Express + Prismaの例

PrismaマネージドのusersテーブルをAuthonと同期状態に保つ完全なWebhookハンドラーです。

routes/webhooks.ts
import express from "express";
import { createHmac, timingSafeEqual } from "crypto";
import { PrismaClient } from "@prisma/client";

const app = express();
const prisma = new PrismaClient();

function verifyWebhook(
  rawBody: Buffer,
  signature: string,
  timestamp: string,
  secret: string,
): boolean {
  const payload = `${timestamp}.${rawBody.toString()}`;
  const expected = createHmac("sha256", secret).update(payload).digest("hex");
  const actual = signature.replace("v1=", "");
  return timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(actual, "hex"));
}

app.post(
  "/webhooks/authon",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    const sig = req.headers["x-authon-signature"] as string;
    const ts = req.headers["x-authon-timestamp"] as string; // ISO 8601

    if (!verifyWebhook(req.body, sig, ts, process.env.AUTHON_WEBHOOK_SECRET!)) {
      return res.status(401).json({ error: "Invalid signature" });
    }

    const { event, data } = JSON.parse(req.body.toString());

    switch (event) {
      case "user.created":
      case "user.updated":
        await prisma.user.upsert({
          where: { authonId: data.user.id },
          create: {
            authonId: data.user.id,
            email: data.user.email,
            name: data.user.displayName,
            avatarUrl: data.user.avatarUrl,
          },
          update: {
            email: data.user.email,
            name: data.user.displayName,
            avatarUrl: data.user.avatarUrl,
          },
        });
        break;

      case "user.deleted":
        await prisma.user.update({
          where: { authonId: data.user.id },
          data: { deletedAt: new Date() }, // soft-delete
        }).catch(() => {});
        break;

      case "user.banned":
        await prisma.user.update({
          where: { authonId: data.user.id },
          data: { suspended: true, suspendedAt: new Date() },
        });
        break;

      case "user.unbanned":
        await prisma.user.update({
          where: { authonId: data.user.id },
          data: { suspended: false, suspendedAt: null },
        });
        break;
    }

    res.json({ received: true });
  }
);

Next.js App Routerの例

App Routerを使用してユーザー同期イベントを処理するNext.js APIルートです。

app/api/webhooks/authon/route.ts
import { createHmac, timingSafeEqual } from "crypto";
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/db";

export async function POST(req: NextRequest) {
  const rawBody = await req.text();
  const signature = req.headers.get("x-authon-signature")!;
  const timestamp = req.headers.get("x-authon-timestamp")!; // ISO 8601

  const payload = `${timestamp}.${rawBody}`;
  const expected = createHmac("sha256", process.env.AUTHON_WEBHOOK_SECRET!)
    .update(payload)
    .digest("hex");
  const actual = signature.replace("v1=", "");

  if (!timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(actual, "hex"))) {
    return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
  }

  const { event, data } = JSON.parse(rawBody);

  if (event === "user.created" || event === "user.updated") {
    await db.user.upsert({
      where: { authonId: data.user.id },
      create: {
        authonId: data.user.id,
        email: data.user.email,
        name: data.user.displayName,
      },
      update: {
        email: data.user.email,
        name: data.user.displayName,
      },
    });
  } else if (event === "user.deleted") {
    await db.user.update({
      where: { authonId: data.user.id },
      data: { deletedAt: new Date() }, // soft-delete
    }).catch(() => {});
  }

  return NextResponse.json({ received: true });
}

エッジケース

Webhookの配信が失敗したら?
Authonは指数バックオフ(1秒、2秒)で最大3回再試行します。すべて失敗するとダッシュボードで失敗としてマークされ、手動で再実行できます。
イベントの順序
イベントは順序通りに配信されますが、リトライ中に順序が変わることがあります。順序が重要な場合はtimestampフィールドで古い更新を検出して無視してください。
初期マイグレーション
Webhookは将来のイベントのみキャプチャします。既存ユーザーの初期同期にはAuthon APIのusers.list()を使用して全ユーザーをページネーションしながらデータベースに投入してください。

逆方向同期(アプリ → Authon)

WebhookはAuthon → アプリ方向を処理します。逆方向 — アプリからAuthonへユーザーを作成・更新する場合 — はシークレットキーでBackend APIを使用してください。

lib/authon-sync.ts
import { AuthonBackend } from "@authon/node";

const authon = new AuthonBackend(process.env.AUTHON_SECRET_KEY!);

// Create a user in Authon from your app (e.g. after admin invite)
export async function createAuthonUser(email: string, name: string) {
  return authon.users.create({
    email,
    displayName: name,
    emailVerified: true,
  });
}

// Update user metadata in Authon when your app data changes
export async function syncMetadataToAuthon(
  authonId: string,
  publicMetadata: Record<string, unknown>,
) {
  return authon.users.update(authonId, { publicMetadata });
}

// Bulk import existing users from your app into Authon
export async function bulkImportToAuthon(
  users: Array<{ email: string; name: string }>,
) {
  for (const user of users) {
    await authon.users.create({
      email: user.email,
      displayName: user.name,
      emailVerified: true,
    });
  }
}

すべてのユーザー管理操作はREST APIエンドポイントで利用可能です POST /v1/backend/users.

Authon — ユニバーサル認証プラットフォーム