Skip to content

All lessons

Area 06 // AI SecurityLesson 98 / 14015 min read

Authorization and human approval for agents

Authorization for agents decides, in code and for every single tool call, whether this agent acting for this user may do this action on this resource, and human approval adds a person's explicit yes before high-impact actions such as payments, deletions or messages to outsiders.

Think of it like this

Analogy

A personal assistant has a company card. They can book travel for their manager (acting on someone's behalf), but they can't book first class unless the manager signs off, and they can never use the card for someone in another department. The card itself enforces a spending limit; the policy says which purchases need a signature.

The problem it solves

An agent often runs with a powerful service account, so it could technically read any customer's orders or issue any refund. If authorization is left to the model ("only help the current user"), one confused step or injected instruction lets one user act on another user's data or trigger costly actions. And some actions are so impactful that no automated check should be enough: a person should say yes.

How it works: authorization for agents

  1. Authenticate the user (JWT and OAuth) and pass their identity to the agent run; the model never decides who the user is.
  2. Define the agent's scopes: the most it may ever do (e.g. orders:read, refunds:create).
  3. Compute effective permissions = agent scopes ∩ user permissions (acting on behalf of the user).
  4. Check every tool call in code before it runs: is the scope allowed, does the resource belong to the user or their tenant, are the arguments within limits (amount, count, recipients)?
  5. Prefer delegated, downstream enforcement: call APIs with the user's own short-lived OAuth token, so the downstream system applies its normal permissions too. As of November 2026, MCP specifies OAuth 2.1 authorization for remote servers (MCP).
  6. Deny clearly: return a tool error the model can explain to the user ("you don't have access to that order"), and log it (audit logging).

How it works: human approval for actions

  1. Classify actions by impact: read-only (no approval), reversible low-impact (no approval, but logged), high-impact or irreversible (payments, deletions, external messages, permission changes, production deploys) → approval.
  2. Add dynamic triggers: amounts above a threshold, unusual recipients, bulk operations, first-time actions.
  3. Pause the agent at an approval gate, persist its state, and ask the right person (human in the loop, e.g. LangGraph's interrupt).
  4. Show exactly what will happen: the action, arguments, affected records and why the agent wants it, in plain language.
  5. Resume or cancel with the decision recorded (who, when, what they saw).
  6. Expire stale requests, and re-check authorization at execution time in case things changed.
  7. Watch for approval fatigue: if people approve 99% without reading, raise thresholds or improve the summaries.
Every tool call passes an authorization check; high-impact ones also wait for a person.

See it in code

Needs
Nothing: plain Python 3.

An authorization function every tool call goes through: scopes, ownership and an approval threshold.

agent_authz.py
from dataclasses import dataclass
 
@dataclass
class User:
    id: str
    permissions: set
 
AGENT_SCOPES = {"orders:read", "refunds:create", "email:send"}   # what the agent itself may ever do
APPROVAL_OVER = 100                                              # refunds above this need a human
 
def authorize(user: User, tool: str, scope: str, args: dict) -> str:
    # 1. The agent acts ON BEHALF OF the user: it gets the intersection, never more than the user has.
    if scope not in AGENT_SCOPES & user.permissions:
        return f"DENY   {tool}: {user.id} lacks {scope}"
    # 2. Resource check: the user may only touch their own orders.
    if args.get("owner") not in (None, user.id):
        return f"DENY   {tool}: order belongs to {args['owner']}"
    # 3. Risky actions pause for a human.
    if tool == "refund" and args["amount"] > APPROVAL_OVER:
        return f"ASK    {tool}: {args['amount']} > {APPROVAL_OVER}, waiting for approval"
    return f"ALLOW  {tool}"
 
alice = User("alice", {"orders:read", "refunds:create"})
for tool, scope, args in [
    ("get_order", "orders:read", {"owner": "alice"}),
    ("get_order", "orders:read", {"owner": "bob"}),
    ("refund", "refunds:create", {"owner": "alice", "amount": 30}),
    ("refund", "refunds:create", {"owner": "alice", "amount": 450}),
    ("send_email", "email:send", {}),
]:
    print(authorize(alice, tool, scope, args))
Output
ALLOW  get_order
DENY   get_order: order belongs to bob
ALLOW  refund
ASK    refund: 450 > 100, waiting for approval
DENY   send_email: alice lacks email:send

The agent may send email in general, but Alice can't, so the agent can't for her. In a real system the owner comes from the database lookup, never from the model's arguments.

A real example

Example

An IT helpdesk agent with approvals

A company's IT agent could reset passwords, add people to groups and provision software. Authorization ran on the requester's identity: employees could only act on their own account; managers on their direct reports. Adding anyone to an admin group, or provisioning licences over $500, paused for approval in the IT team's chat with a summary ("Add Priya to prod-db-admins requested by Tom, her manager, reason: on-call rotation").

In the first quarter the agent handled 4,100 requests; 6% needed approval, and the IT team rejected 31 of them, including a social-engineering attempt where an attacker who'd taken over an intern's account asked to be added to a finance group.

The easy way

Start here

Write one authorize(user, tool, args) function and call it before every tool executes. Start with three rules: the user must have the permission, the resource must be theirs, and anything that moves money, deletes data or contacts outsiders needs a confirmation click.

When you grow: the production setup

Teams use a policy engine (e.g. OPA or Cedar-style policies) shared with the rest of the platform, delegated OAuth tokens with narrow scopes per tool, re-authorization at execution time, risk-based approval rules, an approvals UI with full context and expiry, metrics on approval rates and rejections, and audits of every decision.

Trade-offs

What you gain

  • Stops agents from exceeding the user's own rights.
  • Approvals catch costly mistakes and attacks before they happen.
  • Decisions are logged and explainable.

What it costs you

  • Approvals add delay and need someone available.
  • Policy logic must be maintained alongside the product.
  • Too many approvals lead to rubber-stamping.

When not to use it

  • Don't add approvals to read-only, low-risk actions; spend human attention where impact is high.

Common mistakes

  • Letting the agent run with an all-powerful service account and no per-user checks.
  • Trusting IDs or ownership claims in the model's tool arguments.
  • Approval prompts that say "Allow tool call?" with no details.
  • Not re-checking permissions when an approved action finally executes.
  • No logging of who approved what.

Interview questions

Try answering out loud first, then open the model answer.

How do you implement authorization for an AI agent that acts for users?

Model answer

Authenticate the user and propagate their identity into the agent run; define agent scopes; compute effective permissions as the intersection with the user's permissions; check each tool call in code for scope, resource ownership and argument limits; prefer delegated user tokens so downstream APIs enforce their own permissions; return clear denials; and log all decisions.

When should an agent ask a human for approval, and how?

Model answer

For high-impact, irreversible, external or unusual actions (payments, deletions, external messages, permission changes, bulk operations, values above thresholds). Pause and persist the agent's state, show the approver exactly what will happen and why, record the decision, re-check authorization at execution, expire stale requests, and tune triggers to avoid approval fatigue.

What is the confused deputy problem in agents?

Model answer

A program with more privileges than its caller (the deputy) is tricked into using them on the caller's behalf. An agent with a powerful service account, manipulated by a user or injected content, can access data the user shouldn't. Fix it by making the agent act with the user's own permissions and checking authorization per call.

Quiz

Pick an answer to see whether it’s right and why.

  1. 01What should an agent's effective permissions be?
  2. 02Where is authorization enforced?
  3. 03Which action most clearly needs human approval?
  4. 04What makes an approval prompt useful rather than rubber-stamped?
  5. 05Alice asks the agent about Bob's order. What should happen?

0 of 5 answered

Key terms

Authorization
Deciding whether an authenticated user (or an agent acting for them) is allowed to perform a specific action on a specific resource.
On behalf of (delegation)
An agent or service acting with a user's identity and permissions, so it can never do more than that user could do themselves.
Human in the loop
Designing an AI system to pause for a person to approve, edit, reject or add information at important moments.
Approval gate
A point where an agent pauses and waits for a person to approve or reject a proposed action before it runs.
OAuth
A standard for granting an app limited, revocable access to an account or API through tokens with scopes, without sharing passwords. OAuth 2.1 is used for remote MCP servers.

Before and after

Progress is saved in this browser only.