Документация разработчика

Проверяйте AI-агентов до предоставления доступа к API

Интегрируйте HTTP Message Signatures, проверку целостности тела, OAuth-авторизацию пользователя, политики доверия, защиту MCP и аналитику.

Introduction

AgentBouncer overview

Verify signed AI-agent requests, authenticate end users with OAuth, and apply project-specific access policies.

AgentBouncer verifies HTTP Message Signatures sent by AI agents, resolves the signer public key, validates signature timestamps and replay protection, evaluates optional user OAuth authorization, and returns a final access decision.

The most important integration rule

Use allowed as the final authorization decision. verified reports the cryptographic result, while allowed reports the effective project-policy decision.

verifiedallowedMeaning
truetrueThe signature is valid and the request is allowed.
truefalseThe signature is valid, but the project policy denied access.
falsetrueThe request was not verified but was allowed by MONITOR_ONLY or another permissive setting.
falsefalseThe request failed verification and access was denied.
  • Sign outgoing agent requests with the JavaScript and TypeScript SDK.
  • Verify RFC 9421 HTTP Message Signatures.
  • Require @method, @authority, @path, and Content-Digest coverage for production body requests.
  • Include Signature-Agent in requests created by the SDK signer.
  • Validate that Content-Digest matches the actual request body.
  • Resolve provider and project-owned public keys.
  • Detect reuse of previously consumed signatures.
  • Forward and verify user OAuth access tokens.
  • Apply trust tiers, trust scores, actions, tools, OAuth scopes, and custom rules.
  • Protect MCP tools, APIs, checkout operations, and autonomous workflows.
  • Record verification events for analytics and incident investigation.
Introduction

Quickstart

Install SDK 0.2.0, create a verification client, and protect an endpoint.

  1. 1

    Create a project

    Open the AgentBouncer dashboard, create a project, and configure the externally visible origin of the protected service.

  2. 2

    Start in Monitor only

    Use MONITOR_ONLY during the initial integration. Requests continue to run while AgentBouncer records what would have been blocked.

  3. 3

    Create a project API key

    The API key authenticates your protected backend when it calls AgentBouncer. It is not an agent signing key.

  4. 4

    Install SDK 0.2.0

    Use the SDK on the protected server to verify requests and on your agent backend to create signed requests.

  5. 5

    Authorize with allowed

    Use verification.allowed as the final authorization decision. verified only describes cryptographic verification.

Install SDK 0.2.0
bash
npm install @agentbouncer/sdk@0.2.0
Protected server
typescript
import {
      createAgentBouncer,
    } from "@agentbouncer/sdk";
    
    const agentBouncer =
      createAgentBouncer({
        apiKey:
          process.env.AGENTBOUNCER_API_KEY!,
        publicOrigin:
          "https://api.example.com",
      });
    
    export async function POST(
      request: Request
    ) {
      const verification =
        await agentBouncer.verify({
          request,
          expectedTag: "web-bot-auth",
          action: "tools:call",
          tool: "weather",
        });
    
      if (!verification.allowed) {
        return Response.json(
          {
            error: "agent_access_denied",
            verification,
          },
          {
            status:
              verification.verified
                ? 403
                : 401,
          }
        );
      }
    
      const body = await request.json();
    
      return Response.json({
        ok: true,
        city: body.city,
        agent: verification.agent,
        user: verification.oauth,
      });
    }
Signing agent
typescript
import {
      createAgentBouncerSigner,
    } from "@agentbouncer/sdk";
    
    const signer =
      createAgentBouncerSigner({
        privateJwk: JSON.parse(
          process.env.AGENT_PRIVATE_JWK!
        ),
        keyId:
          process.env.AGENT_KEY_ID,
        signatureAgent:
          process.env.AGENT_SIGNATURE_AGENT!,
        expiresInMs: 60_000,
      });
    
    const request =
      await signer.signJson({
        url:
          "https://api.example.com/api/mcp/weather",
        method: "POST",
        json: {
          city: "Berlin",
        },
        accessToken:
          userOAuthAccessToken,
      });
    
    const response = await fetch(request);

Create a new signature for every request

A successfully verified signature is consumed by replay protection. Do not resend the same signed Request. Call sign() or signJson() again before every attempt, including retries after OAuth authorization.

Concepts

Core concepts

Understand projects, API keys, signing keys, providers, and policies.

ConceptPurpose
ProjectA protected application, API, MCP server, origin, or trust boundary.
Project API keyAuthenticates your backend when calling /api/v1/verify.
Project signing keyIdentifies and signs requests from one of your own agents.
ProviderA registered external AI-agent provider with a signature identity and public-key directory.
Provider keyA public key discovered in the provider HTTP Message Signatures directory.
Verification eventAn immutable record of the cryptographic result, policy decision, signer, risk, and request metadata.
PolicyRules that convert verification and trust information into allowed=true or allowed=false.
User OAuth access tokenAuthenticates the end user independently from the signed agent identity.
OAuth issuerA trusted authorization server configuration containing issuer, JWKS resolution, audience, and optional required scopes.
Content-DigestBinds the HTTP Message Signature to the exact bytes of a request body.
Replay protectionConsumes successfully verified signatures so that the same signature cannot authorize another enforced request.

API keys and signing keys are different

The project API key authorizes calls from your server to AgentBouncer. A signing key is held by an agent and signs the original HTTP request.

Agent and user identity are independent

A valid agent signature does not authenticate an end user. A valid user OAuth token does not authenticate the calling agent. Policies can require both identities.

Dashboard

Create and manage projects

Configure the trust boundary for an application or MCP server.

  1. 1

    Create the project

    Choose a descriptive name such as Production MCP, Checkout API, or Internal automation.

  2. 2

    Set the origin

    Use the public origin that agents call. Reverse-proxy and internal deployment URLs should not replace the externally signed URL.

  3. 3

    Choose a policy

    Start with MONITOR_ONLY, review verification events, and then move to BLOCK_UNKNOWN or a stricter policy.

  4. 4

    Register OAuth issuers when required

    Configure each accepted issuer, optional JWKS URI, expected audience, and optional issuer-level required scopes.

  5. 5

    Create project signing keys if needed

    Use project keys for internal agents, development clients, and integrations that are owned by the project.

  6. 6

    Review events

    Inspect verified, allowed, reason, signer type, provider, project key, action, tool, OAuth status, OAuth issuer, risk, replay status, and policy details.

