Dual-Token Exchange
Microbus separates user authentication from in-bus authorization with two token types and an exchange step at the edge. A long-lived bearer token never travels deeper than the HTTP ingress proxy. The ingress trades it for a short-lived access token that is the only token propagating through the bus. Downstream microservices see only the access token.
Why Two Tokens
A single token would have to satisfy two contradictory requirements:
- Long-lived so the user does not have to re-authenticate every few minutes.
- Short-lived so a stolen token cannot be replayed for long.
Splitting the credential into two tokens lets each side optimize for one requirement:
| Token | Lifetime | Surface | Carries |
|---|---|---|---|
| Bearer token | Long (hours to days) | Edge only - never crosses the bus | User identity, refresh capability |
| Access token | Short (seconds to minutes, operator-tuned) | Bus-wide - propagates through every downstream call | Actor claims used for endpoint authorization |
A leaked bearer token is bad but the bearer token has a small surface: only the edge sees it. A leaked access token has a wide surface but a tight replay window.
The Exchange Flow
Client ──[bearer]──▶ Ingress ──exchange──▶ Access token service
│
└──[access]──▶ Microservice A ──[access]──▶ Microservice B ──▶ ...- The client authenticates and obtains a bearer token from the bearer token service (or an external IDP - see below).
- The client sends a request to the ingress with the bearer token in the
Authorizationheader. - The ingress validates the bearer token’s signature against the pinned JWKS for
bearer.token.coreand extracts its claims. - The ingress calls
access.token.core/Mint(an:666trust-root endpoint) with the bearer token’s claims, optionally transformed. - The access token service signs and returns a short-lived access token containing the actor claims.
- The ingress strips the bearer token, sets the
Microbus-Actorheader to the access token, and forwards the request onto the bus. - Every downstream microservice sees only the access token. Tokens propagate automatically across hops via the
Microbus-Actorheader.
Wrapper Microservice for External IDPs
Most production deployments authenticate against an existing identity provider (Okta, Auth0, Cognito, an internal SSO). The bearer token service ships as a default; nothing forces you to use it.
The pattern is to write a small wrapper microservice that:
- Exposes the same
Authenticate/Refreshendpoints the bearer token service exposes. - Internally validates the external IDP’s token (JWKS fetch from the IDP, OIDC discovery, OAuth introspection - whatever the IDP requires).
- Calls
access.token.core/Mintwith claims derived from the external assertion.
The wrapper registers under bearer.token.core so JWKS pinning in downstream microservices keeps working unchanged. From the rest of the deployment’s point of view, the bearer-token side just produces access tokens; how it does so is an implementation detail of the wrapper.
Claim Transformers
The Mint call from ingress to access token service may add, remove, or normalize claims along the way. Common transformations:
- Add scoped permissions - for example, derive
roles.adminfrom the IDP’sgroupsclaim. - Remove broad capabilities - strip a wildcard scope before letting the access token enter the bus.
- Normalize names - flatten the IDP’s nested claim shapes to flat keys the framework’s
requiredClaimsexpressions consume. - Inject deployment-specific tags - region, tenant, environment.
Transformers are application code in the ingress (or in the wrapper microservice). The framework does not impose a transformer DSL; it just gives you the place where transformation happens.
Lifetime Tuning
The access token’s lifetime is operator-tunable on the access token service. Sensible ranges:
- 30s-2 min for high-security deployments where replay-window minimization matters more than overhead.
- 5-15 min for typical production. Common balance.
- 30+ min only if the deployment cannot tolerate the call rate of frequent re-mints.
Each access token mint is an :666 call from the ingress, so per-request lifetime should be amortized. Mint at session boundaries, not per-API-call. Cache the access token in the user’s session and re-mint when it’s about to expire.
Why the Exchange Happens at the Gate
If the ingress just proxied bearer tokens onto the bus, every microservice would see the user’s long-lived credential. Compromising any one of them would yield long-lived credential material that replays well outside the bus.
By exchanging at the gate:
- The bearer token’s blast radius is bounded to the ingress and the wrapper.
- The bus-wide surface only ever holds short-lived access tokens.
- Downstream microservices perform only signature verification and claim evaluation. They never validate against the IDP and they never touch the user’s bearer credential.
- Replacing the IDP requires touching only the wrapper. Downstream microservices are unchanged.
See Also
- Actor JWT claims propagation - how the access token’s claims are evaluated by
requiredClaimsexpressions. - JWKS pinning - why downstream microservices verify only against
access.token.coreandbearer.token.core. - Trust-Root Tier - why the
Mintendpoints are:666-protected. - Access token service and bearer token service reference docs.