Webhooks v3 security event tokens

HTTP headers convey information about a Webhooks v3 request. But what does the request itself convey? We’re glad you asked that question; it conveys something similar to this:

eyJ0eXAiOiJzZWNldmVudCtqd3QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjFkYzEyMDczNjk5YzY4YzFkYWVlNmM5YTEwMGUyYjQzZmViZGNkOTIifQ.eyJpc3MiOiJBa2FtYWkgSWRlbnRpdHkgQ2xvdWQiLCJpYXQiOjE1NjM0ODg2MzEsImp0aSI6ImI3MDA0NmJkLTQ0YzctNDU3NS1iMWEyLTliODU1NmQxZjA0MCIsImF1ZCI6Imh0dHBzOi8vZXhhbXBsZS5jb20vcGF0aC90by9lbmRwb2ludCIsInR4biI6IjAwMDAwMDAwLTAwMDAtMDAwMC0wMDAwLTAwMDAwMDAwMDAwMCIsInRvZSI6MTU1OTM3MjQwMCwiZXZlbnRzIjp7InVzZXJDcmVkZW50aWFsVXBkYXRlZCI6eyJjYXB0dXJlQXBwbGljYXRpb25JZCI6Inp6eW45Z3k5cjh4ZHk1emtydTR5NTRzeWs2IiwiY2FwdHVyZUNsaWVudElkIjoiZWxycm5pdXg1MWEzbnJoZnd6a2x2ejN0NDZsYjVuMm0iLCJlbnRpdHlUeXBlIjoidXNlciIsImdsb2JhbFN1YiI6ImNhcHR1cmUtdjE6Ly91cy5qYW5yYWluY2FwdHVyZS5jb20venp5bjlneTlyOHhkeTV6a3J1NHk1NHN5azYvdXNlci82YjAwNGJjNS0xNzljLTQ1YzItODE1ZC0zMWIwNjE2OTM3MWQiLCJzdWIiOiI2YjAwNGJjNS0xNzljLTQ1YzItODE1ZC0zMWIwNjE2OTM3MWQiLCJjcmVkZW50aWFsVHlwZSI6InBhc3N3b3JkIiwiaWQiOiIwMDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDAifX19.IvkrGFE3GsK3eTLO\_QvdFKqg4ktJ2sDToHNghMfGUlWNzRLMIpmgsWZXzLv0QMiyatLva7mEshTlfyOje-S\_Y-nUniM9hgHgNg-R0Az9hs2mu\_ORcXEFo9AHayhjvW1bKHcmTI7dlw2fqFl-2VBS594LQspDYfZ4WJ7hexq7OwACB8qp0oVskx\_fc8mHQfy4YnW5GF4XlTcl6CnjYU81qY4hejcnkkg8olbq\_ePUnpTpW8-YO5cPW9nW8KlivRJGWJbEXnffSAd5xwlViJm6iTde2QQVv9pi\_Z6LnrxPQotoGhJOvk\_wkwANsWC9TwDNnlBTiLePCFLU85haWanXcg

Before you schedule an appointment with your eye doctor, that’s how the payload is supposed to look. That’s because payloads are encoded before being sent. 

That also means that payloads need to be decoded after they’ve been received; if they aren’t, then all your database records are going to look like the sample payload shown above. Fortunately, decoding a security event token is pretty easy; that’s because tokens are encoded and not encrypted. Because of that, any application capable of decoding Base64 can decode a webhooks notification, with no password or secret or any other form of authentication required. For example, if you have a Mac you can use Terminal and the base64 app to decode tokens. Here’s the syntax for decoding the token header:

echo eyJ0eXAiOiJzZWNldmVudCtqd3QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjFkYzEyMDczNjk5YzY4YzFkYWVlNm  
M5YTEwMGUyYjQzZmViZGNkOTIifQ | base64 --decode

A decoded token header will look similar to this:

{"alg":"RS256","jku": "https://api.multi.dev.or.janrain.com/00000000-0000-0000-0000-000000000000/login/jwk"," kid":"1dc12073699c68c1daee6c9a100e2b43febdcd92", "typ":"secevent+jwt"]

On a Windows computer, you can use Windows PowerShell to achieve the exact same result:

[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String(' eyJ0eXAiOiJzZWNldmVudCtqd3QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjFkYzEyMDczNjk5YzY4YzFkYWVlNmM5  
YTEwMGUyYjQzZmViZGNkOTIifQ'))

If you’re wondering about security, well, encoding is not intended to provide security: encoding simply ensures that all Identity Cloud security event tokens use the same character encoding. That eliminates the problems that could arise if, say, the webhooks server is using UTF-8 encoding while the listener endpoint is using ISO 8859-1 encoding.

So, yes, someone could decode one of your security event tokens, although it’s hard to see what they would gain by doing so. After all, no personally identifiable information is included in a Webhooks v3 security event token: although you might see a user UUID such as 6b004bc5-179c-45c2-815d-31b06169371d you will never see a user’s name, email address, phone number, or anything else that might give you some insight into who that user is. 

On a similar note, when Webhooks v3 reports a change made in the Identity Cloud, the security event token indicates what was changed (i.e., a user profile attribute or a password) but doesn't report either the previous value or the new value for that item. For example, consider the following notification, which indicates that a user has changed his or her email address:

  "events": {  
  "entityUpdated": {  
"attributes": [  
"email"  
**],**  
      "captureApplicationId": "zzyn9gy9r8xdy5zkru4y54syk6",  
      "captureClientId": "elrrniux51a3nrhfwzklvz3t46lb5n2m",  
      "entityType": "user",  
      "globalSub": "capture-v1://us.janraincapture.com/zzyn9gy9r8xdy5zkru4y54syk6/user/6b004bc5-179c-45c2-815d-31b06169371d",  
      "sub": "6b004bc5-179c-45c2-815d-31b06169371d",  
     "id": "00000000-0000-0000-0000-000000000000"  
   }  
 }

As you can see, the notification tells you that a user account was updated (the entityUpdated event type) and that the attribute changed was the email attribute (which stores the user’s email address). However, neither the user’s old email address nor the user’s new email address is included.

By the way, a fully decoded SET looks something like this:

{  
  "alg": "RS256",  
  "jku": "https://api.multi.dev.or.janrain.com/00000000-0000-0000-0000-000000000000/login/jwk",  
  "kid": "1dc12073699c68c1daee6c9a100e2b43febdcd92",  
  "typ": "secevent+jwt"  

}  
{  
  "iss": "Akamai Identity Cloud",  
  "iat": 1563488631,  
  "jti": "b70046bd-44c7-4575-b1a2-9b8556d1f040",  
  "aud": "https://example.com/path/to/endpoint",  
  "txn": "00000000-0000-0000-0000-000000000000",  
  "toe": 1559372400,  
  "events": {  
      "entityUpdated": {  
        "attributes": [  
          "email"  
          ],      "captureApplicationId": "zzyn9gy9r8xdy5zkru4y54syk6",  
      "captureClientId": "elrrniux51a3nrhfwzklvz3t46lb5n2m",  
      "entityType": "user",  
      "globalSub": "capture-v1://us.janraincapture.com/zzyn9gy9r8xdy5zkru4y54syk6/user/6b004bc5-179c-45c2-815d-31b06169371d",  
      "sub": "6b004bc5-179c-45c2-815d-31b06169371d",  
      "id": "00000000-0000-0000-0000-000000000000"  
    }  
  }  
}  
IvkrGFE3GsK3eTLO\_QvdFKqg4ktJ2sDToHNghMfGUlWNzRLMIpmgsWZXzLv0QMiyatLva7mEshTlfyOje  
-S\_Y-nUniM9hgHgNg-R0Az9hs2mu\_ORcXEFo9AHayhjvW1bKHcmTI7dlw2fqFl2VBS594LQspDYfZ4W  
J7hexq7OwACB8qp0oVskx\_fc8mHQfy4YnW5GF4XlTcl6CnjYU81qY4hejcnkkg8olbq\_ePUnpTpW8-YO5  
cPW9nW8KlivRJGWJbEXnffSAd5xwlViJm6iTde2QQVv9pi\_Z6LnrxPQotoGhJOvk\_wkwANsWC9TwDNn  
lBTiLePCFLU85haWanXcg

Security event token headers

When it comes to JSON Web Tokens, the header section typically serves two purposes: 1) it identifies the token type; and, 2) it identifies the hashing algorithm used to encode the token. Security token headers employed by the ​Akamai​ Identity Cloud cover both of those purposes; in addition, the header section indicates which JSON web keys was used to sign the token.

