Skip to content

SEP: Delegated authorization #1028

@saqadri

Description

@saqadri

SEP: Delegated Authorization

SEP Number: TBD (to be assigned by sponsor)
Title: Delegated Authorization
Author: Sarmad Qadri [email protected]
Status: Proposal
Type: Standards Track
Created: 2025-07-20
Tags: proposal, sep

Abstract

This SEP adds a delegated authorization primitive to the Model Context Protocol that enables servers to request OAuth authorization from clients using an auth/request API. This provides a standardized way for servers to request OAuth authorization from clients in order to access upstream resources. This enables servers to obtain necessary access tokens for upstream APIs through the user's MCP client.

The delegated authorization primitive allows MCP servers to:

  • Request OAuth authorization for upstream resources
  • Receive authorization codes through the client's UI (e.g. user log-in, and other OAuth flows)

Motivation

MCP servers that need to access authenticated upstream resources have no standard way to request user authorization through the MCP protocol. The current MCP specification defines authorization for client-to-server access but lacks a mechanism for server-initiated authorization requests. This gap prevents important use cases:

Real-World Use Cases

  • Agents as MCP servers: Servers which are agents may act as clients to upstream servers which need authorization.
  • Workflow Automation: Servers with tools that orchestrate actions across authenticated platforms
  • API Aggregators: Servers that combine data from multiple authenticated APIs (GitHub, Slack, Jira)

Example Scenario

Consider an MCP server that provides unified access to a user's code repositories:

User → Claude Desktop → Repository MCP Server → GitHub API
                                              → GitLab API
                                              → Bitbucket API

The Repository MCP Server needs to obtain authorization for each upstream API on behalf of the user, but has no standard way to request this authorization through the MCP protocol.

Specification

1. Client Capability

Clients supporting delegated authorization requests declare the capability during initialization:

export interface ClientCapabilities {
  // ... existing fields ...
  
  /**
   * Present if the client supports delegated authorization requests from servers.
   */
  delegated_authorization?: object;
}

2. Authorization Request

The server initiates authorization with a blocking request:

/**
 * Request OAuth authorization from the client for accessing upstream resources.
 * 
 * @category auth/request
 */
export interface DelegatedAuthorizationRequest extends Request {
  method: "auth/request";
  params: {
    /**
     * The complete OAuth authorization URL to open.
     * This URL contains all necessary OAuth parameters including
     * client_id, redirect_uri, scope, state, code_challenge, etc.
     */
    url: string;
    
    /**
     * Human-readable message explaining why authorization is needed.
     */
    message: string;
    
    /**
     * Optional: Alternative redirect URI templates the server supports.
     * Clients SHOULD select the first option they can handle.
     * Templates may include placeholders like {port} or {session}.
     * 
     * Examples:
     * - "http://127.0.0.1:{port}/callback" (desktop clients)
     * - "https://callback.example.com/{session}" (web clients)
     * - "com.example.app://oauth/callback" (mobile clients)
     */
    redirect_uri_options?: string[];
  };
}

3. Authorization Result

The client returns the OAuth callback URL:

/**
 * Result of a delegated authorization request.
 * 
 * @category auth/request
 */
export interface DelegatedAuthorizationResult extends Result {
  /**
   * The complete callback URL from the OAuth provider.
   * Only present if the user completed authorization.
   * Contains the authorization code and state parameters.
   */
  url?: string;
}

4. Protocol Flow

sequenceDiagram
    participant User
    participant Client as MCP Client
    participant Server as MCP Server
    participant OAuth as OAuth Provider

    Server->>Client: auth/request<br/>{url: "oauth_url", message: "authorize upstream resource"}
    Client->>User: Show authorization prompt
    User->>Client: Approves
    Client->>User: Open OAuth URL
    User->>OAuth: Complete OAuth flow
    OAuth->>User: Redirect with code
    User->>Client: Callback URL
    Client-->>Server: {url: "callback_url"}
    Note over Server: Exchange code for token
Loading

Rationale

Design Principles

  1. Minimal Addition: Only two new types, following existing patterns
  2. OAuth Native: Works directly with standard OAuth 2.1 flows
  3. Security First: No token handling in the protocol layer
  4. Clear Semantics: Missing URL means user declined

Alternatives Considered

  1. Notification-based approach: Would require complex callback correlation
  2. Extending elicitation: Authorization has distinct security requirements
  3. Full OAuth proxy: Would violate protocol layering principles

Related Work

  • Elicitation Pattern: This proposal follows the same blocking request pattern
  • Authorization Spec: Complements existing client-to-server auth
  • OAuth 2.1: Aligns with industry-standard authorization flows

Backward Compatibility

This proposal is fully backward compatible:

  1. Optional Capability: Only used when both client and server support it
  2. Additive Change: No modifications to existing protocol elements
  3. Graceful Degradation: Servers can detect lack of client support

Servers MUST check for client capability before sending delegated authorization requests:

if (clientCapabilities.delegated_authorization) {
  // Can use auth/request
} else {
  // Fall back to alternative method
}

Reference Implementation

Server Implementation

