Webhooks

Synchronisation des utilisateurs avec les webhooks

Lorsque vous supprimez ou bannissez un utilisateur dans Authon, la base de données de votre application ne le sait pas automatiquement. Les webhooks résolvent ce problème en livrant des événements en temps réel à votre backend.

Le problème

Sans webhooks, votre application n'a aucun moyen de savoir quand l'état utilisateur d'Authon change. Cela entraîne des données obsolètes — des utilisateurs supprimés qui apparaissent encore, des utilisateurs bannis qui accèdent toujours aux fonctionnalités.

1
Un administrateur supprime un utilisateur dans le tableau de bord Authon
2
L'enregistrement utilisateur d'Authon est supprimé
3
La base de données de votre application contient toujours la ligne utilisateur obsolète — aucune synchronisation n'a eu lieu

La solution

Abonnez-vous aux événements user.*. Lorsqu'ils se déclenchent, mettez à jour votre base de données pour correspondre à l'état d'Authon.

1
Enregistrez un point de terminaison webhook dans le tableau de bord Authon
2
Abonnez-vous à user.created, user.updated, user.deleted, user.banned, user.unbanned
3
Dans votre gestionnaire, faites un upsert ou supprimez la ligne correspondante dans votre base de données

Exemple Express + Prisma

Un gestionnaire webhook complet qui maintient une table d'utilisateurs gérée par Prisma synchronisée avec 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 });
  }
);

Exemple Next.js App Router

Une route API Next.js qui gère les événements de synchronisation des utilisateurs en utilisant l'App Router.

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

Cas limites

Que se passe-t-il si le webhook échoue ?
Authon réessaie jusqu'à 3 fois avec un backoff exponentiel (1s, 2s). Si toutes les tentatives échouent, l'événement est marqué comme échoué dans le tableau de bord où vous pouvez le rejouer manuellement.
Ordre des événements
Les événements sont livrés dans l'ordre mais peuvent arriver dans le désordre lors des nouvelles tentatives. Utilisez le champ timestamp pour détecter et ignorer les mises à jour obsolètes lorsque l'ordre est important.
Migration initiale
Les webhooks ne capturent que les événements futurs. Pour la synchronisation initiale des utilisateurs existants, utilisez l'API Authon users.list() pour parcourir tous les utilisateurs et alimenter votre base de données.

Synchronisation inverse (Application → Authon)

Les webhooks gèrent la direction Authon → Application. Pour la direction inverse — créer ou mettre à jour des utilisateurs dans Authon depuis votre application — utilisez l'API backend avec votre clé secrète.

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

Toutes les opérations de gestion des utilisateurs sont disponibles via le point de terminaison REST API POST /v1/backend/users.

Authon — Plateforme d’authentification universelle