diff --git a/_snippets/snippet-example.mdx b/_snippets/snippet-example.mdx deleted file mode 100644 index 089334c..0000000 --- a/_snippets/snippet-example.mdx +++ /dev/null @@ -1,3 +0,0 @@ -## My Snippet - -This is an example of a reusable snippet diff --git a/guides/custom-emails/email-delivery.mdx b/guides/custom-emails/email-delivery.mdx index c8b87da..6d4252e 100644 --- a/guides/custom-emails/email-delivery.mdx +++ b/guides/custom-emails/email-delivery.mdx @@ -4,6 +4,8 @@ title: Send Custom Emails description: Send emails with your own text and style. --- +import ProFeatureNote from '/snippets/pro-feature-note.mdx' + This guide will walk you through the process of sending your own emails instead of relying on Hanko's default email delivery. ## Create a Webhook @@ -17,63 +19,39 @@ Create a webhook with the event `email.send`. This webhook will be triggered whe ## Disable Email delivery by Hanko -1. Log in to the [Hanko Cloud Console](https://cloud.hanko.io) and select your project. -2. Navigate to `Settings > Email`. -3. Disable the Email delivery by Hanko. + + + + + Log in to Hanko Cloud, select your organization and project and navigate to `Settings > Email delivery`. + + + Find the `Email delivery by Hanko` setting and click the toggle to disable it. + + Once disabled, Hanko will no longer send any emails on your behalf. ## Send your own emails -When an email needs to be sent, the `email.send` webhook event will be triggered, providing you with all the necessary data to send your own email. The webhook payload will include the following information: - -```json -{ - "subject": "", - "body_plain": "", - "body": "", - "to_email_address": "", - "delivered_by_hanko": true, - "accept_language": "", - "type": "passcode", - "data": { - "service_name": "", - "otp_code": "", - "ttl": 1000, - "valid_until": 1000 - } -} -``` - -* `subject`: The subject line of the email. -* `body_plain`: The plain text version of the email body. -* `body`: The HTML version of the email body. (`nullable`) -* `to_email_address`: The recipient's email address. -* `delivered_by_hanko`: Indicates whether the email was delivered by Hanko (true) or not (false). -* `accept_language`: The preferred language for the email content. -* `type`: The type of the email being sent (e.g., "passcode"). -* `data`: Additional data specific to the email type. - -The `data` property in the webhook payload provides type-specific data that can be used to personalize the email further. The structure of the `data` property varies depending on the `type` of the email being sent. +When an email needs to be sent, the `email.send` webhook event will be triggered, providing you with all the necessary +data to send your own email. The webhook token payload will include the following information: -For example, if the email type is "passcode", the `data` property will include the following fields: +import WebhookEmailSendExample from '/snippets/webhooks/email-send-example.mdx'; +import WebhookEmailSendProperties from '/snippets/webhooks/email-send-properties.mdx'; - - - ```json - "service_name": "", - "otp_code": "", - "ttl": 1000, - "valid_until": 1000 - ``` + + + + + + + + - |property|description| - |----|----| - |`service_name`|The name of the service set in the console as project name| - |`otp_code`|The passcode the user can use to log in| - |`ttl`|The validity duration of the passcode in seconds| - |`valid_until`|The Unix timestamp indicating when the passcode expires| - - +The `data` property in the token payload provides type-specific data that can be used to personalize +the email further. The structure of the `data` property varies depending on the `type` +of the email being sent. -Using the provided webhook data, you can compose and send the email to the user using your preferred email service provider or custom email infra. +Using the provided webhook data, you can compose and send the email to the user using your preferred email service +provider or custom email infrastructure. diff --git a/guides/webhooks/introduction.mdx b/guides/webhooks/introduction.mdx index a66a277..03471cc 100644 --- a/guides/webhooks/introduction.mdx +++ b/guides/webhooks/introduction.mdx @@ -1,68 +1,389 @@ --- sidebarTitle: "Webhooks" -title: Learn more about Webhooks -description: Webhooks allow you to get notified on changes +title: Webhooks +description: Learn more about Webhooks --- -Webhooks are an easy way to get informed about changes in your Hanko instance (e.g. user or email updates). -To use webhooks you have to provide an endpoint on your application which can process the events. Please be aware that your -endpoint need to respond with an HTTP status code 200. Else-wise the delivery of the event will not be counted as successful. +import WebhookUserPayloadExample from '/snippets/webhooks/user-payload-example.mdx'; +import WebhookUserPayloadProperties from '/snippets/webhooks/user-payload-properties.mdx'; +import WebhookEmailSendExample from '/snippets/webhooks/email-send-example.mdx'; +import WebhookEmailSendProperties from '/snippets/webhooks/email-send-properties.mdx'; +import ProFeatureNote from '/snippets/pro-feature-note.mdx' + + + +## About webhooks + +Webhooks allow you to subscribe to events within a Hanko project and automatically receive data deliveries to your +server/application whenever those events take place. This allows you to, for example, synchronize user data between your +application(s) and Hanko. + +To create a webhook you specify a callback URL and subscribe to events that occur in your Hanko project. Once an +event that your webhook is subscribed to occurs, Hanko will send an HTTP POST request with data about the event to the +URL that you specified. If your application provides a publicly available HTTP endpoint listening for webhook deliveries +at the configured callback URL, it can react and process webhook data. + + + ```mermaid + sequenceDiagram + participant A as Relying Party + participant B as Hanko + + A->>B: Create webhook + B->>B: Event occurs + B->>A: HTTP POST to callback URL + A->>A: Parse webhook payload + A->>A: Validate event data + A->>A: Process event data + A-->>B: Acknowledge delivery + ``` + + + +## Creating webhooks + +To create a webhook: + + + + Log in to the [Hanko Cloud Console](https://cloud.hanko.io), select your organization and project and navigate to + `Settings > Webhooks`. + + + Click `Create webhook`. Enter a callback URL and select the events that you want to subscribe to. + See [Events](#events) for more information on the events you can subscribe to. + + + +You are free to choose whether you create a single webhook with one HTTP endpoint processing multiple events or +multiple webhooks with more than one HTTP endpoint handling specific events or event groups. + +## Handling webhook deliveries + +To handle webhook deliveries: + + + + In order to handle webhook deliveries, your application must provide a publicly available HTTP POST endpoint + listening for webhook deliveries at the configured callback URL. + + + Once you have an endpoint set up you need to extract the webhook [event payload](#event-payload). It + contains information about which event occurred and the actual event data encoded in a JSON Web Token (JWT). + + + To ensure that your application only processes webhook deliveries that were sent by Hanko and to ensure that + the delivery has not been tampered with, you should validate the JWT's signature before processing the delivery + further. You can use the JSON Web Key Set available through your tenant's + [.well-known](/api-reference/public/well-known/get-json-web-key-set) endpoint to do so. + + + The JWT contained in the webhook payload must be parsed to obtain the event + data from the token payload. The structure of the event data may differ from event type to event type. + For more information, see [Event types and token payloads](#event-types-and-token-payloads). + + + Once you have extracted the event data from the token you can process it according to your application's needs. + + + + +This example uses [express](https://www.npmjs.com/package/express) and the [jose](https://www.npmjs.com/package/jose) +package to parse and verify JWTs. + +```shell +npm install express jose +``` + +The example assumes usage of a single HTTP endpoint for all event types but you could just as well configure +multiple webhooks and use multiple HTTP endpoints. + +```javascript +// These are the dependencies you should have installed for +// this example. +const express = require('express'); +const { createRemoteJWKSet, jwtVerify } = require('jose'); + +const app = express(); + +// Middleware for parsing requests with a JSON payload. +app.use(express.json()); + +// Step 1: This defines a POST endpoint at the `/webhook` path. +// This path should match the path portion of the URL that you +// specified for the callback URL when you created the webhook. +// Once you edit a webhook by updating the callback URL of your +// webhook, you should change this to match the path portion of +// the updated URL for your webhook. +app.post('/webhook', async (req, res) => { + // Step 2: Extract the event and token from the request body. + // You could use the event type to branch and apply + // logic/code for specific event types. + // This example assumes one endpoint for all event types so + // extracting the `event` property may lead to an unused + // variable. + const { event, token } = req.body; + + try { + // This would likely come from your environment/config. + // You can always find your tenant ID on the dashboard + // for your project in the Hanko Cloud Console. + const tenantId = 'your-tenant-id'; + + // See also the API reference: + // http://docs.hanko.io/api-reference/public/well-known/get-json-web-key-set + const jwksUrl = `https://${tenantId}.hanko.io/.well-known/jwks.json`; + + // Step 3 + 4: Fetch the JWKS of your Hanko tenant, verify + // the token signature using the JWKS and extract the + // payload. + const jwks = createRemoteJWKSet(new URL(jwksUrl)); + const { payload } = await jwtVerify(token, jwks); + + console.log('Decoded token payload:', payload); + + // Step 6: Do further processing according to your + // application's needs. + + } catch (error) { + console.error('Error processing the token:', error.message); + } + + // Your endpoint should respond with a 2XX response within 30 seconds + // of receiving a webhook delivery to indicate that the delivery was + // successfully received. If your server takes longer than that to + // respond, then Hanko terminates the connection and considers the + // delivery a failure. + res.sendStatus(202); +}); + +// Start the Express server +const PORT = 3000; +app.listen(PORT, () => { + console.log(`Server is running on http://localhost:${PORT}`); +}); + +``` + Your server **must** return the complete certificate chain otherwise the request will fail. +## Editing/removing webhooks + +You can edit or remove configured webhooks. To do so: + + + + Log in to [Hanko Cloud](https://cloud.hanko.io), select your organization and project and navigate to + `Settings > Webhooks`. + + + Locate the desired webhook and click on the three dots (`...`). Select `Edit` to change either the callback URL + of the webhook or the events to subscribe to. Select `Delete` to remove the webhook entirely. + + + ## Events -When a webhook is triggered it will send you a **JSON** body which contains the event and a jwt. -The JWT contains 2 custom claims: -* **data**: contains the whole object for which the change was made. (e.g.: the whole user object when an email or user is changed/created/deleted) -* **evt**: the event for which the webhook was triggered +There are different types of events you can subscribe to. The event type determines the contents of the event payload +(i.e. the body content of the request to your callback URL in response to an event occurrence). -A typical webhook event looks like: +### Event payload + +The structure of the event payload is the same across all event types. It contains the event type and the event data in the form of a JSON Web +Token (JWT). + + + + ```json user.create + { + "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6...", + "event": "user.create" + } + ``` + + + + The JWT that contains the actual webhook event data. It is a JSON Web Signature (JWS). Webhook recipients + should verify the signature to ensure that the webhook deliveries were sent by Hanko and have not been + tampered with. + + + + The event that triggered this webhook + + + + +### Event types and token payloads + +Events are structured hierarchically with some events subsuming the occurrence of multiple ("sub")-events. These +types of events do not actually appear as the value for the `event` property in the webhook event payload. Subscribing +to these types of events when creating a webhook is a convenient way to group certain event types and allows you to +structure your callback endpoints around these groups. + +A webhook's event data is encoded as a JWT in the webhook's callback request body. You need to parse the token +to access the token's payload which contains the actual event data (see +[Handling webhook deliveries](#handling-webhook-deliveries) for an example). + +#### user + +Subscribing to this event implies subscription to the following events: +[`user.create`](#user-create), +[`user.delete`](#user-delete), +[`user.udpate.email.create`](#user-update-email-create), +[`user.update.email.delete`](#user-update-email-delete), +[`user.update.email.primary`](#user-update-email-primary), +[`user.update.username.create`](#user-update-username-create), +[`user.update.username.delete`](#user-update-username-delete), +[`user.update.username.update`](#user-update-username-update) + +#### user.create + +This event is triggered when a new user is created. + + + + + + + + + + +#### user.delete + +This event is triggered when a user is deleted. + + + + + + + + + + +#### user.update + +Subscribing to this event implies subscription to the following events: +[`user.udpate.email.create`](#user-update-email-create), +[`user.update.email.delete`](#user-update-email-delete), +[`user.update.email.primary`](#user-update-email-primary), +[`user.update.username.create`](#user-update-username-create), +[`user.update.username.delete`](#user-update-username-delete), +[`user.update.username.update`](#user-update-username-update) + + +#### user.update.email + +Subscribing to this event implies subscription to the following events: +[`user.udpate.email.create`](#user-update-email-create), +[`user.update.email.delete`](#user-update-email-delete), +[`user.update.email.primary`](#user-update-email-primary) + +#### user.update.email.create + +This event is triggered when an email is created for a user. + + + + + + + + + + +#### user.update.email.delete + +This event is triggered when a user's email is deleted. + + + + + + + + + + + +#### user.update.email.primary + +This event is triggered when a user's email is set as the primary email. + + + + + + + + + + +#### user.update.username + +Subscribing to this event implies subscription to the following events: +[`user.update.username.create`](#user-update-username-create), +[`user.update.username.delete`](#user-update-username-delete), +[`user.update.username.update`](#user-update-username-update) + +#### user.update.username.create + +This event is triggered when a username is created for a user. + + + + + + + + + -```json -{ - "token": "the-jwt-token-which-contains-the-data", - "event": "name of the event" -} -``` -## Event Types +#### user.update.username.delete -Hanko sends webhooks for the following event types: +This event is triggered when a user's username is deleted. -| Event | Triggers on | -|---------------------------|----------------------------------------------------------------------------------------------------| -| user | user creation, user deletion, user update, email creation, email deletion, change of primary email | -| user.create | user creation | -| user.delete | user deletion | -| user.update | user update, email creation, email deletion, change of primary email | -| user.update.email | email creation, email deletion, change of primary email | -| user.update.email.create | email creation | -| user.update.email.delete | email deletion | -| user.update.email.primary | change of primary email | -| email.send | an email was send | + + + + + + + + -As you can see, events can have subevents. You are able to filter which events you want to receive by either selecting -a parent event when you want to receive all subevents or selecting specific subevents. +#### user.update.username.update -## Restrictions +This event is triggered when a user's username is updated. -Due to security concerns there are some usage restrictions for webhooks in place: + + + + + + + + -* A webhook will be disabled after 30 days without usage. In this scenario usage describes the successful triggering of events -This mechanism can be disabled by setting `webhooks.allow_time_expiration` to `false` +#### email.send -* A webhook will also be disabled when our trigger mechanism is not successful. The mechanism will try 5 attempts of sending events before disabling a webhook. A trigger is unsuccessful when: - * your callback URL is not reachable due to network errors or a wrong url - * your webhook endpoint returns HTTP Status codes >= 400 +This event is triggered when an email is sent. Subscribe to this event if you want +to send customized emails instead of emails based on built-in templates. +See [Custom Emails](/guides/custom-emails/email-delivery) for more information. -The restrictions are not available for webhooks defined in the config file. + + + + + + + + -## Data -When a webhook is triggered it will send you a **JSON** body which contains the event and a jwt. -The JWT contains 2 custom claims: -* **data**: contains the whole object for which the change was made. (e.g.: the whole user object when an email or user is changed/created/deleted) -* **evt**: the event for which the webhook was triggered diff --git a/openapi-admin.yaml b/openapi-admin.yaml index e8f99c3..c3d8ac1 100644 --- a/openapi-admin.yaml +++ b/openapi-admin.yaml @@ -1145,6 +1145,11 @@ components: - user.update.email.create - user.update.email.delete - user.update.email.primary + - user.update.username + - user.update.username.create + - user.update.username.delete + - user.update.username.update + - email.send DatabaseWebhook: type: object title: DatabaseWebhook @@ -1196,6 +1201,11 @@ components: - user.update.email.create - user.update.email.delete - user.update.email.primary + - user.update.username + - user.update.username.create + - user.update.username.delete + - user.update.username.update + - email.send required: - callback - events diff --git a/snippets/pro-feature-note.mdx b/snippets/pro-feature-note.mdx new file mode 100644 index 0000000..282f570 --- /dev/null +++ b/snippets/pro-feature-note.mdx @@ -0,0 +1,3 @@ + + This feature is only available in the Pro or Enterprise [plan](https://www.hanko.io/pricing). + \ No newline at end of file diff --git a/snippets/webhooks/email-send-example.mdx b/snippets/webhooks/email-send-example.mdx new file mode 100644 index 0000000..2dbd33d --- /dev/null +++ b/snippets/webhooks/email-send-example.mdx @@ -0,0 +1,25 @@ +```json +{ + "aud": [ + "Test Service ABC" + ], + "data": { + "subject": "Use passcode 325139 to verify your email address", + "body_plain": "Enter the following passcode to verify your email address:\n\n325139\n\nThe passcode is valid for 5 minutes.", + "to_email_address": "test@example.com", + "delivered_by_hanko": false, + "language": "en", + "type": "passcode", + "data": { + "service_name": "Test Service ABC", + "otp_code": "325139", + "ttl": 300, + "valid_until": 1737128997 + } + }, + "evt": "email.send", + "exp": 1737128997, + "iat": 1737128697, + "sub": "hanko webhooks" +} +``` \ No newline at end of file diff --git a/snippets/webhooks/email-send-properties.mdx b/snippets/webhooks/email-send-properties.mdx new file mode 100644 index 0000000..ccf37fd --- /dev/null +++ b/snippets/webhooks/email-send-properties.mdx @@ -0,0 +1,64 @@ + + The recipients the token is intended for + + + + + The subject line of the email + + + The plain text version of the email body + + + The HTML version of the email body (nullable) + + + The recipient’s email address + + + Indicates whether the email was delivered by Hanko (`true`) or not (`false`). + + + Deprecated, rely on `language` instead. + + The preferred language for the email content. + + + The preferred language for the email content. + + + The type of the email being sent. + + Available options: `login`, `email_login_attempted`, `email_registration_attempted`, `email_verification`, + `recovery` + + + Additional data. + + + The name of the service set in the console as the project name + + + The passcode the user can use to log in + + + The validity duration of the passcode in seconds + + + The Unix timestamp indicating when the passcode expires + + + + + + + The event that triggered the webhook containing this data + + + The expiration date of the token + + + The time at which the token was issued + + + \ No newline at end of file diff --git a/snippets/webhooks/user-payload-example.mdx b/snippets/webhooks/user-payload-example.mdx new file mode 100644 index 0000000..830823f --- /dev/null +++ b/snippets/webhooks/user-payload-example.mdx @@ -0,0 +1,81 @@ +```json +{ + "aud": [ + "Test Service ABC" + ], + "data": { + "created_at": "2025-01-15T12:57:56.724052Z", + "emails": [ + { + "id": "d31be36d-08d7-409f-8437-1920628e6e51", + "address": "test@example.com", + "is_verified": true, + "is_primary": true, + "created_at": "2025-01-15T13:57:56.72784Z", + "updated_at": "2025-01-15T13:57:56.72801Z" + } + ], + "id": "42fbd0dc-28fb-4144-892c-c2c4a0f8f5d8", + "identities": [ + { + "id": "b3af92c5-414c-4c6b-a3ea-82ee263badef", + "provider_id": "123456abcd", + "provider_name": "testprovider", + "email_id": "d31be36d-08d7-409f-8437-1920628e6e51", + "created_at": "2025-01-17T13:40:11Z", + "updated_at": "2025-01-17T13:40:13Z" + } + ], + "otp": { + "id": "a7efd1ee-d7b2-440e-9284-625e06931745", + "created_at": "2025-01-17T13:39:29.081428Z" + }, + "password": { + "id": "6ec87b0c-67db-42ef-9adb-d106734bde02", + "created_at": "2025-01-15T13:57:56.735651Z", + "updated_at": "2025-01-15T13:57:56.735651Z" + }, + "updated_at": "2025-01-15T13:57:56.724255Z", + "username": { + "id": "61580d7d-0c11-4c25-bfca-ace21a14cc01", + "username": "testmakker", + "created_at": "2025-01-15T14:45:00.293001Z", + "updated_at": "2025-01-17T13:46:41.700373Z" + }, + "webauthn_credentials": [ + { + "id": "eaYxbrFQJjl5dW5SAr0KznEmHwAen8HUAiaKN9ijsDY", + "public_key": "pQECAyYgASFYIMq-SVnCDGIJjK2TAJyEQyXNtOw7x_MuEVUQuW80-AOcIlggyEcR_v5C8PuhrThwgx2urmRqviIb7dyXmGr3oyWk2rU", + "attestation_type": "packed", + "aaguid": "70a4ab68-d027-451a-9c86-3b8fd8414f68", + "last_used_at": "2025-01-17T12:38:28.698563Z", + "created_at": "2025-01-17T12:38:28.698563Z", + "transports": [ + "usb" + ], + "backup_eligible": false, + "backup_state": false, + "mfa_only": true + }, + { + "id": "BPtYkS1prrGu1owU3StJwWM5uYtVoD1-h4N_rPHrB84", + "public_key": "pQECAyYgASFYILo4i3yC0V2kciBHL96EOx08h32CXXIFnuUmggHOhkGvIlggQFIp4CeJhzGpCiTNuQQoyiKV7oMLYxM549ctLJXJkZ0", + "attestation_type": "packed", + "aaguid": "649b9062-5892-4223-832b-921c5bce5827", + "last_used_at": "2025-01-17T12:39:06.718171Z", + "created_at": "2025-01-17T12:39:06.718171Z", + "transports": [ + "usb" + ], + "backup_eligible": false, + "backup_state": false, + "mfa_only": false + } + ] + }, + "evt": "", // the corresponding event type + "exp": 1737118303, + "iat": 1737118003, + "sub": "hanko webhooks" +} +``` \ No newline at end of file diff --git a/snippets/webhooks/user-payload-properties.mdx b/snippets/webhooks/user-payload-properties.mdx new file mode 100644 index 0000000..2b8febb --- /dev/null +++ b/snippets/webhooks/user-payload-properties.mdx @@ -0,0 +1,115 @@ + + The recipients the token is intended for + + + + + The ID of the user + + + Registered WebAuthn credentials (passkeys and security keys) of the user + + + The ID authenticator that created the credential + + + Format in which the signature is represented and the various contextual + bindings are incorporated into the attestation statement by the authenticator + + + Indicates whether the credential may be backed up in some fashion such that they may become present + on an authenticator other than their generating authenticator + + + Indicates whether this credential is backed up or not + + + The time of creation of the credential + + + The ID of the credential + + + Indicates when the credential was lst used + + + The public key of the credential (Base64URL string) + + + Communication methods/protocols used to create the credential + + + Indicates whether this is an MFA credential (security key) or a first factor credential + (passkey) + + + + + The username of the user + + + Time of creation of the user + + + Time of last update of the user + + + Representation of the password credential of the user + + + The ID of the password credential + + + Time of creation of the password credential + + + Time of last update of the password credential + + + + + + + The ID of the identity + + + The ID of the user at the third party provider + + + The name of the third party provider + + + The ID of the email the identity is related to + + + Time of creation of the identity + + + Time of last update of the identity + + + + + MFA OTP credential of the user + + + ID of the OTP credential + + + Time of creation of the OTP credential + + + + + + + The event that triggered the webhook containing this data + + + The expiration date of the token + + + The time at which the token was issued + + + \ No newline at end of file