Introduction
Knowing your users is becoming increasingly important today both to increase revenue and to fend off attacks.
Just last week 23andMe was breached through a credential stuffing attack. While there are multiple ways to defend against this kind of attacks, one of the key tools to use is better intelligence on bots and potential malicious traffic coming to your website.
At the same time, Product Led Growth (PLG) is an increasingly prevalent paradigm in today’s market. In PLG, [email protected]
testing out your dashboard could be the VP at a large company that can sign off on a 6-figure deal for your company.
In this article, we show how we can leverage SlashID’s webhooks to enrich the authentication context and customize the user journey and block malicious users.
Specifically, we’ll demonstrate how to store SlashID JWTs, enrich them with risk scoring and attribution information, and block potentially malicious connections originating from Tor.
Hooking the authentication journey
SlashID supports both synchronous and asynchronous webhooks to hook all interactions between a user and SlashID (for example: authentication attempts, registration, changes in attributes and more).
SlashID events are defined using protobuf. These definitions can be used to generate code for unmarshalling and handling SlashID events (for example, as received in webhook requests).
You can create and manage webhooks using a simple REST API - see the full documentation here.
To attach a webhook to an event, we need to make two REST calls:
- Register a webhook
- Attach the webhook to an event
curl -X POST --location 'https://api.slashid.com/organizations/webhooks' \
--header 'SlashID-OrgID: <ORGANIZATION ID>' \
--header 'SlashID-API-Key: <API KEY>' \
--header 'Content-Type: application/json' \
--data '{
"target_url": "https://api.example.com/slashid-webhooks/user-created",
"name": "user created",
"description": "Webhook to receive notifications when a new user is created"
}'
{
"result": {
"description": "Webhook to receive notifications when a new user is created",
"id": "16475b49-2b12-78d7-9012-cfe0e174dcd3",
"name": "user created",
"target_url": "https://api.example.com/slashid-webhooks/user-created"
}
}
curl -X POST --location 'https://api.slashid.com/organizations/webhooks/16475b49-2b12-78d7-9012-cfe0e174dcd3/triggers' \
--header 'SlashID-OrgID: <ORGANIZATION ID>' \
--header 'SlashID-API-Key: <API KEY>' \
--header 'Content-Type: application/json' \
--data '{
"trigger_type": "event",
"trigger_name": "PersonCreated_v1"
}'
Now we have a webhook, we can test whether the hook is triggered through the test-events
endpoint:
curl -X POST --location 'https://api.slashid.com/test-events' \
--header 'SlashID-OrgID: <ORGANIZATION ID>' \
--header 'SlashID-API-Key: <API KEY>' \
--header 'Content-Type: application/json' \
--data '[
{
"event_name": "PersonCreated_v1"
}
]'
If the webhook was registered correctly we’ll receive a call from SlashID.
Validating requests
When handling a request to a webhook, it is essential that you verify the contents of the request before processing it further. With SlashID, we have made this simple by using the JSON Web Token (JWT) and JSON Web Key (JWK) standards. The body of the request to the webhook is a signed and encoded JWT (just like our authentication tokens). In order to verify it, you should first retrieve the verification key JSON Web Key Set (JWKS) for your organization using the API. (Note that this endpoint is rate-limited, so we recommend caching the verification key.) You can then use this key to verify the JWT signature, and decode the body.
Here’s an example on how to validate a webhook request:
...
jwks_client = jwt.PyJWKClient("https://api.slashid.com/organizations/webhooks/verification-jwks", headers={"SlashID-OrgID": "<ORGANIZATION ID>"})
request_data = request.get_data()
try:
header = jwt.get_unverified_header(request_data)
key = jwks_client.get_signing_key(header["kid"]).key
token = jwt.decode(request_data, key, audience="<ORGANIZATION ID>", algorithms=["ES256"])
except Exception as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Token {token} is invalid: {e}",
)
Blocking requests coming from Tor IPs
Malicious actors often use VPNs and Tor to hide their tracks. Generally, traffic coming through a Tor exit node should receive a higher level of scrutiny - whether by blocking traffic or enforcing further authentication verification such as stronger authentication factor or MFA.
To block authentication attempts coming from Tor IP addresses, we can do the following:
- Register a synchronous webhook on
token_minted
- Check if the IP comes from Tor. You can use multiple services for this; in our example, we’ll use seon.io
- Block a request if it comes from Tor
We’ll use the same webhook as in the example above, but now we need to attach it to the token_minted
synchronous hook:
curl -X POST --location 'https://api.slashid.com/organizations/webhooks/16475b49-2b12-78d7-9012-cfe0e174dcd3/triggers' \
--header 'SlashID-OrgID: <ORGANIZATION ID>' \
--header 'SlashID-API-Key: <API KEY>' \
--header 'Content-Type: application/json' \
--data '{
"trigger_type": "sync_hook",
"trigger_name": "token_minted"
}'
We can process the webhook payload as follows (error checking removed for brevity):
...
jwks_client = jwt.PyJWKClient("https://api.slashid.com/organizations/webhooks/verification-jwks", headers={"SlashID-OrgID": "<ORGANIZATION ID>"})
webhookURL = ""
request_data = request.get_data()
header = jwt.get_unverified_header(request_data)
key = jwks_client.get_signing_key(header["kid"]).key
try:
header = jwt.get_unverified_header(request_data)
key = jwks_client.get_signing_key(header["kid"]).key
token = jwt.decode(request_data, key, audience="<ORGANIZATION ID>", algorithms=["ES256"])
if token['target_url'] != webhookURL:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Token {token} is invalid: {e}",
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Token {token} is invalid: {e}",
)
# Now the request has been validated, let's extract the IP address
ip_address = token['request_metadata']['client_ip_address']
print(f"Ip address {ip_address}\n")
# We use the SlashID external credentials to store the Seon API key
seon_key = requests.get("https://api.slashid.com/organizations/config/external-credentials/856b7dec-d2c3-41ab-a151-c615925433e0", headers={"SlashID-OrgID": "<ORGANIZATION ID>", "SlashID-API-Key": "<SLASHID KEY>"})
seon_key = seon_key.json()
seon_api_key_header = seon_key["result"]["json_blob"]
r = requests.get(f"https://api.seon.io/SeonRestService/ip-api/v1.1/{ip_address}", headers=seon_api_key_header)
seon_score = r.json()
## Check if the IP address is a known Tor IP address and if so, deny the request
if seon_score['data']['Tor']:
return HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Bearer token {token} is invalid: {e}",
)
Allowing requests through but enforcing step-up auth for Tor Addresses
We follow the same procedure as above, but instead of returning a 401, we return a custom claim to add to the JWT token that’s returned to the frontend:
...
return {
"tor_address": True,
}
The JWT returned to the frontend will contain a custom claim tor_address
and upon token inspection we can use the <StepUpAuth>
component to force another factor if the user’s IP comes from Tor.
Augmenting the token with marketing intelligence data
We can use the same approach as above to enrich the JWT token with marketing intelligence data on the lead - we’ll use clearbit for this (error checking removed for brevity):
...
jwks_client = jwt.PyJWKClient("https://api.slashid.com/organizations/webhooks/verification-jwks", headers={"SlashID-OrgID": "<ORGANIZATION ID>"})
request_data = request.get_data()
try:
header = jwt.get_unverified_header(request_data)
key = jwks_client.get_signing_key(header["kid"]).key
token = jwt.decode(request_data, key, audience="<ORGANIZATION ID>", algorithms=["ES256"])
except Exception as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Token {token} is invalid: {e}",
)
# Now the request has been validated, let's extract the user handle (email address/phone number)
handle = token['authentications'][0]['handle']['value']
print(f"User handle {handle}\n")
ip_address = token['request_metadata']['client_ip_address']
print(f"Ip address {ip_address}\n")
# We use the SlashID external credentials to store the Clearbit API key
clearbit_key = requests.get("https://api.slashid.com/organizations/config/external-credentials/856b7dec-d2c3-41ab-a151-c615925444b0", headers={"SlashID-OrgID": "<ORGANIZATION ID>", "SlashID-API-Key": "<SLASHID KEY>"})
clearbit_key = clearbit_key.json()
clearbit_api_key_header = clearbit_key["result"]["json_blob"]
## Retrieve the person by IP address + email address
r = requests.get(f"https://person.clearbit.com/v2/people/find?email={handle}&ip_address={ip_address}", headers=clearbit_api_key_header)
clearbit_response = r.json()
return {
"inferred_company": clearbit_response['employment']['name'],
"inferred_seniority": clearbit_response['employment']['seniority'],
"inferred_title": clearbit_response['employment']['title'],
}
The token returned in the frontend will contain the inferred_company
, inferred_seniority
and inferred_title
custom claims. You can then customize the UI flow depending on the persona.
Asynchronous vs Synchronous hooks
As discussed, SlashID exposes a number of events that can be hooked through webhooks. The examples above can also be executed asynchronously using the PersonCreated_v1
(registration) or AuthenticationSucceeded_v1
(authentication) events to collect analytics or trigger workflows (e.g.: an email campaign).
Conclusion
In this brief blog post, we’ve shown how you can combine multiple features of SlashID to make informed decisions about your users - whether it’s a custom conversion flow depending on the buyer persona, or a stronger authentication requirement for a potential bot.
We’d love to hear any feedback you may have. Please contact us at [email protected]! Try out SlashID with a free account.