Field note

AI Agent Authentication vs. Authorization: What’s the Difference?

Learn the difference between AI agent authentication and authorization, including identity, OAuth, scopes, policies, 401/403 codes, and MCP examples.

AI Agent Authentication vs. Authorization: What’s the Difference?

Updated August 1, 2026

AI agent authentication confirms who sent a request and which private key the sender controls. Authorization determines what that agent may do on behalf of a user, with a specific resource, tool, and set of operation parameters. A valid signature or OAuth token does not provide unlimited access on its own.

In Brief

  • Agent authentication answers: “Who sent this request, and which private key do they control?”
  • OAuth authorization describes the permissions that a user, application, or another subject delegated to the client.
  • Action authorization determines whether a specific operation may be performed with the supplied parameters.
  • A valid signature proves the sender’s identity, not the safety of the decision made by that sender.
  • A standard OAuth bearer token can be used by any process that obtains its value, so it does not always prove the agent’s identity.
  • Before executing an MCP tool, the server should normally verify the signature, token, scopes, target resource, operation parameters, replay status, and access policy.
  • The final decision should be represented by allowed, not merely by verified.

Table of Contents

  1. The Key Difference Between Authentication and Authorization
  2. Key Terms in 30 Seconds
  3. What AI Agent Authentication Proves
  4. What AI Agent Authorization Means
  5. Where OAuth Fits into This Model
  6. Agent Identity and User Identity
  7. Why Scopes Are Not Enough
  8. Step-Up Authorization for Sensitive Actions
  9. Three Practical Scenarios
  10. Decision Matrix
  11. Authentication and Authorization in MCP
  12. 401 vs. 403
  13. Why the Model Must Not Make the Final Decision
  14. Recommended Architecture
  15. AgentBouncer Example
  16. Common Mistakes
  17. FAQ

An AI agent sends a correctly signed request:

text
POST /mcp
tools/call → delete_production_database

The server verifies the signature and confirms that the request genuinely came from a known agent.

Should the operation be executed?

No—at least not based on successful authentication alone.

The signature proves who sent the request. It does not tell the server:

  • whether the user authorized deletion of the database;
  • whether the agent has the necessary permissions;
  • whether policy allows this tool to be called;
  • whether the operation matches the current task;
  • whether the agent has been affected by prompt injection;
  • whether the request was intercepted and replayed.

That is why AI agent authentication and authorization are related but independent security layers.

The distinction can be summarized as follows:

text
Authentication:
“Who sent the request?”

User authorization:
“What permissions were delegated to the client?”

Action policy:
“May this exact action be performed right now?”

For autonomous agents, identity verification alone is almost never sufficient.


The Key Difference Between Authentication and Authorization

Authentication normally happens before authorization.

First, the server establishes who is requesting access to the resource. It then determines which actions that subject is allowed to perform.

CheckPrimary questionExample
Agent authenticationWho sent the request?The signature was created with a registered key
User authorizationWhich permissions were delegated?The token contains the calendar.events.create scope
Action authorizationIs this specific operation allowed?The agent may create an event but cannot delete a calendar
Integrity verificationWas the request modified?Content-Digest matches the body
Replay protectionHas this request already been used?The fingerprint is absent from the replay cache
Risk evaluationAre additional restrictions required?A large payment requires user confirmation

These are not alternative security mechanisms. Each layer addresses a different problem.

A useful way to represent the final evaluation is through three independent results:

json
{
  "verified": true,
  "userAuthorized": true,
  "allowed": false
}

In this example:

  • verified means the agent passed cryptographic verification;
  • userAuthorized means valid user authorization was presented;
  • allowed means the policy denied the specific action.

Only allowed should determine whether the tool is executed.


Key Terms in 30 Seconds

TermMeaning
AuthenticationVerification of a subject’s identity or control over a credential
AuthorizationDetermination of which actions a subject may perform
Access tokenA credential representing permissions granted to a client
ScopeA class of permissions, such as files.read or payments.create
PolicyA rule that allows or denies a specific operation

