
Zero Trust API Events
On this page
With OAuth, user facing clients sends access tokens to APIs, which receive sensitive values like the user identity in the JSON Web Token (JWT) format. The access token is cryptographically verifiable and auditable. If a malicious party alters an access token the API detects it and rejects the access token. Otherwise, the API implements authorization using the data from the access token's payload.
However, in a distributed architecture, the entry point API may then trigger additional internal requests to multiple additional backend components. In a zero trust architecture, you should secure all of the requests.
Typically, some backend tasks are asynchronous and processing resumes at a later time, as in the following use cases:
- A publishing component sends event messages that other components consume.
- A batch process runs every day at midnight.
- An administrator approves an action before a workflow continues.
Asynchronous flows often use event messages that contain sensitive data such as user identities or, in some cases, money amounts. When publishing and consuming event messages you should aim to continue to meet security requirements such as those listed below:
- A publisher should only be able to use event messages to access authorized resources.
- A consumer should be able to authorize event based access before making changes to its own data.
- A malicious internal component should not be able to post unauthorized messages.
- A malicious party should not be able to alter sensitive values in event messages.
To address these issues, sensitive identity values that the client sends in its access token should remain cryptographically verifiable and auditable.
Securing Events with JWTs
In a zero trust architecture, publishers could include a cryptographically signed JWT access token with each event message and consumers could verify the access token and apply authorization before processing event payloads. The following illustration demonstrates such a flow.
The following example event message encapsulates sensitive values within a JWT access token. Some systems may support sending the access token in an event message header, separate to the payload.
{"accessToken": "eyJraWQiOiItNDc4MTAzOTYyIiwieDV0IjoiSWlHY ...","payload": {"eventID": "84361cc7-728c-4af7-a14c-0023737e48da","utcTime": "2025-01-06T09:01:12.405Z","items": {...}}}
With this pattern the consuming API no longer needs to trusts any publisher and can instead only accept event messages that contain a valid access token issued by the authorization server. In this way, you can maintain the principle of zero trust.
Token Exchange Design
Typically though, there are problems if the publisher sends the original access token in this manner:
- Consumers may be granted too many privileges.
- The token may expire before the consumer processes it, leading to reliability problems.
To solve these problems the publishing API can use OAuth Token Exchange to swap the original access token for a new access token with reduced privileges and a long enough lifetime. Architects should design the exchanged token to meet the requirements of event consumers and can use the following techniques:
- Use reduced scopes in the exchanged access token.
- Use a different audience in the exchanged access token.
- Use a different expiry time in the exchanged access token.
- Use claims to lock down the exchanged access token.
You can use either downscoping
or upscoping
to produce the exchanged access token. Downscoping removes some of the scopes of the original access token. Upscoping does the same but typically removes all scopes and then adds a new scope, which can itself be low privilege.
Upscoping with Token Exchange
Upscoping can be useful in microservice designs where you want to keep scopes high level in user facing clients, but use additional scopes for backend processing on behalf of the user. If you use user consent, ensure that upscoping does not operate on user resources without the user's approval.
A token exchange request can also bind the exchanged access token to precise identifiers, such as a transaction ID or event ID. Doing so helps to ensure that the access token from an event message cannot be used outside of its original intent, even if replayed somehow.
Publishing Zero Trust Events
Consider an example token exchange request from an Orders service to get a new access token to forward to an Invoices service, where the original access token has a scope of openid profile orders
. The Orders API requests a different scope and audience. The request also sends custom properties to issue to the new access token to restrict its privileges:
POST /oauth/v2/oauth-token HTTP/1.1Host: login.example.comContent-Type: application/x-www-form-urlencodedgrant_type=urn:ietf:params:oauth:grant-type:token-exchange&client_id=orders-api-client&client_secret=Password1&subject_token=eyJraWQiOiItMTcyNTQxNzE2NyIsIng...&subject_token_type=urn:ietf:params:oauth:token-type:access_token&scope=trigger_invoicing&audience=jobs.example.com&transaction_id=6b69df74-339f-416b-84bb-f1f4f32d8f1a&event_id=e80be47d-7282-4f3c-898a-709ca5393aa5
The authorization server either allows or denies the request. Such a deployment enables central policies where a security team can govern any privileges associated to events. This logic can use input data to bind runtime values to the new access token and can give it a longer lifetime than the original access token. The following example exchanged access token could be included in a zero trust event message:
{"jti": "14c4ac0c-7806-401b-b6db-94ae1b6f8d4a","delegationId": "9dd15191-9441-4056-9e56-52b7ad116b18","exp": 1736758872,"nbf": 1736154072,"scope": "trigger_invoicing","iss": "http://localhost:8443/oauth/v2/oauth-anonymous","sub": "demouser","aud": "jobs.example.com","iat": 1736154072,"purpose": "access_token","transaction_id": "0d2e8437-d4e3-40e1-8d6e-a6e17d2c04c7","event_id": "5a704593-6f1f-45e4-886a-e37fe5848dc7"}
The new access token demonstrates the following behaviors:
- Upscoping replaces the original scope with a new scope of
trigger_invoicing
. - The authorization server updates the audience claim to
jobs.example.com
. - The authorization server issues the new access token with a 1 week expiry.
- A custom claim binds the access token to a precise event message.
- A custom claim binds the access token to a precise transaction ID.
For the token exchange to succeed, the Orders API must be registered in the authorization server as an OAuth client with the token exchange capability and access to the trigger_invoicing
scope. Architects could design this scope to grant the Orders API very limited privileges to the Invoices API. In this example, the new audience and scopes would ensure that the access token fails validation if sent to the Invoice API's HTTP endpoints, which would instead require different values.
Consuming Zero Trust Events
When APIs already process JWT access tokens at their HTTP endpoints it is straightforward to extend the API to also process JWTs at messaging endpoints. To validate JWTs, an API downloads token signing public keys from the authorization server and stores them in memory for subsequent requests. Consuming APIs may already do so for their HTTP endpoints.
The following pseudocode shows the type of logic needed to process a zero trust event message. First, the API calls a shared validation routine that performs JWT validation while setting the expected audience and scope values to those for messaging endpoints. The shared routine then verifies that the event ID in the event message matches that in the JWT. The consumer can then use both secured values from the access token claims and other values from the event message to complete its work.
messageBroker.onConsume('OrderCreated', message => {const claims = validateAsyncJobAccessToken(message.accessToken, message.eventID);processOrderCreated(message.payload, claims);});function validateAsyncJobAccessToken(accessToken, expectedEventID) {const options = {requiredAudience: 'jobs.example.com',requiredScope: 'trigger_invoicing',};const claims = validateAccessToken(accessToken, options);if (claims.event_id !=== expectedEventID) {throw new Error('The access token is not for this event ID');}return claims;}
Scaling Zero Trust Events
Zero trust events require only straightforward code and can use the existing key management infrastructure that the authorization server provides. The main challenge when scaling zero trust events is to ensure that there are no security or reliability pitfalls. Consider the following areas in the design of zero trust events.
Use Secure Event Storage
Use a locked down event store and ensure that no person can read messages in the message store, extract JWT access tokens and replay them against API endpoints.
Mitigate Long Lived Access Tokens
Ensure that APIs validate access tokens correctly before introducing low privilege JWTs with a long lifetime. If some APIs accept JWT access tokens without the correct audience and scope checks, introducing long lived access tokens could add new threats.
Long lived access tokens should only be usable at messaging endpoints and only for the message's precise event ID. Aim for behavior where long lived access tokens only operate on specific resources. If such a token is ever somehow replayed once processing is complete, API code should ensure that the result is a no-op.
Design Scopes Effectively
When using zero trust events with microservices, follow Scope Best Practices and avoid scope explosion. Ensure that scopes are easy to reason about, so that both the original client and every upstream API use only a handful of scopes.
Prioritize Events to Secure
Zero trust events are a secure design for high priority messages such as those that deal with money. Some messages may not operate on sensitive data or require the overhead of JWTs.
Consider Publishing Performance
There is a trade-off between security and performance when using zero trust events. When binding exact identifiers to exchanged access tokens, event publishers typically need to make a token exchange request for every event they publish. Such a design has a small overhead that impacts throughput and in some cases you might be able to use alternative designs to provide security guarantees.
Consider Token Signing Key Renewal
An authorization server uses Token Signing Key Rotation and occasionally rotates its token signing public keys. Ensure that the old public key remains in the JSON Web Key Set (JWKS) until any existing access tokens expire. If you use long lived access tokens that last for a week, then leave the old key in place for a week to ensure that consumers do not reject JWTs in zero trust event messages.
Consider Data Recreation
In some architectures you may use patterns like Event Sourcing and occasionally replay all stored events to recreate other data stores. In such use cases any JWTs stored in event messages will likely be expired and also contain digital signatures whose token signing public key no longer exists. You might therefore disable JWT signature and expiry validation for this type of data migration job.
Conclusion
Using JWTs in event messages enables APIs to resume asynchronous workflows and receive sensitive values asserted by the authorization server using public key cryptography. Consider using this technique, especially for high privilege transactions, such as those involving money, to improve security and auditability.
When publishing an event message, use token exchange to get a new JWT access token with reduced permissions, then bind the JWT to the event message that contains it. You can then assign the JWT a long lifetime, so that APIs can reliably consume events at a later time.
Zero trust events require careful design but only straightforward code. See the API Using Zero Trust Events tutorial to run an end-to-end code example on a development computer. The example uses the Curity Identity Server and the popular Apache Kafka event streaming platform.

Gary Archer
Product Marketing Engineer at Curity
Join our Newsletter
Get the latest on identity management, API Security and authentication straight to your inbox.
Start Free Trial
Try the Curity Identity Server for Free. Get up and running in 10 minutes.
Start Free Trial