AI agents are increasingly being connected to enterprise applications, internal APIs, databases, and business systems.
That creates an identity problem that is easy to underestimate.
Suppose a user asks an AI agent:
"Show me my pending invoices."
The agent may need to call several services:
User
|
v
AI Agent
|
+--> Customer API
|
+--> Invoice API
|
+--> Document API
|
+--> Reporting API
The important question is:
Which identity should those downstream services see?
If every agent call uses one shared service identity, the downstream system may only know:
AgentApplication
It does not necessarily know which user initiated the operation.
That makes authorization, auditing, and tenant isolation much harder.
A better design can use On-Behalf-Of (OBO) token exchange, where the agent or application exchanges a user's access token for a token intended for a downstream API.
The resulting flow is:
User
|
| User Access Token
v
Agent Application
|
| OBO Token Exchange
v
Downstream API
|
v
User + Tenant Context
For multi-tenant systems, this identity propagation becomes especially important.
The Multi-Tenant Identity Problem
Consider a SaaS platform with three tenants:
Tenant A
├── User A1
└── User A2
Tenant B
├── User B1
└── User B2
Tenant C
├── User C1
└── User C2
Now imagine an agent service shared by all tenants:
Tenant A User
|
Tenant B User ---> Agent Platform
|
Tenant C User
The agent platform may use a single application identity to access downstream APIs.
If that identity is used for every request:
Agent
|
v
Invoice API
the Invoice API may not know:
Which user?
Which tenant?
Which permissions?
Which original request?
This can result in an authorization model that is too broad.
What OBO Solves
OBO allows a middle-tier application to obtain a downstream access token based on an incoming user token.
Conceptually:
User
|
| Token A
v
Agent API
|
| Exchange Token A
| for Token B
v
Invoice API
Token A represents the user's access to the agent-facing application.
Token B represents delegated access to the downstream API.
The downstream API can then evaluate the identity and permissions represented by the new token.
The important distinction is:
Token A
|
v
Who called the agent?
Token B
|
v
Who is the downstream operation being performed for?
The exact claims and behavior depend on the identity provider and token configuration.
Why Multi-Tenant Agents Need More Than OBO
OBO solves token propagation.
It does not automatically solve tenant isolation.
Consider:
User A
Tenant A
|
v
Agent
|
v
Invoice API
The downstream API still needs to verify that:
User A belongs to Tenant A
and that:
Tenant A
|
v
owns requested invoice
A secure architecture therefore has multiple layers:
Authentication
|
v
Token Validation
|
v
Tenant Resolution
|
v
Authorization
|
v
Resource Ownership
OBO should be treated as one component of this architecture, not the complete authorization solution.
A Reference Architecture
A multi-tenant .NET agent platform can be structured like this:
Identity Provider
|
v
User Login
|
v
Agent Front Door
|
User Access Token
|
v
+----------------------+
| Agent Service |
+----------------------+
| | |
| | |
v v v
OBO #1 OBO #2 OBO #3
| | |
v v v
CRM API Billing Documents
Each downstream API receives a token intended for that API rather than blindly receiving the original token.
Token Exchange Flow
A simplified flow is:
1. User authenticates
|
v
2. User receives access token
|
v
3. User calls agent API
|
v
4. Agent validates incoming token
|
v
5. Agent identifies tenant and user
|
v
6. Agent requests downstream token
|
v
7. Identity provider performs OBO exchange
|
v
8. Agent receives downstream access token
|
v
9. Agent calls API
|
v
10. API validates token and authorization
The downstream API should never simply trust the agent because the agent is an internal service.
It should validate the access token independently.
Token Audience Matters
One common mistake is sending the same token to every downstream service.
For example:
User Token
|
+--> API A
+--> API B
+--> API C
This is not a good general design.
Access tokens are intended for specific resources and scopes.
A better architecture is:
User Token
|
v
Agent
|
+--> Token for API A --> API A
|
+--> Token for API B --> API B
|
+--> Token for API C --> API C
The downstream API should validate that the token is intended for it.
Tenant Context Should Not Come From the URL Alone
Consider:
GET /tenants/tenant-b/invoices
A dangerous implementation might simply trust:
tenant-b
because the authenticated user supplied it.
Instead:
Authenticated User
|
v
Identity Claims
|
v
Tenant Membership
|
v
Requested Tenant
|
v
Authorization Decision
The tenant identifier in the URL is only an input.
It is not proof that the caller belongs to that tenant.
Represent Tenant Membership Explicitly
A .NET application might represent tenant membership as:
public sealed class TenantMembership
{
public required string UserId { get; init; }
public required string TenantId { get; init; }
public required string Role { get; init; }
}
Then the authorization layer can evaluate:
public bool CanAccessTenant(
string userId,
string tenantId)
{
return memberships.Any(x =>
x.UserId == userId &&
x.TenantId == tenantId);
}
In a real application, this information would typically come from a trusted data source rather than an in-memory collection.
Use a Tenant Context Abstraction
Instead of passing tenant IDs through every method manually, define a tenant context:
public interface ITenantContext
{
string TenantId { get; }
string UserId { get; }
}
An implementation can derive these values from the authenticated request after validation.
Application services can then use:
public sealed class InvoiceService
{
private readonly ITenantContext _tenantContext;
public InvoiceService(
ITenantContext tenantContext)
{
_tenantContext = tenantContext;
}
public Task<IReadOnlyList<Invoice>> GetInvoicesAsync(
CancellationToken cancellationToken)
{
var tenantId = _tenantContext.TenantId;
// Query tenant-scoped invoices.
throw new NotImplementedException();
}
}
This helps prevent individual developers from inventing their own tenant-resolution logic.
OBO in an ASP.NET Core Application
The application generally receives an authenticated request:
HTTP Request
|
v
ASP.NET Core Authentication
|
v
ClaimsPrincipal
|
v
Application Service
The incoming token should be validated before it reaches application logic.
A simplified authentication configuration might look like:
builder.Services
.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority =
builder.Configuration["Identity:Authority"];
options.Audience =
builder.Configuration["Identity:Audience"];
});
The exact configuration depends on the identity provider and deployment model.
The important architectural principle is that token validation happens at the API boundary.
Do Not Treat Claims as Automatically Trusted Business Data
A token can contain claims such as:
sub
tid
roles
scope
aud
iss
exp
These claims are useful, but the application must understand what each claim means in its identity architecture.
For example:
tid = Tenant-A
does not necessarily mean:
User can perform every operation in Tenant-A.
Tenant membership and authorization may require additional checks.
Similarly:
role = Admin
does not automatically mean the user is an administrator for every tenant.
Separate Authentication From Authorization
A useful mental model is:
Authentication
---------------
Is this token valid?
Identity
--------
Who is the caller?
Tenant Resolution
-----------------
Which tenant context applies?
Authorization
-------------
What can this caller do?
Resource Authorization
----------------------
Can this caller access this specific resource?
OBO primarily helps propagate delegated identity to another API.
The other layers still need explicit design.
Delegated Access vs Application Access
There are two fundamentally different patterns.
Application-Only Access
Agent
|
v
API
The downstream API sees an application identity.
This can be appropriate for background jobs or operations that do not act on behalf of an individual user.
Delegated User Access
User
|
v
Agent
|
v
API
The downstream operation is performed in the context of a user.
This is where OBO can be useful.
The choice should be based on the business operation.
Not every agent action needs user delegation.
Example: Customer Support Agent
Imagine a support agent that helps users investigate orders.
A user asks:
"Where is my order?"
The agent needs to access:
Order API
Shipping API
Customer API
A reasonable flow is:
User
|
v
Support Agent
|
+--> OBO --> Order API
|
+--> OBO --> Shipping API
|
+--> OBO --> Customer API
Each API can independently enforce authorization.
If the user asks:
"Show me another customer's order."
the Order API should still reject the request if the user does not have access.
The agent should not be considered an authorization boundary.
Why Agent Authorization Is Different
Traditional applications usually have relatively predictable operations.
Agents can dynamically select tools.
For example:
User Request
|
v
Agent
|
+--> Search Customers
|
+--> Read Invoice
|
+--> Create Refund
|
+--> Send Email
These tools have very different risk levels.
A read operation may require:
invoice.read
while a refund may require:
invoice.refund
The agent should not receive unrestricted access simply because one tool needs elevated permissions.
Use Tool-Specific Authorization
Define permissions at the tool level:
public sealed record AgentToolPermission
{
public required string ToolName { get; init; }
public required string RequiredScope { get; init; }
public required string RiskLevel { get; init; }
}
For example:
Tool Scope Risk
------------------------------------------------
GetInvoice invoice.read Low
CreateInvoice invoice.write Medium
IssueRefund invoice.refund High
DeleteCustomer customer.delete Critical
This creates a more controlled agent architecture.
Avoid Giving the Agent a Giant Token
A tempting implementation is:
User
|
v
Agent
|
v
Token with every scope
This increases the blast radius of an agent compromise or authorization mistake.
Instead:
Agent
|
+--> Need invoices?
| |
| v
| Invoice scope
|
+--> Need shipping?
|
v
Shipping scope
Acquire only the downstream permissions required for the operation.
This follows the principle of least privilege.
Tenant-Aware Tool Selection
The agent can also use tenant context when deciding which tools are available.
For example:
Tenant A
|
+--> Invoice Search
+--> Order Search
Tenant B
|
+--> Invoice Search
+--> Order Search
+--> Advanced Reporting
However, tool visibility should not be treated as the final authorization mechanism.
A hidden tool is not equivalent to a protected tool.
The downstream API must still enforce authorization.
Authorization at Every Trust Boundary
Consider:
User
|
v
Agent API
|
v
Agent Runtime
|
v
Tool
|
v
Downstream API
|
v
Database
There are several trust boundaries.
Validate authorization at the relevant boundaries.
Do not assume:
Agent API authenticated
=
Every downstream operation authorized
Instead:
API Gateway
|
v
Agent Authorization
|
v
Tool Authorization
|
v
Downstream API Authorization
The exact number of layers depends on the system.
Token Caching Requires Care
Token acquisition can be expensive.
A naive implementation might request a new downstream token for every tool call:
Join the conversation! Your thoughts help the community grow.