We are excited to release synchronous webhooks, the latest addition to our webhooks features. With synchronous webhooks, you can extend SlashID Access to suit your business needs in a few simple steps, in whatever language and environment makes sense for you.
In this blogpost, we will introduce synchronous webhooks by answering three questions – why, what, and how, including a short example.
The Why
Our initial webhooks release focused on asynchronous webhooks triggered by events, which are ideal for analytics and monitoring.
However, there are many use cases where you need to extend the identity logic in some business-specific way. For example, you may need to enrich an authentication token with information you keep internally, or ensure that certain actions are included in your internal audit log.
With SlashID synchronous webhooks, you can quickly and easily integrate your custom identity logic into SlashID’s existing flows, using the language and deployment environment that makes the most sense for you and your organization. You gain visibility into crucial identity flows, and have the opportunity to affect their outcomes.
The What
Synchronous hooks are the extension points in SlashID flows where your webhooks will be called. Each type of synchronous hook has its own content, and accepts specific responses that can be used to affect the relevant flows.
For this initial release, we have focused on hooks when the user registers or signs-in. Specifically, we hook the point during sign-in after authentication has succeeded, but before we sign and issue the authentication token to the user. The hook content contains the claims that will be present in the issued token. The response from your webhook can optionally contain custom claims as key-value pairs, which will be added as claims in the token before it is issued.
This hook allows you to change the authentication/registration flow and add your custom business logic before the token is returned to the requesting application. Crucially, the webhook is executed before the token is signed to avoid potential security concerns.
The How
Synchronous webhooks build on SlashID’s existing webhooks functionality – so you can use exactly the same APIs to manage your webhooks, and the webhook requests follow the same pattern. You will need to implement the handler for your webhook endpoint, and then you’re ready to start receiving synchronous hook requests.
Let’s see how this works in practice.
First, you need to implement the API for handling webhook requests. In this example, we will fetch some personal details about the user logging in and use these to enrich the token. For brevity, we have omitted error handling.
import (
"encoding/json"
"io"
"net/http"
"net/url"
"github.com/google/tink/go/jwt"
)
const (
sidOrgID = "my-slashid-organization-id"
sidBaseURL = "https://api.slashid.com"
svcBaseURL = "https://my-service.com"
sidWebhookPath = "/sid/webhook"
)
func HandleSlashIDWebhook(r *http.Request, w http.ResponseWriter) {
// Read the body from the request - this will be a signed and encoded JWT
reqBody, _ := io.ReadAll(r.Body)
defer r.Body.Close()
// Retrieve the verification key for your organization using the SlashID APIs
verificationJWKS := getVerificationJWKS()
verificationKeyset, _ := jwt.JWKSetToPublicKeysetHandle(verificationJWKS)
// Build the verifier and validator
verifier, _ := jwt.NewVerifier(verificationKeyset)
issuer := sidBaseURL
aud := sidOrgID
validator, _ := jwt.NewValidator(&jwt.ValidatorOpts{
ExpectedIssuer: &issuer,
AllowMissingExpiration: false,
ExpectedAudience: &aud,
})
// Verify the JWT - this will include the issuer, audience, and expiration
verifiedJWT, err := verifier.VerifyAndDecode(string(reqBody), validator)
if err != nil {
// The request body cannot be verified as coming from SlashID and so should not be trusted!
}
// Check the target URL matches the endpoint this handler is associated with
targetURL, _ := verifiedJWT.StringClaim("target_url")
expectedTargetURL, _ := url.JoinPath(svcBaseURL, sidWebhookPath)
if targetURL != expectedTargetURL {
// This URL does not seem to be the intended target - ignore, log
}
// We are happy that the webhook request is legitimate, so we can handle the actual content
// In this case, we are expecting a token minted sync hook, so the trigger content is the
// claims that will appear in the user token SlashID is about to issue to the authenticated user.
userTokenClaims, _ := verifiedJWT.ObjectClaim("trigger_content")
personID := userTokenClaims["sub"]
// Here we have your business logic to retrieve name (string) and address (array of strings)
name, address := getPersonalDetails(personID.(string))
// Write these as JSON in the response body
respMap := map[string]any{
"name": name,
"address": address,
}
respBody, _ := json.Marshal(respMap)
_, _ = w.Write(respBody)
return
}
Now, we will make two API calls to SlashID - one to create a webhook, and one to create a trigger for that webhook.
curl -X POST --location 'https://api.slashid.com/organizations/webhooks' \
--header 'SlashID-OrgID: my-slashid-organization-id' \
--header 'SlashID-API-Key: my-slashid-api-key' \
--header 'Content-Type: application/json' \
--data '{
"target_url": "https://my-service.com/sid/webhook",
"name": "token minted webhook",
}'
{
"result": {
"id": "e64c7123-497d-7e12-b665-341b5defdec1",
"target_url": "https://my-service.com/sid/webhook",
}
}
curl -X POST --location 'https://api.slashid.com/organizations/webhooks/e64c7123-497d-7e12-b665-341b5defdec1/triggers' \
--header 'SlashID-OrgID: my-slashid-organization-id' \
--header 'SlashID-API-Key: my-slashid-api-key' \
--header 'Content-Type: application/json' \
--data '{
"trigger_type": "sync_hook",
"trigger_name": "token_minted"
}'
Now suppose a user authenticates to your application via SlashID, using an email link.
Once they have successfully authenticated, the SlashID backend prepares a token. Before the token is signed and issued back to the user, SlashID reaches the hook point and checks for webhooks registered for this trigger. It finds the webhook you created earlier, and so your webhook is called.
The body will be a signed JWT, which your handler function verifies and decodes. If we decode the JWT payload, we see that the trigger content contains the claims that will be present in the user token:
{
"aud": "my-slashid-organization-id",
"iss": "https://slashid.local",
"target_url": "https://my-service.com/sid/webhook",
"trigger_name": "token_minted",
"trigger_type": "sync_hook",
"webhook_id": "e64c7123-497d-7e12-b665-341b5defdec1",
"trigger_content": {
"aud": "my-slashid-organization-id",
"authentications": [
{
"handle": {
"type": "email_address",
"value": "[email protected]"
},
"method": "email_link",
"timestamp": "2023-07-18T12:25:56.390080959Z"
}
],
"first_token": false,
"groups": ["admins"],
"groups_claim_name": "groups",
"iss": "https://slashid.local",
"region": "us-iowa",
"sub": "165c7138-545d-7065-8ff2-13850368729"
}
}
Your handler retrieves the personal information, and returns it in the response. The SlashID backend uses this response to enrich the token, which is then signed and issued to the user. The final token payload will look like this:
{
"aud": "my-slashid-organization-id",
"authentications": [
{
"handle": {
"type": "email_address",
"value": "[email protected]"
},
"method": "email_link",
"timestamp": "2023-07-18T12:25:56.390080959Z"
}
],
"first_token": false,
"groups": ["admins"],
"groups_claim_name": "groups",
"iss": "https://slashid.local",
"region": "us-iowa",
"sub": "165c7138-545d-7065-8ff2-13850368729",
"name": "Alex Singh",
"address": ["123 Main Street", "Honolulu", "Hawaii", "96801"]
}
It contains the claims that your webhook received in the request, plus those returned in the response.
Summary
With this release, you can use synchronous webhooks to extend SlashID Access. We will be adding more hooks and features in the near future, so stay tuned for release announcements.
Ready to try SlashID? Register here!
Is there a feature you’d like to see, or have you tried out webhooks and have some feedback? Let us know!