DEV Community

Sameer Khare
Sameer Khare

Posted on AI-assisted

AI Agents Need Their Own Authorization, Not Yours

Every multi-tenant B2B SaaS platform I've worked on eventually converges on the same authorization shape: every request carries a tenant, and within that tenant, a user with some set of permissions. Two axes — which account, which person — and for years that was a complete enough model to build an entire authorization layer on top of.

It stopped being complete the day an AI agent became a caller. Not a human sitting at a browser making requests that happen to be backed by a session token, but a process that decides on its own which API calls to make, sometimes across a dozen steps, with no human confirming each one — acting for a user without being that user's browser session. The two-axis model has no slot for that. It has tenant, and it has user. It doesn't have "something else, calling on the user's behalf, that isn't the user."

This is the auth model I've watched evolve over several iterations of a multi-tenant platform, and it's also, I think, the most interesting authorization problem in software right now — because it's not hypothetical anymore. It's what Auth0, Okta, and every other identity vendor spent a chunk of their 2026 roadmaps on. Worth walking through why the old model breaks and what actually replaces it.

The two-axis model, and where it breaks two years in

The standard shape is familiar to anyone who's built one: a JWT (or an opaque session token resolved server-side) carrying a tenant_id and a user_id, with roles or permissions attached to the user within that tenant.

{
  "tenant_id": "acct_9f21",
  "user_id": "usr_4471",
  "roles": ["billing_admin", "reports_viewer"]
}
Enter fullscreen mode Exit fullscreen mode

A gateway or middleware layer validates the token, resolves the claims, and every downstream service trusts tenant_id and user_id as the scoping boundary for every query it runs. It's a good model, and it holds up for a surprisingly long time. The places it actually breaks, in my experience, aren't exotic:

  • tenant_id drifts from "trusted claim" to "trusted-ish parameter." Somewhere around year two, a service that was only ever called internally — a reporting job, an admin tool, a migration script — starts accepting tenant_id as a request parameter instead of deriving it from the token, because it was faster to build that way and nobody was going to call it cross-tenant on purpose. Then something does, accidentally, and you have a cross-tenant data leak that traces back to one endpoint nobody thought of as "the auth boundary."
  • Roles get coarser, not finer, over time. "Admin" starts out meaning something specific and slowly absorbs every permission nobody wanted to model separately, because adding a new fine-grained role is a migration and a support ticket, and checking role == "admin" is one line. Two years in, half your user base has a role that grants far more than what they actually use.
  • The permission check gets reimplemented per service. Each team building a new microservice writes its own version of "does this user have permission X in this tenant," slightly differently, against a slightly different snapshot of what the roles mean. Nobody notices until an audit asks you to prove that permission logic is consistent across twelve services, and it isn't.

None of these are AI-related. They're just what happens when a two-axis model meets real organizational entropy. But they matter here because the AI-agent problem makes all three worse if you don't fix them first — an agent calling through a loosely-guarded tenant_id parameter, or inheriting a coarse "admin" role, is the same bug with a much larger blast radius, because the agent can trigger it a hundred times a minute without a human noticing the pattern.

What changes when the caller isn't a person

Picture a support-automation agent: a user asks it, in natural language, to "find the three invoices that bounced last month and retry them." The agent doesn't get a pre-approved plan — it decides, at runtime, to call a search API, then a retry-payment API, possibly a few times if the first attempt fails. Nobody clicked "retry" three separate times. One instruction turned into an unknown number of API calls, decided by the agent, not the user.

If you hand that agent the user's own session token, you've quietly made a decision you probably didn't mean to make: the agent now has everything the user has, for as long as that token is valid, with no way to tell, after the fact, "the user did this" from "the agent decided to do this." Three consequences follow directly from that, and none of them are edge cases:

Blast radius stops matching task scope. The task was "retry three invoices." The credential is "everything a billing admin can do in this tenant." If the agent is compromised, or manipulated by something in the data it's reading (a classic prompt-injection path — a malicious string in an invoice description, say, instructing the agent to do something else), the ceiling on what it can do is the user's entire permission set, not the three-API-call task it was asked to do.

Audit trails collapse into one identity. When something goes wrong six weeks later and someone asks "did the user do this, or did the agent," a token that only ever says user_id: usr_4471 can't answer that question. That distinction isn't a nice-to-have in a billing system — it's the difference between a user dispute you can resolve in five minutes and one you can't resolve at all.

