Skip to main content
All posts

Testing Authentication in MCP Apps, ChatGPT Apps, and Claude Connectors (August 2026)

Abe Wheeler
MCP AppsMCP App TestingChatGPT AppsChatGPT App TestingClaude ConnectorsClaude Connector TestingAuthenticationOAuth
Testing OAuth and authentication in MCP Apps, ChatGPT Apps, and Claude Connectors.

Testing OAuth and authentication in MCP Apps, ChatGPT Apps, and Claude Connectors.

TL;DR: Test MCP authentication as a set of contracts, not as one long browser flow. Verify protected resource and authorization server metadata, token signature and claims, end-user and tenant resolution, scope enforcement, cross-user isolation, auth challenges, and app UI states locally. Add token expiry, revocation, key rotation, and refresh failure to provider tests. Run one real connection in every host you support because ChatGPT, Claude, and API-based MCP clients do not all own the same part of the OAuth lifecycle.


Authentication failures in an MCP App rarely come from one place. A host may discover the wrong metadata URL. The authorization server may mint a token for the wrong resource. A server may verify the signature but skip the audience. A tool may confuse the OAuth client ID with the person using the app. The resource iframe may accidentally receive or store a bearer token.

A single successful login does not test those boundaries. It proves one identity, one scope set, one token age, and one host worked once. A useful authentication suite breaks the flow into deterministic checks, then keeps a small live test for the parts owned by the host and identity provider.

This August 2026 refresh follows the November 25, 2025 MCP authorization specification. It also reflects current OpenAI plugin testing and the Claude API MCP connector, where the API caller supplies and refreshes the access token.

Start With the Trust Boundaries

For HTTP-based MCP authorization, three systems take part:

  1. The AI host or other MCP client runs the OAuth client.
  2. The authorization server identifies the resource owner and issues access tokens.
  3. Your MCP server is the protected resource and decides whether each request may run.

Your test plan should prove each handoff:

BoundaryWhat to prove
Client to authorization serverExact redirect URI, PKCE S256, state, client identity, requested scopes, and resource
Authorization server to MCP serverSignature or introspection result, issuer, audience, time claims, scopes, subject, and tenant
MCP server to toolVerified identity and permissions are present before any read or write
Tool to resource iframeOnly safe result data crosses the bridge; access and refresh tokens do not

Do not treat a host hint as access control. Tool securitySchemes, annotations, confirmation prompts, and component state help the host provide the right experience, but the MCP server still authorizes every operation.

Test OAuth Discovery as a Protocol Contract

The current MCP specification requires protected MCP servers to publish OAuth Protected Resource Metadata. A client can learn its URL from a 401 Unauthorized challenge or construct a well-known URL.

For an MCP endpoint at https://mcp.example.com/mcp, test the path-specific location first:

https://mcp.example.com/.well-known/oauth-protected-resource/mcp

You may also serve the root location:

https://mcp.example.com/.well-known/oauth-protected-resource

An unauthorized request should point at the canonical document:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp",
                         scope="invoices:read"

The protected resource document needs the canonical resource and at least one authorization server:

{
  "resource": "https://mcp.example.com/mcp",
  "authorization_servers": ["https://auth.example.com"],
  "scopes_supported": ["invoices:read"]
}

scopes_supported is useful but optional. Do not make optional RFC 9728 fields test requirements unless your client or policy depends on them.

Your contract test should also fetch the authorization server’s RFC 8414 or OpenID Connect discovery document and check that:

  • The issuer matches the issuer you validate.
  • Authorization and token endpoints use HTTPS.
  • code_challenge_methods_supported includes S256.
  • The advertised token endpoint authentication method matches your host setup.
  • The client registration method you rely on is advertised.

Client registration is no longer synonymous with dynamic client registration. The current MCP spec says clients and authorization servers should support Client ID Metadata Documents, may support dynamic client registration, and can use a pre-registered client. Test the method you deploy, plus your documented fallback.

import { describe, expect, it } from 'vitest';

const MCP_URL = 'https://mcp.example.com/mcp';
const RESOURCE_METADATA = 'https://mcp.example.com/.well-known/oauth-protected-resource/mcp';

describe('OAuth discovery', () => {
  it('challenges unauthenticated MCP requests', async () => {
    const response = await fetch(MCP_URL, { method: 'POST' });

    expect(response.status).toBe(401);
    expect(response.headers.get('www-authenticate')).toContain(
      `resource_metadata="${RESOURCE_METADATA}"`
    );
  });

  it('publishes metadata for the exact MCP resource', async () => {
    const response = await fetch(RESOURCE_METADATA);
    const metadata = await response.json();

    expect(response.status).toBe(200);
    expect(metadata.resource).toBe(MCP_URL);
    expect(metadata.authorization_servers).toEqual(['https://auth.example.com']);
  });
});

