An AI agent that works in SAP needs permissions, just like a person. The question is whose.
There are two sets of permissions in play. The user has SAP roles: Ana may see sales orders in her sales organization. The agent should have its own limits: the order-exception agent may read orders and draft release requests, nothing more.
The safe rule is simple: the agent may do only what both the user and the agent are allowed to do. SAP's reference architecture calls this the intersection of user and agent permissions.
Three things follow:
Never give an agent a powerful shared login. It would let every user do what that login can do.
Keep duties apart. The agent that asks for a release must not also approve it.
Test the "no" cases. An agent's permissions are proven by what it is refused, not by what works in the demo.
SAP authorizations encode decades of control design: who may see which customers, who may change prices, who may release a credit block. Auditors test these controls every year. An agent can quietly bypass all of them if it runs with the wrong identity.
Example. A team builds an agent for blocked sales orders. To move fast, the agent calls S/4HANA with one technical user that has wide access. In the pilot, everything works. Then a clerk in sales organization 1010 asks about an order in 1710, and gets the answer. SAP never knew who was asking. The clerk's own role would have refused it.
The risks are concrete:
Data exposure. Users see data their roles don't allow, through the agent.
Unauthorized change. OWASP lists excessive agency as a top risk for AI applications: too many tools, too many permissions, too little human approval.
Segregation of duties (SoD) failure. If one identity can both request and approve a credit release, the control your auditors rely on is gone.
No accountability. When the log says "technical user", nobody can say which person caused a change, or whether the agent acted on its own.
Getting this right early is cheap. Retrofitting identity into a live agent means redesigning its connections, roles and tests.
As of October 2026, SAP's Architecture Center describes the target design for agent identity. Parts of it are still being released, so check the current status before you plan around it.
Agents get identities. SAP Cloud Identity Services stores agents next to people in its Identity Directory, each with a global user ID.
Two operating models. An agent either works in a user's context, or runs autonomously with its own technical identity, tokens and audit trail.
Intersection at runtime. SAP states that the effective permission is the intersection of the user's and the agent's permissions, enforced by the Agent Gateway at every hop.
No backend credentials for outside agents. External agents sign in through SAP Cloud Identity Services. The gateway, not the agent, holds the connection to the SAP system.
SAP's own roles still decide. The target application checks access with its existing authorization concept: roles in S/4HANA, business roles in SAP S/4HANA Cloud.
Not everything is live. SAP dates the policy enforcement point between agents and APIs, tools or MCP servers to the second half of 2026. Its Agentic AI page says two-way communication with third-party and self-hosted agents through the Agent Gateway is not yet supported.
For segregation of duties, SAP Cloud Identity Access Governance offers access analysis and SoD risk monitoring. Agent identities and their roles should go through the same analysis as people's. How Joule applies the user's own authorizations is covered in Grounding on SAP data with authorizations.
#Three ways an agent can hold permissions: a decision guide
Pattern
Who SAP sees
Use it when
Main risk
On behalf of the user (delegated)
The user, with the agent named alongside
A person asks the agent for help in their own work
Users with broad roles give the agent broad reach, unless the agent has its own limits too
Own identity (autonomous)
The agent's own technical identity
Scheduled or event-driven work with no person in the loop, like a nightly check of blocked orders
The agent's role becomes a standing privilege; it needs tight scope and an owner
Shared technical user
One login for every user and agent
Never for user-facing work; at most a sandbox prototype on made-up data
Every user inherits its rights; SoD and audit break
A good default for an assistant: delegated, with the agent's own limits on top. For background agents: own identity, read-mostly, and changes routed to a person.
"The agent only does what the user asks, so it needs no limits of its own." A prompt injection can make the agent ask for things the user never asked. Its own limits cap the damage.
"A technical user is fine if we filter in our code." Then your code re-implements SAP's authorization checks, and any bug leaks data. Prefer SAP checking the real user.
"Read-only is always safe." Reading data the user may not see is a breach. Read scope matters as much as write scope.
"The model knows it isn't allowed to approve." A model's judgment is not a control. OWASP calls for complete mediation: the downstream system checks every request.
"SoD is a finance topic, not an AI topic." An agent with the wrong role is a new identity with a conflict. It belongs in the same analysis.
"If the demo works, the permissions work." Demos test what is allowed. Permission tests check what is refused.
Pick one answer for each question. The explanation appears after you choose.
1An agent helps Ana, an order clerk in one sales organization. What should decide what the agent can see in SAP?
Answer: C. SAP's architecture describes the effective permission as the intersection of user and agent permissions. Ana's roles stop her seeing other sales organizations; the agent's own limits stop it doing more than its job, even if Ana could.
2A team proposes one technical user with wide access for its assistant, plus filtering in code. What is the main business risk?
Answer: B. SAP only sees the technical user, so its own checks can't tell users apart. Any bug in the filter code shows data that the user's role would have refused, and the audit log can't say who asked.
3A nightly agent checks all blocked orders with no person involved. Which identity pattern fits?
Answer: D. SAP describes autonomous agents with their own technical identity, tokens and audit trail. Its role should be tight and have an owner, because there is no user to intersect with.
4The agent can draft a credit release request. Who should be able to approve it?
Answer: C. Requesting and approving are separated so no single identity can complete a critical step alone. An agent that approves its own drafts, or a requester who approves their own, breaks segregation of duties.
5What is the strongest evidence that an agent's permissions are right?
Answer: B. Permissions are proven by what is denied. A system prompt or the model's judgment is not a control; OWASP calls for the downstream system to check every request.
6Your partner says SAP's Agent Gateway will enforce all agent policies. What should you ask?
Answer: D. SAP's architecture pages date the policy enforcement point for APIs, tools and MCP to H2 2026, and say third-party and self-hosted agents can't yet communicate through the gateway. Plan with what is available now.
Ask a question
Testing: only staff see this
Stuck on something in this layer? Ask it here. Questions are answered in the order they arrive, and the answer appears under My questions.
Sign in (free) to ask a question. You can ask anonymously.
Think of each SAP action as a door with two locks. The user's key opens doors their SAP roles allow. The agent's key opens doors its own role allows. A request goes through only if both keys turn.
flowchart LR
R[Agent asks:<br/>read order 4801] --> T{Token valid,<br/>for this gateway?}
T -- no --> X1[401: rejected]
T -- yes --> U{User's roles<br/>allow it?}
U -- no --> D[Denied + trace]
U -- yes --> A{Agent's role<br/>allows it?}
A -- no --> D
A -- yes --> S[Call SAP]
The user's key stops the agent from becoming a way around SAP's authorizations. The agent's key stops a hijacked or confused agent from using everything the user can. SAP's Architecture Center states the same rule as the intersection of user and agent permissions. OWASP's version is complete mediation: the system that owns the data checks every request, whatever the model decided.
Everything in this topic is about building, granting and testing those two keys.
In ABAP-based SAP systems, a permission is checked against an authorization object. SAP Learning describes an object as a group of up to ten fields that are checked together. New objects include ACTVT, the activity, by default, with the values 01 create, 02 change, 03 read and 06 delete. Other fields hold organizational values such as a company code or sales organization.
An authorization is a set of allowed values for an object's fields. Roles bundle authorizations, and users get roles. At runtime, ABAP code runs AUTHORITY-CHECK for an object with the values the action needs. The check passes only if one authorization for that object allows all the values. For reads, SAP Learning recommends CDS access controls as the first choice; they filter what a user gets back.
What this means for an agent:
The check happens inside SAP, against the identity SAP sees. If SAP sees a technical user, it checks the technical user's roles. The person behind the request is invisible.
Organizational fields are your scope. An order clerk's role is usually limited by organizational values. That limit is what a shared login throws away.
Activities separate read from change. An agent that needs to read should hold 03, not 01 or 02.
Object names and field values differ by application and system. Ask your security team which objects a given API checks, or find out with the trace tools below.
#SAP S/4HANA Cloud: business roles and restrictions
In SAP S/4HANA Cloud Public Edition, administrators work with business roles built from business catalogs. Each catalog has restriction types, and each field gets an access category: Write, Read or Value Help. A field can be restricted to specific values or unrestricted. SAP Learning recommends restricting, and using unrestricted only in rare cases.
For system-to-system access, S/4HANA Cloud uses communication arrangements. An arrangement links a communication scenario to a communication system (the caller) and a communication user (its technical login). SAP's tutorial for the Business Partner API creates an arrangement from scenario SAP_COM_0008; the scenario contains the API, and the arrangement gives the communication user its authorization. In other words, a technical integration gets the rights of a whole scenario. That is right for an integration, and too much for an agent acting for one clerk.
The agent needs its own permission set, decided before any code:
Decide
Example for the order-exception agent
Tools
sales_order_get, release_request_draft; no approve tool
SAP activities
Read sales orders; create release requests in the agent's own store only
Organizational scope
All sales organizations it serves, intersected with the user's
Operating model
Delegated for assistant use; a separate read-only identity for the nightly run
Owner
A named person who reviews this access every quarter
OWASP's excessive agency guidance maps directly onto this table. Excessive functionality is too many tools. Excessive permissions is a downstream identity with more rights than the tools need. Excessive autonomy is changes without approval. Its remedies are the same: minimize tools and permissions, execute in the user's context with minimum scope, require approval for high-impact actions, and enforce authorization downstream.
The user signs in to your assistant and gets a token for the assistant. The agent should not forward that token to the tools. It should exchange it for a new one that:
names the user as subject (sub),
names the agent as the actor (act),
targets only the tool gateway (aud),
carries only the tools this step needs (scope),
expires in minutes (exp).
This is the OAuth 2.0 Token Exchange pattern from RFC 8693. A client sends a subject_token (the user's) and, optionally, an actor_token (the agent's), with the grant type urn:ietf:params:oauth:grant-type:token-exchange. RFC 8693 separates two modes. In impersonation, the new token makes the actor indistinguishable from the user. In delegation, the actor keeps its own identity, recorded in the act claim:
Delegation is what you want for agents: every downstream check and log line can see both the person and the agent. RFC 8693 also defines may_act, which states who is allowed to act for a subject.
A token issued for one service must not be accepted by another. The MCP specification of 2026-07-28 makes this explicit for MCP servers: they must validate that a token was issued for them as the audience, and must not accept or pass on any other tokens. MCP clients must name the target server in a resource parameter when they request a token. When a token lacks a scope, the server answers 403 with insufficient_scope, and the client can request more through a step-up flow.
The reason is the confused deputy: a service holding a token meant for someone else can be tricked into using it. If your agent forwards the user's sign-in token to a tool server, and that server forwards it to SAP, nobody in the chain checked whether the token was meant for them. MCP authorization itself is optional in the specification; for anything that reaches SAP data, treat it as required. The protocol is covered in The Model Context Protocol (MCP).
SoD rules list pairs of actions that one identity must not hold together, such as requesting and approving a release. Two checks apply:
Design time: analyze every role, including the agent's, for conflicting pairs. SAP Cloud Identity Access Governance provides access analysis and SoD risk monitoring for this.
Run time: even when a person legitimately holds both rights, the approval step refuses an approval by the person who made the request.
An agent should never hold the approving side. The approval is a separate step, run by a person, that no agent token can reach. SAP tools for agents built the draft tool; this topic adds who may approve it.
When an agent is refused, someone has to find out why. In ABAP systems, SAP Learning describes two tools:
SU53 shows recent failed authorization checks for a user: the object, the values the program needed, and the values the user holds.
STAUTHTRACE records every authorization check while you reproduce the problem, with the object, fields, values and return code.
SAP Learning adds a warning that applies doubly to agents: don't turn a trace straight into a new role. The trace shows what the agent tried; that may include things it should never do. Your own gateway should keep the same kind of record: which principal failed, on which check, needing what. The lab's trace command does that.
You will build unit11/agent_permissions.py, a local model of how an agent's permissions combine with a user's. It has SAP-style authorization objects and roles, an agent with its own role, delegated tokens that name both the user and the agent, an approval step with a segregation-of-duties check, an SU53-style trace, and a permission test matrix you can run after every change.
In VS Code's file list, right-click unit11, choose New File, and name it agent_permissions.py.
Paste the code below and save.
"""Unit 11: Agent permissions and SAP authorizations.
A local lab that models how an agent's permissions combine with the user's: SAP-style
authorization objects and roles, an agent with its own role, a delegated token that names
both the user and the agent, a segregation-of-duties check, and an SU53-style trace of
every failed check. Built-in Python modules only. Everything is made up.
python unit11/agent_permissions.py roles roles per principal + SoD analysis
python unit11/agent_permissions.py call sales_order_get order=4711 --user ana
python unit11/agent_permissions.py call sales_order_get order=4801 --user ana --mode technical
python unit11/agent_permissions.py approve D-0001 --user lee a person approves a draft
python unit11/agent_permissions.py tokens token exchange and rejected tokens
python unit11/agent_permissions.py matrix the permission test suite
python unit11/agent_permissions.py trace failed checks, like SU53
"""
import argparse
import base64
import hashlib
import hmac
import json
import os
import sys
import time
from pathlib import Path
try:
from dotenv import load_dotenv
load_dotenv()
except ImportError: # the lab still runs without python-dotenv
pass
HERE = Path(__file__).resolve().parent
TRACE_FILE = HERE / "auth_trace.jsonl"
DRAFTS_FILE = HERE / "perm_drafts.json"
# --- 1. Authorization objects and roles (made up; real objects and values differ per system) ---
# An authorization object groups fields that are checked together. ACTVT is the activity:
# 01 create, 02 change, 03 display.
OBJECTS = {
"ZLAB_SO": "Sales order (lab)",
"ZLAB_RQ": "Release request (lab)",
"ZLAB_AP": "Release approval (lab)",
}
ROLES = {
"Z_ORDER_CLERK_1010": [("ZLAB_SO", {"SALES_ORG": ["1010"], "ACTVT": ["03"]}),
("ZLAB_RQ", {"SALES_ORG": ["1010"], "ACTVT": ["01"]})],
"Z_ORDER_CLERK_1710": [("ZLAB_SO", {"SALES_ORG": ["1710"], "ACTVT": ["03"]}),
("ZLAB_RQ", {"SALES_ORG": ["1710"], "ACTVT": ["01"]})],
"Z_AUDITOR_1010": [("ZLAB_SO", {"SALES_ORG": ["1010"], "ACTVT": ["03"]})],
"Z_CREDIT_APPROVER": [("ZLAB_SO", {"SALES_ORG": ["*"], "ACTVT": ["03"]}),
("ZLAB_AP", {"SALES_ORG": ["*"], "ACTVT": ["02"]})],
# The agent's own envelope: it may read and draft in any sales org, never approve.
"Z_AGENT_ORDER_EXC": [("ZLAB_SO", {"SALES_ORG": ["*"], "ACTVT": ["03"]}),
("ZLAB_RQ", {"SALES_ORG": ["*"], "ACTVT": ["01"]})],
# An autonomous agent with its own identity: read only, two sales orgs.
"Z_AGENT_NIGHT_READ": [("ZLAB_SO", {"SALES_ORG": ["1010", "1710"], "ACTVT": ["03"]})],
# The anti-pattern: one technical user that can do everything.
"Z_TECH_ALL": [("ZLAB_SO", {"SALES_ORG": ["*"], "ACTVT": ["*"]}),
("ZLAB_RQ", {"SALES_ORG": ["*"], "ACTVT": ["*"]}),
("ZLAB_AP", {"SALES_ORG": ["*"], "ACTVT": ["*"]})],
}
PRINCIPALS = {
# people
"ana": {"kind": "user", "name": "Ana, order clerk 1010", "roles": ["Z_ORDER_CLERK_1010"]},
"ben": {"kind": "user", "name": "Ben, order clerk 1710", "roles": ["Z_ORDER_CLERK_1710"]},
"kim": {"kind": "user", "name": "Kim, auditor 1010", "roles": ["Z_AUDITOR_1010"]},
"lee": {"kind": "user", "name": "Lee, credit approver", "roles": ["Z_CREDIT_APPROVER"]},
# agents
"order-exception-agent": {"kind": "agent", "name": "Order-exception agent",
"roles": ["Z_AGENT_ORDER_EXC"],
"tools": ["sales_order_get", "release_request_draft"]},
"night-batch-agent": {"kind": "agent", "name": "Night batch agent (autonomous)",
"roles": ["Z_AGENT_NIGHT_READ"], "tools": ["sales_order_get"]},
# technical user
"tech-user": {"kind": "technical", "name": "Shared technical user", "roles": ["Z_TECH_ALL"]},
}
# Segregation of duties: no single principal may hold both sides of a rule.
SOD_RULES = [
{"id": "SOD-01", "text": "Request a release and approve releases",
"a": ("ZLAB_RQ", "01"), "b": ("ZLAB_AP", "02")},
]
ORDERS = {
"4711": {"SalesOrganization": "1010", "SoldToParty": "10100001", "Block": "credit"},
"4712": {"SalesOrganization": "1010", "SoldToParty": "10100002", "Block": ""},
"4801": {"SalesOrganization": "1710", "SoldToParty": "17100001", "Block": "credit"},
}
def value_ok(allowed, value):
return "*" in allowed or value in allowed
def authority_check(principal_id, obj, needed):
"""Like ABAP's AUTHORITY-CHECK: pass if ONE authorization for the object matches ALL fields."""
for role in PRINCIPALS[principal_id]["roles"]:
for auth_obj, fields in ROLES[role]:
if auth_obj == obj and all(value_ok(fields.get(f, []), v) for f, v in needed.items()):
return True, None
held = [fields for role in PRINCIPALS[principal_id]["roles"]
for auth_obj, fields in ROLES[role] if auth_obj == obj]
return False, {"principal": principal_id, "object": obj, "needed": needed, "held": held}
def write_trace(entry):
entry["time"] = time.strftime("%Y-%m-%d %H:%M:%S")
with TRACE_FILE.open("a", encoding="utf-8") as f:
f.write(json.dumps(entry) + "\n")
# --- 2. Tokens: a JWT-shaped, HMAC-signed stand-in for what an identity service issues ---
SECRET = os.getenv("LAB_TOKEN_SECRET", "lab-only-secret-change-me").encode()
APP_AUDIENCE = "https://assistant.lab" # the token the user's sign-in gives the app
GATEWAY_AUDIENCE = "https://gateway.lab/tools" # the only audience the tool gateway accepts
def b64(data):
return base64.urlsafe_b64encode(data).rstrip(b"=").decode()
def unb64(text):
return base64.urlsafe_b64decode(text + "=" * (-len(text) % 4))
def mint(claims, ttl=300):
claims = dict(claims, iat=int(time.time()), exp=int(time.time()) + ttl)
head = b64(json.dumps({"alg": "HS256", "typ": "JWT"}).encode())
body = b64(json.dumps(claims).encode())
sig = b64(hmac.new(SECRET, f"{head}.{body}".encode(), hashlib.sha256).digest())
return f"{head}.{body}.{sig}"
def verify(token, audience):
"""Return the claims, or raise PermissionError with the reason."""
try:
head, body, sig = token.split(".")
except ValueError:
raise PermissionError("malformed token")
good = b64(hmac.new(SECRET, f"{head}.{body}".encode(), hashlib.sha256).digest())
if not hmac.compare_digest(sig, good):
raise PermissionError("bad signature")
claims = json.loads(unb64(body))
if claims.get("aud") != audience:
raise PermissionError(f"wrong audience {claims.get('aud')!r}, expected {audience!r}")
if claims.get("exp", 0) < time.time():
raise PermissionError("token expired")
return claims
def sign_in(user_id):
"""What the user's sign-in produces: a token for the assistant app, not for the tools."""
return mint({"sub": user_id, "aud": APP_AUDIENCE, "scope": "assistant"}, ttl=3600)
def exchange(user_token, agent_id, tools):
"""Token exchange (the RFC 8693 pattern): user token in, short-lived delegated token out.
The new token names the user (sub) and the agent (act), targets the gateway only,
and carries only tools that the agent is registered for."""
user = verify(user_token, APP_AUDIENCE)
agent = PRINCIPALS.get(agent_id)
if not agent or agent["kind"] != "agent":
raise PermissionError(f"{agent_id!r} is not a registered agent")
refused = [t for t in tools if t not in agent["tools"]]
if refused:
raise PermissionError(f"agent {agent_id} may not request: {', '.join(refused)}")
return mint({"sub": user["sub"], "act": {"sub": agent_id}, "aud": GATEWAY_AUDIENCE,
"scope": " ".join(tools)}, ttl=300)
# --- 3. The tool gateway: every call is checked for every principal in the chain ---
TOOLS = {
"sales_order_get": {"object": "ZLAB_SO", "actvt": "03", "args": ["order"]},
"release_request_draft": {"object": "ZLAB_RQ", "actvt": "01", "args": ["order", "reason"]},
}
def load_drafts():
return json.loads(DRAFTS_FILE.read_text(encoding="utf-8")) if DRAFTS_FILE.exists() else {}
def gateway(token, tool, args, mode):
"""Check the token, the tool, then the authorization of each principal in the chain."""
claims = verify(token, GATEWAY_AUDIENCE)
if tool not in claims["scope"].split():
raise PermissionError(f"tool {tool!r} not in this token's scope")
spec = TOOLS[tool]
unknown = set(args) - set(spec["args"])
if unknown or not all(a in args for a in spec["args"]):
raise ValueError(f"{tool} needs exactly: {', '.join(spec['args'])}")
if tool == "release_request_draft" and not 20 <= len(args["reason"]) <= 500:
raise ValueError("reason must be 20 to 500 characters")
order = ORDERS.get(args["order"])
if order is None:
raise LookupError(f"order {args['order']} does not exist or you may not see it")
needed = {"SALES_ORG": order["SalesOrganization"], "ACTVT": spec["actvt"]}
user, agent = claims["sub"], claims.get("act", {}).get("sub")
if mode == "technical":
chain = ["tech-user"] # SAP only ever sees the technical user
elif mode == "agent":
chain = [agent] # autonomous: the agent's own identity
else:
chain = [user, agent] # delegated: user AND agent must both pass
for principal in chain:
ok, failure = authority_check(principal, spec["object"], needed)
if not ok:
failure.update({"tool": tool, "mode": mode, "on_behalf_of": user})
write_trace(failure)
# Same answer as a missing order, so the agent can't probe other sales orgs.
raise LookupError(f"order {args['order']} does not exist or you may not see it")
if tool == "sales_order_get":
return {"SalesOrder": args["order"], **order}
drafts = load_drafts()
key = f"{user}:{args['order']}"
for d in drafts.values():
if d["key"] == key and d["status"] == "open":
return {"draft": d["id"], "duplicate": True, "changed_in_sap": False}
draft_id = f"D-{len(drafts) + 1:04d}"
drafts[draft_id] = {"id": draft_id, "key": key, "order": args["order"],
"sales_org": order["SalesOrganization"], "requested_by": user,
"drafted_by": agent, "reason": args["reason"], "status": "open"}
DRAFTS_FILE.write_text(json.dumps(drafts, indent=2), encoding="utf-8")
return {"draft": draft_id, "duplicate": False, "changed_in_sap": False}
def run_tool(user_id, agent_id, tool, args, mode):
"""The agent runtime: sign in, exchange for a delegated token, call the gateway."""
if mode == "agent": # no user: the agent signs in as itself
token = mint({"sub": f"(none: {agent_id} runs alone)", "act": {"sub": agent_id},
"aud": GATEWAY_AUDIENCE, "scope": " ".join(PRINCIPALS[agent_id]["tools"])})
else:
token = exchange(sign_in(user_id), agent_id, [tool])
return gateway(token, tool, args, mode)
def approve(draft_id, approver):
"""The human approval step. Not a tool: no agent token can reach it."""
drafts = load_drafts()
draft = drafts.get(draft_id)
if draft is None or draft["status"] != "open":
raise LookupError(f"no open draft {draft_id}")
if PRINCIPALS.get(approver, {}).get("kind") != "user":
raise PermissionError("only a person can approve")
if approver == draft["requested_by"]:
write_trace({"principal": approver, "object": "SOD-01", "needed": "approver != requester",
"held": draft["requested_by"], "tool": "approve", "mode": "human",
"on_behalf_of": approver})
raise PermissionError("segregation of duties: you requested this release")
ok, failure = authority_check(approver, "ZLAB_AP", {"SALES_ORG": draft["sales_org"], "ACTVT": "02"})
if not ok:
failure.update({"tool": "approve", "mode": "human", "on_behalf_of": approver})
write_trace(failure)
raise PermissionError("you may not approve releases for this sales organization")
draft.update(status="approved", approved_by=approver)
DRAFTS_FILE.write_text(json.dumps(drafts, indent=2), encoding="utf-8")
return {"draft": draft_id, "status": "approved", "next": "the release step calls SAP (sketch only)"}
# --- 4. Commands ---
def holds(principal_id, obj, actvt):
return [fields["SALES_ORG"] for role in PRINCIPALS[principal_id]["roles"]
for o, fields in ROLES[role] if o == obj and value_ok(fields["ACTVT"], actvt)]
def cmd_roles(_):
w = max(len(",".join(p["roles"])) for p in PRINCIPALS.values()) + 2
print(f"{'Principal':<23}{'Kind':<11}{'Roles':<{w}}Can do")
for pid, p in PRINCIPALS.items():
can = []
for role in p["roles"]:
for obj, fields in ROLES[role]:
can.append(f"{obj} {'/'.join(fields['ACTVT'])} in {','.join(fields['SALES_ORG'])}")
print(f"{pid:<23}{p['kind']:<11}{','.join(p['roles']):<{w}}{'; '.join(can)}")
print("\nSegregation of duties")
conflicts = 0
for rule in SOD_RULES:
for pid in PRINCIPALS:
a, b = holds(pid, *rule["a"]), holds(pid, *rule["b"])
if a and b:
conflicts += 1
print(f" CONFLICT {rule['id']} ({rule['text']}): {pid}")
print(f" {conflicts} conflict(s) found")
def parse_args(pairs):
args = {}
for pair in pairs:
if "=" not in pair:
sys.exit(f"Write arguments as key=value, for example order=4711 (got {pair!r})")
k, v = pair.split("=", 1)
args[k] = v
return args
def cmd_call(a):
if a.tool not in TOOLS:
sys.exit(f"Unknown tool {a.tool!r}. Tools: {', '.join(TOOLS)}")
if a.user not in PRINCIPALS or PRINCIPALS[a.user]["kind"] != "user":
sys.exit(f"Unknown user {a.user!r}. Users: ana, ben, kim, lee")
agent = a.agent or ("night-batch-agent" if a.mode == "agent" else "order-exception-agent")
who = f"{agent} alone" if a.mode == "agent" else f"{a.user} via {agent}"
print(f"Mode: {a.mode} | {who}")
try:
print("Allowed:", json.dumps(run_tool(a.user, agent, a.tool, parse_args(a.args), a.mode)))
except (PermissionError, LookupError, ValueError) as e:
print("Denied:", e)
def cmd_approve(a):
try:
print("Allowed:", json.dumps(approve(a.draft, a.user)))
except (PermissionError, LookupError) as e:
print("Denied:", e)
def cmd_tokens(_):
ana_token = sign_in("ana")
print("1. Ana signs in. Her token is for the assistant app:")
print(" ", json.loads(unb64(ana_token.split(".")[1])))
delegated = exchange(ana_token, "order-exception-agent", ["sales_order_get"])
print("2. The agent exchanges it for a delegated token (user in sub, agent in act):")
print(" ", json.loads(unb64(delegated.split(".")[1])))
tests = [
("3. Agent forwards Ana's sign-in token to the gateway (passthrough)",
lambda: gateway(ana_token, "sales_order_get", {"order": "4711"}, "delegated")),
("4. Agent asks for a tool it isn't registered for",
lambda: exchange(ana_token, "order-exception-agent", ["release_request_approve"])),
("5. Agent uses a read token to draft",
lambda: gateway(delegated, "release_request_draft", {"order": "4711", "reason": "x" * 20}, "delegated")),
("6. Someone edits the token to say sub=lee",
lambda: gateway(tamper(delegated, sub="lee"), "sales_order_get", {"order": "4711"}, "delegated")),
("7. A token that expired a minute ago",
lambda: gateway(mint({"sub": "ana", "act": {"sub": "order-exception-agent"},
"aud": GATEWAY_AUDIENCE, "scope": "sales_order_get"}, ttl=-60),
"sales_order_get", {"order": "4711"}, "delegated")),
]
for label, fn in tests:
try:
fn()
print(f"{label}: ALLOWED (this is a bug)")
except (PermissionError, LookupError, ValueError) as e:
print(f"{label}: rejected, {e}")
def tamper(token, **changes):
head, body, sig = token.split(".")
claims = dict(json.loads(unb64(body)), **changes)
return f"{head}.{b64(json.dumps(claims).encode())}.{sig}"
# Expected results in delegated mode: "allow" or "deny". The matrix also asks what the
# shared technical user would do with the same request.
REASON = {"reason": "Customer paid the overdue invoice today."}
AGENT, NIGHT = "order-exception-agent", "night-batch-agent"
MATRIX = [
("ana", AGENT, "sales_order_get", {"order": "4711"}, "allow"),
("ana", AGENT, "sales_order_get", {"order": "4801"}, "deny"),
("ben", AGENT, "sales_order_get", {"order": "4801"}, "allow"),
("ben", AGENT, "sales_order_get", {"order": "4711"}, "deny"),
("kim", AGENT, "sales_order_get", {"order": "4712"}, "allow"),
("kim", AGENT, "release_request_draft", {"order": "4711", **REASON}, "deny"),
("ana", AGENT, "release_request_draft", {"order": "4711", **REASON}, "allow"),
("ana", AGENT, "release_request_draft", {"order": "4801", **REASON}, "deny"),
("ana", NIGHT, "release_request_draft", {"order": "4711", **REASON}, "deny"),
("lee", AGENT, "release_request_draft", {"order": "4801", **REASON}, "deny"),
]
def outcome(user, agent, tool, args, mode):
try:
run_tool(user, agent, tool, args, mode)
return "allow"
except (PermissionError, LookupError, ValueError):
return "deny"
def cmd_matrix(_):
saved = DRAFTS_FILE.read_text(encoding="utf-8") if DRAFTS_FILE.exists() else None
failures = leaks = 0
print(f"{'User':<5}{'Agent':<23}{'Tool':<23}{'Order':<7}{'Expect':<8}{'Got':<7}{'Result':<8}Tech user")
for user, agent, tool, args, expected in MATRIX:
got = outcome(user, agent, tool, args, "delegated")
tech = outcome(user, agent, tool, args, "technical")
result = "PASS" if got == expected else "FAIL"
failures += result == "FAIL"
leak = tech == "allow" and expected == "deny"
leaks += leak
print(f"{user:<5}{agent:<23}{tool:<23}{args['order']:<7}{expected:<8}{got:<7}{result:<8}"
f"{tech}{' <- LEAK' if leak else ''}")
# The matrix must not leave drafts behind.
if saved is None:
DRAFTS_FILE.unlink(missing_ok=True)
else:
DRAFTS_FILE.write_text(saved, encoding="utf-8")
print(f"\n{len(MATRIX) - failures}/{len(MATRIX)} as expected in delegated mode. "
f"The shared technical user would allow {leaks} of the denied requests.")
sys.exit(1 if failures else 0)
def cmd_trace(a):
if not TRACE_FILE.exists():
print("No failed checks recorded yet. Run a call that is denied first.")
return
lines = TRACE_FILE.read_text(encoding="utf-8").splitlines()[-a.last:]
for line in lines:
t = json.loads(line)
print(f"{t['time']} {t['tool']} ({t['mode']}, for {t['on_behalf_of']})")
print(f" failed: {t['principal']} on {t['object']} needed {t['needed']}")
print(f" held: {t['held'] or 'no authorization for this object'}")
def main():
p = argparse.ArgumentParser(description="Agent permissions lab (all data made up).")
sub = p.add_subparsers(dest="command", required=True)
sub.add_parser("roles").set_defaults(fn=cmd_roles)
c = sub.add_parser("call")
c.add_argument("tool")
c.add_argument("args", nargs="*", help="key=value pairs, e.g. order=4711")
c.add_argument("--user", default="ana")
c.add_argument("--agent")
c.add_argument("--mode", choices=["delegated", "agent", "technical"], default="delegated")
c.set_defaults(fn=cmd_call)
ap = sub.add_parser("approve")
ap.add_argument("draft")
ap.add_argument("--user", required=True)
ap.set_defaults(fn=cmd_approve)
sub.add_parser("tokens").set_defaults(fn=cmd_tokens)
sub.add_parser("matrix").set_defaults(fn=cmd_matrix)
t = sub.add_parser("trace")
t.add_argument("--last", type=int, default=5)
t.set_defaults(fn=cmd_trace)
a = p.parse_args()
a.fn(a)
if __name__ == "__main__":
main()
Principal Kind Roles Can do
ana user Z_ORDER_CLERK_1010 ZLAB_SO 03 in 1010; ZLAB_RQ 01 in 1010
ben user Z_ORDER_CLERK_1710 ZLAB_SO 03 in 1710; ZLAB_RQ 01 in 1710
kim user Z_AUDITOR_1010 ZLAB_SO 03 in 1010
lee user Z_CREDIT_APPROVER ZLAB_SO 03 in *; ZLAB_AP 02 in *
order-exception-agent agent Z_AGENT_ORDER_EXC ZLAB_SO 03 in *; ZLAB_RQ 01 in *
night-batch-agent agent Z_AGENT_NIGHT_READ ZLAB_SO 03 in 1010,1710
tech-user technical Z_TECH_ALL ZLAB_SO * in *; ZLAB_RQ * in *; ZLAB_AP * in *
Segregation of duties
CONFLICT SOD-01 (Request a release and approve releases): tech-user
1 conflict(s) found
Read it like a security reviewer. The order-exception agent has a wide organizational scope (*), because it serves clerks in every sales organization, but a narrow functional scope: read orders, create requests, never approve. The user's role supplies the organizational limit at run time. The shared technical user holds everything, so the SoD analysis flags it: one login can both request and approve.
#Step 5: Compare delegated access with a shared technical user
Ask, as Ana, for an order in her own sales organization:
python unit11/agent_permissions.py call sales_order_get order=4711 --user ana
Mode: delegated | ana via order-exception-agent
Allowed: {"SalesOrder": "4711", "SalesOrganization": "1010", "SoldToParty": "10100001", "Block": "credit"}
Ask for an order in sales organization 1710:
python unit11/agent_permissions.py call sales_order_get order=4801 --user ana
Mode: delegated | ana via order-exception-agent
Denied: order 4801 does not exist or you may not see it
The agent may read 1710, but Ana may not. Both keys must turn. The message is the same one a missing order gets, so the agent can't be used to probe which order numbers exist.
Now run the same request as the shared technical user:
python unit11/agent_permissions.py call sales_order_get order=4801 --user ana --mode technical
Mode: technical | ana via order-exception-agent
Allowed: {"SalesOrder": "4801", "SalesOrganization": "1710", "SoldToParty": "17100001", "Block": "credit"}
This is the leak from the business example. SAP only saw tech-user, so Ana's role never came into play.
Mode: agent | night-batch-agent alone
Allowed: {"SalesOrder": "4801", "SalesOrganization": "1710", "SoldToParty": "17100001", "Block": "credit"}
Mode: agent | night-batch-agent alone
Denied: tool 'release_request_draft' not in this token's scope
With no user, the agent's own role is the only key. That is why it must be narrow: read-only, two sales organizations.
Now let Ana use the night agent to draft:
python unit11/agent_permissions.py call release_request_draft order=4711 "reason=Customer paid the overdue invoice today." --user ana --agent night-batch-agent
Mode: delegated | ana via night-batch-agent
Denied: agent night-batch-agent may not request: release_request_draft
Ana may create release requests, but this agent may not. The intersection works in both directions.
#Step 7: Draft, then approve with segregation of duties
Let the agent draft a release request for Ana, twice:
python unit11/agent_permissions.py call release_request_draft order=4711 "reason=Customer paid the overdue invoice today." --user ana
python unit11/agent_permissions.py call release_request_draft order=4711 "reason=Customer paid the overdue invoice today." --user ana
Mode: delegated | ana via order-exception-agent
Allowed: {"draft": "D-0001", "duplicate": false, "changed_in_sap": false}
Mode: delegated | ana via order-exception-agent
Allowed: {"draft": "D-0001", "duplicate": true, "changed_in_sap": false}
The second call returns the same draft, as in SAP tools for agents. Nothing changed in SAP.
Try to approve it as Ana, then as Kim, then as Lee:
python unit11/agent_permissions.py approve D-0001 --user ana
python unit11/agent_permissions.py approve D-0001 --user kim
python unit11/agent_permissions.py approve D-0001 --user lee
Denied: segregation of duties: you requested this release
Denied: you may not approve releases for this sales organization
Allowed: {"draft": "D-0001", "status": "approved", "next": "the release step calls SAP (sketch only)"}
Ana requested it, so she can't approve it. Kim has no approval right. Lee, a different person with the right, can. Notice there is no --agent option on approve: it is not a tool, and no agent token can reach it.
1. Ana signs in. Her token is for the assistant app:
{'sub': 'ana', 'aud': 'https://assistant.lab', 'scope': 'assistant', 'iat': 1791451578, 'exp': 1791455178}
2. The agent exchanges it for a delegated token (user in sub, agent in act):
{'sub': 'ana', 'act': {'sub': 'order-exception-agent'}, 'aud': 'https://gateway.lab/tools', 'scope': 'sales_order_get', 'iat': 1791451578, 'exp': 1791451878}
3. Agent forwards Ana's sign-in token to the gateway (passthrough): rejected, wrong audience 'https://assistant.lab', expected 'https://gateway.lab/tools'
4. Agent asks for a tool it isn't registered for: rejected, agent order-exception-agent may not request: release_request_approve
5. Agent uses a read token to draft: rejected, tool 'release_request_draft' not in this token's scope
6. Someone edits the token to say sub=lee: rejected, bad signature
7. A token that expired a minute ago: rejected, token expired
Each line is a control from the "How it works" section. Line 2 is delegation in the RFC 8693 sense: Ana stays the subject, the agent is the actor, the token lives five minutes and carries one tool. Line 3 is the passthrough the MCP specification forbids. If any line ever says ALLOWED (this is a bug), a control is broken.
User Agent Tool Order Expect Got Result Tech user
ana order-exception-agent sales_order_get 4711 allow allow PASS allow
ana order-exception-agent sales_order_get 4801 deny deny PASS allow <- LEAK
ben order-exception-agent sales_order_get 4801 allow allow PASS allow
ben order-exception-agent sales_order_get 4711 deny deny PASS allow <- LEAK
kim order-exception-agent sales_order_get 4712 allow allow PASS allow
kim order-exception-agent release_request_draft 4711 deny deny PASS allow <- LEAK
ana order-exception-agent release_request_draft 4711 allow allow PASS allow
ana order-exception-agent release_request_draft 4801 deny deny PASS allow <- LEAK
ana night-batch-agent release_request_draft 4711 deny deny PASS deny
lee order-exception-agent release_request_draft 4801 deny deny PASS allow <- LEAK
10/10 as expected in delegated mode. The shared technical user would allow 5 of the denied requests.
Half of the rows expect deny. That is the point: each one is a promise to an auditor. The last column shows what the shared technical user would do with the same request. The night agent row says deny there too, because the token exchange refuses the tool before any SAP identity is involved. The matrix restores your drafts file when it finishes, so it is safe to run any time.
You see the last three failed checks, newest last. After the matrix they look like this (your times differ):
2026-10-08 09:27:09 release_request_draft (delegated, for kim)
failed: kim on ZLAB_RQ needed {'SALES_ORG': '1010', 'ACTVT': '01'}
held: no authorization for this object
2026-10-08 09:27:09 release_request_draft (delegated, for ana)
failed: ana on ZLAB_RQ needed {'SALES_ORG': '1710', 'ACTVT': '01'}
held: [{'SALES_ORG': ['1010'], 'ACTVT': ['01']}]
2026-10-08 09:27:09 release_request_draft (delegated, for lee)
failed: lee on ZLAB_RQ needed {'SALES_ORG': '1710', 'ACTVT': '01'}
held: no authorization for this object
Like SU53, each entry says which principal failed, on which object, what was needed and what was held. It says nothing about the order's contents. If you see No failed checks recorded yet, run a denied call first; that is a valid result.
As of October 2026, here is how the two keys map onto SAP's offerings. SAP's agent identity pieces were announced and dated during 2026, so confirm the status in your landscape before you design around them.
The user's key is SAP's own authorization concept: roles with authorization objects in S/4HANA, business roles with restrictions in S/4HANA Cloud. For SAP to apply it, SAP must see the user. From your own BTP application, that means principal propagation through BTP destinations, set out in Grounding on SAP data with authorizations. Joule follows the same model for its own functions.
SAP's Architecture Center puts agent identities in SAP Cloud Identity Services, in the same Identity Directory as people, with a global user ID. Agents run either in a user's context or autonomously with their own technical identity, tokens and audit trail. That gives your agent's identity an owner, a lifecycle and a place in access reviews, like any other identity.
#The Agent Gateway: intersection as a platform service
In SAP's design, the Agent Gateway handles authentication, principal propagation and policy enforcement, and enforces the intersection of user and agent permissions at every hop. Outside agents sign in through SAP Cloud Identity Services and never receive backend credentials; the gateway connects to the SAP system. As of SAP's pages opened for this topic:
Piece
Status stated by SAP
Agents as identities in SAP Cloud Identity Services
Described in the Architecture Center, May and August 2026
Policy enforcement point between agents and APIs, tools or MCP
"will be released in H2/2026" (Agent Identity page, May 2026)
Two-way communication with third-party and self-hosted agents through the Agent Gateway
Not yet supported (Agentic AI page, August 2026)
MCP Gateway in SAP Integration Suite
Described as governed MCP exposure for SAP and non-SAP APIs; its July 2026 status is covered in SAP tools for agents
Until the gateway covers your case, your own tool gateway carries the agent's key, as in the lab, and SAP carries the user's.
If an autonomous agent needs its own access to S/4HANA Cloud, the established mechanism is a communication arrangement with a communication user, scoped by a communication scenario. Pick the narrowest scenario that contains the APIs the agent needs, and give that communication user to that agent only. A scenario grants its whole set of APIs, so check what else it contains. SAP's API policy also restricts agentic use of its APIs to endorsed routes; SAP tools for agents covers what that means for your design.
Run agent roles through the same SoD analysis as people's. SAP Cloud Identity Access Governance offers continuous access analysis, predefined access rules and SoD risk monitoring. Add your agent identities and their roles to it, and add the rule "an agent never holds an approving activity" to your own rule set.
When SAP refuses a delegated call, use SU53 for the user, or STAUTHTRACE while you reproduce the call, to see the object and values involved. Then decide whether the user or the agent should hold that right. Often the answer is neither.
Security and SAP authorizations. Prefer delegated access with principal propagation. If an agent needs its own SAP access, give it a dedicated, read-mostly identity and the narrowest scenario or role. Never share one login between users or between agents.
Change control. No agent token reaches an approving or changing activity. Changes run in a human approval step with the SoD check and the controls from SAP tools for agents: idempotency, ETag, audit.
Token hygiene. Short lifetimes, one audience, minimal scopes, no passthrough. Keep signing keys and client secrets in a secret store, as in Secrets, API authentication and OAuth.
Evaluation. The permission matrix runs in CI on every change to roles, tools or prompts. Add a row for every incident and every new tool, with both allow and deny cases.
Prompt injection. Assume the agent will sometimes ask for the wrong thing. The agent's own key limits the damage; the user's key stops it from reaching other users' data. See Prompt injection and tool poisoning.
Audit. Log the user, the agent, the tool, the decision and the failed check, never the business data. Keep a human owner for every agent identity, with periodic access reviews.
Operations. In delegated mode, the agent's reach follows the user's roles, because SAP checks the user. Autonomous identities don't follow anyone's roles, so review them on a schedule and remove them when their job ends.
Cost. Identity work is mostly design and configuration time. Budget for the security team's role design and SoD review, not just the developers.
Clean core. Use released APIs and standard roles or business roles. Don't add custom authorization bypasses, privileged CDS access or custom function modules just for the agent.
A new colleague, Mia, is an order clerk in sales organization 1010 who also approves releases there. You will add her, find the two problems this creates, fix one and record both. The findings feed the red-teaming topic at the end of Unit 11.
Open unit11/agent_permissions.py. In PRINCIPALS, directly below the line for lee, add:
Run python unit11/agent_permissions.py roles. You should see a new line, CONFLICT SOD-01 (Request a release and approve releases): mia. This is a design-time SoD conflict.
Run python unit11/agent_permissions.py matrix. The last row fails: Mia can read order 4801 in 1710. Her approver role reads every sales organization (*), and roles add up.
Fix the role. In ROLES, directly above the comment # The agent's own envelope, add a scoped approver role:
Then change Mia's roles to ["Z_ORDER_CLERK_1010", "Z_CREDIT_APPROVER_1010"] and save.
Run matrix again. It should end with 12/12 as expected in delegated mode.
Show the run-time SoD check. Delete unit11/perm_drafts.json if it exists, then run:
python unit11/agent_permissions.py call release_request_draft order=4712 "reason=Customer paid the overdue invoice today." --user mia
python unit11/agent_permissions.py approve D-0001 --user mia
The approval is denied with segregation of duties: you requested this release.
Open unit11/findings.md and add two rows: F-09, area authorizations, what happened user holds request and approve for 1010; SoD conflict at design time, severity medium, status mitigated: run-time check blocks self-approval; and F-10, area authorizations, what happened wildcard read in approver role let a 1010 clerk read 1710 orders through the agent, severity high, status fixed: approver role scoped to 1010.
Commit: git add unit11/agent_permissions.py unit11/findings.md, then git commit -m "Unit 11: scoped approver role and SoD findings".
Done whenmatrix shows 12/12 as expected in delegated mode, approve D-0001 --user mia is denied for segregation of duties, and findings.md has rows F-09 and F-10.
Pick one answer for each question. The explanation appears after you choose.
1In the lab, Ana asks the order-exception agent for order 4801 in sales organization 1710. The agent's role allows all sales organizations. Why is the request denied?
Answer: B. In delegated mode the gateway checks the user's role and then the agent's role. The agent's wide organizational scope doesn't help, because Ana's key doesn't turn for 1710.
2How does SAP's AUTHORITY-CHECK decide whether a user may perform an action?
Answer: C. An authorization object groups fields checked together. The check needs one authorization for that object whose values cover all needed fields, such as the sales organization and the activity.
3Your MCP tool server receives the user's sign-in token for the assistant app. What should it do?
Answer: D. The MCP specification of 2026-07-28 says servers must validate that tokens were issued for them and must not accept or pass on other tokens. Forwarding creates a confused deputy.
4Which token claims express delegation in the RFC 8693 sense?
Answer: A. In delegation the actor keeps its own identity, recorded in the act claim, while the user stays the subject. Impersonation would hide the agent, which loses the agent's key and the audit trail.
5A nightly agent runs with no user. What makes its own role the critical control?
Answer: C. In autonomous mode only the agent's identity is checked. SAP describes such agents as having their own technical identity, tokens and audit trail; their role must be narrow and owned.
6A business owner asks you to let the agent approve release requests under a small amount. What do you do?
Answer: B. An agent with the approving activity creates an SoD conflict, and a model's judgment is not a control. Keep the approval step outside the agent's reach; speed it up instead, for example with a simpler queue for small amounts.
7The agent is refused by SAP in a delegated call, and STAUTHTRACE shows the missing object. What is the right next step?
Answer: D. SAP Learning warns against turning trace results straight into roles. The trace shows what the agent tried, which may be something it should never do.
8Why does the lab's permission matrix include rows that expect "deny"?
Answer: C. Each deny row is a promise, such as "a 1010 clerk can't see 1710 orders". The matrix runs on every change and fails if a promise breaks, which the technical-user column shows happening.
Ask a question
Testing: only staff see this
Stuck on something in this layer? Ask it here. Questions are answered in the order they arrive, and the answer appears under My questions.
Sign in (free) to ask a question. You can ask anonymously.
Sources
Agentic AI & AI Agents (SAP Architecture Center, updated 27 August 2026)— two operation models in SAP Cloud Identity Services: agents in a user's context, and autonomous agents with their own technical identity, tokens and audit trail; the effective permission at runtime is the intersection of user and agent permissions, enforced by the Agent Gateway at every hop; Agent Gateway handles authentication, principal propagation and policy enforcement; communication with third-party and self-hosted agents through Agent Gateway not yet supported; MCP Gateway in SAP Integration Suite
AI Agent Identity & Governance (SAP Architecture Center, updated 12 May 2026)— agents stored in the Identity Directory alongside people, each with a global user ID; external agents authenticate through SAP Cloud Identity Services and receive no backend credentials, the Agent Gateway manages the backend connection; target applications authorize with existing SAP authorization concepts; policy enforcement point between agent and APIs, tools or MCP planned for H2 2026
Preventing Unauthorized Access to Data (SAP Learning)— an authorization object groups up to ten fields checked together; ACTVT defaults 01 create, 02 change, 03 read, 06 delete; AUTHORITY-CHECK in ABAP code protects writes; CDS access controls are the first choice against unauthorized reads
SAP Cloud Identity Access Governance (sap.com)— continuous access analysis; risk remediation and mitigation monitoring for segregation of duties; predefined access policies and rules; audit reporting
LLM06:2025 Excessive Agency (OWASP Gen AI Security Project)— root causes excessive functionality, permissions and autonomy; minimize extensions and permissions; execute in the user's context with minimum scope; require user approval; complete mediation in downstream systems
Authorization, MCP specification 2026-07-28 (modelcontextprotocol.io)— authorization optional; MCP servers MUST validate that tokens were issued for them as audience; MUST NOT accept or transit other tokens; clients MUST send the resource parameter (RFC 8707); 403 insufficient_scope and step-up authorization; least-privilege scope selection
RFC 8693: OAuth 2.0 Token Exchange (IETF)— grant type urn:ietf:params:oauth:grant-type:token-exchange; subject_token and actor_token; delegation vs. impersonation; act claim names the current actor; may_act claim