Lifecycle and revocation stop lining up. A human session token is scoped to a login session — log out, and it's gone. An agent's task might run for seconds or, in an async workflow, for minutes across a queue. You want a way to kill the agent's ability to act without logging the user out of everything else, and a shared token gives you no lever to pull.

Worth being explicit about scope here: none of these three consequences are actually caused by multi-tenancy. A single-tenant application — no tenant_id anywhere, just user_id and roles — has the identical failure mode the moment an agent starts calling its APIs on a user's behalf. Same blast-radius mismatch, same collapsed audit trail, same revocation problem. Multi-tenancy doesn't create this; it stacks one more, independent risk on top of it — an over-scoped or compromised agent's overreach can potentially cross tenant boundaries too, not just exceed the task within one account. Everything below applies to both; multi-tenant SaaS is just the setting I've actually had to build it in.

A third identity: the actor, not just the subject

The fix isn't a new invention — it's an old OAuth pattern (RFC 8693, Token Exchange) that suddenly got a lot more relevant: the act claim, which represents delegation rather than impersonation. The distinction the RFC draws is exactly the one that matters here. Impersonation means principal A becomes indistinguishable from B — A gets B's rights and B's identity. Delegation means A keeps its own identity while acting for B — the token says, explicitly, "this is the agent, acting on behalf of this user, with this narrower scope."

Extending the two-axis claims to carry that looks like this:

{
  "tenant_id": "acct_9f21",
  "user_id": "usr_4471",
  "roles": ["billing_admin", "reports_viewer"],
  "act": {
    "agent_id": "agt_invoice_retry_v3",
    "on_behalf_of": "usr_4471",
    "scope": ["invoices:read", "payments:retry"],
    "task_id": "task_88c1",
    "exp": 1758470400
  }
}
Enter fullscreen mode Exit fullscreen mode

The critical property isn't that the claim exists — it's what the authorization check does with it. A naive implementation checks "does user_id have this permission" and ignores act entirely, which defeats the entire point. The check that actually contains the blast radius is an intersection: the caller is authorized only if the action is within the user's permissions and within the actor's granted scope.

boolean isAuthorized(Token token, String requiredPermission) {
    boolean userHasPermission = permissionService
        .hasPermission(token.tenantId(), token.userId(), requiredPermission);

    if (token.actor() == null) {
        return userHasPermission; // ordinary human session, unchanged
    }

    boolean actorInScope = token.actor().scope().contains(requiredPermission);
    boolean taskNotExpired = Instant.now().isBefore(token.actor().exp());

    // Delegated calls need BOTH: the user could do this, AND
    // the agent was explicitly scoped to do this specific thing.
    return userHasPermission && actorInScope && taskNotExpired;
}
Enter fullscreen mode Exit fullscreen mode

An agent scoped to ["invoices:read", "payments:retry"] can retry three bounced invoices all day long. It cannot call payments:refund or users:delete, even though the human it's acting for technically could — because the actor's scope, not the user's full role, is now the ceiling for anything the agent does. That's the actual fix: not "trust the agent less," but "give the agent its own, narrower credential instead of quietly reusing the human's."

This isn't a hypothetical extension — it's where the vendors are already building

This pattern isn't me speculating about where auth architecture might go. It's what Auth0 shipped in 2026 under exactly this framing: treating an AI agent as a first-class principal, distinct from the user, with its own identity and audit trail rather than being folded into the user's. Their on-behalf-of token exchange does the delegation handshake directly — a server trades a user's access token for a correctly-scoped downstream token instead of forwarding the original credential everywhere. Their Token Vault isolates the third-party credentials an agent needs (a calendar API key, a payments provider token) per tenant, so a bug in one organization's agent configuration can't reach across tenant boundaries. Okta's parallel work — Agent SSO, and fine-grained authorization built for permission checks at agent-call volume rather than human-click volume — is solving the same problem from the identity-provider side.

The fact that two competing identity vendors converged on "agent needs its own scoped, delegated, revocable credential, not the user's" in the same year is a good signal this is the actual shape of the problem, not one implementation's opinion about it.

What's still genuinely unsolved

