---
title: "Does Your Agent Want to See Other People? Identity-Chained Authorization with Auth0 Token Vault."
description: "The Grand prize winner of Auth0's Authorized to Act hackathon, Department of Incidents, talks about identity-chained authorization using Auth0 Token Vault to secure AI agents."
authors:
  - name: "Atib Jawad Zion"
    url: "https://auth0.com/blog/authors/atib-jawad-zion/"
date: "Jun 23, 2026"
category: "AI"
tags: ["token vault", "ai", "ai agents"]
url: "https://auth0.com/blog/identity-chained-authorization-auth0-token-vault/"
---

# Does Your Agent Want to See Other People? Identity-Chained Authorization with Auth0 Token Vault.

<style>
    
  /* Increases spacing between bullet points */   
    li {padding-bottom: .7em; }

</style>
*TL;DR: As AI agents handle increasingly complex, asynchronous workflows, relying on shared service accounts creates significant security and auditing risks. This article introduces Identity-Chained Authorization, a secure pattern utilizing Auth0 Token Vault and durable workflows to allow agents to act on behalf of specific users. This architecture provides three critical guarantees for agentic systems: zero credential exposure, just-in-time access, and verifiable accountability.*

AI has undoubtedly been a multiplying force for human productivity. But in the workplace, almost nobody works alone, and everyone executing at 2x or even 10x speeds creates a coordination bottleneck.  

To push work forward, AI output has to be passed around manually, in meetings, on Slack, and in private messages. An agent, constrained to one user and their own session, assists each person in isolation and cannot coordinate across principals. Each step still requires a human to push it to the next.

Engineering incident response is the sharpest example of this gap. When something breaks in production, the team collectively needs to triage, diagnose, revert, review, merge, and notify, in that order and with accountability at every step. Even with AI assistance, the chain of custody remains manual because no existing tool can hold the entire chain and act across it.