A typical Identity Cloud security event token (SET) header looks similar to this:

{  
 "alg": "RS256",  
 "kid": "1dc12073699c68c1daee6c9a100e2b43febdcd92",  
 "jku": "https://v1.api.us.janrain.com/00000000-0000-0000-0000-000000000000/login/jwk",  
  "typ": "secevent+jwt"  
}

The claims (alg, kid, and jku) used in the token header are described in the following table:

ClaimDescription
algIdentifies the cryptographic algorithm used to sign the token. For webhooks, this value will always be RS256, which references the hashing algorithm RSASSA-PKCS1-v1_5 using SHA-256.
kidKey identifier, a case-sensitive string that indicates the JSON Web Key used to sign the token. Each JSON Web Key includes a kid property that corresponds to the kid property shown in the token header.
jkuJSON Web Key Set URL. URL of your JSON Web Key Set. For example:

https://v1.api.us.janrain.com/00000000-0000-0000-0000-000000000000/login/jwk
typToken type. Indicates the kind of token that was transmitted. For Webhooks v3, the typ will always be set to secevent+jwt, indicating that this is a security event JSON Web Token.

The security even token payload

The Security Event Token (SET) payload contains a collection of name/value pairs (also known as claims) that describe the event and when it occurred. For example:

{  
 "iss": "https://v1.api.us.janrain.com/e0a70b4f-1eef-4856-bcdb-f050fee66aae/webhooks",  
 "iat": 1563488631,  
 "jti": "b70046bd-44c7-4575-b1a2-9b8556d1f040",  
 "aud": "https://example.com/path/to/endpoint",  
 "txn": "00000000-0000-0000-0000-000000000000",  
 "toe": 1559372400,  
 "events": {  
   "entityUpdated": {  
     "attributes": [  
         "email"  
         ],  
     "captureApplicationId": "zzyn9gy9r8xdy5zkru4y54syk6",  
     "captureClientId": "elrrniux51a3nrhfwzklvz3t46lb5n2m",  
     "entityType": "user",  
     "globalSub": "capture-v1://us.janraincapture.com/zzyn9gy9r8xdy5zkru4y54syk6/user/6b004bc5-179c-45c2-815d-31b06169371d",  
     "sub": "6b004bc5-179c-45c2-815d-31b06169371d",  
     "id": "00000000-0000-0000-0000-000000000000"  
   }  
 }  
}

These claims are explored in more detail in the following table:

ClaimDescription
issSpecifies the entity that issued the token. For Webhooks v3, the issuer is the Webhooks domain followed by the customer ID followed by webhooks. For example:

https://v1.api.us.janrain.com/e0a70b4f-1eef-4856-bcdb-f050fee66aae/webhooks
iatSpecifies the date and time when the token was issued. The iat("issued at time") claim is formatted using Unix epoch time, which represents the number of seconds that have elapsed since 00:00:00 Coordinated Universal Time (UTC) on January 1, 1970. For example, the value 1553405263 represents Saturday, March 23, 2019 at 22:27:43 Pacific Daylight Time.
jtiUnique identifier for the webhook payload. The jti claim is identical to the event Id property used by the Webhook API endpoints. Among other things, this means you can employ the value of the jti claim and the /{customerId}/webhooks/subscriptions/{subscriptionId}/events/{eventId} endpoint to view detailed information about the event: just use the value of the jti claim as the value of the variable {eventId}.
audIntended audience for the webhooks notification. This will always be the URL of the listener endpoint specified by the customer.
txnUnique identifier assigned to the request as it passed through the ​Akamai​ API gateway.
toeDate and time when the event occurred; this might occasionally differ from the time that the token was issued (the iat claim). The toe claim is formatted using Unix epoch time, which represents the number of seconds that have elapsed since 00:00:00 Coordinated Universal Time (UTC) on January 1, 1970. For example, the value 1553405263 represents Saturday, March 23, 2019 at 22:27:43 Pacific Daylight Time.
eventsThe actual event itself. The claims specified in any given webhooks notification will vary depending on the type of event.
captureApplicationIdUnique identifier of the ​Akamai​ Identity Cloud application associated with the event.
captureClientIdUnique identifier of the API client associated with the event.
entityTypeName of the entity type database associated with the event.
global_subURI that points to the user record within the Identity Cloud user profile store.

"sub": "capture-v1://us.janraincapture.com/zzyn9gy9r8xdy5zkru4y54syk6/user/6b004bc5-179c-45c2-815d-31b06169371d"

In the preceding URL, zzyn9gy9r8xdy5zkru4y54syk6 represents the unique identifier of the Identity Cloud application and 6b004bc5-179c-45c2-815d-31b06169371d represents the user’s UUID (Universally Unique Identifier).

This claim is primarily for internal Identity Cloud use.
subUUID of the user account associated with the event (for example, the UUID of the user account that was just modified). This claim will not be present if no user accounts are associated with the event.
IdUnique ​Akamai​ customer ID.

If you’re familiar with JSON Web Tokens, you might have noticed that at least two commonly-used claims – exp (Expiration Time) and sub (Subject) – are missing from the token payload. There’s a good reason (actually, two good reasons) for that. For one, security event tokens don’t expire; consequently, there’s no reason to include the exp claim. For another, the sub claim is used whenever applicable. However, that claim will always be included as part of the events claim, and won’t stand out on its own.

The security event token signature

At the bottom of each security event token you’ll see a block of text that looks similar to this:

IvkrGFE3GsK3eTLO\_QvdFKqg4ktJ2sDToHNghMfGUlWNzRLMIpmgsWZXzLv0QMiyatLva7mEshTlfyOje-  
S\_Y-nUniM9hgHgNg-R0Az9hs2mu\_ORcXEFo9AHayhjvW1bKHcmTI7dlw2fqFl2VBS594LQspDYfZ4WJ  
7hexq7OwACB8qp0oVskx\_fc8mHQfy4YnW5GF4XlTcl6CnjYU81qY4hejcnkkg8olbq\_ePUnpTpW8-YO5c  
PW9nW8KlivRJGWJbEXnffSAd5xwlViJm6iTde2QQVv9pi\_Z6LnrxPQotoGhJOvk\_wkwANsWC9TwDNnl\  
BTiLePCFLU85haWanXcg

This is the token signature, a hashed string that enables you to verify the validity of the token. Signatures are created by:

  1. Combining the header, the payload, and a secret (i.e., a password).

  2. Hashing that combination using one of the allowed algorithms. The most commonly-used hashing algorithm is HMAC SHA256 (aka HS256).

And how do you read and a token signature? For that, you need to use JSON web keys.


A note about webhook signatures

Keep in mind that signatures are used for verifying security event tokens, not for safeguarding the contents of those tokens. For example, suppose you have an encoded token, but you don’t have the JSON Web Key used to sign that token. That’s fine: you can still decode the token. You just won’t be able to validate the signature:

That's the expected behavior, and, as we’ve noted before, that’s fine: after all, webhook notifications don’t contain any personally-identifiable information, which means that there's little (if any) risk involved in someone decoding a notification that was never meant for them.


Did this page help you?