The honest gap, and it's a real one: least-privilege scoping assumes you know the task in advance. "Retry three invoices" scopes cleanly to invoices:read and payments:retry. But a lot of what makes agents useful is that they don't follow a pre-approved plan — they decide their next step based on what the last one returned. An agent that starts with "summarize this account's activity" might reasonably decide, three steps in, that it needs a permission nobody scoped it for, and there's no clean answer yet for "should the agent get to ask for broader scope mid-task, and if so, who approves that without breaking the whole point of an unattended agent." Static, task-scoped tokens are a solid answer for well-defined tasks; they don't fully solve open-ended agent loops, and I haven't seen an identity vendor claim otherwise yet.

Revocation mid-task has a similar rough edge. Killing an agent's token after it's made 3 of an intended 10 calls is easy to do — revoke the token, next call fails — but reasoning about what state that leaves behind (three payments retried, seven not, and no transaction wrapping the whole task) is an application-level problem the auth layer doesn't solve for you. It just makes sure the agent can't keep going once you've decided to stop it.

Testing this

The check worth writing isn't "does the middleware read the act claim correctly" — that's a unit test and it's necessary but not sufficient. The one that actually catches the bug that matters is adversarial: mint a token where the user's role would permit an action, but the actor's scope doesn't, and assert the request is rejected. Then do the reverse — an actor scope that's broader than what makes sense, paired with a user who's been demoted or had a role removed — and confirm the user-side check still wins. The bug this pattern is actually guarding against is someone shipping a permission check that reads one claim and forgets the other; a test suite that only exercises the happy path where both claims agree won't catch it.

Takeaways

  • An AI agent acting on a user's behalf is a third identity — the actor — and needs to be represented as one, not folded into the user's token. This isn't a multi-tenant-specific problem; it shows up identically in a single-tenant app with just a user_id. Multi-tenancy just adds a second, independent risk on top (cross-tenant overreach).
  • Reusing the user's session token for an agent silently maximizes blast radius to "everything the user can do," for the lifetime of that token, with no way to distinguish the agent's actions from the user's in an audit trail.
  • Delegation isn't impersonation. RFC 8693's act/may_act claims formalize the distinction: the agent keeps its own identity and a narrower, explicit scope, instead of becoming indistinguishable from the user it's acting for.
  • The authorization check has to be an intersection, not a swap. A delegated call needs to pass both the user's permission check and the actor's scope check — checking only one defeats the purpose.
  • This is where identity vendors are actually building right now, not a speculative extension — Auth0's on-behalf-of token exchange and Token Vault, Okta's Agent SSO, all converge on the same "agent as first-class, scoped, revocable principal" shape.
  • Static task scoping doesn't fully solve open-ended agent loops, and mid-task revocation stops the agent but doesn't clean up partial state on its own — both are open problems worth being honest about, not glossed over.
  • Test the claim intersection adversarially, not just the happy path — a user-permits/actor-denies case and an actor-permits/user-denies case, not just the case where both claims happen to agree.

Top comments (2)

Collapse
 
deanlee profile image
Dean Lee

The core breakdown with bearer delegation is not just identity confusion. It is an unhedged mismatch in execution variance.

Standard SaaS authorization was designed around human tempo, where an authenticated session implies one or two discrete mutations per decision cycle. An autonomous agent flips that ratio. A single prompt can trigger dozens of orchestrated API calls across multiple services in seconds. Even if every single request is strictly permitted under the user role, the composite blast radius of that execution chain is completely unconstrained.

Treating the agent as a distinct third principal alongside tenant and user is necessary. But without bounding the execution envelope (limiting the rate of state mutation, setting cumulative dollar or record thresholds, and requiring step-up validation when a workflow branches outside its initial plan), a separate agent token mostly just ensures the postmortem has a cleaner audit log. The real authorization boundary has to price cumulative side effects, not just authenticate caller identity at the gateway.

Collapse
 
jo-do profile image
Jo Do

The extra principal is the key move, but I think task scope has to be first-class too. An agent token that says who delegated it and which agent holds it can still be wildly overpowered if it only inherits a role. I want the authorization server to bind an operation budget: allowed resources, verbs, audience, expiry, and ideally a stable task or approval ID.

That also makes audit logs useful. "Agent X used User Y's token" explains identity; "Agent X performed operation Z under grant G, which allowed exactly this" explains authority. The latter is what lets you distinguish a legitimate multi-step plan from a prompt-injected detour without reconstructing intent from a transcript.