Skip to content

Handling webhooks

To effectively manage incoming webhooks from Ory Kratos, you can employ the KratosWebHookHandlerBase class. The following example demonstrates how to synchronize identity data received from Kratos through webhooks with your database using MassTransit.

Packages

Package Link Application in section
LeanCode.Kratos NuGet version (LeanCode.Kratos) Webhook handling

Configuring webhooks in Kratos

Let's begin by incorporating a webhook into our Kratos configuration. This webhook triggers after a user completes the registration process using a username/email and password combination. It sends a POST request to the /kratos/sync-identity endpoint. The request includes an API key specified in the X-Api-Key header and the user's Kratos identity in the request body.

# . . .
  flows:
    registration:
      ui_url: https://${domain}/registration
      lifespan: 1h
      enabled: true
      after:
        password:
          hooks:
            - hook: web_hook
              config:
                url: "${api}/kratos/sync-identity"
                method: POST
                auth:
                  type: api_key
                  config:
                    in: header
                    name: X-Api-Key
                    value: "${web_hook_api_key}"
                body: file:///etc/kratos/webhook.identity.mapper.jsonnet
                response:
                  ignore: true
                  parse: false

# . . .

The webhook.identity.mapper.jsonnet content is as follows:

function(ctx) {
  identity: ctx.identity,
}

Endpoint mapping in application configuration

Now, let's map the KratosIdentitySyncHandler class to the kratos webhook endpoint, which in this case is /kratos/sync-identity, within the application configuration:

protected override void ConfigureApp(IApplicationBuilder app)
{
    // . . .

    app.UseAuthentication()
        .UseEndpoints(endpoints =>
        {
            // . . .

            endpoints.MapPost(
                "/kratos/sync-identity",
                ctx => ctx.RequestServices
                    .GetRequiredService<KratosIdentitySyncHandler>()
                    .HandleAsync(ctx)
            );

            // . . .
        });

    // . . .
}

Defining the Webhook Handler

Then, you can define the webhook handler to handle incoming requests. In this example, we deserialize request into Identity class, verify it inside a webhook and send an event that the identity has been updated, which will be handled by the appropriate IConsumer using MassTransit. The handling of the API key check is already managed within the KratosWebHookHandlerBase class, eliminating the necessity to perform this action in HandleCoreAsync(...). By default, KratosWebHookHandlerBase uses the X-Api-Key header to verify the API key. However, if required, this behavior can be altered by overriding the ApiKeyHeaderName property.

public partial class KratosIdentitySyncHandler : KratosWebHookHandlerBase
{
    private readonly IBus bus;

    public KratosIdentitySyncHandler(
        KratosWebHookHandlerConfig config,
        IBus bus)
        : base(config)
    {
        this.bus = bus;
    }

    protected override async Task HandleCoreAsync(HttpContext ctx)
    {
        // Deserialize request into `Identity` class
        // from `LeanCode.Kratos` package.
        var body = await JsonSerializer.DeserializeAsync(
            ctx.Request.Body,
            KratosIdentitySyncHandlerContext.Default.RequestBody,
            ctx.RequestAborted
        );

        var identity = body.Identity;

        if (identity is null)
        {
            // Helper method defined in KratosWebHookHandlerBase.
            await WriteErrorResponseAsync(
                ctx,
                new List<ErrorMessage>
                {
                    new ErrorMessage(
                        null,
                        new List<DetailedMessage>
                        {
                            new DetailedMessage(
                                1,
                                "identity is null",
                                "error",
                                null),
                        }),
                },
                422
            );
            return;
        }
        else if (identity.Id == default)
        {
            await WriteErrorResponseAsync(
                ctx,
                new List<ErrorMessage>
                {
                    new ErrorMessage(
                        null,
                        new List<DetailedMessage>
                        {
                            new DetailedMessage(
                                2,
                                "identity.id is empty",
                                "error",
                                null),
                        }),
                },
                422
            );
            return;
        }

        // Send a message via MassTransit that identity has been updated.
        await bus.Publish(
            new KratosIdentityUpdated(Guid.NewGuid(), Time.UtcNow, identity),
            ctx.RequestAborted);

        ctx.Response.StatusCode = 200;
    }

    public record struct RequestBody(
        [property: JsonPropertyName("identity")] Identity? Identity);

    [JsonSourceGenerationOptions(
        PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower)]
    [JsonSerializable(typeof(RequestBody))]
    private partial class KratosIdentitySyncHandlerContext
        : JsonSerializerContext { }
}

Tip

To read about LeanCode CoreLibrary MassTransit integration visit here.

Updating the Database

With the KratosIdentitySyncHandler in place, you can process incoming Kratos webhooks and handle them appropriately. The following code snippet updates the database table that stores identities (assuming that KratosIdentities table is already present in your database) when a KratosIdentityUpdated event is received:

public sealed record class KratosIdentityUpdated(
    Guid Id,
    DateTime DateOccurred,
    Identity Identity)
    : IDomainEvent;

public class SyncKratosIdentity : IConsumer<KratosIdentityUpdated>
{
    private readonly CoreDbContext dbContext;

    public SyncKratosIdentity(CoreDbContext dbContext)
    {
        this.dbContext = dbContext;
    }

    public async Task Consume(ConsumeContext<KratosIdentityUpdated> context)
    {
        var kratosIdentity = context.Message.Identity;
        var identityId = kratosIdentity.Id;

        var dbIdentity = await dbContext.KratosIdentities.FindAsync(
            keyValues: new[] { (object)identityId },
            context.CancellationToken
        );

        // Database transaction will be commited at the end of the pipeline
        // assuming `CommitDatabaseTransactionMiddleware` was added
        if (dbIdentity is null)
        {
            dbIdentity = new(kratosIdentity);
            dbContext.KratosIdentities.Add(dbIdentity);
        }
        else
        {
            dbIdentity.Update(kratosIdentity);
            dbContext.KratosIdentities.Update(dbIdentity);
        }
    }
}

Tip

To read about handling events visit here and to read about the pipeline visit here