From Service Accounts to Agents: The Identity Progression Behind an AI Chatbot
Auth0's on-behalf-of token exchange and, more recently, Agent as Principal exist to solve a problem that most AI chatbot backends don't realise they have until someone asks the wrong question in an access review: who, or what, is actually calling this API?
I've been building a citizen-services chatbot for an apidays workshop - a small government portal called SmartGov where a citizen chats with an assistant that can check the status of a benefits case and submit an appeal on their behalf. The interesting part of that build was never the chatbot itself; it was the token that the chatbot's backend presents to the downstream case-service API every time a tool call fires. That token has gone through three distinct identity models over the life of the project, and each one answers "who is calling this API" a little more honestly than the last. In this post I will walk through all three, using SmartGov's real audiences, clients, and token claims as the worked example throughout.
The three models are as follows.
- Machine-to-machine credentials - the chatbot backend as a generic service account
- On-behalf-of token exchange (RFC 8693) - the citizen as subject, exchanged across every hop
- Agent as Principal - the agent gets its own identity, carried alongside the citizen's
Era 1: Machine-to-Machine, or "the chatbot is just a service account"
The obvious way to let a chatbot backend call an API is the oldest way anything calls an API: register the backend as a machine-to-machine application, grant it a client_credentials token against the target audience, and scope that token down to whatever the chatbot is allowed to do - in SmartGov's case, something like submit:appeal.

This works, and for a huge number of backend-to-backend integrations it's still the right answer. The problem is specific to the moment a human enters the picture. Decode that token and sub is the chatbot's own client ID. There is no citizen anywhere in it. Every appeal the chatbot submits, for every citizen who has ever used it, looks identical from the API's point of view: the same static principal, the same static scope, over and over.
That's fine right up until you need to answer a question like "show me every appeal this chatbot has ever submitted on behalf of citizen X" from the token alone, or "revoke this chatbot's access to citizen X specifically, without touching anyone else." Neither is possible. The token authorises the service, not the request, so it never carried a user in the first place. The chatbot is, from the API's perspective, a single over-privileged account that happens to be driven by an LLM instead of a cron job.
Era 2: On-Behalf-Of Token Exchange, or "carry the citizen through every hop"
The fix is RFC 8693 OAuth 2.0 Token Exchange, which Auth0 implements as on-behalf-of token exchange. Instead of the chatbot minting its own service-account token, it exchanges the citizen's own access token - the one they got from logging in - for a new token scoped to whatever it needs to call next. The citizen's identity rides along on every hop; the chatbot never gets to invent a request that isn't traceable back to a real user.
SmartGov's chat backend actually needs two hops, not one. The citizen's chat message reaches the chatbot backend, which needs a token to call its own MCP tool server, which in turn needs a further exchanged token to call the case-service API where the appeal actually lands.

One detail cost real debugging time to learn: each hop needs its own exchanging client, not one shared client reused across hops. Auth0 checks the exchanging client's own resource_server_identifier against the subject token's aud. That's a different relationship to the request's audience parameter - resource_server_identifier is a fixed property of the client itself, set once at creation and checked automatically on every exchange, not something you pass at request time the way you do with audience. SmartGov's first hop exchanges a token audienced for the app's own API; the second hop exchanges a token audienced for the MCP server. One client can't have two resource_server_identifier values, so it needs two separate client registrations, one per hop, even though conceptually it's "the same chatbot" doing both exchanges. I've written about the specific error messages and dead ends this produces in Six Checks Later: The OBO Bug That Took Two Sessions To Find if you want the full debugging trail rather than the summary.
What you get for that complexity is real: decode any token in the chain and sub is always the citizen's own user ID, aud changes hop to hop, and every downstream API can independently re-check authorisation against the actual user, not the chatbot. SmartGov re-checks Auth0 FGA relationships at both the tool-call layer and the case-service layer for exactly this reason - defence in depth only works if the token you're checking still names a real person.
The gap OBO doesn't close
Notice what's missing from that second diagram: the agent itself. sub is the citizen on every hop, start to finish. The act claim, if you inspect it, identifies which Auth0 client performed the exchange - a client ID, not an agent. There is no field anywhere in that token chain that says "this specific AI assistant made this call."
For SmartGov that gap showed up concretely once the build had a real authorization model to compare it against. Auth0 FGA already models a distinct agent type - agent_delegate, agent_can_submit_appeal - as the object of a delegation check, answering "is this agent allowed to act for this citizen." But nothing in the token/credential layer ever asserted an agent as the actor making the request. The agent had a name in the authorization model and no identity at all in the identity model. Every audit log entry, every "who did this" question, resolved to the citizen, never to the assistant that actually made the call - which matters the moment you want to revoke one agent's access without touching the citizen's own session, or answer "which of my three assistants submitted this appeal" when there's more than one.
Era 3: Agent as Principal, or "give the agent its own identity"
Agent as Principal closes that gap without throwing away the OBO model that already works. The citizen stays the subject throughout - nothing about who the token's primary principal is needs to change - but the agent making the call gets registered as its own entity via POST /api/v2/agents, associated with the client that performs the exchange, and the target resource servers opt in via agent_subject_claims: 'auth0-v1'. Once that's enabled, exchanged tokens start carrying a client_profile/act claim that names the agent, not just the exchanging client's raw ID.

The shape of the request doesn't change. It's the same two-hop exchange SmartGov already had, which means this is additive rather than a rebuild. What changes is that decoding the token now answers two questions at once instead of one: who is this being done for (sub, still the citizen), and which agent actually did it (act, the registered agent identity). That second answer didn't exist anywhere before. It also means the FGA-side agent:<uid>-assistant identifier SmartGov already mints for authorization checks can finally be the same string an operator greps tenant logs for, rather than an authorization-side name with nothing on the token side to match it to.
For anyone weighing this migration on a live system: existing token consumers keep working exactly as before until you deliberately update them to read the new claims. Nothing downstream needs to assume sub_profile is always user either - Auth0's own docs are explicit that this assumption breaks once agents are in the picture, since sub_profile and client_profile can now each independently be an agent or a human depending on which side of a given hop you're looking at.
Why this progression matters beyond one demo
Agent as Principal: A Purpose-Built Identity for Agents and the accompanying whitepaper on first-class agent identity lay out Auth0's own reasoning for building a dedicated primitive rather than reusing what already existed for users and machines: without a native identity primitive for agents, teams end up registering every agent as either a generic M2M client or, worse, as if it were a human user. Neither model gives you agent-specific lifecycle management, agent-specific revocation, or an honest answer to "which agent did this" in an audit log.
SmartGov's three eras are a small, concrete version of that argument. Era 1 couldn't name the citizen. Era 2 could name the citizen but not the agent. Era 3 names both, in the same token, without discarding the token-exchange chain that made Era 2 worth building in the first place. If you're building an AI agent that calls APIs on a user's behalf today, on-behalf-of token exchange is still the right starting point. But before you get three services deep into hop-by-hop exchanges, know that there's now a purpose-built way to make the agent itself part of that chain, rather than a name that only exists in your authorization model.
Next Steps
This is the first post in a short series on AI agent identity - the next one will cover what actually changes in code and tenant configuration to move an existing OBO chain like SmartGov's onto Agent as Principal, including the specific register-associate-opt-in sequence and what the debug tooling looks like once act starts carrying a real agent identity. If you want the broader authorization picture SmartGov sits inside, Auth0 FGA and Model Context Protocol are the two other pieces worth reading up on in the meantime.