Related concepts—including bearer tokens, proof of possession, replay protection, and HTTP Message Signatures—are covered in the previous article: “AI Agent Authentication: Identity, Authorization, and Access Control”.


What AI Agent Authentication Proves

Practical AI agent authentication can involve three steps:

  1. The server receives a signed request.
  2. It verifies the signature using a public key.
  3. It associates that key with a registered agent, provider, or deployment.

The result might look like this:

json
{
  "verified": true,
  "agent": "https://agent.example.com",
  "provider": "Example AI",
  "keyId": "agent-key-2026-08",
  "trustTier": 2
}

The server now knows that the request was signed by the owner of the corresponding private key.

But this field:

json
{
  "verified": true
}

does not mean:

json
{
  "allowed": true
}

RFC 9421 allows a recipient to verify that signed HTTP message components were not modified in transit and were signed by the owner of the corresponding key. It does not define a complete trust model, business policy, or authorization rules.

What Authentication Does Not Prove

Successful identity verification does not confirm that:

  • the user authorized the requested action;
  • the agent may access the specified MCP tool;
  • the payment amount is within the permitted limit;
  • the recipient is on an allowlist;
  • the agent’s decision was not caused by a malicious instruction;
  • the request is not a replay of a previously accepted message;
  • the key does not belong to a compromised deployment;
  • the operation matches the user’s original intent.

Authentication provides input for subsequent authorization. It does not replace authorization.

For requests with a body, integrity verification through Content-Digest is also important. Signed components and replay protection are covered in more detail in the AI agent authentication guide.


What AI Agent Authorization Means

Authorization determines which operations a subject may perform after all required credentials have been verified.

A decision may depend on a combination of:

text
agent identity
+ user identity
+ OAuth scopes
+ resource
+ action
+ tool
+ parameters
+ risk
+ current context

The same agent may receive different decisions:

text
calendar.events.read
→ ALLOW

calendar.events.create
→ ALLOW

calendar.calendars.delete
→ DENY

payments.send, amount=20 USD
→ ALLOW

payments.send, amount=5000 USD
→ REQUIRE_USER_CONFIRMATION

This is action-level authorization: the server evaluates not only the subject and the general permission class, but also the exact action being requested.

Naming Convention Used in the Examples

To avoid mixing OAuth scopes, internal policy actions, and tool names, the examples use a consistent naming style:

text
OAuth scope → dot notation:
payments.create

Policy action → colon notation:
payments:create

MCP tool → snake_case:
payments_send

These values are semantically related, but they do not have to be identical strings.


Where OAuth Fits into This Model

OAuth is designed to grant limited access to a protected resource.

In a user-facing scenario, an access token may represent permissions that the resource owner delegated to the client:

json
{
  "iss": "https://auth.example.com",
  "sub": "user-1842",
  "aud": "https://mcp.example.com",
  "scope": "calendar.read calendar.events.create",
  "exp": 1785603600
}

At a minimum, the server should verify:

  • the trusted issuer in iss;
  • the intended resource or audience;
  • the token’s expiration time;
  • the required scopes;
  • other mandatory claims and restrictions;
  • the JWT signature or introspection result, depending on the token format and profile.

OAuth defines a delegated-access model, but an access token does not necessarily represent a human user. It may be issued to a workload, application, or client_credentials flow.

The server should interpret sub as a user only when that meaning is guaranteed by the token profile and authorization server configuration.

OAuth 2.0 defines an access token as a credential representing granted authorization, while scopes limit the access granted to the client (RFC 6749, Sections 1.4 and 3.3).

Why an OAuth Token Is Not Always the Agent’s Identity

Consider this request:

http
Authorization: Bearer USER_ACCESS_TOKEN

A standard bearer token can be used without any additional proof of key possession.

If the token value is obtained by:

  • a malicious process;
  • another agent;
  • a compromised MCP server;
  • an employee with access to logs;
  • a third-party integration,

the server may see a valid token but still be unable to determine which software subject actually presented it.

In other words:

text
OAuth access token
→ describes the access that was granted

Request signature
→ proves control over the sender’s key

These signals complement one another.

