No. 20 · JUN 2026 · 8 Min Read

When User Code Acts as the User

Abstract

SSO gets the user in, but the real work is carrying that identity into the sandbox: code that acts as the user, scoped sessions, and per-user secrets.

In A Sandbox for User Code the token was a given. It showed up carrying a tenant and a role, and ABAC over the path did the rest. That was a deliberate gloss, because the identity underneath is its own machine, and it is the part that separates a demo from something a regulated customer will sign off on.

The reason it matters: the useful version of user code does not just run in a sealed box doing arithmetic on data you handed it. It acts as the user. It reads what they can read, writes what they can write, calls the third-party systems they personally connected, and decrypts the secrets that belong to them and nobody else. The whole value is that the program behaves as if the person using it had written and run it themselves, holding exactly their access and nothing more. Sometimes they did write it. Increasingly they did not: one person builds the thing and a whole team uses it, each acting as themselves. The engineering problem is getting a real identity to survive the trip from a browser, through your control plane, into a microVM, and onto a decrypt call, without ever widening into a credential that can act for anyone else.

Four moves. The identity comes in. The session carries it. The code acts as the user instead of as a service. The secrets are sealed to that same identity.

The Identity Comes In

Authentication is not where you want to be original. The pattern is federated single sign-on with one token format coming out the back.

Cognito is the front door, but its job is identity-provider-agnostic plumbing rather than a user database. Your enterprise customers already have an IdP, Okta or Entra or Google or something older, and they are not going to let you mint passwords for their staff. So you federate: Cognito accepts a SAML or OIDC assertion from the customer’s IdP, and each tenant brings their own. What you get on the other side is a single OIDC token whose claims you control. A pre-token-generation Lambda is where you stamp the claims that matter downstream: tenant, role, entitlements. The same shape the rest of the system reasons about.

This is the OAuth layer doing its job. The agent authenticates the human, your authorization server issues a scoped token, and every hop after this validates that token rather than holding a password. AWS made the agent-specific version of this explicit with Bedrock AgentCore Identity, whose inbound auth takes a JWT via an OIDC discovery URL and is deliberately IdP-agnostic across Cognito, Okta, and Entra. You can build the federation yourself or buy that shape. The output is the same: one trustworthy token, claims you stamped, customer’s own login.

The Session Carries It

A token with claims is not yet an AWS permission. The bridge is principal tags.

Cognito identity pools map the token’s claims onto the user’s temporary IAM session as principal tags. You add the https://aws.amazon.com/tags claim and grant Cognito sts:TagSession, and the assumed-role session comes out wearing two tags that do two different jobs: the caller’s tenant and the caller’s user id. The tenant is the wall. Your IAM and KMS policies gate on aws:PrincipalTag/tenant, so the credentials that reach the sandbox cannot touch another tenant’s data before any of your code runs. The user id is who the code acts as inside that wall, so two people in the same tenant get their own access and never a shared one. The cross-tenant read that got denied in the sandbox piece is denied here by STS and IAM, not by an if statement you have to remember to write.

This is the plumbing under the policy. The sandbox article said the path is the resource and the tenant attribute decides access. This is where those attributes come from. Tenant and user both rode in on the token and got stapled to the session, one as the boundary and one as the identity the work is done under.

The Code Acts as the User, Not the Service

Most systems get this move wrong.

The lazy architecture runs every tenant’s code under one fat service role. That role can see all tenants, reach all secrets, call everything, and the only thing standing between tenant A’s program and tenant B’s data is your application code behaving perfectly. One bug, one injection, one clever prompt, and the wall is gone, because the wall was never real.

The correct architecture gives the running code the user’s identity, scoped down so it can do less than the user, never more. Two mechanisms carry it.

For your own AWS resources, AssumeRole with a session policy. Session policies only ever intersect with the role’s permissions, so they can restrict but never expand. You hand the sandbox a session that is the user’s tenant tag, plus a policy narrowed to exactly this run. Staple SourceIdentity to it, and the human’s identity is pinned to the session and written into the requestParameters of every CloudTrail event the code produces. Least privilege and a clean audit trail in one call: every action is attributable to a person, through a role, not to an anonymous service account.

