Skip to main content

AI agents: ID-JAG delegation

~20 minutes. A copilot or chatbot acts for a person -- when Alice asks her assistant to check her tickets, the MCP server applies Alice's permissions and the audit log says "Alice, via agent X". ID-JAG (Identity Assertion JWT Authorization Grant, also called XAA) makes that the only thing the agent can do.

Full SDK code

For the complete runnable program and API details, see ID-JAG delegation (Python SDK).

How it works

Why not just give the agent a service account?

ApproachProblem
A powerful service accountEvery user gets the same over-broad access; the audit log says "the bot did it"
Handing the user's token to the agentThe agent can do everything the user can, everywhere, invisibly
ID-JAGScoped, short-lived tokens that carry both identities -- either side can revoke

The delegated token says exactly what happened:

{
"sub": "alice@example.com", // whose authority
"act": { "client_id": "agent-x" }, // who is acting
"scope": "my_mcp:read ...", // only what was requested & approved
"aud": "https://your-mcp/mcp" // only this server
}

Step 1 -- Register the agent

Sidebar -> WORKSPACE -> Agents -- every AI agent in the workspace, with the server it connects to, its status, and when it last got a token. Click + Register agent:

Agents page

Two fields:

Register AI agent

  1. Name -- a human label, e.g. research-agent
  2. Redirect URI -- where the user's browser lands after login. For the SDK's browser_login(), keep the loopback default (http://localhost:8126/callback); a web app uses its own OAuth callback URL.

Step 2 -- Copy the credentials (shown once)

Agent registered -- credentials

Put them in the agent's environment -- the secret is not shown again:

AUTHSEC_ISSUER=https://app.authsec.ai
AGENT_CLIENT_ID=<client id>
AGENT_CLIENT_SECRET=<the secret you copied>
MCP_URL=https://your-mcp-server.example.com/mcp

Step 3 -- Wire the agent

One call does the whole dance -- browser login, token exchange, delegated token. The first run raises PendingApprovalError; wrap it with poll_until_approved so the agent waits for the admin:

from authsec_sdk import (
AgentIdentity, browser_login,
PendingApprovalError, ApprovalDeniedError, poll_until_approved,
)

id_token = await browser_login(issuer=ISSUER, client_id=CLIENT_ID, resource=MCP_URL)

agent = AgentIdentity(issuer=ISSUER, client_id=CLIENT_ID,
client_secret=SECRET, idp_issuer=ISSUER)
async with agent:
try:
token = await agent.access_for(
MCP_URL,
user_session={"subject_token": id_token},
requested_scopes=["my_mcp:read"],
)
except PendingApprovalError as e:
print("Waiting for admin approval in the dashboard...")
token = await poll_until_approved(
agent, MCP_URL, e.status_url,
user_session={"subject_token": id_token},
requested_scopes=["my_mcp:read"],
)
except ApprovalDeniedError:
raise SystemExit("Admin declined access.")

Full program and details: Python SDK -- ID-JAG delegation.

Scopes must exist on the target server

Check the PRM document (/.well-known/oauth-protected-resource/mcp, scopes_supported). Requesting a scope the server does not define looks exactly like "waiting for approval forever" -- this is the #1 gotcha.

Step 4 -- First run: the double opt-in

Two one-time approvals, one from each side of the trust boundary:

The user consents. The browser opens to AuthSec login; the user signs in and sees a consent screen listing the requested scopes. They click Allow -- once.

The admin approves. The agent's first call parks as a pending request on the application's Connections tab. Approve it and pick the role the agent gets (e.g. Readonly) -- a polling agent picks up the approval automatically, no restart needed:

Connections tab -- access requests and active connections

From then on the same user + agent + server combination goes straight through: login -> cached token -> tools. No consent re-run, no pending request, no admin involvement.

Verify

  • WORKSPACE -> Agents shows the agent, its connected server, status, and last token time.
  • Application -> Connections tab shows the agent as an active connection with the user, role, and scopes.
  • A tools/list call with the delegated token returns only the tools the granted scopes allow.

Revoking

Either side can kill the delegation at any time -- without touching the user's own access:

Per agent -- Agents page -> the row's ... menu -> Revoke connection. The agent's next call fails with ConnectionRevokedError:

Agents list -- Revoke connection

Per identity on the app -- application -> Access tab -> Who has access -> the row's ... -> Revoke access. Works for any identity; affects only this application:

Who has access -- Revoke access

Troubleshooting

SymptomCauseFix
Stuck "waiting for approval", nothing on the Connections tabRequested scopes do not exist on the target serverUse scopes from the server's PRM scopes_supported
redirect_uri mismatch at loginAgent registered with a different redirect URIMatch the registration (default http://localhost:8126/callback)
Token works, then suddenly 401sConnection revoked or token expired mid-sessionRetry once; on ConnectionRevokedError, re-request access
Agent missing from the Agents pageIt has never successfully connectedNormal -- it appears after its first server connection

Error reference

ExceptionMeaningWhat to do
PendingApprovalErrorFirst contact -- admin approval pendingpoll_until_approved(...)
ApprovalDeniedErrorAdmin declined the requestInform the user; do not retry
ConnectionRevokedErrorPreviously approved connection revokedRe-request access or inform the user
TrustedIssuerMissingErrorIdP not trusted by AuthSecConfigure the identity provider in the dashboard
CredentialInvalidErrorAgent client_id/secret wrongRe-copy from registration

Next

All callers are connected. Monitor and manage them:

-> Watching it all -- Connections tab, M2M Logs, and the Access tab's Who has access list.