Sender-constrained mechanisms such as DPoP or mTLS-bound access tokens can reduce the risk of token theft. DPoP binds a token to a public key and requires proof that the presenter controls the corresponding private key (RFC 9449).

Proof of possession still does not answer the central business question: should this specific operation be allowed?


Agent Identity and User Identity Are Not the Same

A typical autonomous-agent request may contain two independent identities:

text
Agent identity:
verified through an HTTP Message Signature

User authorization:
verified through an OAuth access token

For example:

http
POST /mcp HTTP/1.1
Host: mcp.example.com
Content-Type: application/json
Authorization: Bearer USER_ACCESS_TOKEN
Content-Digest: sha-256=:BODY_DIGEST:
Signature-Agent: "https://agent.example.com"

Signature-Input: sig1=("@method" "@authority" "@path" "content-digest" "signature-agent");
  created=1785600000;
  expires=1785600060;
  keyid="agent-key-2026-08";
  tag="web-bot-auth"

Signature: sig1=:BASE64_SIGNATURE:

The tag="web-bot-auth" parameter associates the signature with the expected purpose of the request. A server may reject a cryptographically valid signature if the endpoint expects a different intent tag.

After verification, the server has two separate contexts:

json
{
  "agent": {
    "identity": "https://agent.example.com",
    "verified": true
  },
  "user": {
    "subject": "user-1842",
    "scopes": [
      "documents.read",
      "documents.summarize"
    ]
  }
}

A third decision is still required:

json
{
  "action": "documents:export",
  "tool": "documents_export",
  "destination": "https://external.example.com/upload",
  "allowed": false,
  "reason": "external_destination_not_allowed"
}

The user authorized the client to read documents and create summaries. That does not mean the agent may export the original files to an external server.

