Skip to main content

Webhooks

Outbound, HMAC-signed delivery of authentication events. Subscribers are notified in near-real-time when users register, log in, log out, enroll MFA, get removed from orgs, etc. — so you can mirror IdentSphere's auth events into your own audit log or trigger downstream automation without polling.

The engine lives in the identsphere-webhooks crate. It hooks into the audit pipeline: every audit event that maps to a webhook event type is dispatched as it's logged. Events that don't map (failed logins, list operations) are not delivered — use the audit-log API for those.

Enabling

Webhooks are off by default. Turn them on with one env var — a comma-separated list of url|secret pairs:

IDENTSPHERE_WEBHOOK_ENDPOINTS="https://app.example.com/hooks/identsphere|whsec_abc123"
# multiple subscribers fan out — each gets its own signed delivery:
IDENTSPHERE_WEBHOOK_ENDPOINTS="https://a.example.com/h|secretA,https://b.example.com/h|secretB"

Each endpoint's secret is the HMAC signing key for deliveries to that URL. Rotate by changing the value and redeploying.

Delivery

Every delivery is a POST with these headers:

POST {your_url}
Content-Type: application/json
X-IdentSphere-Event-Id: 3f1c… (UUID, stable across any future retries)
X-IdentSphere-Event-Type: session_created
X-IdentSphere-Timestamp: 1748448000
X-IdentSphere-Signature: t=1748448000,v1=abc…hmac_sha256_hex

{
"id": "3f1c…",
"event": "session_created",
"timestamp": 1748448000,
"data": {
"organization_id": "…",
"actor_id": "…",
"actor_type": "user",
"action": "auth.login",
"resource_type": "session",
"resource_id": "…",
"ip_address": "203.0.113.7",
"user_agent": "Mozilla/5.0 …",
"request_id": "7f3a…",
"outcome": "success",
"metadata": { … }
}
}

ip_address, user_agent, and request_id carry the request context of the event — use them to attach IP/geo + device to auth events (login, MFA, passkey) on the receiving side. They are null for system-originated events that have no request context.

Semantics

  • Best-effort, fire-and-forget, non-blocking. Delivery never blocks or fails the originating auth request. Each endpoint is delivered on its own task; non-2xx responses and transport errors are logged, not retried (v1).
  • No per-endpoint filtering — every subscriber receives every mapped event. Filter on event in your handler.
  • Need at-least-once durability? Point a subscriber at a thin relay (a queue producer — SQS / Kafka / a Cloudflare Worker) and fan out from there, or treat the audit-log API as the durable source of truth and use webhooks only as the low-latency nudge.

Signing & verification

The signature is HMAC-SHA256(secret, "{timestamp}.{raw_body}"), hex-encoded, carried as v1= in X-IdentSphere-Signature. Verify it against the raw request body before parsing:

import crypto from 'crypto';

function verify(rawBody: string, header: string, secret: string): boolean {
const parts = Object.fromEntries(header.split(',').map((kv) => kv.split('=')));
const t = parts.t;
const v1 = parts.v1;

// Reject anything older than 5 minutes — protects against replay.
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;

const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex');

// constant-time compare
return (
v1.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))
);
}

Same scheme as Stripe / GitHub — the timestamp prefix defeats replay.

Event types

Event names are snake_case (matching the event field and the X-IdentSphere-Event-Type header). Each is fanned out from one or more audit actions:

eventFired by (audit action)
user_createdauth.register
session_createdauth.login, auth.email_otp.login, auth.passkey.login, auth.login.saml
session_revokedauth.logout, auth.refresh.reuse_detected (family revoked on theft)
mfa_enrolled / mfa_disabledauth.mfa.enabled / auth.mfa.disabled
passkey_added / passkey_removedauth.passkey.added / auth.passkey.removed
password_changedauth.password_reset.consumed
member_invitedmembers.invite
member_joinedmembers.invitation_accepted
member_removedmembers.remove
member_role_changedmembers.change_role

The mapping is identsphere_webhooks::event_type_for; the full set is the EventType enum in identsphere_webhooks.

Local development

Point an endpoint at webhook.site or an ngrok tunnel to inspect live deliveries from a localhost (or staging) IdentSphere instance — the signature scheme is identical to production.

Custom binaries

Hosts building their own server binary can construct the dispatcher directly instead of using the env var:

use identsphere_webhooks::{WebhookDispatcher, WebhookEndpoint};

let dispatcher = WebhookDispatcher::new(vec![WebhookEndpoint {
url: "https://hooks.example.com/identsphere".into(),
secret: b"whsec_abc123".to_vec(),
}]);
let audit_service = AuditService::new(db).with_observer(std::sync::Arc::new(dispatcher));