Run this against localhost, staging, and production. Proxies often cause discovery failures by dropping WWW-Authenticate, rewriting the path, or changing the public scheme and host used to build resource.

Test the Token Validator Before the OAuth Flow

Keep claim validation in a function that accepts a token and returns a verified principal. That makes the security rules easy to test without mocking a whole server module or making network calls.

For JWT access tokens, generate a test key pair and mint tokens with explicit claims. For opaque tokens, stub the authorization server’s introspection response and test active and inactive results. Cover at least:

  • Missing, malformed, and non-Bearer authorization headers.
  • An unapproved signing algorithm or invalid signature.
  • Unknown kid and a JWKS key rotation.
  • Wrong issuer.
  • Wrong audience or resource.
  • Expired exp and future nbf, with your allowed clock skew at both boundaries.
  • Missing subject, tenant, or other claims your policy requires.
  • Missing and extra scopes.
  • Revoked or inactive tokens.
  • A failed or timed-out JWKS or introspection request.

Use an allowlist for signing algorithms and pin issuer and audience in the verifier. Do not decode a JWT and treat its unsigned payload as identity.

export type Principal = {
  subject: string;
  tenantId: string;
  scopes: Set<string>;
};

export async function verifyAccessToken(token: string): Promise<Principal> {
  const { payload } = await jwtVerify(token, jwks, {
    algorithms: ['RS256'],
    issuer: AUTH_ISSUER,
    audience: MCP_RESOURCE,
    clockTolerance: 30,
  });

  if (typeof payload.sub !== 'string') throw new Error('missing subject');
  if (typeof payload.tenant_id !== 'string') throw new Error('missing tenant');

  return {
    subject: payload.sub,
    tenantId: payload.tenant_id,
    scopes: new Set(typeof payload.scope === 'string' ? payload.scope.split(' ') : []),
  };
}

Keep raw token values out of test failure messages and production logs. Log a safe request ID, issuer, denial reason, tool name, and policy decision instead.

Keep OAuth Client Identity Separate From User Identity

This distinction catches a common data-isolation bug. In the MCP TypeScript SDK, AuthInfo.clientId means the OAuth client ID associated with the token. It is not the end-user subject.

Two people using the same host can share one OAuth client ID. If a handler queries data with extra.authInfo.clientId, it can collapse many users into the same account boundary.

Resolve the user and tenant during token validation, then attach those verified values to your own request context. If you use AuthInfo.extra, define and validate its shape in one place:

import type { AuthInfo } from 'sunpeak/mcp';

type VerifiedAuth = AuthInfo & {
  extra: {
    subject: string;
    tenantId: string;
  };
};

export function principalFrom(auth: AuthInfo | undefined) {
  const extra = auth?.extra as VerifiedAuth['extra'] | undefined;
  if (!extra?.subject || !extra.tenantId) throw new Error('Unauthenticated');
  return extra;
}

Then write an isolation table, not one happy-path assertion:

TokenRequested recordExpected result
User A, tenant 1User A recordAllowed
User A, tenant 1User B record in tenant 1Denied
User A, tenant 1Record with same ID in tenant 2Denied
User B, tenant 1User B recordAllowed
Admin, tenant 1Allowed tenant 1 recordAllowed and audited

Apply the same table to reads, writes, exports, pagination cursors, search results, cached results, and backend-only tools called by the iframe. A cache key that omits tenant and subject can undo correct authorization in the handler.

Test Scopes and Step-Up Authorization

A missing or invalid token and an insufficiently scoped token are different failures.

  • Return 401 Unauthorized when authorization is missing or the token is invalid or expired.
  • Return 403 Forbidden when the token is valid but lacks permission. Include error="insufficient_scope", the needed scope, and resource_metadata in the challenge.

Test least-privilege behavior with a scope matrix:

ToolNo tokeninvoices:readinvoices:write
list_invoices401AllowedDenied unless write includes read
update_invoice401403 with invoices:write challengeAllowed

For ChatGPT tool-level OAuth, test both pieces OpenAI currently documents:

  1. The tool declares the right securitySchemes.
  2. An auth error result includes _meta["mcp/www_authenticate"] with an error and error_description.
return {
  isError: true,
  content: [{ type: 'text', text: 'Sign in to update this invoice.' }],
  _meta: {
    'mcp/www_authenticate': [
      'Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp", error="insufficient_scope", error_description="Invoice write permission is required"',
    ],
  },
};

