Skip to main content
Version: 2026.04.0

Webhooks

Primus Auth Broker can notify external services of authentication events by posting signed JSON payloads to any HTTPS endpoint you control. This enables real-time audit trails, provisioning pipelines, and analytics integrations without polling.


Enable Webhooks

Add a Webhooks section to your PrimusAuth configuration:

appsettings.json
{
"PrimusAuth": {
"Webhooks": {
"Enabled": true,
"Endpoint": "https://hooks.example.com/auth-events",
"SigningSecret": "USE-USER-SECRETS-NOT-HERE",
"TimeoutSeconds": 5,
"Events": []
}
}
}
FieldTypeDefaultAllowed valuesDescription
Enabledboolfalsetrue | falseActivates webhook dispatch.
Endpointstring""Any HTTPS URLHTTPS URL that receives event payloads. Required when Enabled is true.
SigningSecretstring""Any string (32+ chars recommended)Your chosen secret used to compute the HMAC-SHA256 signature on each payload. Generate a long random string — store it in user secrets or CI secret injection, never in source-controlled config files.
TimeoutSecondsint5Positive integerHTTP client timeout per webhook call. Auth is delayed by up to this duration if the endpoint is slow — keep it low.
Eventsstring[][]Array of event name stringsEvents to send. Empty array means all events. See Event Reference below for valid names.

Environment Variable Override

ASP.NET Core maps __ to : so all config keys work as environment variables (e.g. PrimusAuth__Webhooks__Enabled=true, PrimusAuth__Webhooks__SigningSecret=your-secret).


Event Reference

All events share the same envelope:

{
"event": "user.login",
"timestamp": "2026-03-13T10:30:00.0000000+00:00",
"data": { ... }
}

Each POST also includes an X-Primus-Event: <event-name> header alongside X-Primus-Signature.

EventWhen fireddata fields
user.loginSuccessful authentication (any provider)email, provider — provider values: "local", "azure", "auth0", "google", "okta", or "saml:{scheme}". Local login also includes id.
user.logoutExplicit sign-outemail
user.provisionedFirst-login JIT provisioning (OIDC only)email, provider
login.failedInvalid credentials on local loginemail, provider: "local", reason: "invalid_credentials"
mfa.enrolledUser completes TOTP enrollmentemail
mfa.verifiedUser passes MFA challengeemail
mfa.disabledUser removes TOTP from accountemail

Verifying Signatures

Every webhook POST includes an X-Primus-Signature header:

X-Primus-Signature: sha256=<hex>

On your receiving endpoint, compute the HMAC-SHA256 of the raw request body using your SigningSecret and compare it to the X-Primus-Signature header value. The format is sha256=<lowercase-hex>.

Use a constant-time comparison (e.g. CryptographicOperations.FixedTimeEquals) rather than == to prevent timing attacks. If the signatures do not match, return HTTP 401 and discard the payload.


Filtering Events

To receive only specific events, pass their names in the Events array:

{
"PrimusAuth": {
"Webhooks": {
"Enabled": true,
"Endpoint": "https://hooks.example.com/auth-events",
"SigningSecret": "...",
"Events": ["user.login", "login.failed"]
}
}
}

Events not in the list are silently dropped before the HTTP call is made.


Custom Dispatcher

By default, Primus uses HttpPrimusWebhookDispatcher (awaited inline using IHttpClientFactory). You can replace it by registering your own IPrimusWebhookDispatcher implementation before calling AddPrimusAuthBroker:

// Replace before AddPrimusAuthBroker
builder.Services.AddScoped<IPrimusWebhookDispatcher, MyAuditQueueDispatcher>();
builder.Services.AddPrimusAuthBroker(/* ... */);

The interface has a single method:

public interface IPrimusWebhookDispatcher
{
Task DispatchAsync(string eventName, object payload);
}

Behaviour Notes

  • Webhook dispatch is awaited inline — if the endpoint is slow, authentication is delayed by up to TimeoutSeconds seconds (default: 5). Keep TimeoutSeconds low, or use a queue-based custom dispatcher to decouple dispatch from the auth response entirely.
  • Failures (exceptions, timeouts, non-2xx responses) are caught and logged at Warning level via the standard ASP.NET ILogger. No retry logic is built-in; use a queue-based custom dispatcher if retry semantics are required.
  • The dispatcher is registered as a scoped service, so it participates in the same DI scope as the HTTP request.

Next Steps