Recommended project boundaries

Use separate projects for production and development. Consider separate projects when applications have materially different trust or authorization requirements.

Dashboard

Project API keys

Authenticate your server when calling the AgentBouncer API.

Project API keys use the ab_live_ prefix and must only be stored on your backend. Never expose them in browser JavaScript, public repositories, or client-side environment variables.

Protected server environment
bash
AGENTBOUNCER_API_KEY=ab_live_YOUR_API_KEY
        AGENTBOUNCER_PUBLIC_ORIGIN=https://api.example.com
        
        # Optional. The SDK uses the production endpoint by default.
        AGENTBOUNCER_VERIFY_URL=https://agentbouncer.io/api/v1/verify
Signing agent environment
bash
AGENT_KEY_ID=YOUR_KEY_ID
        AGENT_SIGNATURE_AGENT=https://agent.example.com
        AGENT_PRIVATE_JWK='{"kty":"OKP","crv":"Ed25519","x":"...","d":"...","kid":"..."}'
  • Store production keys in your hosting provider secret manager.
  • Rotate a key if it was logged or committed accidentally.
  • Do not send the API key to the incoming AI agent.
  • Do not confuse the AgentBouncer API key with a private signing JWK.
  • User OAuth access tokens are separate from both the AgentBouncer API key and the agent private signing key.
Integration

REST API integration

Call AgentBouncer from any language or backend framework.

POST /api/v1/verify
http
POST /api/v1/verify HTTP/1.1
        Host: agentbouncer.io
        Authorization: Bearer ab_live_YOUR_API_KEY
        X-Agent-User-Token: Bearer USER_OAUTH_ACCESS_TOKEN
        Content-Type: application/json
        Accept: application/json
        
        {
          "url": "https://merchant.example/api/mcp/weather",
          "method": "POST",
          "headers": {
            "signature": "sig1=:...:",
            "signature-input": "sig1=(\"@method\" \"@authority\" \"@path\" \"content-digest\" \"signature-agent\");created=...;expires=...;keyid=\"...\";tag=\"web-bot-auth\"",
            "signature-agent": "\"https://agent.example\"",
            "content-digest": "sha-256=:...:",
            "content-type": "application/json"
          },
          "bodyDigest": "sha-256=:...:",
          "expectedTag": "web-bot-auth",
          "action": "tools:call",
          "tool": "weather",
          "userAgent": "ExampleAgent/1.0"
        }
FieldRequiredDescription
urlYesThe exact external URL whose request was signed.
methodRecommendedThe original HTTP method. Defaults to GET.
headersRecommendedOriginal request headers used for signature verification. Signature fields may alternatively be supplied through signature, signatureInput, and signatureAgent.
signatureConditionalThe Signature header value. Required if it is not included in headers.
signatureInputConditionalThe Signature-Input header value. Required if it is not included in headers.
signatureAgentNoThe Signature-Agent structured-field value. It may alternatively be included in headers.
expectedTagNoThe intent tag expected by the protected endpoint.
actionNoA semantic operation such as tools:call, products:read, checkout, or orders:write.
toolNoThe MCP tool or application capability being accessed.
userAgentNoThe original requester User-Agent for analytics.
bodyDigestRecommended for body requestsThe Content-Digest value covered by the signature. The protected server must separately compare it with the actual request body.
X-Agent-User-TokenNoUser OAuth access token forwarded separately from the agent signature. Send it as a Bearer token on the request to AgentBouncer.

REST verification cannot see the original body

The AgentBouncer API receives signature metadata but not the original protected request bytes. If you call the REST API manually, compare Content-Digest with the actual body inside the protected application before calling /api/v1/verify. The JavaScript SDK performs this check automatically.

Preserve the signed URL

Do not replace the public target URL with the AgentBouncer verification endpoint URL, an internal container URL, or a reverse-proxy URL.

Integration

JavaScript and TypeScript SDK

Sign outgoing agent requests, verify incoming requests, validate Content-Digest, and forward user OAuth tokens.

Install SDK 0.2.0
bash
npm install @agentbouncer/sdk@0.2.0

Runtime requirement

@agentbouncer/sdk 0.2.0 requires Node.js 18 or newer and uses the standard Request, Headers, and fetch APIs.

  • createAgentBouncer() creates a server-side verification client.
  • createAgentBouncerSigner() creates signed outgoing HTTP requests.
  • signJson() serializes JSON and creates Content-Digest automatically.
  • verify() validates Content-Digest against the actual incoming body by default.
  • verify() forwards the incoming Authorization bearer token as the user OAuth token by default.
  • isOAuthRequired() detects requests that require user authorization.
  • isOAuthScopeDenied() detects insufficient OAuth scopes.
  • getRequiredOAuthScopes() returns scopes from an OAuth challenge hint.
Create a verification client
typescript
import {
      createAgentBouncer,
    } from "@agentbouncer/sdk";
    
    export const agentBouncer =
      createAgentBouncer({
        apiKey:
          process.env.AGENTBOUNCER_API_KEY!,
    
        publicOrigin:
          process.env.AGENTBOUNCER_PUBLIC_ORIGIN,
    
        verifyUrl:
          process.env.AGENTBOUNCER_VERIFY_URL,
    
        timeoutMs: 5_000,
    
        // Default: true.
        validateContentDigest: true,
      });
Verify an incoming request
typescript
const verification =
      await agentBouncer.verify({
        request,
        expectedTag: "web-bot-auth",
        action: "tools:call",
        tool: "weather",
    
        // Default: true.
        // Extracts Authorization: Bearer ...
        // and forwards it to AgentBouncer.
        forwardAuthorization: true,
      });
    
    if (!verification.allowed) {
      return Response.json(
        {
          error: "agent_access_denied",
          verification,
        },
        {
          status:
            verification.verified
              ? 403
              : 401,
        }
      );
    }

Verify before reading the body

Call verify() before request.json(), request.text(), or request.arrayBuffer(). The SDK uses request.clone() to validate Content-Digest without consuming the original request body.

