Guide · AI and GenAI
AI Agent Authorization: Authentication vs Authorization Guide
28 min readUpdated By Saket Bhatnagar
Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.
In short
- Authentication vs authorization: AI agents need both checks. Authentication proves who is asking. Authorization decides what may happen next, for that person, on that exact record.
- When an agent acts on behalf of a user, it should carry that user's limited permission. It should never hold a master key: one shared login that reads every customer's data.
- OAuth 2.0, an open standard for delegated access, lets a user approve a short list of named permissions, called scopes. The app gets an expiring token, never her password.
- Check permissions inside each tool, in ordinary code, on every call. A rule kept only in the prompt (the text the model reads) can be talked around. That gap creates a confused deputy: a helper tricked into using its own power for someone else.
- Beginners can stop after the confused deputy section. Engineers then get tested tool code, token refresh, and how MCP, the open standard for connecting AI apps to tools, checks tokens in its 2026-07-28 version.
What goes wrong when an AI agent can log in but cannot check permissions?
It shows one customer's data to another customer, even though every login worked. That is the classic mix-up of authentication vs authorization. AI agents make it easier to hit, because the model asks for data on the user's behalf.
Picture a common setup. An invoicing app serves about 2,400 small shops, and each shop sees only its own bills. The setup is made up, but the problem is real. Every evening, the owner of a stationery shop emails reminders to customers who have not paid.
Before any AI, this was simple. The owner logs in with her password and a one-time code sent to her phone. After that, every database query filters by her shop, so she never sees the hardware shop's bills next door.
Then the team adds an AI assistant. The owner types a request like "remind everyone older than 30 days", and the agent finds those invoices and emails them. The agent has two tools: find_invoice and send_invoice. A tool is an action your program lets the model ask for; your code does the actual work.
The first version took a shortcut, and it was the wrong one. Both tools used one database key that could read every shop's invoices. In testing, someone logged in as the hardware shop asked for invoice INV-2041.
That invoice belongs to the stationery shop and is for Rs 18,500. The agent fetched it and showed it. Nothing was hacked: the login worked, and the app knew exactly who the tester was.
The missing piece was a second check: may this person see this invoice? That gap, between knowing who someone is and knowing what they may do, is what this guide is about.
Which words do you need before talking about agent security?
You need six words to follow the rest of this guide. Each one gets a plain meaning here and an example from the invoicing app.
Authentication. Authentication means proving who someone is. Example: the owner types her password and the code sent to her phone, so the app knows it is her. Engineers often shorten it to authN.
Authorization. Authorization means deciding what a person, or a program, is allowed to do. Example: the owner may send invoices from her own shop, but not from any other shop. The short form is authZ.
Permission. A permission is one specific thing someone may do, like "read invoices" or "send invoices". Authorization is the act of checking permissions before an action runs.
Token. A token is a long, random-looking string that a program shows instead of a password, like a pass. Example: after the owner logs in, her app holds a token that says "this is the stationery shop" until it expires.
Scope. A scope is a label on a token that names what the token may be used for. Example: a token with the scope invoices:read can list invoices but cannot send them.
Delegation. Delegation means one party lets another act for it, within limits. Example: the owner lets the assistant send her payment reminders, but not delete her customers.
A train journey shows the difference between the first two words. The ticket checker matches your ID card to your name on the reservation chart: that is authentication. Then he checks that your ticket is for this train, coach and berth: that is authorization.
Where the picture breaks: a ticket checker looks once per journey. An agent's tools must check on every call, because each call can touch a different record.
Authentication vs authorization: AI agents need two checks
Authentication tells your system who is asking, and authorization decides what that asker may do right now. With an agent, both questions get harder, because there are two askers: the person, and the agent working for her.
In the old app, the owner clicked a button and the server checked both things at once. Now she types a sentence, the model picks a tool, and the tool touches data. The model sits in the middle with no identity of its own.
So your system must now answer four questions. Who is the user behind this chat? Which app is making this call? May this user do this to this exact invoice? And did the user allow this app to do it for her?
The diagram follows one request from the owner. Look for the two checks: one at the front door, and one right next to the data.
Two checks on one assistant request
Drag to exploreLogin proves who the owner is once; the check inside the tool decides, on every call, what she may touch.
Diagram connections:
- Shop owner types a request to Login check (authN)
- Login check (authN) to The assistant's model (Verified login)
- The assistant's model to send_invoice tool (Tool call)
- send_invoice tool to Permission check (authZ)
- Permission check (authZ) to Invoice database (Yes)
- Permission check (authZ) to Polite refusal (No)
- Shop owner types a request: Send INV-2041 to my customer
- Login check (authN): Password and phone code, done once
- The assistant's model: Picks send_invoice with INV-2041
- send_invoice tool: Takes the user from the verified login, not from the model
- Permission check (authZ): Right scope? Invoice owned by her shop?
- Invoice database: Invoices of all 2,400 shops
- Polite refusal: Returned to the model as plain text
The first version had the login check and skipped the second one. The table puts the two side by side. One naming trap: HTTP status 401 is officially called "Unauthorized", but it means the request lacks valid login credentials.
The last row uses the OWASP Top 10, a ranked list of web application risks from OWASP, a non-profit web security project. As of October 2026, its 2025 edition is the latest one.
| Authentication | Authorization | |
|---|---|---|
| Question it answers | Who is asking? | What may they do, and to which data? |
| Invoicing example | The owner logs in with password and phone code | The stationery shop may send INV-2041; the hardware shop may not |
| How often it runs | At login, then a token stands in for her | On every tool call, for every item touched |
| What it relies on | Password, one-time code or a sign-in service | Scopes on the token plus rules in your code |
| Usual HTTP status when it fails | 401: no valid credentials | 403: valid credentials, but no permission for this |
| Rank in OWASP Top 10:2025 | A07, authentication failures | A01, broken access control |
What does acting on behalf of a user mean?
Acting on behalf of a user means the agent works with that user's permissions, not with bigger ones of its own. When the assistant sends a reminder for the owner, it should do only what she could do herself, and often less.
For each request, three things must all say yes. The user may do it. The user allowed this app to do it. The tool is built to do it. If any one says no, the answer is no.
Back on the train, if the owner books a ticket for her son, he travels on her booking, in the berth she paid for. Being on the train does not let him pick any empty berth.
How does OAuth 2.0 give an AI agent delegated access?
OAuth 2.0 is an open standard that lets a user give an app limited access to her account on another service, without sharing her password. She sees a consent screen, agrees to a short list of scopes, and the app receives a token for only those scopes.
Here is why the invoicing app needs it. The owner wants reminders to come from her own shop email address, which her customers already know. That account lives with an email provider, a separate company, so the assistant needs permission to send from it.
The old, unsafe way would be to ask for her email password and store it. Then the app could read her whole inbox and change her settings. If it leaked, everything leaked, and she could not cut off just this app without changing her password.
OAuth 2.0 was written to replace exactly this pattern. Its specification is RFC 6749, from October 2012. RFCs are the numbered documents that publish internet standards and best practices. RFC 6749 lists these same problems with apps storing a user's password.
OAuth gives names to four roles. The owner is the resource owner: the person whose data it is. The email service holding her account is the resource server. The provider's login and consent page belongs to the authorization server, which hands out tokens. The invoicing app is the client: the app asking for access.
Follow the numbered arrows below. Notice that the owner types her email password only on the email provider's own page, never into the invoicing app.
The owner connects her shop email
Drag to exploreShe logs in on the provider's page; the invoicing app only receives a limited token, and it never passes through her browser.
Diagram connections:
- Invoicing app (client) to Provider login and consent (1. Sends her browser here)
- Owner (resource owner) to Provider login and consent (2. Logs in, clicks Allow)
- Provider login and consent to App server swaps the code (3. Hands back a one-time code)
- App server swaps the code to Encrypted token store (4. Saves the tokens)
- Encrypted token store to send_invoice tool (5. Fresh access token)
- send_invoice tool to Email service (6. Sends the reminder)
- Owner (resource owner): Owns the shop email account
- Invoicing app (client): Asks for the send-mail scope only
- Provider login and consent: The authorization server: issues tokens
- App server swaps the code: Private request trades the code for tokens
- Encrypted token store: Access and refresh tokens, on the server
- send_invoice tool: Attaches the access token to each send
- Email service: Resource server: checks token and scope, then sends
Two details need a word. First, the team registers the app with the email provider once and gets a client ID, a public name for the app that is not a secret. Second, at arrow 3 her browser brings back only a one-time code.
The app's server then trades that code for tokens in a separate, private request. In that request, the server also proves it is the real app, for example with a client secret: the app's own password, kept only on the server. So the tokens never pass through her browser.
Usually two kinds of token come back. An access token is the pass the tool shows on each call, and it is short-lived: say 15 minutes in this made-up setup. A refresh token lasts longer, say 30 days here, and is used only to get a new access token.
That is why the owner does not have to click Allow every 15 minutes. Issuing a refresh token is optional, so some providers will not give one.
A small extra check called PKCE (proof key for code exchange) protects the code swap. The app makes a secret before sending the owner to the provider, then proves it still has that secret when it swaps the code. Someone who steals only the code cannot swap it for tokens.
RFC 9700, the OAuth security best practice document from January 2025, recommends PKCE for server-side apps like this one. It requires PKCE for apps that cannot keep a secret, such as phone apps.
Should an AI agent use a service account or the user's token?
Use the user's token whenever the agent acts for a specific person, and use a service account only for work that belongs to your system itself. Most real agents need both, kept clearly apart.
A service account is an identity for a program, not a person. It proves itself with its own secret, not a human password. In OAuth, an app asking for a token for itself uses the client credentials grant.
Example: the invoicing app's nightly backup job runs as a service account called billing-backup. A user token, also called a delegated token, carries one person's identity and the scopes she agreed to.
The danger of a service account is its size. The first version ran every chat through one service account that could read all 2,400 shops, so the agent could too. Whatever the model asked for, the database said yes.
Here is how the team splits the work now:
- The owner chatting with the assistant: her user token. Every tool call runs as the stationery shop.
- Sending email from her shop address: her delegated email token, with the send-mail scope only.
- The nightly reminder job she switched on: a service account, but it handles one shop at a time and passes that shop's ID into every query.
- Backups and reports for the team itself: a service account that the chat can never reach.
What is the confused deputy problem in AI agents?
A confused deputy is a program that has more power than the person using it, and gets tricked into using that power for them. The program is not evil. It is confused about whose authority it is using.
The idea is decades old: Norm Hardy described it in a 1988 paper called "The Confused Deputy". It fits agents closely, and the assistant is the deputy.
Pause and predict: in the first version, whose permission did the database check when the hardware shop asked for INV-2041?
The answer is nobody's. The tool used its own master key, not the tester's rights, and the database saw a valid key. The diagram shows where the question "whose invoice is this?" went missing.
The confused deputy in the first version
Drag to exploreThe tester had no right to INV-2041; the agent's master key did, and no box between them asked who owns it.
Diagram connections:
- Tester from hardware shop to The assistant's model (Show me INV-2041)
- The assistant's model to find_invoice tool
- find_invoice tool to Missing ownership check
- Missing ownership check to Invoice database (Skipped)
- Invoice database to Another shop's invoice
- Tester from hardware shop: Logged in correctly, as the hardware shop
- The assistant's model: Wants to help, so it picks find_invoice
- find_invoice tool: Uses one master key for every shop
- Missing ownership check: Nobody asks whose invoice this is
- Invoice database: Sees a valid master key and says yes
- Another shop's invoice: INV-2041, Rs 18,500, shown to the wrong shop
Agents make this more likely. The model tries to help anyone who asks politely, and a request can even hide inside text the agent reads. That trick is called prompt injection, and it has its own guide. If a tool has more power than the user, someone will find a sentence that makes the agent use it.
The same trap appears one level up. If the invoicing app forwarded a token it received to another service without checking it, that service might trust it blindly. The MCP security guide calls this token passthrough, forbids it, and links it to the confused deputy problem.
You fix a confused deputy with a few plain habits:
- Run every tool call with the requesting user's identity, never with a shared master key.
- Check ownership of the exact item asked for: INV-2041 belongs to the stationery shop, so only that shop may see it.
- Accept only tokens that were issued for your own service, and get a separate token for any service you call next.
- Record every action with both identities: who asked, and which app did it.
How do you check AI agent authorization in tools instead of prompts?
You check it in the code of each tool, using the identity from the verified login, every time the tool runs. The prompt can describe the rules to the model, but it cannot enforce them.
The team's first fix was one line in the system prompt, the standing instructions the model reads before each chat. It said: "Only show invoices from the user's own shop." In ordinary chats, the model followed it. But a prompt is text the model reads and weighs, not a lock.
One clever sentence, or one plain mistake, and the model asks for the wrong invoice anyway. Keep the rule in the prompt if you like, so the model explains refusals well, but never only there.
The real fix moves the rule to a place the model cannot reach. The model only chooses which tool to call and which invoice number to pass. Who the user is comes from the session: a record your server built when the owner logged in.
Here is the send_invoice tool after the fix. The helpers find_invoice, get_email_token, send_email and audit_log stand in for your own code. The session object holds the verified shop, the user and the scopes on her token.
def send_invoice(session, invoice_id):
shop_id = session.shop_id # from the verified login
if "invoices:send" not in session.scopes:
return "Not allowed: this connection cannot send invoices."
invoice = find_invoice(invoice_id) # invoice_id came from the model
if invoice is None or invoice.shop_id != shop_id:
return "Not allowed: no such invoice for your shop."
token = get_email_token(shop_id) # the model never sees this
send_email(token, invoice.customer_email, invoice.pdf)
audit_log(user=session.user_id, app="invoice-assistant",
action="send_invoice", item=invoice_id)
return f"Sent {invoice_id} to {invoice.customer_email}."What just happened? The function takes two inputs. The first, session, comes from your server, and the model never touches it. The second, invoice_id, comes from the model, so treat it like any outside input: it may be a mistake, or a tricky request may have steered it.
The first if checks the scope. If the owner connected the assistant only for reading, the tool refuses to send. The second if is the ownership check that was missing before.
A missing invoice and another shop's invoice get the same reply, so a tester cannot learn which invoice numbers exist. Only after both checks pass does the tool fetch the email token and send.
Then audit_log saves a record of who did what, with both identities: the user and the app. A refusal comes back as plain text, so the model can explain it politely instead of the program crashing.
How can you check that the ownership rule works?
Write one test per refusal, with fake helpers in place of the database and email service. The block below sets up one invoice, three sessions and the four calls that matter.
Paste send_invoice above it and run the file with Python 3. All the values are fake, including the token string.
from types import SimpleNamespace as NS
INVOICES = {"INV-2041": NS(shop_id="stationery-shop",
customer_email="[email protected]", pdf=b"%PDF")}
SENT, LOG = [], []
def find_invoice(invoice_id):
return INVOICES.get(invoice_id)
def get_email_token(shop_id):
return "fake-token-not-real"
def send_email(token, to, pdf):
SENT.append(to)
def audit_log(**record):
LOG.append(record)
both = {"invoices:read", "invoices:send"}
owner = NS(shop_id="stationery-shop", user_id="u-17", scopes=both)
other = NS(shop_id="hardware-shop", user_id="u-88", scopes=both)
read_only = NS(shop_id="stationery-shop", user_id="u-17", scopes={"invoices:read"})
print(send_invoice(other, "INV-2041"))
print(send_invoice(read_only, "INV-2041"))
print(send_invoice(owner, "INV-9999"))
print(send_invoice(owner, "INV-2041"))
print(len(SENT), "email sent, logged as", LOG[0]["user"], LOG[0]["app"])You should see exactly these five lines. Only the owner's own invoice is sent, and only one email goes out:
Not allowed: no such invoice for your shop.
Not allowed: this connection cannot send invoices.
Not allowed: no such invoice for your shop.
Sent INV-2041 to [email protected].
1 email sent, logged as u-17 invoice-assistantIf the second line says "Sent", your scope check is missing. If the first line says "Sent", your ownership check is missing, and you have rebuilt the confused deputy.
How should an AI agent store tokens and handle expiry?
Store tokens on your server, encrypted and out of the model's view, and expect every access token to expire soon. Refresh it in code just before you need it, and ask the user to connect again when refreshing fails.
While it is valid, a token works like a key: whoever holds it can use it, without any password. Whoever holds the owner's email token can send mail as her shop.
Here is the get_email_token helper that send_invoice called above. The vault object stands for an encrypted store, keyed by shop and service. The refresh_tokens helper also stands for your own code: it asks the email provider for a new pair of tokens.
from datetime import datetime, timedelta, timezone
def get_email_token(shop_id):
saved = vault.load(shop_id, "email") # encrypted at rest
if saved is None:
raise NeedsConsent("email") # ask the owner to connect
soon = datetime.now(timezone.utc) + timedelta(minutes=2)
if max(saved.expires_at, soon) == soon: # expired, or about to
try:
saved = refresh_tokens("email", saved.refresh_token)
except RefreshRejected:
vault.delete(shop_id, "email")
raise NeedsConsent("email")
vault.save(shop_id, "email", saved) # keep the new refresh token
return saved.access_tokenThe first lines load the saved tokens for the stationery shop. If there are none, she never connected her email, or she disconnected it. The agent must then ask her, not guess or fall back to a company account.
Next, max picks the later of two times: the expiry, and two minutes from now. If the later one is the two-minute mark, the token is expired or about to expire. Then refresh_tokens trades the refresh token for a fresh pair before the send. The saved expires_at must carry a timezone, as datetime.now(timezone.utc) does; a time without one makes the comparison raise TypeError.
Some providers return a new refresh token each time and retire the old one. This is called refresh token rotation, and it is why the code always saves the new pair. RFC 9700 requires rotation, or tokens bound to one app, for apps that cannot keep a secret. MCP requires its authorization servers to rotate refresh tokens for those apps.
If the provider refuses, perhaps because the owner removed the app in her email settings, the code deletes the old tokens. NeedsConsent and RefreshRejected are error types you would define, and the agent turns NeedsConsent into a friendly "please reconnect your email" message.
The team follows these rules for every token it holds:
- Never put a token in the prompt, the chat history or a tool result. The model does not need it, and anything in the model's view can end up in a reply or a log.
- Never put a token in a web address. RFC 9700 and the MCP specification both forbid access tokens in the query string, the part after the question mark. Send the token in the Authorization header, a standard field of an HTTP request.
- Encrypt tokens before you save them, and keep them on the server, keyed by user or shop and by service.
- Keep access tokens short-lived, so a leaked one stops working soon.
- Mask tokens in logs. A debug line that prints a whole request can leak every token inside it.
- Let users disconnect, and delete their tokens the moment they do.
How does MCP authorization work in the 2026-07-28 specification?
When a server reached over HTTP protects its tools, MCP uses OAuth. The MCP server is the resource server, the AI app is the OAuth client, and an authorization server handles login and consent. It is the same delegated access you saw above, written into a standard.
MCP, the Model Context Protocol, is an open protocol that lets an AI app find and call tools offered by another program, called an MCP server. The invoicing app now runs one. The owner can connect her own AI assistant app to it and ask which customers owe her more than Rs 10,000.
The diagram shows what happens the first time she connects, following the 2026-07-28 version. Start at her AI app, follow the numbers, and then look at the two arrows leaving the MCP server.
MCP authorization when the owner connects her AI app
Drag to exploreHer token is made for one MCP server; for anything further, that server uses a different token it got separately.
Diagram connections:
- Her AI app (MCP client) to Invoicing MCP server (1. Call with no token, gets 401)
- Her AI app (MCP client) to Invoicing app login (2. Login request for this server)
- Shop owner to Invoicing app login (3. Logs in, approves)
- Invoicing app login to AI app retries with token (4. Token for this server only)
- AI app retries with token to Invoicing MCP server (5. Calls with the token)
- Invoicing MCP server to Invoice database (Her shop's data only)
- Invoicing MCP server to Email service (Separate email token)
- Shop owner: Approves access once, in her browser
- Her AI app (MCP client): Wants to call the invoicing tools
- AI app retries with token: Token goes in the Authorization header
- Invoicing MCP server: Resource server: checks each token was made for it
- Invoicing app login: Authorization server: issues tokens
- Invoice database: Each tool still checks shop ownership
- Email service: Gets the email token, never the MCP one
First, the AI app calls the invoicing MCP server with no token. The server replies 401. A header on that reply can point to a small public file, called protected resource metadata, that names the authorization server to use. Without that header, the app looks for the file at a fixed well-known address.
The app then needs a client ID. If it was not registered in advance, the 2026-07-28 version prefers a client ID metadata document, when the login service supports one. The client ID is then an https web address that serves a small file describing the app.
Dynamic client registration, the older automatic way to get a client ID, is marked deprecated in this version and kept only for older authorization servers.
Next, the app sends the owner's browser to the invoicing app's login page. The request must use PKCE and include a resource value naming the exact MCP server the token is for.
The owner logs in and approves. The app receives an access token and sends it in the Authorization header on every request. An invalid or expired token gets a 401. A valid token with too small a scope should get a 403, and the app can ask her to approve more.
Now look at the arrow to the email service. When the MCP server sends an invoice email, it uses the email token the owner approved earlier, which the email provider issued. The specification says an MCP server must not pass through the token it received.
How does an MCP server check that a token was made for it?
It checks the token's audience: the field that names which server the token is for. The specification says an MCP server must reject tokens not issued for it as the intended audience.
Many authorization servers issue access tokens as JWTs (JSON Web Tokens, signed text that carries claims such as the user, expiry and audience). RFC 9068 defines that format. For tokens that are not JWTs, the server can ask the authorization server about each token, a step called token introspection (RFC 7662).
Here is a check for JWT access tokens using PyJWT, a common Python library. The two addresses use the reserved .example domain, so they are clearly fake.
import jwt # PyJWT 2.15.1, installed with: pip install "pyjwt[crypto]==2.15.1"
MCP_SERVER = "https://mcp.invoices.example/mcp" # this server's own address
ISSUER = "https://login.invoices.example" # the authorization server
def verify_access_token(raw_token, public_key):
typ = jwt.get_unverified_header(raw_token).get("typ")
if typ not in ("at+jwt", "application/at+jwt"):
raise jwt.InvalidTokenError("not a JWT access token")
claims = jwt.decode(
raw_token, public_key,
algorithms=["RS256"], # a fixed list, never read from the token
audience=MCP_SERVER, # made for this server, or rejected
issuer=ISSUER,
options={"require": ["exp", "aud", "iss", "sub"]},
)
return claims["sub"], set(claims.get("scope", "").split())The header check follows RFC 9068, which says access tokens carry the type at+jwt. The fixed algorithms list stops a token from choosing its own, weaker check. The audience and issuer arguments make PyJWT reject tokens meant for another server or from another login service.
To check it, sign four test tokens with a throwaway key and pass each one in. On PyJWT 2.15.1 the results are:
token made for this server: ok, user shop-17, scopes ['invoices:read']
token made for another API: InvalidAudienceError: Audience doesn't match
expired token: ExpiredSignatureError: Signature has expired
token from another issuer: InvalidIssuerError: Invalid issuerThe second line shows why the audience check matters. The same login service signed that token, but for a different API, such as a reports API. Only the audience check stops it being replayed here. A token from the separate email provider would fail even earlier, on the signature check.
One more change in this version: MCP has no protocol-level sessions. A server that needs state, like a draft batch of reminders, hands out an ID that comes back as a normal tool argument.
The security guide says possessing such an ID must never count as authentication. Store each ID against the user ID from the verified token, and refuse it from anyone else.
How do you explain AI agent authorization in a system design interview?
Start by separating the two questions, then show where each check lives in your design. Interviewers listen for whether you trust the model with permissions, and the right answer is that you do not.
Split the two questions
Say who logs in and which token proves it. Then say what each identity may do.
Name both identities
For every agent action, name the user it acts for and the app doing the work.
Choose the token for each job
User tokens for work on behalf of a person, service accounts only for system jobs, OAuth consent for other services.
Put the checks in the tools
Every tool checks scope and ownership in code, using the verified session, not the model's words.
Handle storage and expiry
Encrypted storage on the server, short-lived access tokens, refresh in code, and a reconnect path.
Mention the standard
Over MCP, the server checks each token's audience and never passes the token on to another service.
Questions people ask
- Is authentication or authorization more important for AI agents?
- You need both, but OWASP ranks authorization failures as the bigger risk. In the OWASP Top 10:2025, broken access control ranks first and authentication failures rank seventh. One likely reason: login is often handled by a proven library or sign-in service, while permission checks are code your team writes for every tool.
- Can an AI agent log in with my password for me?
- It should not. RFC 9700, the OAuth security best practice, says the grant where an app collects the user's password must not be used. Instead, the user approves a limited scope on the service's own consent page, and the agent's code receives a token for that scope only.
- What is the difference between a scope and a permission check?
- A scope is a label on a token that says which kind of action it may be used for, like invoices:send. A permission check is the full decision your code makes, including the exact item, like whether INV-2041 belongs to this shop. Scopes narrow what a token can do, but your code still makes the final call.
- How long should an access token last?
- There is no single right number, but shorter is safer. The MCP specification says authorization servers should issue short-lived access tokens to reduce the damage from a leak. Many systems pair access tokens that last minutes with a longer-lived refresh token that can be revoked.
- Does MCP require OAuth?
- Not always, because authorization is optional in the MCP specification. When a server that uses an HTTP-based transport protects its tools, it should follow the MCP authorization spec, which builds on the OAuth 2.1 draft. Local servers that run over stdio should take credentials from their environment instead.
- Is the confused deputy problem the same as prompt injection?
- No, they are related but different. The confused deputy is the design flaw: a tool that holds more power than the person using it. Prompt injection is one way to trigger that flaw, by hiding instructions in text the agent reads. Fixing the deputy limits what any such trick can achieve.
- Can my MCP server forward the token it received to another API?
- No. The MCP specification says a server must not pass through the token it received from the client. If your server calls another API, it acts as that API's own OAuth client and uses a separate token issued for that API. This keeps each token tied to one audience.
Sources and further reading
- Model Context Protocol specification, version 2026-07-28: Authorization
- Model Context Protocol specification, version 2026-07-28: Authorization Security Considerations
- Model Context Protocol: Security Best Practices for 2026-07-28 (token passthrough, state handles, scope minimization)
- Model Context Protocol: Versioning (current version)
- IETF RFC 6749: The OAuth 2.0 Authorization Framework
- IETF RFC 9700: Best Current Practice for OAuth 2.0 Security
- IETF RFC 9068: JSON Web Token Profile for OAuth 2.0 Access Tokens
- IETF Datatracker: The OAuth 2.1 Authorization Framework (draft status)
- OWASP Authorization Cheat Sheet
- OWASP Top 10:2025
- MDN Web Docs: 401 Unauthorized
- IETF RFC 7662: OAuth 2.0 Token Introspection
- Norm Hardy: The Confused Deputy (Operating Systems Review, October 1988), course reading copy