MCP App Authentication: How to Add OAuth 2.1 to Your MCP App (July 2026)

Add OAuth 2.1 authentication to your MCP App for ChatGPT, Claude, and other hosts.
TL;DR: Use OAuth when your MCP App, ChatGPT App, or Claude Connector needs private user data or write access. Publish protected resource metadata on your MCP server, publish OAuth metadata on your authorization server, use authorization code with PKCE, carry the resource value through the flow, and validate issuer, audience, expiry, and scopes on every MCP request. In ChatGPT, tool-level OAuth also depends on securitySchemes and _meta["mcp/www_authenticate"].
Most MCP Apps start without authentication. You expose a tool, return structuredContent, render a resource, and test the app locally. That works until the app needs a user’s documents, issues, invoices, calendar, CRM records, analytics, or account settings.
At that point the app needs a real user identity. For MCP Apps, the standard path is OAuth. The host runs the user-facing flow, your identity provider issues tokens, and your MCP server validates those tokens before it runs tools.
This July 2026 refresh adds the parts developers now tend to miss: protected resource metadata, the resource parameter, Client ID Metadata Documents, per-tool securitySchemes, WWW-Authenticate challenges, and a local testing plan that catches auth bugs before review.
When Auth Is Worth Adding
Do not add OAuth because every app feels more official with a sign-in button. Add it when the server needs identity, permissions, or auditability.
You can usually skip auth when:
- Every user sees the same public data.
- The tool only reads public documentation or public APIs.
- The app is a local prototype with fixed mock data.
- The tool runs inside a trusted internal service account and never handles per-user data.
You probably need auth when:
- Tool output depends on the current user.
- A tool can create, update, delete, send, approve, or export data.
- You need to restrict data by workspace, tenant, role, or plan.
- Reviewers need to verify that the app asks for the right scopes.
- You need logs that show which user took which action.
Without auth, the host does not magically send your application’s user ID. Your MCP server sees an MCP request, not your web app session. If the server needs user-specific data, it needs a token it can verify.
The Current MCP OAuth Shape
An authenticated MCP App has three parties:
- Your MCP server, which is the protected resource server.
- Your authorization server, which is your identity provider.
- The AI host, such as ChatGPT or Claude, which is the OAuth client acting for the user.
The flow is still the authorization-code flow with PKCE, but MCP adds a resource-server discovery step so the host knows how to authenticate for your server.
The high-level flow:
- The host tries to call your MCP server or inspect its metadata.
- Your server exposes protected resource metadata at
/.well-known/oauth-protected-resource, or points to that URL in aWWW-Authenticatechallenge. - The host reads that metadata and finds your authorization server.
- The host reads the authorization server’s OAuth or OpenID Connect metadata.
- The host identifies itself as an OAuth client, using a Client ID Metadata Document, dynamic client registration, or a predefined client.
- The host sends the user through authorization code with PKCE.
- The authorization server returns tokens to the host’s redirect URL.
- The host sends
Authorization: Bearer <token>on MCP requests. - Your MCP server verifies the token before it runs tools.
That last step belongs to your server. ChatGPT or Claude can carry the token, but they do not make it safe for your backend to trust without checking it.
Protected Resource Metadata
Your MCP server needs an HTTPS document that tells hosts which authorization server protects it.
Serve a document like this:
{
"resource": "https://your-mcp.example.com",
"authorization_servers": ["https://auth.yourcompany.com"],
"scopes_supported": ["files:read", "files:write"],
"resource_documentation": "https://yourcompany.com/docs/mcp"
}
The important fields are:
resource: the canonical identifier for your MCP server. The host sends this through the OAuth flow, and your token should be minted for this value.authorization_servers: one or more issuer base URLs for your OAuth provider.scopes_supported: the scopes your tools may request.resource_documentation: optional, but useful for admins and review teams.
If a request arrives without a usable token, return a challenge that points back to that metadata:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://your-mcp.example.com/.well-known/oauth-protected-resource",
scope="files:read"
This is easy to overlook because a local unauthenticated app can still render. Authenticated production hosts need this discovery path to know what to do next.
Authorization Server Metadata
Your identity provider should publish one of these discovery documents:
/.well-known/oauth-authorization-server/.well-known/openid-configuration
The document tells the host where to send the user, where to exchange the authorization code, and how the host should identify itself.
A minimal useful shape looks like this:
{
"issuer": "https://auth.yourcompany.com",
"authorization_endpoint": "https://auth.yourcompany.com/oauth/authorize",
"token_endpoint": "https://auth.yourcompany.com/oauth/token",
"client_id_metadata_document_supported": true,
"registration_endpoint": "https://auth.yourcompany.com/oauth/register",
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none", "private_key_jwt"],
"scopes_supported": ["files:read", "files:write"]
}
For OAuth 2.1-style MCP auth, check these details first:
- PKCE support is advertised with
S256. - Redirect URIs match exactly.
- The authorization server copies the MCP
resourcevalue into the access token, often asaud. - The token endpoint accepts the client authentication method the host will use.
- Advertised scopes are actually enabled for the app client.
The resource part matters because an access token issued for one API should not work against another MCP server just because both trust the same identity provider.
Client Registration in ChatGPT
ChatGPT supports three ways to identify itself to your authorization server:
- Client ID Metadata Documents, often shortened to CIMD.
- Dynamic client registration, or DCR.
- A predefined OAuth client that you configure ahead of time.
For new ChatGPT App auth work, prefer CIMD when your authorization server supports it. With CIMD, ChatGPT uses an HTTPS metadata document URL as its client_id. Your authorization server fetches that document, validates the client metadata and redirect URIs, and treats the URL as ChatGPT’s stable client identity.
DCR still works. It lets ChatGPT call your registration_endpoint, receive a generated client_id, and reuse it for that app instance. The tradeoff is operational: DCR can create many clients across many app instances, while CIMD gives your auth server one stable identity to reason about.
For local and review setup, add the ChatGPT redirect URL shown on the app management page to your authorization server allowlist. Current ChatGPT OAuth flows use URLs under:
https://chatgpt.com/connector/oauth/{callback_id}
Published older apps may still have legacy redirect URLs, but new setup should follow the URL ChatGPT shows for that app.
Tool-Level Auth in ChatGPT
There are two pieces to ChatGPT tool-level auth:
- Metadata that says a tool can or must use OAuth.
- Runtime errors that tell ChatGPT to show the linking UI.
Declare auth per tool with securitySchemes. A public tool can use noauth. A private tool should use oauth2 and list the scopes it needs.
server.registerTool(
'create_doc',
{
title: 'Create document',
description: 'Create a document in the user account.',
inputSchema: { title: z.string() },
outputSchema: {},
securitySchemes: [{ type: 'oauth2', scopes: ['docs.write'] }],
},
async ({ title }, extra) => {
const auth = extra.authInfo;
if (!auth?.token) {
return {
content: [{ type: 'text', text: 'Authentication required.' }],
_meta: {
'mcp/www_authenticate': [
'Bearer resource_metadata="https://your-mcp.example.com/.well-known/oauth-protected-resource", error="invalid_token", error_description="Sign in to create documents"',
],
},
isError: true,
};
}
return {
content: [{ type: 'text', text: `Created ${title}.` }],
structuredContent: {},
};
}
);
Even if securitySchemes tells the host a tool needs OAuth, your server must still enforce it. Treat host metadata as UX and token validation as security.
Server-Side Token Validation
On the server, auth comes down to one rule: validate before doing work.
In a sunpeak project, the server-side hook is auth() in src/server.ts:
import type { IncomingMessage } from 'node:http';
import type { AuthInfo } from 'sunpeak/mcp';
export async function auth(req: IncomingMessage): Promise<AuthInfo | null> {
const header = req.headers.authorization;
if (!header?.startsWith('Bearer ')) return null;
const token = header.slice('Bearer '.length);
const payload = await validateJwt(token);
if (!payload) return null;
return {
token,
clientId: payload.sub,
scopes: payload.scope?.split(' ') ?? [],
};
}
Then tools can read the identity from extra.authInfo:
export default async function (args: Args, extra: ToolHandlerExtra) {
const userId = extra.authInfo?.clientId;
if (!userId) {
return {
content: [{ type: 'text', text: 'Authentication required.' }],
isError: true,
};
}
return {
structuredContent: {
records: await db.recordsForUser(userId),
},
};
}
A solid JWT validation function checks:
- Signature against the issuer’s JWKS.
issmatches your authorization server.audorresourcematches your MCP server.exphas not passed.nbf, if present, is not in the future.- Required scopes are present.
- Tenant, workspace, or role policy matches the tool being called.
The jose package is a practical default in Node.js:
import { createRemoteJWKSet, jwtVerify } from 'jose';
const JWKS = createRemoteJWKSet(
new URL('https://auth.yourcompany.com/.well-known/jwks.json')
);
async function validateJwt(token: string) {
try {
const { payload } = await jwtVerify(token, JWKS, {
issuer: 'https://auth.yourcompany.com',
audience: 'https://your-mcp.example.com',
});
return payload;
} catch {
return null;
}
}
Reject expired, wrong-audience, wrong-issuer, or underscoped tokens with a 401 or an MCP tool error that includes the auth challenge. Do not fall back to a default user in production.
Passing Tokens to App UI
Your resource component runs in a sandboxed iframe. It should not assume it can read the OAuth token from cookies, browser storage, or the host bridge.
The safer pattern is:
- The host sends the token to your MCP server.
- Your server validates the token.
- Tools use the token server-side when possible.
- If the UI must call your API directly, pass only the narrow data or short-lived token the UI needs through the tool result.
export default async function (args: Args, extra: ToolHandlerExtra) {
const token = extra.authInfo?.token;
const dashboard = await fetchDashboardForUser(token);
return {
structuredContent: {
dashboard,
apiBaseUrl: 'https://api.yourcompany.com',
},
};
}
If the iframe calls your API directly, add that API origin to the resource CSP connectDomains. See the MCP App CSP guide before debugging a fetch that works in a normal browser tab but fails in ChatGPT or Claude.
Claude Connector Auth
Claude Connectors are remote MCP servers that Claude can call. For custom connector development, you add the server URL in Claude settings. For production connectors that access private user data, plan on an interactive OAuth flow and a test account for review.
The same server-side principles apply:
- Serve a remote HTTPS MCP endpoint.
- Keep the OAuth flow user-based.
- Validate every Bearer token on the server.
- Scope tokens to the user and resource.
- Keep host-specific callback URLs in the OAuth provider allowlist.
Do not design a Claude Connector around a machine-to-machine OAuth grant for user data. If a tool needs background service access, use your own backend credential inside the tool handler, then still authorize the Claude user before returning private data.
If you support both ChatGPT and Claude, use one shared token-validation layer where possible, but configure each host as its own OAuth client. That keeps redirect URLs, consent screens, scopes, test accounts, and review behavior clear.
Local Testing Plan
Auth bugs are expensive when you only find them inside a live host. Split the work into three test layers.
Test tools without OAuth
Use a fixed dev identity so tool logic and resource rendering keep moving:
export async function auth(req: IncomingMessage): Promise<AuthInfo | null> {
if (process.env.NODE_ENV === 'development') {
return {
token: 'dev-token',
clientId: 'test-user',
scopes: ['docs.read', 'docs.write'],
};
}
return validateProductionRequest(req);
}
With sunpeak, the Inspector renders the app locally in replicated ChatGPT and Claude runtimes. That lets you test tool output, structuredContent, _meta, display modes, themes, and resource behavior before touching a real OAuth provider.
Unit test token validation
Write tests for the failure cases first:
import { describe, expect, it } from 'vitest';
describe('auth', () => {
it('rejects requests without bearer tokens', async () => {
const req = { headers: {} } as IncomingMessage;
expect(await auth(req)).toBeNull();
});
it('rejects expired tokens', async () => {
const req = {
headers: { authorization: 'Bearer expired-token' },
} as IncomingMessage;
expect(await auth(req)).toBeNull();
});
it('rejects tokens for a different resource', async () => {
const req = {
headers: { authorization: `Bearer ${wrongAudienceToken}` },
} as IncomingMessage;
expect(await auth(req)).toBeNull();
});
it('returns AuthInfo for a valid scoped token', async () => {
const req = {
headers: { authorization: `Bearer ${validToken}` },
} as IncomingMessage;
const result = await auth(req);
expect(result?.clientId).toBe('user-123');
expect(result?.scopes).toContain('docs.read');
});
});
Add protocol tests for metadata too. Assert that:
/.well-known/oauth-protected-resourcereturns valid JSON.resourcematches your production MCP server URL.authorization_serverspoints at the expected issuer.- Tool
securitySchemesmatch the scopes the handler enforces. - Missing-token tool errors include
_meta["mcp/www_authenticate"].
Run one real OAuth flow
When metadata and token validation pass locally, expose your MCP server over HTTPS and connect it to the real host.
For ChatGPT:
- Start your MCP server.
- Expose it with a tunnel.
- Enable Developer mode in ChatGPT under Settings > Security and login.
- Open Settings > Plugins or go to
chatgpt.com/plugins. - Add the HTTPS
/mcpURL. - Copy the redirect URL ChatGPT shows into your authorization server.
- Run a tool that needs OAuth and confirm the linking UI appears.
For Claude, add the remote MCP server as a custom connector and run a tool that requires private user data. If auth fails, compare the host callback URL, discovery metadata, issuer, audience, scopes, and token endpoint settings before changing application code.
Common Mistakes
Missing protected resource metadata
If the host cannot discover /.well-known/oauth-protected-resource, it cannot know which authorization server protects your MCP server. Return the metadata document directly and point to it from 401 challenges.
Ignoring the resource parameter
The resource value ties the OAuth token to your MCP server. Configure your authorization server to copy it into aud or an equivalent resource claim, then reject tokens that do not match.
Depending only on dynamic client registration
DCR works, but CIMD is easier to administer at scale when your provider supports it. If you use DCR, monitor client creation and cleanup so review and customer tenants do not pile up stale clients.
Forgetting securitySchemes
For ChatGPT, tool-level OAuth needs per-tool securitySchemes and a runtime auth challenge. If either half is missing, the host may not show the linking UI when the user hits a protected tool.
Validating the signature but not the claims
A valid JWT signature only proves that your issuer signed the token. You still need to check issuer, audience, expiry, not-before, scopes, and tenant policy.
Putting durable tokens in iframe storage
Resource iframe storage is not a stable auth store. Hosts sandbox resources, and storage can differ across sessions or display modes. Keep token handling server-side when you can, and pass only what the UI actually needs.
Requesting broad scopes
Ask for the smallest scopes that let the tool work. Broad read/write scopes make consent screens scarier and make connector review harder.
Where sunpeak Helps
Auth touches server code, tool metadata, resource behavior, host setup, and tests. sunpeak keeps those pieces close to the code that owns them.
Use npx sunpeak new for a new MCP App, or npx sunpeak inspect --server https://your-server.example.com/mcp to inspect an existing server. The local Inspector lets you test the same app against ChatGPT and Claude runtime replicas, switch display modes and themes, replay fixed tool results, and run Playwright tests in CI without spending host credits on every edit.
That does not replace one real OAuth test in each target host. It does mean you can arrive at that test with your metadata, scopes, token validation, and app UI already checked.
Start with the sunpeak authorization guide, then pair it with the OpenAI Apps SDK auth guide and the MCP authorization spec.
Get Started
npx sunpeak newFurther Reading
- Security testing MCP Apps - auth flows, token handling, scopes, and review checks
- Testing authentication in MCP Apps - repeatable tests for login and identity states
- MCP App resource metadata - CSP, permissions, and ChatGPT compatibility fields
- MCP App tool results - content, structuredContent, and _meta
- Claude Connector OAuth Authentication - Claude-specific auth setup
- MCP App framework - build and test portable apps with sunpeak
- ChatGPT App framework - local ChatGPT App testing with sunpeak
- Claude Connector framework - local Claude Connector testing with sunpeak
- sunpeak authorization guide
- OpenAI Apps SDK authentication guide
- OpenAI Apps SDK quickstart
- MCP authorization specification
- Claude custom connectors with remote MCP
Frequently Asked Questions
Do MCP Apps require authentication?
No. Authentication is only required when an MCP App accesses private user data, performs user-specific writes, or needs account-level policy checks. Public tools, docs lookup tools, and generic demos can run without auth. For private data and write actions, use OAuth 2.1 and validate every request on your MCP server.
What OAuth version do MCP Apps use?
Protected MCP servers use OAuth 2.1-style authorization with the authorization-code flow, PKCE, exact redirect URIs, protected resource metadata, and bearer access tokens. The current MCP authorization spec also requires the resource server identity to be carried through the flow so your server can verify the token audience.
How does ChatGPT handle MCP App authentication?
ChatGPT acts as the OAuth client for the user. It discovers protected resource metadata from your MCP server, discovers OAuth metadata from your authorization server, identifies or registers itself as a client through Client ID Metadata Documents, dynamic client registration, or a predefined client, then runs authorization code with PKCE and sends Bearer tokens on MCP requests.
What is Client ID Metadata Document support?
Client ID Metadata Documents, or CIMD, let ChatGPT use an HTTPS metadata document URL as its client_id when your authorization server supports it. OpenAI recommends CIMD when available because it gives the authorization server a stable client identity without creating a new dynamic client for every app instance.
How do I trigger the OAuth linking UI in ChatGPT?
Publish protected resource metadata, declare each tool auth policy with securitySchemes, and return a tool error that includes _meta["mcp/www_authenticate"] when the token is missing or invalid. The challenge should point at your protected resource metadata and include a clear error and error_description.
How should an MCP server validate access tokens?
Treat every incoming token as untrusted. Verify the signature, issuer, audience or resource claim, expiry, not-before time, required scopes, and any app policy before running a tool. If validation fails, return 401 with a WWW-Authenticate challenge so the host can reauthorize the user.
Can I test MCP App authentication locally?
Yes. Unit test token validation with fixed test tokens, use a local dev identity for ordinary tool and UI work, then run a full OAuth flow through a public HTTPS tunnel when you need to validate the real host path. The MCP Inspector and the sunpeak Inspector help catch metadata, tool result, and iframe bugs before live host testing.
Do ChatGPT Apps and Claude Connectors need separate OAuth configuration?
Usually yes. The same MCP server can validate tokens from both hosts, but each host has its own client identity, redirect URL, connector settings, and review expectations. Keep shared token validation in one server layer, then configure separate OAuth clients or redirect URI allowlists for each host.