Zero Trust API Events

Zero Trust API Events

Intro

Software companies use multiple processes to implement their backend workflows. Actions are often triggered by end users, and the user facing app can provide a secure credential:

Business Processing

At least some of the processing involved is usually asynchronous, which could be performed in various technical ways, as in the following examples:

  • An event is used to asynchronously inform another components
  • An end of day batch process is scheduled to run every midnight
  • An administrator has to approve an action before the workflow can proceed

This type of processing often involves a message being stored by a middleware component such as a service bus, message queue or workflow system. This type of component is typically internal to the backend and not exposed to the internet. The event message can then contain sensitive data as follows:

{
  "transactionID": 234745,
  "userID": 1234,
  "amount": 2000,
  "utcTime": "2022-07-06T09:23:19.884Z"
}

When dealing with event messages it is easy to lose track of identity control, which can result in security threats inside the network. In some setups a malicious party might be able to act as a man in the middle (MITM) and post their own messages, or tamper with those stored, for example to change values in the event data.

Secure Event Flow Overview

In a zero trust architecture, a more secure option is to ensure that the data in event messages can be digitally verified when the asynchronous workflow resumes. The high level process would then work as follows, where publishers must send a cryptographically signed JWT access token with each event message, and consumers must verify the JWT signature before processing the event message:

Secure Flow

An example secured event message might then have the following structure:

{
  "accessToken": "eyJraWQiOiItNDc4MTAzOTYyIiwieDV0IjoiSWlHY ...",
  "payload": {
    "transactionID": 234745,
    "userID": 1234,
    "amount": 2000,
    "utcTime": "2022-07-06T09:23:19.884Z"
  }
}

It is then necessary to ensure that consuming events remains reliable, and that the access token in the event data does not become a security vulnerability. In particular the following requirements should be satisfied:

RequirementBehavior
Long Token LifetimeThe token must not expire when the workflow resumes, so that events can continue to be reliably consumed
Reduced Token PermissionsA malicious party must not be able to read the token from event data and replay it against other API endpoints
Token Bound to EventThe token must be bound to the exact event message and not usable against other transactions

Token Exchange

The preferred option for using JWT access tokens in event messages is for the publishing API to use Token Exchange to get a new token for the user and include that in the event message. The user facing app will be issued with a short lived access token that typically lasts for around 15 minutes, with claims similar to the following:

{
  "iat": 1657215570,
  "nbf": 1657215570,
  "exp": 1657215870,
  "exp": 1688652218,
  "scope": "openid profile orders trigger_payments",
  "iss": "https://idsvr.example.com/oauth/v2/oauth-anonymous",
  "sub": "2e1ba75dad2b62d8620ee9722caad54b02d7086edbd4c15529962ca26d04e103",
  "aud": "api.example.com",
  "purpose": "access_token",
}

The publishing API can exchange the incoming token by calling the Identity Server using a message of the following form. The token exchange request includes details that bind the event to the JWT access token being issued. This includes key identifiers such as a transaction ID, and a hash of the event data:

Token Exchange Message

The resulting token is issued with a reduced scope but can have a long lifetime, such as 1 year. The JWT is digitally signed by your Identity Server, a trusted authority, which is the only party that owns the private key used to produce it. The trigger_payments scope can be considered a meta-scope, used only by a single event processing endpoint. Your API design must ensure that the token is rejected if sent to any other API endpoint:

{
  "iat": 1657215570,
  "nbf": 1657215570,
  "exp": 1688751570,
  "scope": "trigger_payments",
  "iss": "https://idsvr.example.com/oauth/v2/oauth-anonymous",
  "sub": "2e1ba75dad2b62d8620ee9722caad54b02d7086edbd4c15529962ca26d04e103",
  "aud": "api.example.com",
  "purpose": "access_token",
  "transaction_id": "22fc326d-23e4-5fc3-f803-e989854704e7",
  "event_payload_hash": "ee8d4bd25789578c2dffcfc11c63b42c69c149af50e80dc592b5bdf552ae94ab"
}

Token exchange therefore enables an overall flow with the following key behavior. No additional key management is used, and APIs simply need to validate JWT access tokens, which they will typically already be doing for HTTP requests:

End to End Flow

Consuming Secured Events

In consuming APIs some extra code is needed to first perform standard JWT validation when an event message is received. Received claims can then be trusted since they have been digitally verified using the public key of the Identity Server. Next the event data must be verified against received claims. Once all checks pass, the event can be processed reliably, in the usual way:

messageBroker.onConsume('OrderCreated', message => {

   const claims = authorize(message.accessToken);
   verifyEventData(message.payload, claims);
   processEvent(message.payload);

});

The consumer will verify that the event payload matches that in the JWT. If a malicious party somehow manages to tamper with an event message at rest, such as to change a money value, the message will fail validation. Other identifying values such as a transaction ID should also be added to the token.

If an attacker can somehow access event data, they might be able to replay the event message multiple times, with the access token still valid. API messaging solutions typically cope with this by treating the replayed message as a no-op, due to the transaction ID already existing in that API.

Downstream Services

In some cases the consuming API will also need to call downstream APIs, either via an HTTP request or an event. In this case the token exchange can include additional meta-scope values. If required, the consuming API can perform its own token exchange, or simply forward the long lived access token. This will result in scopes flowing something like the following, where downstream services are also designed to deal carefully with long lived access tokens:

Downstream Flow

Replaying of Event Messages

Events are consumed quickly, but companies may want to use patterns such as Event Sourcing, where the system's entire data can be recreated from stored event messages at a later point in time. When using JWTs in event messages, it is important to ensure that replaying of old events is reliable and does not result in expiry errors. This can be managed by configuring the JWT's exp claim to a suitably long time.

Another issue when replaying events is that the Identity Server's token signing public keys may have been rotated, which could lead to JWT signature validation failure. Consider Self Contained JWTs when using event sourcing, so that the token signing public key at the time of publishing is encapsulated within the JWT header of the event message, and there is no dependency on the current signing key.

Conclusion

Using JWTs in event messages enables you to resume asynchronous workflows using strong identity guarantees, asserted by your Identity Server via public key cryptography. Consider using this technique, especially for high privilege transactions, such as those involving money, so that they can be audited.

This pattern adds a small performance overhead, due to the need for an additional token exchange request. Design the JWT access token to be bound to a specific event and to use reduced permissions, after which you can assign it a long lifetime, and events can be consumed reliably.

Simple code can be used, in any technology stack, to build this type of solution. For an end-to-end code example that you can run locally, using the Curity Identity Server and the popular Apache Kafka event streaming platform, see the API Using Zero Trust Events tutorial.