For the user’s connected third-party systems, OAuth 2.0 Token Exchange, RFC 8693. The spec is precise about a distinction that matters here. With only a subject token, you get impersonation: the downstream service sees the user and nothing else. With a subject token and an actor token, you get delegation, and the issued token carries an act claim naming who is acting on behalf of whom. The authorization server gates the whole thing with a may_act claim, its way of stating that this party is allowed to act for that user. Delegation is the better default. Impersonation is invisible in the logs; delegation records that the user’s code, acting through your platform, did the thing, which is exactly the line a compliance team will ask to see.

AgentCore Identity ships this as a product. Its outbound auth has a two-legged flow where the agent acts as itself and a three-legged flow where it acts on behalf of a specific human, and a token vault that binds agent identity, user identity, and the stored OAuth tokens together so one user’s tokens can never be pulled while serving another. If you would rather not build the token plumbing, that is the managed version of the outbound half, fetching and isolating the user’s third-party tokens, and on AWS it is worth using. The fit is only partial, though. AgentCore binds those tokens to an agent’s workload identity, which is the right model when a trusted agent acts for a user. It does not hand a scoped-down session to the untrusted code running in your sandbox. That session, the one the user’s own program executes inside, you still mint yourself with STS.

The principle underneath both mechanisms is the same one the sandbox piece leaned on. This is correct by construction. The credentials inside the sandbox were never wide enough to act for another tenant, so acting for another tenant is not a guarded mistake. It is unrepresentable, and it is signed in the audit log with the name of the person responsible.

The Secrets Are Sealed

User code needs secrets. An API key for the account the user connected, credentials for their database, a key to decrypt their own records. Some belong to the whole tenant; some belong to one person inside it. The tempting design is one secrets store the service role reads wholesale and decrypts into the sandbox, which reintroduces the fat-role problem one layer down.

KMS encryption context closes it. Seal each secret with an encryption context that names whoever owns it, the tenant for shared secrets and the user for personal ones, and tie decryption to the matching tag on the session:

"Condition": {
  "StringEquals": {
    "kms:EncryptionContext:tenant": "${aws:PrincipalTag/tenant}"
  }
}

Encryption context is authenticated additional data, not a secret. The decrypt only succeeds if the exact same context is supplied, and the condition above means a session tagged for tenant A can only ever decrypt ciphertext that was sealed for tenant A, even when every tenant shares one KMS key. Code that somehow got its hands on tenant B’s ciphertext still cannot turn it into plaintext, because its session’s tag does not match the context the key requires. Swap tenant for user in that condition and the same trick seals a secret to one person, so even a teammate in the same tenant cannot read it. For workloads where the isolation has to be a hard, auditable boundary, each tenant gets their own key, with its own kill switch: disable the key and that tenant’s data goes instantly unreadable. Where cost rules, the shared key with per-tenant encryption context gets you most of the way at a fraction of the price. AWS lays out that cost-conscious version directly.

This is the same mechanism again. The tenant and user tags that scoped the filesystem path and narrowed the session are the tags that gate the decrypt. Storage, execution, and secrets, all governed by the same two tags and the same principal-tag trick, in three places.

One Identity, All the Way Down

The chain is a single thread pulled through every layer. SSO gets the user in and hands you a token you control. The claims become a scoped session instead of a fat service role: the tenant as the wall, the user as the identity the work is done under. The running code acts as that user, narrowed below what they can do and named in every line of the audit log. The secrets it can decrypt are gated by the same two tags that gate its files. The identity that signed in through the customer’s IdP is the identity doing the work in the microVM and the identity on the key, and at no point in the chain does anything run as “the platform” with the standing to cross a tenant or stand in for another user.

That is what keeping it on your own infrastructure actually buys, and it is the thing the laptop with a pasted API key can never offer. Not just that the code runs where you can see it, but that it runs as exactly who it should be, with exactly what they are allowed, and leaves a record saying so.

That is the identity of a single run. The product it adds up to, the Apps your users build and the connectors they snap onto them, all fed by one broker, is the last piece: A Credential Broker for User Code.