When an IAM Identity Center Token Exchange Loses Its Response
2026-09-24 - 8 min read
What happens when Identity Center exchanges your assertion, then the connection drops before you receive the token? AWS needs an idempotent way to recover that result.
We were building a hosted Model Context Protocol (MCP) service. Each request carried a Microsoft Entra ID token for the user. The service needed to exchange that assertion through AWS IAM Identity Center before it could call a downstream AWS application on the user's behalf.
The service ran on Lambda. Keeping Identity Center's response only in memory would lose it when an execution environment disappeared, and separate environments needed to coordinate the first exchange. We chose DynamoDB for a durable shared record and conditional writes. Then we ran into the harder problem: what if Identity Center consumes the assertion and the connection fails before our service receives the issued token?


A durable cache can protect a token you received. It cannot recover a token that Identity Center issued but your application never got.
This is a guide to that failure boundary, not a claim that every IAM Identity Center flow has the same behavior. The specific path is the JWT bearer grant on CreateTokenWithIAM with a trusted external issuer.
Two tokens, two owners
The external Entra JWT arrives with MCP requests. It identifies the user to the service and serves as the assertion for the JWT bearer grant. Identity Center verifies it and returns an Identity Center access token, with a refresh token when applicable. The downstream AWS application accepts the Identity Center token, not the external JWT. AWS describes this trusted token issuer flow explicitly.
An external OAuth client holds the Entra refresh token and sends the access token with MCP requests; the hosted service never receives the client's refresh credential. Microsoft's refresh-token contract lets that client obtain a new access token, but our service cannot make it renew or assume it will retry a failed tool call automatically. The client's renewal and reconnect behavior still needs live validation.
The external client owns the Entra renewal path. Our hosted MCP only receives the access token it sends.
In our implementation, the incoming assertion can accompany multiple MCP requests while valid, but we attempt to exchange that exact assertion once. We persist the issued access token for those subsequent requests. We do not use the Identity Center refresh grant in this implementation; a new upstream assertion starts a new record. That is an implementation choice, not a statement that the API lacks refresh support.
Lambda makes a process-local cache insufficient, but Lambda is not the cause of the exchange failure. A container or VM can lose a response during a network interruption too. DynamoDB gives us shared, durable application state; it does not share a transaction with Identity Center.
The exchange and the write are different commit points
AWS says that if IAM Identity Center receives a request to exchange a token it has already exchanged, the request fails. The CreateTokenWithIAM request has no client-supplied idempotency key, and its response carries the issued access token. The documented OIDC operations do not provide a way to retrieve a prior exchange result by client key.
DynamoDB can conditionally create a claim or publish a result. Its transaction scope ends at DynamoDB. Identity Center can consume the assertion whether or not our process ever publishes the response.
The application can coordinate its workers, but it cannot make the exchange and publication one transaction.
The DynamoDB claim prevents competing workers from exchanging the same assertion. It cannot retrieve Identity Center's lost response.
A network partition can therefore leave the client unable to tell whether Identity Center committed the exchange. If it did and the response never arrived, trying the same assertion again is not a recovery protocol. Nor is changing the cache from DynamoDB to a broker: the external exchange and your store still do not commit together.
This is an ambiguous outcome, not a normal transient failure. Disabling automatic SDK retries for the JWT-bearer exchange prevents your client from replaying an uncertain grant; it does not make a lost response retrievable.
Separate the four failure windows
| Boundary | What the caller can establish | Safe application response |
|---|---|---|
| Exchange demonstrably not sent | This attempt did not consume the assertion. | Retrying could be safe if non-delivery is provable; our implementation still fails closed rather than inferring this from a timeout. |
| Exchange sent, response lost | Identity Center might have consumed the assertion. | Do not replay it. If no published result exists, require a fresh upstream assertion. |
| Token received, DynamoDB write acknowledgment lost | The owner still has the token, but the write may have committed. | Strongly read the exact record. A matching published result confirms success; otherwise retry only that owner's identical publication while the result remains in hand. |
| Published result confirmed | The shared store holds the issued token. | Reuse it within its validity window. |
The third row is easy to mistake for the second. A strongly consistent DynamoDB GetItem that finds the exact result confirms a successful write. A miss does not prove that a timed-out write still in flight cannot commit later. Do not hand the claim to another owner because one read returned nothing.
A positive read can settle a lost write acknowledgment. A negative read cannot rewind an exchange or rule out an in-flight write.
Coordinate callers without pretending to own Identity Center
Our application-side approach is deliberately conservative. It derives a DynamoDB key from the exact incoming assertion, target application, and region; the key contains a hash of the assertion rather than the raw bearer credential. A conditional PutItem with attribute_not_exists(pk) selects one owner. A strongly consistent read either finds a published token or sees a pending claim.
The owner sends the JWT-bearer grant with one SDK attempt. If it receives the token, it publishes the result under an owner condition and returns the token only after publication is confirmed. The write retains the same owner and item contents when retried. Conditional writes coordinate our workers; they do not grant us a retryable IdC operation.
read exact assertion record consistently
if published and still valid: return stored token
if claimed by another worker: report busy, then require reauthorization if abandoned
conditionally create claim with owner identity
if claim outcome is uncertain: confirm ownership before any exchange
send JWT-bearer grant once; do not replay an uncertain grant
if response arrives: publish identical result under owner condition
return token only after publication is confirmed
if response is lost: fail closed; obtain a fresh assertionThere is a short advisory deadline for a pending claim, but expiration is not permission to steal it. A claimant could be paused just after Identity Center consumed the assertion. After the deadline, other callers are directed to reauthorize rather than attempt the same exchange. The original owner can still publish the result it already holds. DynamoDB time-to-live is eventual cleanup, not a lease-transfer mechanism.
We also bound the stored result's usable lifetime by the earlier of the incoming assertion's expiry and the issued access token's expiry, with a safety margin. That avoids serving a credential after the caller's authorization window closes. No upstream JWT or refresh token is stored in the cache, and error messages do not include bearer credentials.
Test the failure policy, not just the happy path
The important tests inject uncertainty at each boundary. A second worker must not exchange an assertion already claimed by another. A lost claim acknowledgment must not trigger an exchange unless ownership is positively confirmed. A failed or timed-out IdC call must not replay the same assertion. A lost publication acknowledgment can succeed on a matching strong read; an unpublished token must never be returned.
- Concurrent callers: One claim owner sends the grant; the others observe pending or published state.
- Owner dies after exchange: The claim remains non-transferable; later callers require a new assertion.
- Ambiguous store write: Retry only the identical owner-conditioned publication while the owner still holds the result.
- Expired caller token: Reject it even if a stored Identity Center token has time remaining.
These paths have local test coverage, including process-failure simulation. They are not a claim that the revised implementation has been deployed and validated against live Identity Center and DynamoDB together. A live storage or network test is a separate gate. For more on why process memory is an unreliable shared state boundary in this runtime, see our Lambda antipatterns guide.
The API contract AWS should add
The application can make its own publication idempotent. It cannot make CreateTokenWithIAM and DynamoDB one transaction. The missing contract is narrower and more useful than a distributed transaction: allow a client to supply an idempotency key for a JWT-bearer exchange, or retrieve the result of a previously accepted exchange by a stable client key after a response is lost. The result would need a documented retention window and the same authorization checks as the original exchange.
Without that, a network failure leaves a real possibility of a consumed assertion and an inaccessible issued token. Developers can coordinate callers, refuse unsafe retries, and ask the user for a fresh assertion. AWS should give them a way to recover the exchange result instead.