Do not put an access token in result _meta. The field is hidden from the model in compatible hosts, but it is still delivered to the component.

Test the UI Without Giving It a Token

The app resource should render authorization state from safe tool results. It should not read the OAuth access token from the bridge, cookies, query strings, or browser storage.

In sunpeak, deterministic simulation files live in tests/simulations/. Create fixtures for every state the resource can show:

{
  "tool": "list-invoices",
  "userMessage": "Show my invoices",
  "toolInput": {},
  "toolResult": {
    "structuredContent": {
      "status": "ready",
      "invoices": [{ "id": "INV-1", "amount": 100 }]
    }
  }
}

Add separate fixtures for signed out, empty, forbidden, reauthorization required, provider unavailable, and successful reconnect. If the resource calls a protected server tool, use the simulation’s serverTools responses to model 401, 403, success, cancellation, and retry.

Your Inspector tests should assert the visible state and the security boundary:

  • No token appears in content, structuredContent, result _meta, the DOM, URLs, or console output.
  • No token is written to localStorage, sessionStorage, IndexedDB, analytics, or error reporting.
  • A signed-out state does not display cached data from the previous user.
  • A 403 names the missing permission without claiming the user is signed out.
  • Reconnect does not duplicate a write if the host retries the tool call.

sunpeak runs these resource tests in replicated ChatGPT and Claude runtimes, so you can cover host bridge behavior, display modes, and UI states without spending host credits on every change. The simulation guide has the current fixture format.

Exercise Token Expiry, Revocation, and Key Rotation

The host owns refresh-token storage in an interactive OAuth flow, but your server still owns correct access-token rejection. Do not assume every host will refresh and retry in the same way.

Test the lifecycle as observable server behavior:

  1. A valid short-lived token works.
  2. The same token receives 401 after expiry.
  3. A newly issued token works without restarting the MCP session.
  4. A revoked opaque token or denylisted JWT fails.
  5. A token signed with a newly published key works after JWKS refresh.
  6. A removed signing key stops working after the allowed cache window.
  7. Refresh failure or denied consent returns a stable signed-out state.
  8. Reauthorization attempts have a limit, so a bad scope configuration cannot loop forever.

Use fake clocks for unit tests and a development authorization tenant for provider tests. If your provider rotates refresh tokens, also test reuse detection and recovery in the client you control.

Test Each Client’s Actual Auth Responsibility

The shared MCP protocol does not make every client workflow identical.

ChatGPT and OpenAI plugins

Use MCP Inspector to run the quick OAuth flow before connecting ChatGPT. Then test in ChatGPT Developer mode with a public Streamable HTTP /mcp endpoint or Secure MCP Tunnel. Verify metadata discovery, linking UI, consent, initial tool call, scope step-up, reconnect, and unlink.

OpenAI’s current connection and testing guide also recommends representative tool inputs, missing identifiers, empty results, schema errors, annotations, confirmations, and model-readable output. Authentication should be part of that set, not a separate one-time check.

Claude

Test interactive Claude connectors in the Claude product because callback URLs, workspace policy, consent, and reconnect behavior belong to that host.

For the Claude API MCP connector, the responsibility is different. The API caller obtains an access token, sends it as authorization_token, and refreshes it when needed. The current connector uses the mcp-client-2025-11-20 beta interface. Test token acquisition and refresh in your API client, then test the MCP server’s 401 and 403 behavior independently. Anthropic’s MCP connector documentation describes using MCP Inspector’s Quick OAuth Flow to get a token for development.

Other hosts

For every host you claim to support, record:

  • Streamable HTTP and protocol version support.
  • Discovery URL behavior.
  • Client registration method.
  • Redirect URI and workspace policy.
  • Tool-level auth challenge support.
  • Scope step-up and reconnect behavior.
  • Whether the client or API caller owns token refresh.

Run the same server-side contract suite for all hosts, then add only the host-specific checks that differ.

Put the Right Tests in CI

Authentication tests should run at different frequencies based on determinism and cost.

GateTests
Every pull requestMetadata schema, 401 and 403 challenges, token claims, scopes, subject and tenant isolation, tool handlers, sunpeak simulations, and no-token-leak assertions
Scheduled provider testReal discovery, authorization code with PKCE, introspection, expiry, revocation, JWKS rotation, and provider policy
Main, nightly, or releaseLive ChatGPT and Claude connection, linking, consent, step-up, reconnect, and unlink

Generate test signing keys during the test run or keep non-production fixtures in the repository. Store provider credentials and browser sessions in your CI secret store, restrict who can run live workflows, and never print tokens in artifacts.