Pass a user token explicitly
typescript
const verification =
      await agentBouncer.verify({
        request,
        action: "orders:create",
        tool: "orders",
    
        userToken:
          userOAuthAccessToken,
    
        forwardAuthorization: false,
      });
Create a signing client
typescript
import {
      createAgentBouncerSigner,
    } from "@agentbouncer/sdk";
    
    const signer =
      createAgentBouncerSigner({
        privateJwk: JSON.parse(
          process.env.AGENT_PRIVATE_JWK!
        ),
    
        keyId:
          process.env.AGENT_KEY_ID,
    
        signatureAgent:
          process.env.AGENT_SIGNATURE_AGENT!,
    
        expiresInMs: 60_000,
      });
Sign and send JSON
typescript
const signedRequest =
      await signer.signJson({
        url:
          "https://mcp.example.com/api/mcp/weather",
    
        method: "POST",
    
        headers: {
          "user-agent":
            "Example Agent/1.0",
        },
    
        json: {
          city: "Berlin",
        },
    
        accessToken:
          userOAuthAccessToken,
      });
    
    const response =
      await fetch(signedRequest);

For requests with a body, the signer covers @method, @authority, @path, content-digest, and signature-agent. The OAuth Authorization header is added after the HTTP Message Signature is created and is not included in the covered components.

OAuth result helpers
typescript
import {
      getRequiredOAuthScopes,
      isOAuthRequired,
      isOAuthScopeDenied,
    } from "@agentbouncer/sdk";
    
    if (!verification.allowed) {
      if (isOAuthRequired(verification)) {
        const requiredScopes =
          getRequiredOAuthScopes(
            verification
          );
    
        return Response.json(
          {
            error: "oauth_required",
            requiredScopes,
            verification,
          },
          {
            status: 401,
          }
        );
      }
    
      if (
        isOAuthScopeDenied(
          verification
        )
      ) {
        return Response.json(
          {
            error:
              "insufficient_scope",
            verification,
          },
          {
            status: 403,
          }
        );
      }
    }
Content-Digest utilities
typescript
import {
      createContentDigest,
      verifyContentDigest,
      verifyRequestContentDigest,
    } from "@agentbouncer/sdk";
    
    const body =
      JSON.stringify({
        city: "Berlin",
      });
    
    const contentDigest =
      createContentDigest(body);
    
    const valid =
      verifyContentDigest(
        body,
        contentDigest
      );
    
    const requestCheck =
      await verifyRequestContentDigest(
        request
      );
require()
typescript
import {
      AgentBouncerDeniedError,
      AgentBouncerError,
    } from "@agentbouncer/sdk";
    
    try {
      const verification =
        await agentBouncer.require({
          request,
          action: "tools:call",
          tool: "weather",
        });
    
      return runProtectedTool({
        agent: verification.agent,
        user: verification.oauth,
      });
    } catch (error) {
      if (
        error instanceof
        AgentBouncerDeniedError
      ) {
        return Response.json(
          {
            error:
              "agent_access_denied",
            verification:
              error.verification,
          },
          {
            status:
              error.verification.verified
                ? 403
                : 401,
          }
        );
      }
    
      if (
        error instanceof
        AgentBouncerError
      ) {
        return Response.json(
          {
            error:
              "agentbouncer_unavailable",
          },
          {
            status: 503,
          }
        );
      }
    
      throw error;
    }

verify() or require()?

Use verify() when you need custom OAuth challenges, WWW-Authenticate headers, or different responses for insufficient scopes. Use require() for simpler allow-or-throw integrations.

Integration

Next.js App Router

Protect a Next.js route while preserving the external request URL.

src/lib/agentbouncer-client.ts
typescript
import {
              createAgentBouncer,
            } from "@agentbouncer/sdk";
            
            const apiKey =
              process.env.AGENTBOUNCER_API_KEY;
            
            const publicOrigin =
              process.env.AGENTBOUNCER_PUBLIC_ORIGIN;
            
            if (!apiKey) {
              throw new Error(
                "Missing AGENTBOUNCER_API_KEY"
              );
            }
            
            if (!publicOrigin) {
              throw new Error(
                "Missing AGENTBOUNCER_PUBLIC_ORIGIN"
              );
            }
            
            export const agentBouncer =
              createAgentBouncer({
                apiKey,
                publicOrigin,
                verifyUrl:
                  process.env.AGENTBOUNCER_VERIFY_URL,
                timeoutMs: 5_000,
                validateContentDigest: true,
              });
src/app/api/mcp/weather/route.ts
typescript
import {
        AgentBouncerDeniedError,
        AgentBouncerError,
      } from "@agentbouncer/sdk";
      import {
        NextRequest,
        NextResponse,
      } from "next/server";
      import {
        agentBouncer,
      } from "@/lib/agentbouncer-client";
      
      export const runtime = "nodejs";
      export const dynamic = "force-dynamic";
      
      export async function POST(
        req: NextRequest
      ) {
        const requestId = crypto.randomUUID();
      
        try {
          const verification =
            await agentBouncer.verify({
              request: req,
              expectedTag: "web-bot-auth",
              action: "tools:call",
              tool: "weather",
              forwardAuthorization: true,
            });
          if (!verification.allowed) {
            }  
      
          const body = await req
            .json()
            .catch(() => ({}));
      
          const city =
            typeof body.city === "string"
              ? body.city
              : "Berlin";
      
          return NextResponse.json({
            ok: true,
            city,
            weather: {
              temperatureC: 21,
              conditions: "Clear",
            },
            agent: verification.agent,
            requestId,
          });
        } catch (error) {
          if (
            error instanceof
            AgentBouncerDeniedError
          ) {
            return NextResponse.json(
              {
                ok: false,
                error: "mcp_access_denied",
                reason:
                  error.verification.reason,
                requestId,
              },
              {
                status:
                  error.verification.verified
                    ? 403
                    : 401,
              }
            );
          }
      
          console.error(
            "AgentBouncer verification failed",
            {
              requestId,
              error,
            }
          );
      
          return NextResponse.json(
            {
              ok: false,
              error:
                "agentbouncer_unavailable",
              message:
                error instanceof
                AgentBouncerError
                  ? error.message
                  : "Unable to verify the request.",
              requestId,
            },
            {
              status: 503,
            }
          );
        }
      }

Verification must run before req.json()