async function authorizeWithUpstream(client: MCPClient, provider: string) {
  // Build OAuth URL with PKCE
  const state = generateSecureRandom();
  const codeVerifier = generateCodeVerifier();
  const codeChallenge = generateCodeChallenge(codeVerifier);
  
  const authUrl = new URL('https://github.com/login/oauth/authorize');
  authUrl.searchParams.set('client_id', CLIENT_ID);
  authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
  authUrl.searchParams.set('scope', 'repo read:user');
  authUrl.searchParams.set('state', state);
  authUrl.searchParams.set('code_challenge', codeChallenge);
  authUrl.searchParams.set('code_challenge_method', 'S256');
  
  // Request authorization
  const result = await client.request('auth/request', {
    url: authUrl.toString(),
    message: `GitHub authorization required to access private repositories`
  });
  
  if (!result.url) {
    throw new Error('Authorization declined by user');
  }
  
  // Parse callback
  const callback = new URL(result.url);
  const code = callback.searchParams.get('code');
  const returnedState = callback.searchParams.get('state');
  
  // Validate state
  if (returnedState !== state) {
    throw new Error('State mismatch - possible CSRF');
  }
  
  // Exchange code for token
  const token = await exchangeCodeForToken(code, codeVerifier);
  return token;
}

// Multi-platform support example
async function authorizeWithRedirectOptions(client: MCPClient) {
  const authUrl = new URL('https://github.com/login/oauth/authorize');
  authUrl.searchParams.set('client_id', CLIENT_ID);
  authUrl.searchParams.set('redirect_uri', 'http://localhost:8080/callback');
  // ... other OAuth parameters
  
  const result = await client.request('auth/request', {
    url: authUrl.toString(),
    message: 'GitHub authorization required',
    redirect_uri_options: [
      'http://127.0.0.1:{port}/callback',      // Desktop clients
      'https://auth.myserver.com/{session}',   // Web clients  
      'com.mycompany.mcp://oauth/callback'     // Mobile clients
    ]
  });
}

Client Implementation

async function handleDelegatedAuthorizationRequest(params: DelegatedAuthorizationRequestParams) {
  // Show UI to user
  const userChoice = await showAuthorizationDialog({
    message: params.message,
    domain: new URL(params.url).hostname
  });
  
  if (!userChoice.approved) {
    return {}; // User declined
  }
  
  // Open OAuth URL
  await openBrowser(params.url);
  
  // Capture callback (automatic or manual)
  const callbackUrl = await captureOAuthCallback();
  
  return { url: callbackUrl };
}

OAuth Callback Capture

The captureOAuthCallback() implementation should follow RFC 8252: OAuth 2.0 for Native Apps, which defines standard approaches for native applications:

  1. Loopback Interface Redirect (Recommended):

    • Start a temporary HTTP server on 127.0.0.1 with an ephemeral port
    • Configure OAuth provider with http://127.0.0.1:{port}/callback as redirect URI
    • Automatically capture the callback when the browser redirects
    • This approach is used by tools like GitHub CLI and follows Google's OAuth 2.0 guidelines for native apps
  2. Manual Entry (Fallback):

    • For environments where local servers aren't feasible
    • User manually copies the callback URL from the browser

This approach is used by standard tools like GitHub CLI, Google Cloud SDK, and AWS CLI.

Security Implications

OAuth Security Requirements

Servers implementing delegated authorization MUST follow OAuth 2.1 security best practices:

  1. PKCE Required: Servers MUST use PKCE (RFC 7636) for all OAuth flows, even for confidential clients
  2. State Validation: Servers MUST generate cryptographically secure state values and validate them in callbacks
  3. Redirect URI Registration: All redirect URIs MUST be pre-registered with the OAuth provider
  4. Token Isolation: Access tokens MUST never traverse the MCP protocol - only authorization codes

Client Security Requirements

  1. Domain Display: Clients MUST prominently display the OAuth provider domain before opening URLs
  2. User Consent: Clients MUST obtain explicit user consent before opening authorization URLs
  3. No Token Storage: Clients MUST NOT log or store authorization codes or callback URLs
  4. Secure Browser: Clients MUST open URLs in a secure browser context that prevents inspection
  5. URL Validation: Clients MUST validate callback URLs before returning them to the server

Preventing Common Attacks

Following RFC 6819: OAuth 2.0 Threat Model:

Attack Vector Mitigation
CSRF / Code Confusion Server-generated and validated state parameter
Code Injection PKCE with S256 challenge method
Open Redirect Pre-registered redirect URIs only
Token Leakage Only authorization codes traverse MCP, never access tokens
Phishing Client displays OAuth provider domain prominently

Redirect URI Selection

When using redirect_uri_options:

  1. Clients MUST select the first option they can fully support
  2. Clients MUST NOT substitute arbitrary redirect URIs
  3. Servers MUST validate that the selected URI matches their pre-registered patterns

Implementation Guidance

For Clients

  1. Extract and display the OAuth provider domain
  2. Provide both automatic and manual callback capture
  3. Validate callback URLs before returning
  4. Clear all authorization state after completion

For Servers

  1. Always use PKCE, even for confidential clients
  2. Generate cryptographically secure state values
  3. Validate all OAuth parameters in callbacks
  4. Handle both success and error responses

Error Handling

OAuth errors are indicated by the error parameter in the callback URL:

?error=access_denied&error_description=User+denied+access

Standard OAuth error codes include:

  • access_denied: User denied authorization
  • invalid_scope: Requested scope is invalid
  • server_error: Authorization server error

Conclusion

Delegated authorization allows server-initiated authorization flows that is compliant with existing MCP patterns as well as OAuth2.1 standards. It enables MCP servers to grow more sophisticated while still maintaining security primitives.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions