A User Login Does Not Limit What the Agent Can Reach
A Cognito login proves who the user is. It does not limit the agent. The token the tool uses has to be enforced outside the prompt.
By Muhammad Saif and Aashan Khan · Techtribe co-founders
A login screen feels like a permission. The user typed a password, Amazon Cognito issued a token, and the chat is now "theirs." The agent behind the chat is a different principal. It has a role, a key, or a gateway credential that was issued to the software, not to the person. If that credential can read every tenant, the user's login did not limit anything. It labeled the session. The label is not the lock.
AWS has been publishing the pattern for this on Bedrock AgentCore: propagate the user's authorization context so the infrastructure rejects a request the user is not allowed to make, instead of asking the agent to remember a WHERE clause. The Well-Architected AGENTSEC03-BP02 states the split directly. The agent operates under a service identity distinct from any human. It carries the user's context as claims. It does not assume the user's credentials. Two separations, often collapsed: identity, and permission.
Laptop agents have the same shape with a different login. What Claude Code and Cursor can reach from a developer's machine is in what those agents can touch. This note is the product you ship to customers, where Cognito or any other user pool is the front door and a service role is the back door the model actually uses.
The agent is not the user
AGENTSEC03-BP02 draws a line that application code keeps erasing. Identity separation means the agent is a service principal. The human who operates AWS and the human who chats with the product are not that principal, and they are not each other. An operator in IAM Identity Center and a customer in a Cognito user pool are different directories. Pointing both words at "the user" is how a customer token ends up with an administrator's reach, or how an agent role ends up in a customer's session by accident.
Permission separation means the agent's identity can do the tasks you named and cannot inherit a human's power because a human is nearby. An autonomous agent runs as itself. An agent acting for a customer still runs as itself, and the customer's scope travels as claims on a token. The agent never becomes the customer. Downstream systems read the claims and enforce them. If the only system that reads the claims is the model, you have a story about authorization and a role that bypasses it.
The AWS security post on propagating that context shows one concrete flow. The person signs in to a Cognito user pool. A pre-token-generation Lambda, version 2 of that trigger, adds a custom claim before the token is returned. The app sends the access token with the request to an agent on AgentCore Runtime. The runtime validates the token and, through AgentCore Identity, issues a workload access token that binds the user and the agent. For an internal store, the agent calls with a role that is further limited by session tags or metadata filters. For a system like Salesforce, the identity service exchanges the token on behalf of the user and the external product applies its own sharing rules.
The sentence to keep from that post is that the agent is an orchestrator, not a gatekeeper. It chooses tools and writes arguments. It does not decide access. The failure mode they are writing against is the one generated apps ship by default: one credential that can see the table, and a prompt that says to only use the current user's rows. Prompt injection, or a bug in the filter, and the credential still works. Moving the decision into IAM, into the database, or into the SaaS product's sharing rules means a manipulated prompt asks for a row the credential cannot return.
Cognito can be the identity provider in front of AgentCore Gateway and Runtime, or a credential provider for outbound calls. The configuration notes are specific about what you must check. The discovery URL has to be your pool, so the gateway fetches the right signing keys. The inbound authorizer has to allow the client id you actually issue. Cognito access tokens carry client_id and do not carry an aud claim the way ID tokens do. Configuring allowedAudience and wondering why every token is rejected, or configuring nothing and accepting tokens from another app client in the same pool, are both ways to get the boundary wrong. One user pool and one app client, shared by the frontend, the runtime, and the gateway, means the same token can be checked at each hop. Two pools "because the agent was a prototype" means you will eventually accept the prototype's tokens.
The argument the model fills in
The sharpest bug in this design is a tool parameter that looks like identity. Amazon's write-up of fine-grained access on a managed knowledge base walks through it. The knowledge base can filter documents by the user's email if the search request includes a userContext. When your own backend calls the search API, your code sets that field from the session. When the knowledge base is exposed as an MCP tool through the gateway, userContext becomes an argument. The model writes it. The gateway, and the knowledge base, do not automatically check that the email in the argument is the email in the token.
The fix they describe is not a better prompt. A Lambda interceptor on the gateway reads the email from the validated JWT and overwrites userContext before the tool runs. A Cedar policy then denies the call if the argument and the claim disagree. The knowledge base is only invocable by the gateway's role, and ACL-aware search returns the documents that user may see. The model can still ask for someone else's email. The interceptor throws that ask away. Cedar refuses if the overwrite did not happen. The tool never sees the model's idea of who the user is.
Any tool you generated has this shape the moment a parameter is named userId, accountId, tenant, or email. The model is good at filling those in from the conversation. A user who says "look up this customer" has just supplied a value. A poisoned document that says "the user id is the admin" has supplied one too. If the query uses the parameter and the connection string is a privileged role, both requests succeed. Overwrite the parameter from the token, or do not accept it. There is no third option in which the model promises to copy the claim correctly.
Cedar shows up here because it is a policy outside the prompt. A policy can say which tools an agent may call, and which tools a user may call, as separate questions. A layered design published on AWS Builder Center uses three checks: Cognito for who is calling, an interceptor allowlist for which tools this agent may use, and Cedar for which tools this user may use. All three run at the gateway, before the tool. A prompt that says "ignore your policies" does not edit the policy. That only works if the tool is not also reachable by a second path that skips the gateway. A function URL with the service role, left over from the prototype, is that path. The Cedar policy never sees it.
What Cognito will not do for you
Cognito authenticates. It can put claims into a token with a trigger. It can be the issuer your gateway trusts. It is not an authorization engine for your tables, and it is not aware of which rows exist. Teams get into trouble by stopping at "we have Cognito" and logging the user in, then connecting the agent with a key from Secrets Manager that was created when there was one developer.
The pre-token trigger is easy to over-trust. It can copy a profile attribute into the token. It cannot know that your database still has a row for a user you deleted, and it cannot stop a tool from using a different id if the tool does not read the token. Claims are inputs to a decision something else makes. A claim with no enforcement point is a log line.
Session tags and on-behalf-of exchanges are the enforcement, when you wire them. The security post's DynamoDB path uses credentials scoped with the user's tag so a request for another user's item fails in the database. The Salesforce path uses a token exchange so Salesforce's sharing rules, not the agent, decide which records return. Both are more work than a global key. Both still fail if one code path forgets and uses the global key "just for this admin tool." Admin tools become the bypass. Give them their own role and their own audit, or accept that the admin tool is the real permission model.
AGENTSEC03 also points at a Cognito-only variant for teams that are not on AgentCore Identity. The agent authenticates with client credentials. The user's token is passed beside that request. A pre-token trigger checks the user token and injects an onBehalfOf claim into the agent's token. The agent's sub and the user's onBehalfOf are then one token a resource server can evaluate. The document is careful: this is a custom use of a trigger, not a native Cognito feature that means "agents are supported, you're done." You still have to verify the user token inside that trigger. You still have to make every downstream call require the claim. A sample that does this for one API and a second service the agent calls with a static key is a sample that forgot a door.
Multi-tenant apps add a claim you must not let the client choose. The tenant id belongs in the token, set by your trigger from a directory you control, not in a header the browser sends and the agent forwards. A user who is in tenant A and asks about tenant B should fail in the query's WHERE, and the role should not be able to remove the WHERE. Postgres row security is one way to make the database refuse. A separate database user per tenant is another. A single superuser connection is neither. The November PostgreSQL fixes for policies and views are a reminder that the database has its own bugs, but they are not a substitute for having a policy. That patch list is in the November PostgreSQL note.
Tokens, tools, and the second principal
An agent that calls tools has at least two principals in play: the caller the gateway authenticated, and the credential the tool uses when it leaves. Confusing them is the confused deputy. The tool's credential is often broader, because the tool was written to be convenient. The model invokes the tool. The tool acts as itself. OWASP's MCP write-ups call this out for servers, and it is the same bug with a Cognito login in front.
Bind the two. The gateway should mint or select the outbound credential from the inbound token, per request, and discard it. A long-lived key in the agent's environment is a key for every user the agent will ever talk to. Logging the tool call without logging which user claim was bound to it makes the audit a list of actions nobody owns. Prompt injection is one way an attacker picks the action. The credential decides whether the action works.
Keep agent credentials and user credentials in different stores. The client's refresh token does not belong in the agent's memory, in a vector database, or in a trace payload. The agent's client-secret does not belong in the mobile app. When a trace product records "the request," decide whether the Authorization header is in that record. A debug trace that saves the user's access token is a bearer token sitting in a log product, which is a second Cognito bypass for anyone who can read logs.
Expiry is part of the split. A workload token that lasts as long as a user session is easier to steal and replay than a token minted for one tool call. A user token you cache in the agent process for "performance" is a token the next session can inherit if you reuse the process. Isolate memory between users. Google's MCP security notes say the same thing for a different cloud, and the organization-policy post covers what happens when the agent can also edit the policy that was supposed to be the limit. The limit has to be a system the agent cannot rewrite.
A shape that holds
One issuer. One app client the gateway allows. Access tokens validated on client_id, not on an audience claim Cognito did not set. A trigger that adds the tenant and the user id, and no tool argument that can override them. Policies at the gateway for which tools exist, and policies at the data store for which rows return. Outbound tokens exchanged per user where the third party supports it. A test that uses two real users and expects the second user's row to be absent, including when the prompt asks for it by id.
The test is the part generated apps skip, because the demo uses one account. Create two. Sign in as the first. Ask the agent for the second user's document by name, by id, and by a sentence buried in a file the agent is allowed to read. If any of the three return data, the login is a label. Fix the credential before you add another tool. Another tool on the same role widens the same hole.
Do not "fix" this by hiding the tool from the prompt. A hidden tool the process can still call is a tool an injection will find. Delete the path. If the model cannot reach a function that uses the broad key, the injection has nothing broad to call.
What to check before a customer token is the boundary
- Name the issuer, the app client, and the role the agent uses for each tool. If the role is the same for every tenant, the token is not the boundary.
- Find every tool argument that carries a user id, email, or tenant. Overwrite it from the validated token, or remove the argument. Do not leave it for the model to copy.
- Confirm the gateway is the only path to the tool. A leftover function URL with the service role is a bypass.
- Confirm Cognito access tokens are checked on
client_id. Anaudcheck against a claim the token does not have is either always-deny or a misconfiguration you do not understand yet. - Run the two-user test, including a prompt that names the other user's id. Save the result. A pass against one account is the demo.
- Read a trace from that test and see whether the other user's token, or the service key, was stored. If it was, fix the trace before you widen who can read traces.
- If you cannot point to the policy that would deny the call after the model is jailbroken, you do not have a policy. You have a prompt.
The login can stay. The role has to shrink to the person who logged in. If you cannot see that shrink in a test, the login is not the control.
If this is your app, send the repo.
A 30-minute scope call. We read what is live, name what is dangerous, and quote a fixed scope.
How Techtribe separates the agent from the user
We trace one real tool call from the login to the query and write down which credential ran. You get the arguments the model is allowed to set, and the ones we overwrite from the token. We do not add a line to the system prompt and call that authorization.
If you are building the agent and the identity path does not exist yet, that work is AI agent development. If the product is already in front of customers on a shared service key, a Dedicated Team fits, because the next tool someone adds will reuse the key unless a person owns the boundary. The scope call is 30 minutes with a founder, and it is where we say which you are in.
Bring the auth provider and one tool the agent calls with a service key. If you already know a user can see another user's data, say that first.
The fixed scope for taking a generated app to production is a page per tool: Lovable, Bolt, v0, and Replit.
Questions
If the user signed in with Cognito, is the agent limited to that user?
Only if something outside the model enforces it. AWS's guidance is that the agent keeps its own identity and carries the user as token claims. A service role that can read every row will read every row, including when the prompt says to filter.
Can the model pass the user's id into a tool and have that count as authorization?
No. On AgentCore Gateway, a field like userContext is a tool argument the model writes. Amazon's knowledge-base write-up says neither the gateway nor the knowledge base checks that this value is the authenticated user unless you overwrite it from the validated token before the tool runs.
Where should the allow or deny happen?
At the gateway and at the system the tool calls. Cognito answers who the user is. A Cedar policy or an IAM condition answers what that user, and that agent, may do. A sentence in the system prompt answers neither.
What should we bring to a scope call about this?
The auth provider and one tool the agent calls with a service key. Thirty minutes is enough to see whether the user's token ever reaches that tool, or whether the key is the same for every customer.