Webhooks

通过 Webhooks 同步用户

当您在 Authon 中删除或封禁用户时,您应用的数据库不会自动感知。Webhooks 通过向您的后端实时投递事件来解决这个问题。

问题所在

没有 Webhooks,您的应用无从得知 Authon 用户状态何时变化。这会导致数据陈旧 — 已删除的用户仍然显示,已封禁的用户仍可访问功能。

1
管理员在 Authon 控制台中删除用户
2
Authon 的用户记录被移除
3
您应用的数据库仍保留陈旧的用户行 — 未发生同步

解决方案

订阅 user.* 事件。当事件触发时,更新您的数据库以匹配 Authon 的状态。

1
在 Authon 控制台中注册 Webhook 端点
2
订阅 user.created、user.updated、user.deleted、user.banned、user.unbanned
3
在处理器中,对数据库中对应的行执行 upsert 或 delete 操作

Express + Prisma 示例

一个完整的 Webhook 处理器,使 Prisma 管理的 users 表与 Authon 保持同步。

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 以指数退避方式最多重试3次(1秒、2秒)。若所有尝试均失败,事件将在控制台中标记为失败,您可以手动重放。
事件顺序
事件按顺序投递,但重试期间可能乱序到达。当顺序重要时,使用 timestamp 字段检测并忽略过期更新。
初始迁移
Webhooks 仅捕获未来事件。对于现有用户的初始同步,请使用 Authon API 的 users.list() 分页遍历所有用户并填充您的数据库。

反向同步(应用 → Authon)

Webhooks 处理 Authon → 应用方向的同步。反向同步 — 从您的应用在 Authon 中创建或更新用户 — 请使用带私密密钥的后端 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 — Universal Authentication Platform