The SDK validates Content-Digest against the original request bytes. Run agentBouncer.verify() before parsing the incoming request body.

Integration

Manual REST integration

Call the REST API manually while validating the request body and forwarding user OAuth separately.

Manual integrations must validate the body locally

The REST verification endpoint cannot access the original protected request body. Before calling AgentBouncer, compare Content-Digest with the exact incoming bytes. Prefer the JavaScript SDK when your runtime supports it.

verify-request-content-digest.ts
typescript
import {
          createHash,
          timingSafeEqual,
        } from "node:crypto";
        
        function parseSha256Digest(
          value: string
        ) {
          const match = value.match(
            /(?:^|,\s*)sha-256=:([^:]+):(?:\s*,|$)/i
          );
        
          if (!match?.[1]) {
            return null;
          }
        
          return Buffer.from(
            match[1],
            "base64"
          );
        }
        
        export async function verifyIncomingContentDigest(
          request: Request
        ) {
          const contentDigest =
            request.headers.get(
              "content-digest"
            );
        
          if (!contentDigest) {
            return {
              present: false,
              valid: null,
              contentDigest: null,
            };
          }
        
          if (
            request.method === "GET" ||
            request.method === "HEAD"
          ) {
            return {
              present: true,
              valid: true,
              contentDigest,
            };
          }
        
          const expected =
            parseSha256Digest(
              contentDigest
            );
        
          if (!expected) {
            return {
              present: true,
              valid: false,
              contentDigest,
            };
          }
        
          const body = Buffer.from(
            await request
              .clone()
              .arrayBuffer()
          );
        
          const actual =
            createHash("sha256")
              .update(body)
              .digest();
        
          const valid =
            actual.length ===
              expected.length &&
            timingSafeEqual(
              actual,
              expected
            );
        
          return {
            present: true,
            valid,
            contentDigest,
          };
        }
verify-agent-request.ts
typescript
import type {
          NextRequest,
        } from "next/server";
        
        import {
          pickVerificationHeaders,
        } from "./pick-verification-headers";
        
        import {
          verifyIncomingContentDigest,
        } from "./verify-request-content-digest";
        
        function resolvePublicTargetUrl(
          req: NextRequest
        ) {
          const publicOrigin =
            process.env
              .AGENTBOUNCER_PUBLIC_ORIGIN;
        
          if (!publicOrigin) {
            return req.nextUrl.toString();
          }
        
          return new URL(
            `${req.nextUrl.pathname}${req.nextUrl.search}`,
            publicOrigin
          ).toString();
        }
        
        export async function verifyAgentRequest(
          req: NextRequest,
          options?: {
            expectedTag?: string;
            action?: string;
            tool?: string;
          }
        ) {
          const digestCheck =
            await verifyIncomingContentDigest(
              req
            );
        
          if (
            digestCheck.present &&
            digestCheck.valid === false
          ) {
            return {
              verified: false,
              allowed: false,
              reason:
                "content_digest_mismatch",
              checks: {
                signature: "unknown",
                replay: "unknown",
                contentDigest:
                  "invalid",
                policy: "blocked",
              },
            };
          }
        
          const requestHeaders:
            Record<string, string> = {
              Authorization:
                `Bearer ${process.env.AGENTBOUNCER_API_KEY}`,
              "Content-Type":
                "application/json",
              Accept:
                "application/json",
            };
        
          const userAuthorization =
            req.headers.get(
              "authorization"
            );
        
          if (userAuthorization) {
            requestHeaders[
              "X-Agent-User-Token"
            ] = userAuthorization;
          }
        
          const response = await fetch(
            process.env
              .AGENTBOUNCER_VERIFY_URL ??
              "https://agentbouncer.io/api/v1/verify",
            {
              method: "POST",
              headers:
                requestHeaders,
              body: JSON.stringify({
                url:
                  resolvePublicTargetUrl(
                    req
                  ),
                method:
                  req.method,
                headers:
                  pickVerificationHeaders(
                    req
                  ),
                bodyDigest:
                  digestCheck
                    .contentDigest,
                expectedTag:
                  options?.expectedTag ??
                  null,
                action:
                  options?.action ??
                  null,
                tool:
                  options?.tool ??
                  null,
                userAgent:
                  req.headers.get(
                    "user-agent"
                  ),
              }),
              cache: "no-store",
            }
          );
        
          const result =
            await response
              .json()
              .catch(() => null);
        
          if (!response.ok) {
            throw new Error(
              result?.detail ||
              result?.reason ||
              `AgentBouncer returned HTTP ${response.status}.`
            );
          }
        
          if (
            !result ||
            typeof result.verified !==
              "boolean" ||
            typeof result.allowed !==
              "boolean"
          ) {
            throw new Error(
              "AgentBouncer returned an invalid response."
            );
          }
        
          return result;
        }

Authorization is not part of the agent signature

Do not add the incoming Authorization header to the signed-header payload. Forward it separately to AgentBouncer as X-Agent-User-Token.

Integration

Express integration

Protect Express routes using the REST verification endpoint.

Recommended Express approach

Use @agentbouncer/sdk with a standard Request constructed from the original URL, method, headers, and raw body. Manual REST integrations must implement equivalent Content-Digest validation.

typescript
import type {
    NextFunction,
    Request,
    Response,
  } from "express";
  
  export async function requireVerifiedAgent(
    req: Request,
    res: Response,
    next: NextFunction
  ) {
    try {
      const protocol =
        req.headers["x-forwarded-proto"] || req.protocol;
  
      const host =
        req.headers["x-forwarded-host"] ||
        req.headers.host;
  
      const targetUrl =
        `${protocol}://${host}${req.originalUrl}`;
  
      const verifyResponse = await fetch(
        "https://agentbouncer.io/api/v1/verify",
        {
          method: "POST",
          headers: {
            Authorization:
              `Bearer ${process.env.AGENTBOUNCER_API_KEY}`,
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            url: targetUrl,
            method: req.method,
            headers: {
              signature: req.headers.signature,
              "signature-input":
                req.headers["signature-input"],
              "signature-agent":
                req.headers["signature-agent"],
              "content-type":
                req.headers["content-type"],
              "content-digest":
                req.headers["content-digest"],
            },
            expectedTag: "web-bot-auth",
            action: "api:call",
          }),
        }
      );
  
      const verification = await verifyResponse.json();
  
      if (!verification.allowed) {
        res.status(verification.verified ? 403 : 401).json({
          ok: false,
          error: "agent_access_denied",
          verification,
        });
  
        return;
      }
  
      res.locals.agentVerification = verification;
      next();
    } catch (error) {
      next(error);
    }
  }
