웹훅

웹훅으로 사용자 동기화

Authon에서 사용자를 삭제하거나 차단해도 앱의 데이터베이스는 자동으로 알 수 없습니다. 웹훅은 실시간 이벤트를 백엔드에 전달하여 이 문제를 해결합니다.

문제

웹훅 없이는 Authon의 사용자 상태가 변경될 때 앱이 알 방법이 없습니다. 삭제된 사용자가 계속 표시되거나, 차단된 사용자가 여전히 기능에 접근하는 등 오래된 데이터가 남게 됩니다.

1
관리자가 Authon 대시보드에서 사용자 삭제
2
Authon의 사용자 레코드가 제거됨
3
앱 데이터베이스에는 오래된 사용자 행이 그대로 남아있음 — 동기화 없음

해결책

user.* 이벤트를 구독하세요. 이벤트가 발생하면 데이터베이스를 Authon 상태에 맞게 업데이트하세요.

1
Authon 대시보드에서 웹훅 엔드포인트 등록
2
user.created, user.updated, user.deleted, user.banned, user.unbanned 구독
3
핸들러에서 데이터베이스의 해당 행을 upsert 또는 삭제

Express + Prisma 예제

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 });
}

엣지 케이스

웹훅 전달이 실패하면?
Authon은 지수 백오프(1초, 2초)로 최대 3회 재시도합니다. 모든 시도가 실패하면 이벤트는 대시보드에서 실패로 표시되고 수동으로 재실행할 수 있습니다.
이벤트 순서
이벤트는 순서대로 전달되지만 재시도 중에 순서가 바뀔 수 있습니다. 순서가 중요한 경우 timestamp 필드를 사용하여 오래된 업데이트를 감지하고 무시하세요.
초기 마이그레이션
웹훅은 미래 이벤트만 캡처합니다. 기존 사용자의 초기 동기화는 Authon API의 users.list()를 사용하여 모든 사용자를 페이지네이션하며 데이터베이스에 채워넣으세요.

역방향 동기화 (앱 → Authon)

웹훅은 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 — 범용 인증 플랫폼