Cognito now does the token exchange we had to build
TL;DR. On 12 November AWS announced that Amazon Cognito now implements OAuth 2.0 token exchange, RFC 8693, at its token endpoint and in a new
GetDerivedTokenAPI. When we wrote post 05 of our AgentCore series it did not, so we built the exchange AgentCore Identity needs ourselves, a front door on API Gateway and Lambda, a second user pool and four Lambda triggers. We had the native exchange running behind AgentCore Identity the next day, in both of its modes. It does the job of the front door, the exchange pool and the triggers. The token comes from the customer's own pool, and in delegation mode it names the agent as well as the customer. Four things caught us out, and they are below.
What post 05 had to build
Post 05 gave the series' order support agent a token for the order service, minted for the one customer it was serving, through AgentCore Identity's on-behalf-of exchange. That exchange is RFC 8693, and at the time Cognito's token endpoint offered only the authorisation code, refresh token and client credentials grants.
So we followed AWS's own sample and built the missing piece. An endpoint on API Gateway and Lambda spoke RFC 8693 to AgentCore Identity. A second Cognito pool minted the token through its custom authentication flow, and four Lambda triggers verified the customer's token and wrote the customer_id claim that the gateway's Cedar policy checks. That came to about 450 lines of Terraform and 700 of Python before the tests. Post 05 called it a teaching component and said that in production the exchange belongs to the identity provider.
Post 07 then showed what it cost at run time. The first exchange of a turn failed after 6.1 seconds while the front door and the triggers started cold, and a retry covered for it.
What Cognito does now
An app client that is allowed to exchange tokens presents a customer's access token and gets back a new access token for an audience it names. AWS's guide calls it "well suited to service-to-service and agent-based scenarios". There are two modes. Impersonation issues a token that stands for the customer alone. Delegation adds an actor token, here the agent's own client credentials token, and the issued token carries an act claim that names the agent. Cognito does not evaluate may_act, so which agents may act for a customer is still decided by which app clients you allow to exchange, and by your policy.
Cognito now does the checking our front door and triggers did, and on scopes it is stricter. The issued token can only carry scopes the customer's token already held and the client may issue, a pre token generation trigger cannot add one, and asking for any other scope is refused. The audience must be a resource server registered in the pool. The customer must be enabled and confirmed, and the subject token's signature, expiry and revocation are checked. No refresh token is issued. If you still want a claim of your own, the pre token generation trigger runs for an exchange too.
This is the token AgentCore Identity brought back in our test, in delegation mode, with the ids shortened.
{
"username": "c-1000",
"client_id": "<agent client>",
"aud": "orders",
"act": {
"sub": "<agent client>",
"client_id": "<agent client>",
"iss": "https://cognito-idp.us-east-1.amazonaws.com/<customer pool>",
"token_type": "urn:ietf:params:oauth:token-type:access_token"
}
}
What it removes from post 05
| Post 05 | With Cognito's exchange | |
|---|---|---|
| Exchange endpoint | API Gateway and a Lambda front door | The customer pool's token endpoint |
| Pool that mints the token | A second pool, only for the exchange | The customer pool |
| Lambda triggers | Define, create and verify challenge, and pre token generation | None |
| Credential provider | Points at the front door | Points at the customer pool |
| Gateway trusts | The exchange pool, with the orders client as audience | The customer pool, with the orders resource server as audience |
| Cedar compares | customer_id, which a trigger copied from username | username itself |
| Token names | The customer | The customer, and in delegation the agent |
| Lambdas to start cold | Five | None |
AgentCore's side keeps its shape. The runtime's inbound auth, the workload identity and the token vault do the same jobs. The credential provider stays a custom OAuth2 provider, now on the customer pool's discovery URL, with the agent's app client and its secret, the token exchange grant, and actor token content NONE for impersonation or M2M for delegation. That setting, not the agent's code, chooses the mode. For delegation the app client also needs the client credentials grant and a scope of its own, orders/read here, which AgentCore asks for to get the actor token. The gateway's authorizer and the Cedar policy move to the customer pool and its claims.
In the agent, the request names the audience, asks for no scopes and says what kind of token the customer's is. The token came back with no scope claim, as the customer's only scope is not one the agent's client may issue. The API reference lists at least one scope as required, but the service accepted an empty list.
result = client.get_resource_oauth2_token(
workloadIdentityToken=workload_token,
resourceCredentialProviderName=os.environ["OBO_PROVIDER_NAME"],
oauth2Flow="ON_BEHALF_OF_TOKEN_EXCHANGE",
scopes=[],
audiences=["orders"],
customParameters={
"subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
},
)
What caught us out
- The token endpoint wanted managed login. Exchanges returned
token_exchange_requires_managed_loginuntil the pool's domain used managed login version 2. Switching an existing domain over had not taken effect after five minutes, and creating the domain on version 2 worked at once. The guide asks only for a domain, so this may change.GetDerivedTokenneeds no domain, but AgentCore Identity uses the token endpoint. - The subject must be typed as an access token. Cognito accepts
urn:ietf:params:oauth:token-type:access_tokenand refuses the generic JWT type as unsupported. AgentCore Identity's exchange failed with HTTP 400 until we passed the type incustomParameters, as above. - Scopes cannot be added, and that is the safer rule. Post 05's trigger put
orders/readinto the minted token, a scope the customer's own token never carried. Nothing in post 05 checked it, so it did no harm there, but our exchange was granting the agent something the customer had not been given. For an agent acting for a customer, the exchange should only narrow what the customer's token allows. Cognito enforces that, so asking fororders/readwas refused. Here the agent asks for none and the audience and Cedar do the work. To use the scope, the customer has to be granted it when they sign in, through Cognito's OAuth flow rather than its sign in API. - The tooling is behind the service. The current boto3, 1.44.2, does not model
GetDerivedToken, so we signed that call by hand, though it did pass the new app client flows,ALLOW_TOKEN_EXCHANGE_IMPERSONATIONandALLOW_TOKEN_EXCHANGE_DELEGATION, through to the service. The Terraform AWS provider, at 6.73.0, refuses those two flows, so for now set them through the API. The token endpoint's reference page still lists three grants. The pool must be on the Essentials or Plus plan, as post 05's exchange pool already was, and the guide lists the feature being enabled for your account as a prerequisite. Ours already had it.
One more thing to check rather than a trap. Post 05 minted five minute tokens. Ours lived just under an hour, as long as the customer's token had left, which was also the agent client's default access token lifetime. Look at the lifetime you get before you rely on it.
Where that leaves the series
This is how we would build post 05 today. The exchange would live in the customer's own pool, in delegation mode, so the token the gateway checks names both the customer and the agent acting for them. We ran the exchange end to end through AgentCore Identity in both modes. We have not yet run the gateway and Cedar against the new token. The gateway would check the customer pool's signature and the orders audience, which should also keep refusing the customer's own token, as it carries no aud. Cedar would compare username, the claim post 05's trigger copied into customer_id. That is the next thing we run.
If you followed post 05, the front door and the exchange pool are on their way out. If you are about to build on-behalf-of access for an agent on AWS, start with Cognito's exchange.
Sources
- https://aws.amazon.com/about-aws/whats-new/2026/11/cognito-token-exchange/
- https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-token-exchange.html
- https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_GetDerivedToken.html
- https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/on-behalf-of-token-exchange.html
- https://datatracker.ietf.org/doc/html/rfc8693
- https://github.com/levantar-ai/demos/tree/main/agentcore/05-identity