AI agents are increasingly being used to perform actions across multiple business systems.
An agent may receive a request from a user, call an internal API, retrieve customer information, invoke another service, and continue the workflow without direct interaction from the user.
In a single-tenant application, this can already be challenging.
In a multi-tenant system, the security model becomes significantly more important.
Consider this flow:
User
|
v
AI Agent
|
+----> Customer API
|
+----> Billing API
|
+----> Document API
|
+----> Reporting API
The critical question is:
Which identity should each downstream service trust?
If every request uses the same application identity, downstream services may lose the ability to distinguish users and tenants.
A stronger architecture preserves the original user's security context while allowing the agent to act on the user's behalf.
This is where On-Behalf-Of (OBO) token exchange becomes useful.
The Multi-Tenant Agent Identity Problem
Imagine an application serving three organizations:
Tenant A
--------
User A1
User A2
Tenant B
--------
User B1
User B2
Tenant C
--------
User C1
User C2
A user from Tenant A asks an agent:
"Show me the open invoices for my organization."
The agent may need to call:
Invoice API
A naive implementation might use:
User
|
v
Agent
|
| Application Token
v
Invoice API
The Invoice API sees:
Agent Application
It does not necessarily know:
Which user?
Which tenant?
Which permissions?
The agent has effectively become a shared identity.
That creates a confused-deputy risk.
What Is On-Behalf-Of Token Exchange?
OBO allows a service to exchange a user's access token for another token intended for a downstream resource.
Conceptually:
User
|
| User Token
v
Agent Application
|
| OBO Exchange
v
Downstream API Token
|
v
Invoice API
The downstream token represents delegated access rather than simply being a generic application credential.
The downstream API can then validate the token and apply its own authorization rules.
The Security Context
A useful model is:
User Identity
|
v
Tenant Context
|
v
Permissions
|
v
Agent
|
v
Delegated Token
|
v
Downstream API
The important principle is:
The agent should not be allowed to invent the user's identity or tenant.
The security context should originate from trusted authentication infrastructure.
Application Token vs Delegated Token
Consider two approaches.
Application Identity
Agent
|
| Application Token
v
API
The API sees:
agent-service
The application may need to perform all user-level authorization itself.
Delegated Identity
User
|
v
Agent
|
| Delegated Token
v
API
The API can evaluate the delegated identity and permissions.
This provides a stronger identity chain when downstream services need user-aware authorization.
Multi-Tenant Identity Flow
A typical architecture can look like:
Identity Provider
|
v
User
|
v
Agent Application
|
OBO Exchange
|
+---------------+---------------+
| | |
v v v
Customer API Billing API Document API
| | |
+---------------+---------------+
|
Tenant Policies
Each downstream API should independently validate the received token.
Never Trust a Tenant ID From the Prompt
Consider this request:
"Show me invoices for tenant-b."
The agent should not simply generate:
{
"tenantId": "tenant-b"
}
and pass it to the API.
The authenticated user's tenant context should come from trusted identity information.
For example:
Authenticated User
|
v
Tenant = tenant-a
|
v
Agent Request
|
v
Invoice API
|
X
tenant-b
The model does not get to redefine the security boundary.
Tenant Context Should Be Derived, Not Invented
A dangerous pattern is:
public async Task<IReadOnlyList<Invoice>> GetInvoices(
string tenantId)
{
return await db.Invoices
.Where(x => x.TenantId == tenantId)
.ToListAsync();
}
If tenantId originates from model-generated input, the agent can potentially request another tenant.
A safer approach derives tenant context from authenticated security information:
public async Task<IReadOnlyList<Invoice>> GetInvoices(
UserContext userContext)
{
var tenantId = userContext.TenantId;
return await db.Invoices
.Where(x => x.TenantId == tenantId)
.ToListAsync();
}
The user request can contain filters.
It should not determine the authorization boundary.
OBO Token Exchange Flow
A simplified flow looks like:
1. User authenticates
|
v
2. Agent receives user token
|
v
3. Agent requests delegated token
|
v
4. Identity system validates delegation
|
v
5. New token issued for API
|
v
6. Agent calls downstream API
|
v
7. API validates token
|
v
8. API applies authorization
The agent should not modify the original token.
It requests another token for the specific downstream resource.
Why Audience Matters
Tokens are generally intended for particular resources.
A token issued for:
Agent API
should not automatically be accepted by:
Billing API
The downstream service should validate the token's intended audience.
Conceptually:
Token
|
+--> Issuer
+--> Audience
+--> Subject
+--> Tenant
+--> Permissions
+--> Expiration
This prevents a token intended for one service from being blindly accepted by another.
Token Validation in ASP.NET Core
A downstream ASP.NET Core API can configure JWT bearer authentication:
builder.Services
.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = configuration["Identity:Authority"];
options.TokenValidationParameters =
new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true
};
});
The exact authority, audience, and validation configuration depends on the identity architecture.
The important principle is that the API must validate the token independently.
Authorization After Authentication
Authentication establishes:
Who is calling?
Authorization establishes:
What can they do?
For example:
[Authorize(Policy = "Invoices.Read")]
[HttpGet("invoices")]
public async Task<IActionResult> GetInvoices()
{
// Authorization already evaluated.
}
A downstream API should not assume that a valid token automatically means the caller can perform every operation.
Tenant-Aware Authorization
Suppose a token contains:
tenant_id = tenant-a
and:
permission = invoices.read
The API can enforce:
Tenant = tenant-a
Permission = invoices.read
before accessing data.
Conceptually:
Request
|
v
Token Validation
|
v
Tenant Validation
|
v
Permission Validation
|
v
Data Access
Tenant Isolation in the Data Layer
Application-level authorization is important, but the data query should also respect tenant scope.
For example:
var tenantId = userContext.TenantId;
var invoices = await db.Invoices
.Where(x => x.TenantId == tenantId)
.ToListAsync(cancellationToken);
This creates a direct relationship between:
Authenticated Context
|
v
Tenant Scope
|
v
Database Query
Do not depend on the model to supply the tenant identifier.
OBO Does Not Automatically Solve Tenant Isolation
This is an important distinction.
OBO can preserve delegated identity.
It does not automatically guarantee:
Tenant A -> Tenant A data only
The downstream API still needs correct authorization and data isolation.
A secure architecture therefore has:
OBO
+
Token Validation
+
Permission Checks
+
Tenant Filtering
=
Delegated Multi-Tenant Access
Delegated Permissions vs Application Permissions
A downstream API may support two different access models.
Delegated Access
User
|
v
Agent
|
v
API
The operation is performed on behalf of a user.
Application Access
Agent
|
v
API
The application acts independently.
These models should not be mixed accidentally.
For user-driven agent workflows, delegated access is often more appropriate when the downstream API needs to enforce the user's permissions.
Scope Design
Avoid creating one broad permission:
api.access
for every operation.
Prefer focused scopes or permissions such as:
invoices.read
invoices.write
customers.read
documents.read
reports.create
The exact naming scheme should match the organization's authorization model.
Fine-grained permissions reduce the impact of a compromised or misconfigured agent.
Least Privilege for Agents
An agent should receive only the permissions required for the current workflow.
For example:
Customer Support Agent
----------------------
customers.read
tickets.read
tickets.write
It should not automatically receive:
billing.admin
users.delete
security.manage
even if those capabilities exist somewhere in the organization.
Tool Permissions and API Permissions Are Different
An agent may have access to a tool:
invoice.search
but that does not mean every invoice operation is allowed.
The tool can call:
GET /invoices
with delegated access.
But:
DELETE /invoices/{id}
should require a different authorization policy.
Think of permissions at multiple layers:
Agent Capability
|
v
Tool Permission
|
v
API Permission
|
v
Tenant Authorization
|
v
Data Authorization
Avoid Shared High-Privilege Tokens
A dangerous architecture is:
All Users
|
v
Agent
|
v
Admin Token
|
v
All APIs
If the agent is manipulated, the attacker effectively gains the privileges of the shared administrator identity.
A delegated model is safer for user-driven operations:
User
|
v
Agent
|
v
Delegated Token
|
v
API
The API can enforce the user's permitted operations.
Token Lifetime
Delegated tokens should have appropriate lifetimes.
A long-running agent workflow creates an interesting problem:
Workflow Starts
|
v
Token Issued
|
v
Long Processing
|
v
Token Expires
|
v
Next API Call
The workflow must have a secure way to obtain a valid token when needed.
Do not simply store a user access token indefinitely.
Never Store Access Tokens in Workflow State
Avoid:
{
"workflowId": "wf-100",
"accessToken": "eyJ..."
}
Durable workflow state may be persisted for long periods.
Instead, store only the information required to reconstruct the authorized operation.
For example:
{
"workflowId": "wf-100",
"userId": "user-123",
"tenantId": "tenant-a",
"operation": "invoice-review"
}
A secure token acquisition mechanism can obtain the appropriate credential when the next operation is executed.
Background Workflows Need Special Attention
Consider:
HTTP Request
|
v
Agent
|
v
Start Background Workflow
|
v
HTTP Request Ends
The user's original request is now finished.
The background workflow may continue for several minutes or hours.
The application should not assume that the original access token remains valid for the entire workflow.
Instead, define how authorization is evaluated for long-running operations.
Reauthorization for Long-Running Workflows
For sensitive actions:
Workflow Starts
|
v
Authorization Check
|
v
Wait
|
v
Authorization Check
|
v
Sensitive Operation
This can prevent an operation from continuing with permissions that have since been revoked.
The exact reauthorization strategy depends on the business requirement.
User Revocation
Suppose:
10:00
User has invoices.write
10:05
Workflow starts
10:10
Permission revoked
10:20
Workflow attempts invoice update
The system should define whether the update is:
Allowed
or:
Denied
For sensitive systems, current authorization should generally be evaluated before executing the protected operation.
Tenant Switching Attacks
Agents may receive prompts such as:
"Switch to Tenant B and show me its invoices."
The system should not interpret this as a valid identity transition.
A tenant switch is an authorization event.
It should require an explicit mechanism rather than natural-language instructions.
For example:
User
|
v
Tenant Selection
|
v
Authorization
|
v
New Security Context
The model should never create the new security context itself.
Cross-Tenant Testing
Create a test environment with:
Tenant A
--------
Invoice A1
Invoice A2
Tenant B
--------
Invoice B1
Invoice B2
Then authenticate as Tenant A and test:
"Show Invoice A1."
Join the conversation! Your thoughts help the community grow.