Webhooks

Benutzer mit Webhooks synchronisieren

Wenn Sie einen Benutzer in Authon löschen oder sperren, weiß die Datenbank Ihrer App davon nicht automatisch. Webhooks lösen dieses Problem, indem sie Echtzeit-Ereignisse an Ihr Backend liefern.

Das Problem

Ohne Webhooks hat Ihre App keine Möglichkeit zu erfahren, wenn sich der Benutzerstatus in Authon ändert. Dies führt zu veralteten Daten — gelöschte Benutzer erscheinen weiterhin, gesperrte Benutzer greifen weiterhin auf Funktionen zu.

1
Administrator löscht einen Benutzer im Authon-Dashboard
2
Authons Benutzerdatensatz wird entfernt
3
Die Datenbank Ihrer App hat noch die veraltete Benutzerzeile — keine Synchronisation erfolgte

Die Lösung

Abonnieren Sie user.*-Ereignisse. Wenn sie ausgelöst werden, aktualisieren Sie Ihre Datenbank, um Authons Status anzupassen.

1
Registrieren Sie einen Webhook-Endpunkt im Authon-Dashboard
2
Abonnieren Sie user.created, user.updated, user.deleted, user.banned, user.unbanned
3
Führen Sie in Ihrem Handler ein Upsert durch oder löschen Sie die entsprechende Zeile in Ihrer Datenbank

Express + Prisma Beispiel

Ein vollständiger Webhook-Handler, der eine von Prisma verwaltete Benutzertabelle mit Authon synchron hält.

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 Beispiel

Eine Next.js-API-Route, die Benutzer-Synchronisationsereignisse mit dem App Router verarbeitet.

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

Randfälle

Was passiert, wenn der Webhook fehlschlägt?
Authon wiederholt den Versuch bis zu 3 Mal mit exponentiell ansteigendem Backoff (1s, 2s). Wenn alle Versuche fehlschlagen, wird das Ereignis im Dashboard als fehlgeschlagen markiert, wo Sie es manuell erneut abspielen können.
Ereignisreihenfolge
Ereignisse werden in Reihenfolge zugestellt, können aber bei Wiederholungen außer der Reihe ankommen. Verwenden Sie das Zeitstempel-Feld, um veraltete Aktualisierungen zu erkennen und zu ignorieren, wenn die Reihenfolge wichtig ist.
Erstmigration
Webhooks erfassen nur zukünftige Ereignisse. Für die erste Synchronisation vorhandener Benutzer verwenden Sie die Authon-API users.list(), um alle Benutzer zu durchblättern und Ihre Datenbank zu befüllen.

Rückwärtssynchronisation (App → Authon)

Webhooks verarbeiten die Richtung Authon → App. Für die umgekehrte Richtung — Erstellen oder Aktualisieren von Benutzern in Authon aus Ihrer App — verwenden Sie die Backend-API mit Ihrem geheimen Schlüssel.

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

Alle Benutzerverwaltungsoperationen sind über den REST-API-Endpunkt verfügbar POST /v1/backend/users.

Authon — Universelle Authentifizierungsplattform