![Separation of AI agent identity, user identity, and the policy engine’s final decision


Why Scopes Are Not Enough

Scopes define the boundaries of granted access, but they are usually broader than an individual operation.

A scope such as:

text
payments.create

may permit payment creation. It does not answer:

  • how much money may be sent;
  • which currencies are permitted;
  • which recipients are allowed;
  • how many operations may be performed per day;
  • whether international transfers are permitted;
  • whether user confirmation is required;
  • whether a newly registered agent may make the call;
  • whether the operation matches the user’s task.

Those decisions require a policy engine.

A conceptual policy might look like this:

text
IF
  agent.trustTier <= 2
  AND user.scope includes "payments.create"
  AND action == "payments:create"
  AND tool == "payments_send"
  AND amount <= 100
  AND recipient in user.allowlist
  AND replay == false
THEN
  ALLOW
ELSE
  REQUIRE_CONFIRMATION or DENY

This is pseudocode, not the syntax of a specific product. In practice, the check may be implemented with Cedar, Open Policy Agent, a custom policy engine, or AgentBouncer project policies.

A scope is an input to authorization, not the final authorization decision.


Step-Up Authorization for Sensitive Actions

Not every tool needs the maximum set of OAuth scopes from the beginning.

For example, when the client first connects, it may request only:

text
documents.read

If the user later asks the agent to export a document, the server may respond:

http
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope",
  scope="documents.export",
  resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource"
Cache-Control: no-store

The client then starts another authorization flow and requests an expanded set of permissions.

Under the current MCP specification, the client must calculate the union of the previously requested and newly required scopes:

text
previous scopes:
documents.read

challenged scopes:
documents.export

new authorization request:
documents.read documents.export

This prevents a step-up flow from issuing a new export token while accidentally dropping the previously granted read permission.

The server should return all scopes required for the current operation at once instead of requesting them one at a time across multiple cycles. The client should also limit the number of retries to avoid an infinite authorization loop. These requirements are defined by the MCP 2026-07-28 step-up flow.

A new scope still does not guarantee that the operation will be performed:

text
documents.export
+ destination in corporate allowlist
→ ALLOW

documents.export
+ destination controlled by unknown third party
→ DENY

Step-up authorization expands delegated permissions. It does not bypass action-level policy.


Three Scenarios That Show the Difference

Scenario 1: Known Agent Without an OAuth Token

The agent calls:

text
calendar_create_event

The signature is valid, and the identity is known. However, policy requires user authorization with this scope:

text
calendar.events.create

Result:

json
{
  "verified": true,
  "userAuthorized": false,
  "allowed": false,
  "reason": "oauth_token_required"
}

Practical response:

http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer
  resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
  scope="calendar.events.create"
Cache-Control: no-store

The agent has been authenticated, but the required user credential is missing.

Scenario 2: Valid Signature but Insufficient Scope

The agent presents a token with this scope:

text
calendar.read

But it requests:

text
calendar.events.create

Result:

json
{
  "verified": true,
  "userAuthorized": true,
  "allowed": false,
  "reason": "insufficient_scope"
}

Response:

http
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer
  error="insufficient_scope",
  scope="calendar.events.create",
  resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource"
Cache-Control: no-store

The credentials are valid, but they do not provide the required permission.

Scenario 3: All Credentials Are Valid, but the Action Is Dangerous

The agent has:

  • a valid signature;
  • a known identity;
  • an OAuth token;
  • the payments.create scope.

It requests:

json
{
  "tool": "payments_send",
  "amount": 5000,
  "currency": "USD",
  "recipient": "new-recipient"
}

Policy permits automatic payments only up to $100, and payments to new recipients require manual confirmation.

Result:

json
{
  "verified": true,
  "userAuthorized": true,
  "allowed": false,
  "reason": "user_confirmation_required"
}

The current request is not executed. The server may return 403 with an application-level error code and create a separate confirmation flow:

http
HTTP/1.1 403 Forbidden
Content-Type: application/json
Cache-Control: no-store
json
{
  "error": "user_confirmation_required",
  "confirmationId": "confirm-7f3c",
  "expiresIn": 300
}

The confirmation must be bound to the exact operation parameters:

text
tool
+ amount
+ currency
+ recipient
+ expiration

The server must not accept an abstract confirmation such as “allow the next payment,” because the agent could change the parameters between confirmation and execution.


Decision Matrix

Agent signatureOAuthScopes and policyResultHTTP
Invalid or missingAny—invalid_agent_credentials401
ValidNot requiredPolicy allowsExecute200
ValidMissingOAuth requiredoauth_required401
ValidInvalid or expired—invalid_token401
ValidValidInsufficient scopesinsufficient_scope403
ValidValidPolicy deniesagent_access_denied403
ValidValidConfirmation requireduser_confirmation_required403
ValidValidEverything allowedExecute200
Replay detectedAny—replay_detected401
AnyAnyRate limit exceededrate_limit_exceeded429
Required verification unavailableAnyDecision unknownverification_unavailable503

For temporary failures, it is often useful to include:

http
Retry-After: 30

A more detailed error taxonomy, including 429 and 503, is available in “AI Agent Authentication: Identity, Authorization, and Access Control”.

The central integration rule is:

ts
if (!verification.allowed) {
  deny();
}

Checking only verified is not enough.

AgentBouncer separates these fields intentionally: verified represents the cryptographic verification result, while allowed represents the project policy’s final decision. In monitor mode, even a verified: false request may receive allowed: true, so execution must always depend on the final result.


Authentication and Authorization in MCP

MCP Authorization governs how clients access remote MCP servers through OAuth.

In the current MCP revision dated July 28, 2026:

  • authorization remains an optional protocol feature;
  • HTTP transports use an OAuth-based model;
  • the MCP server acts as an OAuth resource server;
  • the client discovers authorization servers through Protected Resource Metadata;
  • the client must send the resource parameter;
  • the server must accept only tokens intended for its resource;
  • authorization code flows require PKCE;
  • stdio servers must not use the HTTP OAuth flow and generally receive credentials from their environment;
  • clients must handle step-up authorization correctly;
  • OAuth credentials must be bound to the issuer of the authorization server that issued them;
  • Dynamic Client Registration remains available for compatibility but is deprecated in favor of Client ID Metadata Documents.

The official MCP Authorization specification for revision 2026-07-28 defines audience validation, Protected Resource Metadata, 401 responses for missing or invalid tokens, and 403 responses for insufficient scopes.

What Changed in MCP 2026-07-28

Revision 2026-07-28 is the largest protocol update since MCP was introduced. It includes:

  • a stateless core without a mandatory initialize handshake;
  • removal of protocol-level sessions and Mcp-Session-Id;
  • server/discover for discovering server versions, capabilities, and identity;
  • Multi-Round-Trip Requests for obtaining additional data;
  • mandatory Mcp-Method and Mcp-Name headers for HTTP routing;
  • cacheable list results through ttlMs and cacheScope;
  • a formal extensions framework;
  • stronger OAuth and OpenID Connect compatibility;
  • a formal capability lifecycle: Active → Deprecated → Removed.

The complete list is available in the MCP 2026-07-28 changelog.

Adopting the new revision does not mean that every 2025-11-25 implementation immediately stops working. Clients and servers may maintain backward compatibility and negotiate a supported protocol version. Features formally marked as Deprecated remain in the specification for at least twelve months before they may be removed.

Validating iss and Preventing Mix-Up Attacks

MCP 2026-07-28 strengthens issuer validation in OAuth flows.

Before redirecting the user, the client stores the expected issuer from the verified authorization server metadata document. When the authorization response arrives, the client validates the iss parameter according to RFC 9207.

If the authorization server declares:

json
{
  "authorization_response_iss_parameter_supported": true
}

but does not return iss, the client must reject the response.

When iss is present, it must be compared with the stored issuer using exact string comparison. The client must not automatically change letter casing, remove trailing slashes, or normalize the URL.

This check reduces the risk of OAuth authorization server mix-up attacks, especially for MCP clients that connect to many independent servers.

Credentials Must Be Bound to the Issuer

A client ID and its associated credentials are valid in the context of the authorization server that issued them.

If an MCP resource moves from one authorization server to another, the client must not automatically reuse the old credentials. Instead, it should:

  1. discover the new authorization server;
  2. validate its metadata;
  3. locate credentials bound specifically to that issuer;
  4. register again if necessary.

This prevents a client secret, refresh token, or other credential from being sent to the wrong server.

Client ID Metadata Documents Instead of DCR

MCP 2026-07-28 recommends OAuth Client ID Metadata Documents as the primary client ID mechanism.

Dynamic Client Registration under RFC 7591 may still be supported, but MCP now marks it as deprecated and retains it for backward compatibility.

Before beginning the authorization flow, a client may obtain a client ID in one of three ways:

  1. a Client ID Metadata Document;
  2. pre-registration;
  3. Dynamic Client Registration.

DCR should not be removed from existing integrations without a compatibility review. New implementations, however, should be designed with Client ID Metadata Document support.

Protected Resource Metadata

Protected Resource Metadata is standardized by RFC 9728. The metadata may describe:

  • authorization servers;
  • supported scopes;
  • bearer-token transmission methods;
  • DPoP support;
  • other properties of the protected resource.

Example:

json
{
  "resource": "https://mcp.example.com",
  "authorization_servers": [
    "https://auth.example.com"
  ],
  "scopes_supported": [
    "documents.read",
    "documents.write"
  ],
  "bearer_methods_supported": [
    "header"
  ]
}

What MCP OAuth Does Not Solve Automatically

The MCP OAuth model does not create a universal cryptographic identity for the AI agent itself.

It can tell the server:

text
The client presented a token
that is valid for this MCP resource
and contains the specified scopes.

But answering a separate question—

text
Which agent, provider, or workload
sent this HTTP request?

—may require an additional verification mechanism:

  • HTTP Message Signatures;
  • mTLS;
  • a DPoP-bound access token;
  • workload identity;
  • a registered signing key.

This is especially important when a single user token may potentially be used by multiple software participants.

Specification status as of August 1, 2026: the current MCP revision is 2026-07-28. OAuth 2.1 remains an Internet-Draft rather than a published RFC. The current IETF revision is draft-ietf-oauth-v2-1-15, dated March 2, 2026, and scheduled to expire on September 3, 2026.


401 vs. 403: A Quick Reference

HTTP status names can be confusing: 401 Unauthorized is used when a request lacks valid authentication credentials.

Return 401 when:

  • a required signature is missing;
  • signature verification fails;
  • the signing key is unknown;
  • the OAuth token is missing;
  • the token is expired, revoked, or invalid;
  • the token is intended for another resource;
  • DPoP proof verification fails;
  • a signed-request replay is detected.
http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer
  resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource"
Cache-Control: no-store

Return 403 when:

  • the credentials have already been accepted;
  • the token lacks required scopes;
  • the tool is unavailable to this agent;
  • the user cannot access the object;
  • a policy limit has been exceeded;
  • the recipient is prohibited;
  • the action requires separate confirmation.
http
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer
  error="insufficient_scope",
  scope="payments.create",
  resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource"
Cache-Control: no-store

RFC 6750 defines 401 behavior for invalid bearer tokens and 403 with insufficient_scope when the granted permissions are insufficient. General status-code semantics are defined in RFC 9110.

A more detailed discussion of 401, 403, 429, and 503 is available in the first article in this series.


Why the Final Decision Must Not Be Left to the Model

You can add a rule to the system prompt:

text
Never send payments over $100.

But this is an instruction for the model, not a reliable security boundary.

A prompt injection attack may try to convince the agent that:

  • the operation has already been confirmed;
  • the limit has been temporarily disabled;
  • an external recipient is actually internal;
  • the tool needs to be called only for testing;
  • the system instruction no longer applies.

If the model then sends the request, its cryptographic signature will still be valid.

Rules that limit real-world consequences must therefore be enforced:

  • by the MCP server;
  • at the API gateway;
  • in the downstream API;
  • by a separate policy engine;
  • immediately before an irreversible operation.

OWASP recommends least privilege, separate authorization for sensitive tools, validation of tool-call parameters, and avoiding reliance on prompt instructions alone. Human-in-the-loop confirmation is also recommended for irreversible or expensive operations.

The model may propose an action:

json
{
  "tool": "payments_send",
  "amount": 5000
}

Trusted code must independently decide:

json
{
  "allowed": false,
  "reason": "transaction_limit_exceeded"
}

text
AI agent
  │
  │ Signed request
  │ User OAuth token
  ▼
MCP endpoint or API gateway
  │
  ├── 1. Verify Content-Digest
  ├── 2. Verify HTTP Message Signature
  ├── 3. Resolve agent identity
  ├── 4. Validate timestamps and intent tag
  ├── 5. Apply replay protection
  ├── 6. Validate OAuth issuer and audience
  ├── 7. Validate scopes
  ├── 8. Resolve action and tool
  ├── 9. Validate parameters and limits
  └── 10. Perform final policy evaluation
             │
             ├── DENY
             ├── REQUIRE_CONFIRMATION
             └── ALLOW
                    ▼
                 MCP tool

Every check must be completed before the tool is called.

For requests with a body, Content-Digest should be verified against the original bytes before JSON.parse() is called. RFC 9530 defines Content-Digest as a digest of the actual HTTP message content.

The order matters:

text
receive raw request
→ verify digest
→ verify signature
→ check replay status
→ verify OAuth
→ apply policy
→ parse arguments
→ execute tool

If the application modifies or reserializes the JSON before checking the digest, it is no longer working with the same bytes that the sender signed.


Practical Example with AgentBouncer

ts
import {
  createAgentBouncer,
  getRequiredOAuthScopes,
  isOAuthRequired,
  isOAuthScopeDenied,
} from "@agentbouncer/sdk";

const publicOrigin =
  process.env.AGENTBOUNCER_PUBLIC_ORIGIN!;

const resourceMetadataUrl =
  `${publicOrigin}/.well-known/oauth-protected-resource`;

const agentBouncer = createAgentBouncer({
  apiKey:
    process.env.AGENTBOUNCER_API_KEY!,
  publicOrigin,
  validateContentDigest: true,
});

export async function POST(
  request: Request,
): Promise<Response> {
  const verification =
    await agentBouncer.verify({
      request,
      expectedTag: "web-bot-auth",
      action: "payments:create",
      tool: "payments_send",
      forwardAuthorization: true,
    });

  if (!verification.allowed) {
    const headers = new Headers({
      "Cache-Control": "no-store",
    });

    if (isOAuthRequired(verification)) {
      const requiredScopes =
        getRequiredOAuthScopes(verification);

      headers.set(
        "WWW-Authenticate",
        `Bearer resource_metadata="${resourceMetadataUrl}", scope="${requiredScopes.join(" ")}"`,
      );

      return Response.json(
        {
          error: "oauth_required",
          requiredScopes,
        },
        {
          status: 401,
          headers,
        },
      );
    }

    if (isOAuthScopeDenied(verification)) {
      const requiredScopes =
        getRequiredOAuthScopes(verification);

      headers.set(
        "WWW-Authenticate",
        `Bearer error="insufficient_scope", scope="${requiredScopes.join(" ")}", resource_metadata="${resourceMetadataUrl}"`,
      );

      return Response.json(
        {
          error: "insufficient_scope",
          requiredScopes,
        },
        {
          status: 403,
          headers,
        },
      );
    }

    return Response.json(
      {
        error: verification.verified
          ? "agent_access_denied"
          : "invalid_agent_credentials",
        reason: verification.reason,
      },
      {
        status:
          verification.verified
            ? 403
            : 401,
        headers,
      },
    );
  }

  const payload = await request.json();

  return await executePaymentTool(payload);
}

In this example:

  • verified shows whether cryptographic identity verification succeeded;
  • the OAuth helpers determine whether a token is required and whether its scopes are sufficient;
  • allowed contains the final decision;
  • expectedTag verifies the signature’s intended purpose;
  • the body is parsed only after all checks have passed;
  • tool execution begins only when allowed: true.

The current AgentBouncer documentation confirms that verify() checks Content-Digest before the body is read, forwards bearer tokens for OAuth verification by default, and provides the isOAuthRequired(), isOAuthScopeDenied(), and getRequiredOAuthScopes() helpers.


Common Mistakes

1. Treating a Valid Signature as Permission

A signature proves control over a key. It does not grant permission to perform every action.

Correct:

ts
if (!verification.allowed) {
  deny();
}

Incorrect:

ts
if (verification.verified) {
  executeTool();
}

2. Treating an OAuth Token as the Agent’s Identity

A bearer token describes granted access, but it does not always prove which process presented it.

If identifying the software participant matters, use a separate signature, workload identity, or sender-constrained token.

3. Checking Only Scopes

A scope may be a necessary condition, but the operation parameters must still pass policy evaluation.

text
payments.create

does not mean:

text
any amount
+ any recipient
+ any country
+ unlimited operations

4. Using One Global Scope

text
mcp.full_access

A broad scope makes integration easier, but prevents the server from separating:

  • reads;
  • modifications;
  • deletions;
  • exports;
  • administrative operations;
  • financial actions.

A better design is:

text
documents.read
documents.write
documents.export
documents.delete

5. Failing to Validate the Audience

A token issued for another API must not be accepted automatically by an MCP server.

MCP requires clients to send resource, and servers must accept only tokens issued for the specific protected resource. A server must also avoid accepting or forwarding tokens intended for other services.

6. Forwarding the User’s Token Downstream

An MCP server must not simply forward the incoming access token to a downstream API.

The next resource needs a separate token with an appropriate audience. One standardized way to obtain it is OAuth Token Exchange under RFC 8693.

Example:

text
Incoming token:
aud = https://mcp.example.com

Downstream API:
https://payments.example.com

The first token must not be sent directly to the Payments API. The MCP server may exchange it for a narrower token:

text
aud = https://payments.example.com
scope = payments.execute

RFC 8693 allows a resource server to exchange an incoming token for a new credential intended for a specific downstream service.

7. Applying Policy After Calling the Tool

A check performed after data has been deleted or a payment has been sent does not protect the system.

Authorization must happen immediately before the side effect.

8. Allowing the Model to Override Restrictions

The model may propose an action, but trusted code must make the final authorization decision.

The server must not trust model-provided arguments such as:

json
{
  "limitOverride": true,
  "userAlreadyConfirmed": true
}

without independent verification.

9. Failing to Distinguish Users and Agents in Logs

Audit logs should retain both contexts:

json
{
  "agent": "https://agent.example.com",
  "agentKeyId": "agent-key-2026-08",
  "userSubject": "user-1842",
  "action": "payments:create",
  "tool": "payments_send",
  "decision": "DENY",
  "reason": "transaction_limit_exceeded",
  "policyVersion": "payments-v12"
}

For sensitive operations, it is also useful to record:

  • the risk level;
  • the confirmation ID;
  • a hash of the confirmed operation parameters;
  • the token issuer;
  • scopes;
  • replay status;
  • the policy version.

10. Using verified Instead of allowed

This mistake turns successful cryptographic verification into unrestricted permission.

In monitor mode, an unverified request may temporarily receive allowed: true so that the team can study real traffic before enforcing blocking policies.

Therefore:

text
verified
→ signature verification result

allowed
→ final operation execution decision

11. Repeating Step-Up Authorization Indefinitely

If the server keeps returning insufficient_scope, the client must not start an infinite series of OAuth flows.

It should:

  • combine old and new scopes;
  • limit the number of retries;
  • associate each attempt with the resource and operation;
  • treat the denial as permanent after several failed attempts.

12. Reusing Credentials from Another Issuer

Client IDs, client secrets, and refresh tokens must be bound to the authorization server that issued them.

When the issuer changes, the client must repeat discovery and register again if necessary.


Frequently Asked Questions

What is the difference between AI agent authentication and authorization?

Authentication establishes the agent’s identity or confirms control over a credential. Authorization determines which actions the agent may perform based on the user, scopes, resource, tool, operation parameters, and access policy.

Is OAuth a form of agent authentication?

Not necessarily. OAuth primarily represents granted permissions. Verifying the identity of the calling agent may require a separate HTTP Message Signature, certificate, workload identity, or proof-of-possession mechanism.

Is an HTTP Message Signature enough?

No. A signature protects selected message components and proves control over the signing key. It does not determine whether the business operation is allowed.

What is the difference between a scope and a policy?

A scope describes a class of permissions, such as files.write. A policy evaluates the specific operation, including the file, owner, size, destination, risk level, request parameters, and current context.

Can an authenticated agent receive a 403 response?

Yes. An established identity does not mean that the subject may perform every action. For example, the agent may be known to the server but lack access to a particular MCP tool.

When should a server return 401 instead of 403?

A server normally returns 401 when credentials are missing or invalid. It returns 403 when the credentials were accepted but the scopes or policy permissions are insufficient.

Should user identity and agent identity be verified separately?

Yes. The user and the agent are different participants. The user delegates permissions, while the agent technically constructs and sends the request.

What should an MCP server check before calling a tool?

At a minimum:

  • the agent’s signature;
  • body integrity;
  • signature expiration;
  • the intent tag;
  • replay status;
  • token issuer and audience;
  • OAuth scopes;
  • the requested tool;
  • operation parameters;
  • quotas and limits;
  • the final access-policy decision.

Conclusion

AI agent authentication and authorization solve different problems.

Authentication confirms:

text
“This request was signed by the owner of a known key.”

OAuth states:

text
“The client has been granted specific permissions.”

The access policy makes the final decision:

text
“This agent, with these permissions, may perform
this action with these parameters right now.”

Without the first layer, the server does not know who sent the request. Without the second, it does not know which permissions were delegated to the client. Without the third, it cannot determine whether the exact operation is allowed.

AgentBouncer combines HTTP Message Signature verification, agent identity, user OAuth authorization, replay protection, and project policies into a single final allowed decision.

Protect your API or MCP server with AgentBouncer—start in monitor mode, study real agent traffic, and enable blocking policies only when you are ready.

Previous article: “AI Agent Authentication: Identity, Authorization, and Access Control”.

Next article in the series: “MCP Authentication and Authorization: The Complete Guide”—coming soon.

Reference file

Sources