Integration

Protect MCP tools

Apply AgentBouncer decisions before executing MCP operations.

Set action to tools:call and tool to the MCP tool name. This makes verification events and custom policy rules specific to the operation being requested.

MCP authorization pattern
typescript
import {
          isOAuthRequired,
          isOAuthScopeDenied,
        } from "@agentbouncer/sdk";
        
        const verification =
          await agentBouncer.verify({
            request,
            expectedTag:
              "web-bot-auth",
            action:
              "tools:call",
            tool:
              toolName,
          });
        
        if (!verification.allowed) {
          if (
            isOAuthRequired(
              verification
            )
          ) {
            return {
              status: 401,
              error:
                "oauth_required",
              verification,
            };
          }
        
          if (
            isOAuthScopeDenied(
              verification
            )
          ) {
            return {
              status: 403,
              error:
                "insufficient_scope",
              verification,
            };
          }
        
          return {
            status:
              verification.verified
                ? 403
                : 401,
            error:
              "mcp_access_denied",
            verification,
          };
        }
        
        // Parse and execute the tool only
        // after verification.allowed is true.

Verify before parsing tool arguments

When the MCP request body is covered by Content-Digest, call verify() before reading or transforming the request body.

OperationSuggested actionSuggested tool
Read an MCP resourceresources:readresource name
List toolstools:list*
Call a tooltools:calltool name
Read productsproducts:readcatalog
Create checkoutcheckout:createcheckout
Submit an orderorders:createorders
Examples

Complete MCP and OAuth example

A complete test implementation with a signing agent, protected MCP route, OAuth authorization-code flow, PKCE, and user scopes.

Reference implementation

This example is a test implementation rather than a required AgentBouncer architecture. Replace the weather tool, OAuth server, cookies, routes, and scopes with your application-specific equivalents.

Each file is shown below together with its purpose. Expand a block to inspect or copy its complete source code.

client/src/app/(standalone)/weather-demo/page.tsx
tsx

Демонстрационный интерфейс, который вызывает weather endpoint и перенаправляет пользователя на OAuth, если сервер возвращает oauthRequired.

client/app/api/demo/weather/route.ts
typescript

Создаёт новый подписанный MCP-запрос и добавляет к нему пользовательский access token.

client/app/api/demo/oauth/start/route.ts
typescript

Создаёт OAuth state, PKCE verifier и challenge, после чего перенаправляет пользователя на endpoint авторизации.

client/app/api/demo/oauth/callback/route.ts
typescript

Проверяет OAuth state, обменивает authorization code и сохраняет полученный access token.

mcp/src/app/api/mcp/weather/route.ts
typescript

Проверяет подпись агента, digest тела запроса, OAuth access token и необходимые scopes перед выполнением weather tool.

mcp/src/app/api/oauth/authorize/route.ts
typescript

Проверяет запрос авторизации и выдаёт короткоживущий authorization code, связанный с PKCE и запрошенным resource.

mcp/src/app/api/oauth/token/route.ts
typescript

Поглощает authorization code, проверяет PKCE и выдаёт подписанный JWT access token.

mcp/src/app/api/oauth/jwks/route.ts
typescript

Публикует открытый ключ, с помощью которого защищённые ресурсы проверяют OAuth access tokens.

mcp/src/lib/mcp-oauth.ts
typescript

Содержит общую реализацию authorization codes, PKCE-хешей, OAuth signing keys и создания access tokens.

mcp/.well-known/oauth-authorization-server
typescript

Публикует метаданные OAuth-сервера, включая authorization, token и JWKS endpoints.

mcp/.well-known/oauth-protected-resource/api/mcp
typescript

Публикует метаданные защищённого ресурса, адрес OAuth-сервера и scopes, поддерживаемые MCP endpoint.

Do not reuse the first signature after OAuth

The first request may pass cryptographic verification and be consumed by replay protection before policy returns oauth_token_required. After OAuth completes, the agent must create a new signature and send a new Request.

Agent identities

Project signing keys

Create identities for agents owned directly by your project.

  1. 1

    Issue a project key

    Open the project Agent keys tab and create a descriptive key such as Production MCP agent or Internal CRM.

  2. 2

    Save the private JWK

    The private key is shown once. Store it in the agent secret manager.

  3. 3

    Keep the public identity

    AgentBouncer stores the public JWK, key ID, signature-agent identity, scopes, and policy configuration.

  4. 4

    Sign requests

    Use the private JWK to produce Signature and Signature-Input headers for the exact target URL.

  5. 5

    Revoke compromised keys

    Delete or revoke a project key immediately if its private JWK is exposed.

Example environment variables
bash
AGENT_KEY_ID=YOUR_KEY_ID
  AGENT_SIGNATURE_AGENT=https://agentbouncer.io/project-agents/PROJECT_ID/KEY_ID
  AGENT_PRIVATE_JWK='{"kty":"OKP","crv":"Ed25519","x":"...","d":"..."}'
Create a signer
typescript
import {
          createAgentBouncerSigner,
        } from "@agentbouncer/sdk";
        
        const signer =
          createAgentBouncerSigner({
            privateJwk:
              JSON.parse(
                process.env
                  .AGENT_PRIVATE_JWK!
              ),
            keyId:
              process.env
                .AGENT_KEY_ID,
            signatureAgent:
              process.env
                .AGENT_SIGNATURE_AGENT!,
            expiresInMs:
              60_000,
          });

The private JWK is shown only once

Store it immediately in a secret manager. If the private JWK is exposed in logs, chat, documentation, source control, or build output, revoke the key and issue a replacement.

Agent identities

Register an agent provider

Publish a provider identity and public-key directory.

  1. 1

    Create the provider

    Choose a stable slug, provider name, website, signature-agent origin, and directory URL.

  2. 2

    Publish the public JWKS

    Expose public keys at the configured HTTP Message Signatures directory.

  3. 3

    Refresh keys

    Ask AgentBouncer to load and validate the directory.

  4. 4

    Verify the provider domain

    Publish the AgentBouncer DNS TXT record or verification file.

  5. 5

    Sign production requests

    Use a private key corresponding to an active public key in the directory.

