example-remote-server/docs/oauth-flow.md
Basil Hosmer 7b7aa513f5
Improve client.js and curl-examples.sh usability
Fix PKCE implementation, SSE parsing, error handling, and usage clarity.
Add --help flag and improve output messaging for both examples.
2025-10-07 14:36:41 -04:00

5.8 KiB

OAuth 2.0 + PKCE Flow Analysis

This document details the complete OAuth 2.0 authorization code flow with PKCE as implemented in this reference server with separate authorization and resource servers.

Flow Overview

The server implements OAuth 2.1 with PKCE (Proof Key for Code Exchange) for secure authorization. The architecture separates the authorization server (port 3001) from the MCP resource server (port 3232).

1. Client Registration

Purpose: Register the application with the OAuth server (happens once during app setup).

Flow:

App → Auth Server: POST /register {"client_name": "...", "redirect_uris": [...]}
Auth Server → App: {"client_id": "XYZ", "client_secret": "ABC", ...}

Storage:

  • Redis key: auth:client:{clientId}
  • Expiry: 30 days (long-lived app credentials)
  • Server: Auth Server (port 3001)

2. Authorization Request

Purpose: User initiates connection to MCP server.

Flow:

App → Auth Server: GET /authorize?
  client_id=XYZ
  &redirect_uri=http://localhost:3000/callback
  &code_challenge=<SHA256 of verifier>
  &code_challenge_method=S256
  &state=<CSRF token>

Auth Server: Saves pending authorization, shows authorization page

Storage:

  • Redis key: auth:pending:{authCode}
  • Expiry: 10 minutes (short-lived temporary state)
  • Data: redirect_uri, code_challenge, code_challenge_method, client_id, state
  • Server: Auth Server (port 3001)

3. User Authentication & Authorization

Purpose: Authenticate the user and obtain consent.

Flow:

Auth Server → User: Shows auth page with "Continue to Authentication" button
User → Auth Server: Clicks button
Auth Server → Upstream IDP: Redirects to /mock-upstream-idp/authorize
Upstream IDP → User: Shows user selection UI
User → Upstream IDP: Selects/creates user ID
Upstream IDP → Auth Server: Redirects to /mock-upstream-idp/callback?userId=X
Auth Server: Validates user, issues authorization code
Auth Server → App: Redirects to app's redirect_uri with code

Note: In production, the upstream IDP would be an external provider (Auth0, Okta, Google, GitHub, etc.). The /mock-upstream-idp/* endpoints simulate this for demonstration purposes.


4. Authorization Code Exchange

Purpose: Exchange authorization code for access and refresh tokens.

Flow:

App → Auth Server: POST /token
  grant_type=authorization_code
  &client_id=XYZ
  &client_secret=ABC
  &code=<authorization code>
  &redirect_uri=http://localhost:3000/callback
  &code_verifier=<original random string>

Auth Server:
  1. Validates code_verifier matches code_challenge (PKCE)
  2. Validates redirect_uri matches original
  3. Validates client credentials
  4. Issues tokens

Auth Server → App:
  {
    "access_token": "...",
    "refresh_token": "...",
    "expires_in": 604800,
    "token_type": "Bearer"
  }

Storage:

  • auth:exch:{authCode} - Token exchange record (prevents replay attacks)
  • auth:installation:{accessToken} - Active MCP installation
  • auth:refresh:{refreshToken} - Refresh token mapping
  • Expiry: 10 minutes for exchange record, 7 days for installation
  • Server: Auth Server (port 3001)

5. Using Access Tokens

Purpose: Access MCP resources with the token.

Flow:

App → MCP Server: POST /mcp
  Authorization: Bearer <access_token>
  Mcp-Session-Id: <session_id>
  {MCP request}

MCP Server → Auth Server: POST /introspect
  token=<access_token>

Auth Server → MCP Server:
  {
    "active": true,
    "userId": "...",
    "exp": ...,
    "aud": "http://localhost:3232"
  }

MCP Server: Validates audience, serves MCP resource

Token Validation Process:

  1. MCP Server receives request with bearer token
  2. Calls Auth Server's /introspect endpoint (RFC 7662)
  3. Auth Server validates token and returns metadata
  4. MCP Server validates audience matches BASE_URI
  5. Request proceeds with authenticated user context

This separation allows the Auth Server to be replaced with commercial providers (Auth0, Okta) while keeping the MCP server unchanged.


6. Token Refresh

Purpose: Obtain new access token when current one expires.

Flow:

App → Auth Server: POST /token
  grant_type=refresh_token
  &refresh_token=<refresh token>
  &client_id=XYZ
  &client_secret=ABC

Auth Server:
  1. Validates refresh_token
  2. Issues new access_token (and optionally new refresh_token)
  3. Updates installation record

Auth Server → App: {"access_token": "...", ...}

Storage:

  • Reads: auth:refresh:{refreshToken} → gets access token
  • Reads: auth:installation:{accessToken} → gets installation
  • Writes: New auth:installation:{newAccessToken}
  • Writes: New auth:refresh:{newRefreshToken}
  • Expiry: 7 days
  • Server: Auth Server (port 3001)

Data Lifecycle Hierarchy

Timeline (shortest to longest expiry):

  1. OAuth flow state (10 minutes) - auth:pending, auth:exch
  2. User sessions (7 days) - auth:installation, auth:refresh
  3. Client credentials (30 days) - auth:client

This hierarchy ensures each layer outlives the layers it supports.


Security: PKCE (Proof Key for Code Exchange)

PKCE prevents authorization code interception attacks:

  1. Authorization request: Client sends code_challenge (SHA256 hash of random string)
  2. Token exchange: Client must provide original code_verifier
  3. Server validates: SHA256(code_verifier) must equal stored code_challenge

This ensures only the client that initiated the flow can exchange the code, even if the code is intercepted.


References