[Department of Incidents](https://devpost.com/software/department-of-incidents), the grand prize winner of Auth0's [Authorized to Act hackathon](https://authorizedtoact.devpost.com/), proposes a different model: one agent, multiple engineers, each action executed on behalf of the correct human's identity. The agent doesn't just assist; it drives — acting as a true member of the team.

## Agent Credential Anti-Patterns

When an agent needs to act on a team’s behalf, using a single shared credential is lacking accountability.

A **service account token** erases authorship. Every commit, PR, and merge shows up as `incidents-bot`. Code-owner review requirements may block bot-opened PRs outright in organizations that require human authors. The audit log tells you the bot acted; it tells you nothing about who authorized it or why.

A **long-lived personal access token** stored per engineer pins accountability to one person. That engineer's token acts on every incident regardless of who is on call. It can't be rotated per-user, scoped per-action, or revoked cleanly when someone leaves the team. If it leaks, the blast radius is the entire organization.

The third option, **passing credentials as LLM tool parameters**, is the worst of all:

```ts
// What you do NOT want
openRevertPr: tool({
  parameters: z.object({
    githubToken: z.string(), // ← credential in model context. Never.
    commitSha: z.string(),
  })
})
```

The credential lives in the model's context window, in every log line, and potentially in fine-tuning data. There is no recovery path if the model surfaces it.

In high-stakes scenarios where a clear chain of accountability is required, all three approaches fall short. What closes the gap is every GitHub action executing on behalf of a real engineer, [delegated fresh at the moment of execution](https://auth0.com/blog/the-many-faces-of-oauth2-token-exchange), and invisible to the model.

## What Is Identity-Chained Authorization

Identity-chained authorization is a security model where an application or AI agent executes actions on behalf of a specific user by dynamically resolving their individual identity and permissions at runtime.

Instead of relying on a broad, shared service account, the system securely stores the individual user's access credentials (such as OAuth tokens) during onboarding. Whenever the agent needs to perform an action (like an API or tool call), it retrieves and exchanges these tokens to act under the appropriate human principal's identity. This ensures that the agent's actions are strictly limited to the permissions of the consenting user and provides a clear, accurate audit trail of who authorized each specific action.

## How Does the Department of Incidents Platform Work?

Department of Incidents is an agentic, human(s)-in-the-loop incident response platform. When an Application Performance Monitoring (APM) alert fires, the agent wakes up, without any active browser session or human prompting, and begins investigating using GitHub tools:

<picture>
<img src="https://images.ctfassets.net/23aumh6u8s0i/64A0PQaKHH1KdxZZ0uw8bs/1589271a8f025ec8f572503ff5e0ec4d/IncidentResponseFlow.png" alt="Department of Incidents incident response flow diagram" style="width:100%; margin: 1em auto; border: solid black 0px; border-radius: 0px;">
</picture>

Once the agent identifies a culprit, it surfaces an inline approval request. The on-call engineer approves or pushes back. If they approve, the revert PR goes out under their GitHub identity. The agent pulls in the code owner for review. Final merge approval routes back to the on-call engineer. The merge executes under whoever said yes.

Human-in-the-loop stands as a vital part of the architecture: the approval is what confers identity.

## What is the Department of Incidents Architecture?

Department of Incidents is built around explicit permission boundaries. Tools are classified as open (read-only investigation: fetching commits, diagnosing, looking up code owners) or protected (mutating github: opening PRs, merging). Open tools execute immediately; protected tools pause the agentic loop and require human approval before any action is taken.

Identity delegation is handled by Auth0 Token Vault. During onboarding, each engineer connects their GitHub account via Auth0's connected accounts flow, which stores their GitHub tokens (repo and `read:user` scopes) in Token Vault. Their Auth0 refresh token is encrypted and stored separately. When the agent needs to act on GitHub on an engineer's behalf, it resolves their identity at runtime via Token Vault's token exchange endpoint (`POST /oauth/token`, RFC 8693). No active browser session **is** required. Every GitHub operation carries a real human author; there is no service account.

Human-in-the-loop is implemented via Vercel workflow hooks. When the agent calls a protected tool, execution suspends: an `awaiting_approval` event is broadcast to the dashboard via Pusher, and the workflow blocks on a hook. Approval comes from the dashboard. On approval, the hook resumes with the approving engineer's identity, and the action executes under their Token Vault credential. On decline, the agent receives a rejection and decides how to proceed.

The agent runs inside a durable [Workflow Development Kit (WDK)](https://vercel.com/docs/workflows) workflow so it can stay alive across multi-hour human approval waits without consuming CPU hours. It reasons, calls tools, gets results, and keeps going. Tools emit Pusher events as side effects, and its reasoning and output are broadcast to the dashboard with Server Sent Events (SSE). A custom generative UI layer transforms these into an activity stream that's backed up with a Postgres database and remains as an audit trail.

The following picture represents the components of the system and their interactions:

<picture>
<img src="https://images.ctfassets.net/23aumh6u8s0i/2Ed3Uy5pNZ1sxsQ8WTKPI9/e5164322cf41959a601e75901ee892c3/DepartmentofIncidentsArchitecture.png" alt="Department of Incidents architecture diagram" style="width:100%; margin: 1em auto; border: solid black 0px; border-radius: 0px;">
</picture>

## The LLM Never Sees a Credential

During onboarding, engineers authorize Department of Incidents to their GitHub account via [Auth0 Connected Accounts](https://auth0.com/docs/secure/call-apis-on-users-behalf/token-vault/connected-accounts-for-token-vault), which stores their GitHub tokens with `repo` and `read:user` scopes in Token Vault. Their Auth0 refresh token is encrypted with AES-256-GCM and stored separately in the application database ([`db/schema.ts:21-30`](https://github.com/zion-off/incidents/blob/main/db/schema.ts#L21-L30)).

At runtime, the model's tool schemas contain zero credential parameters. For example, when executing the [`open_revert_pr`](https://github.com/zion-off/incidents/blob/main/agent/tools/definitions/open-revert-pr.ts#L9-L24) tool, the model sees only what it needs to express intent:

```typescript
// agent/tools/definitions/open-revert-pr.ts:9-24
const inputSchema = z.object({
  commits: commitInputSchema.array().min(1).describe(
    "One or more commits to revert, oldest first..."
  ),
  title: z.string().describe("Pull request title..."),
  description: z.string().describe("Pull request body..."),
});
```

Just the commits, title, and description, without an `engineerId`, `repoOwner`, or token of any kind.

The credential logic lives in `dispatch` functions inside `toolRegistry`, which is never passed to `streamText`. The model only receives `agentTools`, the [AI SDK](https://auth0.com/blog/connect-oauth2-service-to-ai-agents-with-auth0-token-vault) tool definitions:

```typescript
// agent/tools/index.ts:122-124
export const agentTools: Record<ToolName, Tool> = Object.fromEntries(
  Object.entries(toolRegistry).map(([name, entry]) => [name, entry.definition]),
) as Record<ToolName, Tool>;
```

The `engineerId` that ultimately triggers the token exchange never comes from the model, but instead from `getOnCallEngineer()`, a DB query run by the harness ([`agent/steps/approval.ts:36-44`](https://github.com/zion-off/incidents/blob/main/agent/steps/approval.ts#L36-L44)). The harness even explicitly marks the boundary in event metadata, separating what the model provided (`args`) from what the harness injected (`harnessInjected: { repoOwner, repoName, assignedEngineerHandle }`), visible in the activity stream for every tool call.

<picture>
<img src="https://images.ctfassets.net/23aumh6u8s0i/5RNmc3VTeleIeCXGaxEQQ2/51554a1cf3043dceb140234b1f653f91/Auth0TokenExchangeFlow.png" alt="Auth0 Token Exchange flow diagram" style="width:100%; margin: 1em auto; border: solid black 0px; border-radius: 0px;">
</picture>

## Approval Is What Confers Identity

The agent nominates an engineer when it calls a protected tool, but it does not yet have their identity. The GitHub action executes under whoever actually approves. The `engineerId` is returned by the approval hook ([`agent/tools/dispatch.ts:106-110`](https://github.com/zion-off/incidents/blob/main/agent/tools/dispatch.ts#L106-L110)) only after the engineer clicks Approve. The [Auth0 token exchange happens only then]( https://auth0.com/docs/api/authentication/on-behalf-of-token-exchange/get-token). [Authority to act as that person](https://auth0.com/blog/do-not-let-your-agent-go-rogue) materializes at the moment of approval, not before.

This is also why the on-call engineer is fixed at incident registration and cannot be changed mid-incident. Changing ownership mid-investigation would split the audit trail, resulting in different identities for different phases of the same incident. Who investigated under which identity, and why did that change? Accountability is established at the moment of assignment. The next incident assigns ownership to whoever is on call then.

The agent's ability to act under real human identities and the team's ability to oversee those actions turned out to be the same mechanism.

## Dealing with Token Timing in Durable Workflows

Department of Incidents runs inside a durable workflow, a [Vercel WDK workflow](https://vercel.com/docs/workflows) that can suspend for hours while waiting for human approval, then resume exactly where it left off without consuming CPU. A GitHub access token fetched before that suspension will be expired by the time the action executes. The failure mode: the workflow resumes, the action fires, and it 401s for no obvious reason, from no obvious cause.

The fix is structural: tokens are fetched inside the `"use step"` execution boundary that runs *after* the approval hook resolves rather than before.

Tracing the call chain in `open-revert-pr.ts`:

```ts
// agent/tools/definitions/open-revert-pr.ts:73-91
export const dispatch: ToolDispatchFn = async (input, context, hooks) => {
  const approval = await runApprovalFlow(input, context, hooks);
  // ↑ workflow suspends here — hours may pass
  if (!approval.granted) return approval.dispatchResult;

  const result = await executeOpenRevertPr({
    engineerId: approval.engineerId, // ← identity from post-approval hook
    ...
  });
  ...
};
```

```ts
// agent/tools/definitions/open-revert-pr.ts:32-71
export async function executeOpenRevertPr(args: { engineerId: string; ... }) {
  "use step"; // ← token fetch happens inside this boundary
  const branchName = await createRevertBranch(args.engineerId, ...);
```

`createRevertBranch` calls `getOctokit(engineerId)` ([`services/github/client.ts`](https://github.com/zion-off/incidents/blob/main/services/github/client.ts#L19-L22)`:19-22`) which calls `getGitHubToken(engineerId)` ([`lib/auth0-ai.ts`](https://github.com/zion-off/incidents/blob/main/lib/auth0-ai.ts#L50-L64)`:50-64`), which makes [a live RFC 8693 exchange against Auth0](https://auth0.com/docs/api/authentication/custom-token-exchange/get-token):

```ts
// lib/auth0-ai.ts:26-40
const response = await fetch(`https://${AUTH0_DOMAIN}/oauth/token`, {
  method: "POST",
  body: JSON.stringify({
    grant_type:
      "urn:auth0:params:oauth:grant-type:token-exchange:federated-connection-access-token",
    subject_token_type: "urn:ietf:params:oauth:token-type:refresh_token",
    requested_token_type:
      "http://auth0.com/oauth/token-type/federated-connection-access-token",
    subject_token: refreshToken,
    connection: "github",
    client_id: AUTH0_CLIENT_ID,
    client_secret: AUTH0_CLIENT_SECRET,
  }),
});
```

The sequence, annotated:

1. The workflow begins when dispatch() is called.  
2. This initiates runApprovalFlow(), which pauses the process and waits for human intervention. This suspension can last for hours.  
3. When an engineer approves the action, the workflow resumes, now associated with that specific engineerId.  
4. Next, executeOpenRevertPr("use step") is called. This is the point where the process to get a fresh token begins.  
5. The getGitHubToken() function is invoked.  
6. It reads an encrypted refresh token from the database.  
7. An RFC 8693 POST request is sent to Auth0, exchanging the refresh token for a fresh access token.  
8. Finally, the required GitHub API calls are executed using this new token.

Here is the sequence visualized:

<picture>
<img src="https://images.ctfassets.net/23aumh6u8s0i/1dt6vh3Kv8PMe9zUAYlUSL/abe4ec5728072a2b3b37d1b0aeed36ac/DurableWorkflowTokenLifecycle.png" alt="Department of Incidents durable workflow flow diagram" style="width:100%; margin: 1em auto; border: solid black 0px; border-radius: 0px;">
</picture>

There is no caching at any layer. Every protected action fetches a fresh token at the start of its execution step. The token's lifetime starts at the moment of execution, not the moment of proposal.

The implementation began with the `@auth0/ai-vercel` [SDK's token exchange utility](https://auth0.com/docs/get-started/auth0-for-ai-agents), which exchanges the *currently active* user's token. However, for a background agent with no active browser session, that was a dead end. Reading the underlying API docs revealed that the same RFC 8693 call works with any stored Auth0 refresh token, so calling the endpoint directly, with a stored refresh token resolved by `engineerId`, made the exchange work from any execution context, under any engineer's identity, without requiring an active session.

## Expired Tokens and the Nested Interrupt

Even with fresh token fetches at execution time, there is one more failure mode to plan for: Auth0 refresh tokens can expire or be revoked between when an engineer completed onboarding and when their token is needed.

The on-call engineer and the code owner reviewer are usually different people with tokens of different ages. `approve_pr` targets the reviewer by GitHub handle, a completely different engineer from whoever opened the PR. Either can hit expiry independently on the same incident.

The system handles this gracefully with a typed `GitHubTokenExpiredError` ([`lib/auth0-ai.ts`](https://github.com/zion-off/incidents/blob/main/lib/auth0-ai.ts#L9-L17)`:9-17`) error. Every protected tool catches it and returns a structured result the model can read and act on:

```ts
// agent/tools/definitions/open-revert-pr.ts:61-70
} catch (error) {
  if (error instanceof GitHubTokenExpiredError) {
    return {
      status: "github_token_expired",
      engineerId: error.engineerId,
      message:
        "The engineer's GitHub token has expired. Call ask_engineer_to_reauthorize with this engineerId, then retry.",
    };
  }
}
```

The model reads `{ status: "github_token_expired", engineerId }` and calls `ask_engineer_to_reauthorize`. That tool opens a *second* workflow suspension, a nested interrupt inside the outer approval loop ([`agent/tools/dispatch.ts`](https://github.com/zion-off/incidents/blob/main/agent/tools/dispatch.ts#L113-L155)`:113-155`).

Here is the sequence annotated:

1. The initial workflow is suspended, waiting for an engineer's approval.  
2. After the engineer approves, the system attempts to get a GitHub token by contacting Auth0, but this fails because the token has expired, resulting in a 4xx error.  
3. This failure triggers a GitHubTokenExpiredError.  
4. The system identifies the error status as github\_token\_expired and notes the engineerId associated with the failed token.  
5. The AI model is then instructed to call ask\_engineer\_to\_reauthorize.  
6. This initiates a second, nested suspension within the original approval loop, pausing the workflow again.  
7. The targeted engineer is prompted to re-authenticate, typically through a Token Vault popup.  
8. Upon successful re-authentication, the workflow resumes, the token exchange is automatically retried, and the process continues to a successful completion.

And here is the sequence visualized:

<picture>
<img src="https://images.ctfassets.net/23aumh6u8s0i/19yYv1CQwVjkEmptB7KfG7/3121960fa55914c36a4f820f96acb813/NestedInterruptPatternForTokenExpiryrecoverypng.png" alt="Sequence diagram for the nested re-authentication interrupt pattern" style="width:100%; margin: 1em auto; border: solid black 0px; border-radius: 0px;">
</picture>
 
Two separate `defineHook` calls ([`agent/hooks.ts`](https://github.com/zion-off/incidents/blob/main/agent/hooks.ts)) handle the two suspensions: `approvalHook` for action approval, `reAuthHook` for token re-authorization. Each has its own resume path and its own timeout.

An engineer who completed onboarding months ago and hasn't been on call since is a likely candidate with stale tokens. The nested interrupt pattern, approval suspend → execution → token expiry → re-auth suspend → retry, is the architecture to plan for when designing a potentially long running workflow..

## One Identity, Resolved Twice

The system closes the identity loop by resolving the same identity in two places: once in the backend for action and once on the frontend for presentation. The backend's durable workflow, or "harness," resolves an engineerId to authorize actions, while the frontend independently resolves the viewing user's engineerId to render the correct UI.

`getViewer()` ([`lib/viewer.ts`](https://github.com/zion-off/incidents/blob/main/lib/viewer.ts#L6-L12)`:6-12`) resolves the current Auth0 session server-side to `{ engineerId, githubHandle }`. When the harness targets an engineer for approval or re-auth, it stamps that engineer's `engineerId` into the event metadata persisted to `incident_events` and broadcast via Pusher. Every engineer watching the incident receives the same event.

`EventCard` ([`components/event-card.tsx`](https://github.com/zion-off/incidents/blob/main/lib/viewer.ts#L6-L12)`:15-16`) computes a single boolean before rendering:

```ts
const isTargetEngineer =
  props.viewer?.engineerId === props.event.metadata?.engineerId;
```

If true, the targeted engineer sees live Approve or Re-authorize buttons. Everyone else sees "Waiting for @handle."

## What the Identity-Chained Authorization Pattern Gives You

The architecture described above provides three guarantees regardless of the domain:

* **The model never sees a credential.** The LLM's tool schemas are [structurally incapable of expressing credentials](https://auth0.com/blog/why-ai-agents-need-their-own-permission-model). The harness resolves identity independently. Even if the model were compromised or logged, there is nothing to extract.

* **Tokens are always fresh at execution time.** Durable workflows can suspend for arbitrary durations. Fetching tokens inside step boundaries after approval hooks resolve ensures the exchange always happens immediately before use, eliminating stale token troubles.

* **The audit trail is real.** Every GitHub action carries a human author, the person who approved it. The activity stream shows who consented to each action and in what capacity.

These properties are not specific to incident response. Any multi-stakeholder, agentic, asynchronous workflow has the same shape: an agent coordinates, humans authorize, and each authorization materializes the right identity at the right moment. Whether deployment gates, expense approvals, code review orchestration, legal document signing, the primitives are the same. The one-to-many agent model that Token Vault enables opens up new avenues for human-AI collaboration in any domain where accountability matters.

The agent's ability to act [under real human identities](https://auth0.com/blog/ai-agents-are-not-users) and the team's ability to oversee those actions turned out to be the same mechanism.

## Try It Out

The Department of Incidents [source code is available on GitHub](https://github.com/zion-off/incidents). It includes the full durable workflow harness, the Token Vault exchange implementation, the approval hooks, the nested re-auth interrupt pattern, and the generative UI layer.

To build your own agentic systems with this pattern, [sign up for a free Auth0 account](https://auth0.com/signup). To set up Auth0 Token Vault with Connected Accounts in your own project, start with the [Token Vault documentation](https://auth0.com/docs/secure/tokens/token-vault) and the [Connected Accounts setup guide](https://auth0.com/docs/secure/call-apis-on-users-behalf/token-vault/connected-accounts-for-token-vault). The RFC 8693 exchange endpoint is documented under [Refresh Token Exchange with Token Vault](https://auth0.com/docs/secure/call-apis-on-users-behalf/token-vault/refresh-token-exchange-with-token-vault).

The Department of Incidents took the grand prize at Auth0's [Authorized to Act hackathon](https://authorizedtoact.devpost.com/). The full submission and project context are on [Devpost](https://devpost.com/software/department-of-incidents).

## Join the Auth0 for Startups Program

With the Auth0 for Startups program, get one year of B2B Professional capabilities so your users and AI agents can more securely access what they need. The startup plan includes: up to 100,000 monthly active users, access to Organizations, inbound SCIM, and more. [Apply today](https://auth0.com/startups).