Example JWKS document
json
{
    "keys": [
      {
        "kty": "OKP",
        "crv": "Ed25519",
        "kid": "provider-key-2026-01",
        "alg": "EdDSA",
        "use": "sig",
        "x": "PUBLIC_KEY_MATERIAL"
      }
    ]
  }
Recommended public location
text
https://agent.example/.well-known/http-message-signatures-directory/jwks.json

Never publish the private JWK

Only public JWK properties belong in the provider directory. The private d property must remain secret.

Authorization

Policy modes

Choose how verified identity and trust data affect access.

ModeBehavior
MONITOR_ONLYRecords verification and policy failures but allows the request.
BLOCK_UNKNOWNBlocks unsigned requests and unknown identities.
ALLOW_TIER_1_ONLYAllows only the highest-trust provider tier.
ALLOW_TIER_1_AND_2Allows trusted Tier 1 and Tier 2 providers.
CUSTOMEvaluates provider, tier, project-key, action, tool, and optional user OAuth requirements. Matching DENY rules take precedence over matching ALLOW rules; otherwise defaultEffect is applied.

OAuth requirements belong to ALLOW rules

OAuth is evaluated after subject, action, and tool matching on an ALLOW rule. A matching DENY rule is applied first and does not produce an OAuth challenge.

Recommended rollout

Begin with MONITOR_ONLY, inspect real traffic, define actions and tools, test project keys, and only then enable blocking.

Authorization

Custom policies

Create fine-grained allow and deny rules.

Example custom policy
json
{
    "version": 1,
    "defaultEffect": "DENY",
    "rules": [
      {
        "id": "rule_allow_tier_1_read",
        "name": "Allow Tier 1 catalog reads",
        "enabled": true,
        "effect": "ALLOW",
        "subject": {
          "type": "PROVIDER_TIER",
          "tiers": ["TIER_1"]
        },
        "actions": ["products:read"],
        "tools": ["catalog"]
      },
      {
        "id": "rule_deny_external_checkout",
        "name": "Deny external checkout",
        "enabled": true,
        "effect": "DENY",
        "subject": {
          "type": "ANY_PROVIDER"
        },
        "actions": ["checkout:create"],
        "tools": ["checkout"]
      },
      {
"id": "rule_allow_oauth_checkout",
"name": "Allow checkout with user authorization",
"enabled": true,
"effect": "ALLOW",
"subject": {
"type": "ANY_PROJECT_KEY"
},
"actions": [
"checkout:create"
],
"tools": [
"checkout"
],
"oauth": {
"required": true,
"scopes": [
  "checkout:write"
]
}
}
    ]
  }
  • DENY rules take precedence over ALLOW rules.
  • An empty actions array matches any action.
  • An empty tools array matches any tool.
  • The wildcard * matches any action or tool.
  • If no rule matches, defaultEffect is applied.
  • Use PROJECT_KEY to target selected internal agent keys.
  • OAuth requirements are evaluated on matching ALLOW rules.
  • oauth.scopes requires every listed scope.
  • oauth.anyScopes requires at least one listed scope.
  • A matching DENY rule takes precedence before OAuth requirements are evaluated.
Authorization

User OAuth authorization

Combine a verified agent identity with an independently authenticated end user.

Agent identity and user identity are separate. The HTTP Message Signature identifies the calling agent. The OAuth access token identifies the user who authorized that agent to access a protected resource.

CredentialIdentifiesSent to
Agent private signing keyThe AI agent or agent providerThe protected MCP or API endpoint through HTTP Message Signatures
AgentBouncer project API keyThe protected AgentBouncer projectAgentBouncer /api/v1/verify
User OAuth access tokenThe authorizing end userThe protected endpoint and then AgentBouncer as X-Agent-User-Token
  1. 1

    Register an OAuth issuer

    Add the issuer, optional JWKS URI, expected audience, and optional globally required scopes to the AgentBouncer project.

  2. 2

    Add OAuth to a custom ALLOW rule

    Set oauth.required to true and configure scopes or anyScopes.

  3. 3

    Send the signed request

    The agent creates a new HTTP Message Signature and optionally adds Authorization: Bearer USER_TOKEN after signing.

  4. 4

    Forward the token

    The verification SDK extracts the incoming Authorization bearer token and forwards it to AgentBouncer separately.

  5. 5

    Return an OAuth challenge

    If authorization is required, respond with HTTP 401 and a WWW-Authenticate header describing the protected resource and scopes.

  6. 6

    Create a new signature after OAuth

    After the user completes OAuth, create and send a new signed request. Never reuse the signature from the unauthenticated attempt.

Custom policy with required OAuth scopes
json
{
      "version": 1,
      "defaultEffect": "DENY",
      "rules": [
        {
          "id": "allow_weather_with_oauth",
          "name": "Allow weather with user authorization",
          "enabled": true,
          "effect": "ALLOW",
          "subject": {
            "type": "ANY_PROJECT_KEY"
          },
          "actions": [
            "read.weather"
          ],
          "tools": [
            "weather"
          ],
          "oauth": {
            "required": true,
            "scopes": [
              "mcp:weather:read"
            ]
          }
        }
      ]
    }
OAuth challenge response
http
HTTP/1.1 401 Unauthorized
    Cache-Control: no-store
    WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/api/mcp", scope="mcp:weather:read"
    
    {
      "error": "oauth_required",
      "verification": {
        "verified": true,
        "allowed": false,
        "reason": "oauth_token_required",
        "hint": {
          "type": "oauth_required",
          "requiredScopes": [
            "mcp:weather:read"
          ],
          "registeredIssuers": [
            "https://auth.example.com"
          ]
        }
      }
    }
Insufficient scope response
http
HTTP/1.1 403 Forbidden
    Cache-Control: no-store
    WWW-Authenticate: Bearer error="insufficient_scope", scope="mcp:weather:read"
    
    {
      "error": "insufficient_scope",
      "verification": {
        "verified": true,
        "allowed": false,
        "reason": "oauth_missing_scopes:mcp:weather:read"
      }
    }