For writes, include idempotency and audit assertions. OAuth retries, network retries, and a user clicking twice can all repeat a tool call. Authorization proves who may act; it does not make the action safe to repeat.

Authentication Test Checklist

Before shipping an authenticated MCP App, ChatGPT App, or Claude Connector, check that:

  • Protected resource metadata resolves at the expected URL and names the exact MCP resource.
  • A 401 challenge includes resource_metadata and the needed initial scope.
  • Authorization server metadata advertises PKCE S256 and the client registration method you use.
  • JWT or introspection validation checks issuer, audience, time claims, scopes, and token activity.
  • End-user subject and tenant are separate from the OAuth clientId.
  • Cross-user and cross-tenant reads, writes, caches, cursors, and exports fail closed.
  • Missing or invalid tokens return 401, while valid underscoped tokens return 403.
  • ChatGPT tool auth includes securitySchemes and _meta["mcp/www_authenticate"].
  • The resource iframe and logs never receive access or refresh tokens.
  • Expiry, revocation, JWKS rotation, reconnect, retry limits, and duplicate writes are tested.
  • Each target host has one live OAuth test that matches its current client behavior.

Where sunpeak Fits

sunpeak helps you move most authentication testing out of manual host sessions. Use its MCP App Inspector and test fixtures to run tool calls, render fixed authorization states, test server-tool interactions, and exercise the same app in replicated ChatGPT and Claude runtimes. Those checks run locally and in CI, with HMR and automatic rebuilds during development.

Keep the small live suite too. The goal is to spend live host time on host-owned behavior, not on claim validation, tenant isolation, or UI states that deterministic tests can cover faster.

Start with the sunpeak authorization guide, use the MCP authorization specification as the protocol source, and add this matrix to your broader MCP App testing strategy.

Get Started

Documentation →
npx sunpeak new

Further Reading

Frequently Asked Questions

How do I test authentication in an MCP App without a production OAuth provider?

Use a local signing key or development identity provider to test discovery, JWT validation, tool authorization, tenant isolation, and UI states. Mint short-lived test tokens with fixed issuer, audience, subject, tenant, and scope claims. Reserve a real provider and host for a small end-to-end suite that checks the redirect, consent, token exchange, and reconnect path.

Which OAuth discovery endpoints should an MCP server test?

Test the protected resource metadata URL for your MCP endpoint, the root fallback URL when you support it, and the authorization server metadata URL. An unauthenticated request should return 401 with a WWW-Authenticate challenge that points to protected resource metadata. Confirm that resource and authorization_servers are correct, and that PKCE S256 and your client registration method are advertised by the authorization server.

Is AuthInfo.clientId the authenticated user ID?

No. In the MCP TypeScript SDK, AuthInfo.clientId is the OAuth client ID associated with the token. Resolve the end-user subject and tenant while validating the token, then place those verified values in your own request context or AuthInfo.extra. Test that tools use the subject and tenant for data access rather than treating clientId as a user ID.

How do I test that one MCP App user cannot see another user's data?

Seed two users in two tenants, including records with the same object ID, then call each protected tool as both users. Assert that every read and write includes the verified subject and tenant, and test caches, pagination cursors, exports, and server-tool calls for the same isolation rule.

How should I test expired, revoked, or underscoped access tokens?

Test each case separately. Missing, malformed, expired, revoked, wrong-issuer, and wrong-audience tokens should fail authentication with 401. A valid token that lacks permission should receive 403 with an insufficient_scope challenge. Also test clock skew, JWKS key rotation, refresh failure, reconnect, and a retry limit so the client cannot enter an authorization loop.

Can an MCP App resource iframe read the OAuth access token?

It should not. The host sends the token to the MCP server, and the server validates it before running tools. Test that tokens never appear in content, structuredContent, result metadata, DOM text, browser storage, URLs, analytics, or logs. Drive signed-in, signed-out, forbidden, empty, and reconnect UI states with safe result data.

How do I test OAuth in ChatGPT and Claude?

Start with MCP Inspector and sunpeak Inspector tests, then connect the deployed or tunneled MCP endpoint to each target host. ChatGPT tool-level OAuth needs securitySchemes, protected resource metadata, and an mcp/www_authenticate result challenge. For the Claude API MCP connector, the API caller obtains, supplies, and refreshes authorization_token, so test that caller-managed lifecycle separately.

Which authentication tests belong in CI/CD?

Run discovery contract tests, token validator tests, tool authorization and tenant-isolation tests, and deterministic sunpeak Inspector tests on every pull request. Run provider integration tests on a schedule, and keep live ChatGPT or Claude OAuth tests for main, nightly, or release gates because they depend on external accounts, browser sessions, and provider state.