OAuth statusMeaning
noneNo user access token was provided.
validThe token signature, issuer, audience, expiration, and issuer-level scopes were accepted.
invalidToken verification failed, required claims were invalid, or issuer-level scopes were missing.
unknown_issuerThe token issuer is not registered for the AgentBouncer project.
malformedThe provided value is not a valid JWT or has no issuer claim.

OAuth flow remains application-specific

The SDK forwards and classifies OAuth tokens, but authorization-code, PKCE, redirects, consent, cookies, token storage, and refresh-token handling remain the responsibility of the agent application and OAuth authorization server.

API reference

Verification response

Understand the fields returned by the verification API.

json
{
          "verified": true,
          "allowed": true,
          "reason": null,
          "checks": {
            "signature": "valid",
            "timestamp": "valid",
            "replay": "checked",
            "provider": "unknown",
            "projectKey": "known",
            "policy": "matched"
          },
          "agent": {
            "type": "PROJECT_KEY",
            "provider": null,
            "providerSlug": null,
            "projectKey": {
              "id": "key_id",
              "name": "Production MCP agent",
              "scopes": []
            },
            "signatureAgent": "https://agentbouncer.io/project-agents/...",
            "keyid": "..."
          },
          "request": {
            "method": "POST",
            "path": "/api/mcp/weather",
            "tag": "web-bot-auth",
            "expectedTag": "web-bot-auth",
            "action": "read.weather",
            "tool": "weather"
          },
          "risk": {
            "score": 5,
            "level": "low"
          },
          "oauth": {
            "status": "valid",
            "authenticated": true,
            "sub": "user_123",
            "issuer": "https://auth.example.com",
            "scopes": [
              "mcp:weather:read"
            ],
            "error": null
          },
          "policy": {
            "mode": "CUSTOM",
            "wouldBlockReason": null,
            "matchedRule": {
              "id": "allow_weather_with_oauth",
              "name": "Allow weather with user authorization",
              "effect": "ALLOW"
            },
            "defaultEffectApplied": false
          }
        }

Replay protection

After successful cryptographic verification, AgentBouncer consumes the signature in replay storage. A second enforced request with the same Signature and key ID is rejected with replay_detected.

FieldMeaning
verifiedWhether the cryptographic identity verification succeeded.
allowedThe final authorization decision after applying project policy.
reasonThe effective denial reason, or null when access is allowed.
checksIndividual signature, timestamp, replay, provider, project-key, and policy results. unknown means that a check was not performed or sufficient information was not available.
agentResolved provider or project-key identity.
riskCalculated risk score and level.
policyPolicy mode, matching rule, and hypothetical block reason.
oauthUser OAuth verification status, subject, issuer, scopes, and error.
hintOptional OAuth challenge metadata containing required scopes and registered issuers.
missingSignedComponentsRequired HTTP Message Signature components missing from a body-request signature profile.
checks.contentDigestLocal SDK body-integrity result. It is normally present when the SDK rejects a request with content_digest_mismatch before contacting AgentBouncer.
API reference

Reasons and errors

Common verification and policy decision reasons.

ReasonDescription
unauthorizedMissing or invalid AgentBouncer project API key.
invalid_requestThe verification payload is missing a required field.
invalid_urlurl is not a valid absolute target URL.
no_signatureSignature or Signature-Input is missing.
no_keyidSignature-Input does not contain keyid.
unknown_keyidNo active project or provider key was found.
not_yet_validThe signature creation time is in the future.
expiredThe signature has expired.
window_too_longThe created-to-expires signature window exceeds the permitted maximum.
bad_signatureCryptographic verification failed.
unknown_providerThe key provider is not recognized.
provider_blockedThe provider is suspended, banned, fraudulent, or revoked.
provider_not_trustedThe project requires a trusted provider.
unsupported_tagThe signature intent tag is unsupported.
tag_mismatchThe actual signature tag does not match expectedTag.
tier_too_lowThe provider does not satisfy the required tier.
trust_score_too_lowThe provider trust score is below the project threshold.
project_keys_disabledProject-owned signing keys are disabled.
custom_policy_deniedA matching custom DENY rule rejected the request.
custom_policy_no_matchNo custom rule matched and the default effect is DENY.
signature_time_requiredA production signature must include both created and expires.
replay_detectedThe same signature and key ID were already used.
weak_signature_profileThe signature does not cover all required request components.
content_digest_mismatchThe incoming request body does not match Content-Digest. This result is produced locally by the SDK.
oauth_token_requiredA matching policy rule requires a user OAuth access token.
oauth_token_noneNo user OAuth token was provided.
oauth_token_invalidThe OAuth token failed signature, expiration, audience, issuer-level scope, or claims validation.
oauth_token_malformedThe provided OAuth token is not a valid JWT or has no issuer.
oauth_token_unknown_issuerThe OAuth token issuer is not registered for this project.
oauth_missing_scopes:<scopes>The token does not contain every scope required by the matching policy rule.
oauth_missing_any_scope:<scopes>The token does not contain any of the alternative scopes required by the matching policy rule.

Required body-request signature profile

For POST, PUT, and PATCH requests in production enforcement mode, the signature must cover @method, @authority, @path, and content-digest. Signature-Agent is also included by the SDK signer.

Operations

Verification events

Monitor real traffic and investigate authorization decisions.

  • Filter events by day, week, month, all time, or custom dates.
  • Compare verified requests with allowed requests.
  • Use allowed, not verified, when labeling an event as accepted or rejected.
  • Inspect provider tier, status, trust score, abuse score, risk, action, and tool.
  • Review wouldBlockReason for requests allowed by MONITOR_ONLY.
  • Export event data as CSV for offline analysis.
  • Inspect OAuth status, subject, and issuer for user-authorized requests.
  • Treat replay_detected as a security event rather than a normal policy denial.
  • Content-Digest mismatches detected locally by the SDK do not create a remote verification event.
  • Review oauth_token_unknown_issuer and oauth_token_invalid separately from missing user authorization.
verifiedallowedRecommended UI label
truetrueAllowed
truefalseDenied by policy
falsetrueAllowed in monitor mode
falsefalseVerification failed
Operations

Security guidance

Deploy AgentBouncer without leaking secrets or weakening verification.

  • Keep AgentBouncer API keys on the server.
  • Keep private signing JWKs in a secret manager.
  • Publish only public JWK properties.
  • Use HTTPS for target URLs, signature-agent identities, and key directories.
  • Preserve the original externally visible URL and HTTP method.
  • Do not forward Authorization, Cookie, Set-Cookie, x-api-key, or proxy credentials in verification payloads.
  • Exclude reverse-proxy and observability headers unless they are intentionally signed.
  • Use short signature validity windows.
  • Revoke compromised project and provider keys.
  • Start policy changes in MONITOR_ONLY before enforcing them.
  • Run verification before parsing the incoming request body.
  • Never reuse a signed Request, including after OAuth authorization or a retry.
  • Add the user OAuth token after creating the HTTP Message Signature.
  • Do not include Authorization in the covered signature components.
  • Return Cache-Control: no-store for verification and OAuth responses.
  • Use short signature validity windows; the maximum supported window is five minutes.

Automatic body-integrity validation

@agentbouncer/sdk validates Content-Digest against the actual incoming Request body before calling the remote verification API. A mismatch returns content_digest_mismatch locally and does not consume the signature in replay storage.

Operations

Troubleshooting

Diagnose signature, URL, key-directory, and policy problems.

ProblemWhat to check
Invalid Signature headerEnsure Signature, Signature-Input, and Signature-Agent are each included exactly once.
bad_signatureCompare the externally visible URL, method, authority, covered components, public key, Signature-Agent, and Content-Digest.
signature_time_requiredInclude both created and expires in Signature-Input. createAgentBouncerSigner() adds them automatically.
not_yet_validCheck clock synchronization on the signing agent and protected server. AgentBouncer allows 30 seconds of clock tolerance.
expiredCreate a new signed Request. Do not retry an expired signature.
window_too_longUse expiresInMs between 1 and 300000. A one-minute validity window is recommended.
content_digest_mismatchMake sure the exact bytes sent over HTTP are the same bytes used to create Content-Digest. Do not serialize JSON twice with different formatting.
weak_signature_profileSign @method, @authority, @path, and content-digest. The SDK signer also covers signature-agent automatically.
replay_detectedCreate a new signature for every attempt. Never resend an already verified signed Request.
replay_detected after OAuthDo not resend the original unauthenticated signature. Call sign() or signJson() again after OAuth completes.
unknown_keyidConfirm that the project key is active or refresh the provider public-key directory.
unknown_providerConfirm that Signature-Agent matches the registered provider identity and that the provider key directory contains the requested key ID.
tag_mismatchCompare expectedTag with the tag in Signature-Input.
custom_policy_no_matchCheck subject type, project key ID, provider tier, action, tool, and defaultEffect.
oauth_token_requiredStart the application-specific OAuth flow, obtain a user access token, and create a new signed request containing Authorization: Bearer USER_TOKEN.
oauth_token_unknown_issuerRegister the normalized token issuer in the AgentBouncer project and confirm that discovery or jwksUri is available.
oauth_token_invalidCheck the JWT signature, expiration, issuer, expected audience, JWKS URI, and issuer-level required scopes.
oauth_token_malformedSend a JWT access token containing an iss claim. Opaque access tokens are not supported by the current verifier.
oauth_missing_scopes:<scopes>Request every scope required by oauth.scopes on the matching ALLOW rule.
oauth_missing_any_scope:<scopes>Request at least one scope listed in oauth.anyScopes on the matching ALLOW rule.
MONITOR_ONLY still blocksMake sure your application checks only verification.allowed. Do not reject solely because verified is false.
Wrong @authorityUse the external host that the agent signed, not an internal deployment or reverse-proxy host.
verification_timeoutCheck connectivity to AgentBouncer and increase timeoutMs only when necessary. The SDK default is 5000 milliseconds.
agentbouncer_unavailableTreat network, timeout, API authentication, and malformed-response failures separately from an allowed=false authorization decision.

A retry requires a new signature

Retries are new HTTP requests. Call sign() or signJson() again instead of reusing the previous signed Request, even when the previous attempt failed because OAuth authorization was required.

Correct authorization gate
typescript
if (!verification.allowed) {
    return Response.json(
      {
        ok: false,
        error: "agent_access_denied",
        verification,
      },
      {
        status: verification.verified ? 403 : 401,
      }
    );
  }
Incorrect authorization gate
typescript
// Incorrect: blocks MONITOR_ONLY requests.
  if (!verification.verified || !verification.allowed) {
    return new Response("Denied", {
      status: 403,
    });
  }
Operations

Production checklist

Review the integration before enabling blocking policies.

  • The AgentBouncer project API key is stored only on the protected server.
  • Private agent signing JWKs are stored only in the signing agent secret manager.
  • OAuth issuer private signing keys are never stored in AgentBouncer or published through JWKS.
  • The production project uses the correct externally visible public origin.
  • The application sends the exact signed target URL and HTTP method.
  • Signature, Signature-Input, Signature-Agent, and Content-Digest are preserved exactly once.
  • POST, PUT, and PATCH signatures cover @method, @authority, @path, and content-digest.
  • Verification runs before parsing or transforming the incoming request body.
  • Content-Digest is compared with the exact incoming request bytes.
  • The authorization gate checks verification.allowed.
  • Actions and MCP tool names are populated consistently with custom policy rules.
  • A new signature is created for every request and retry.
  • A new signature is created after the user completes OAuth.
  • Authorization is added after the HTTP Message Signature is created.
  • Authorization is not included in the covered signature components.
  • Accepted OAuth issuers, JWKS resolution, audiences, and scopes are configured.
  • OAuth access tokens have short expiration times and are stored securely.
  • OAuth and verification responses use Cache-Control: no-store where appropriate.
  • MONITOR_ONLY traffic has been reviewed before enabling enforcement.
  • Known providers and project keys resolve correctly.
  • Custom policies have an intentional defaultEffect.
  • Matching DENY rules and OAuth-protected ALLOW rules have been tested.
  • Replay protection has been tested by intentionally resending a signature.
  • Content-Digest rejection has been tested by changing the body after signing.
  • OAuth required, invalid token, unknown issuer, and insufficient scope responses have been tested.
  • Key revocation and rotation procedures are documented.
  • Verification event exports and incident investigation procedures have been tested.