Skip to main content
OAuth System Clarification: DeployStack implements three distinct OAuth systems:
  1. User → DeployStack OAuth (Social Login) - See OAuth Providers
  2. MCP Client → DeployStack OAuth (API Access) - See OAuth2 Server - How VS Code, Cursor, Claude.ai authenticate to satellite APIs
  3. User → MCP Server OAuth (External Service Access) - This document - How users authorize external services like Notion, Box, Linear
This document covers system #3 - OAuth authentication with external MCP servers.

Overview

This document covers the backend implementation for OAuth 2.1 authentication with external MCP servers that require user authorization, such as Notion, Box, Linear, and GitHub Copilot.

When MCP Servers Require OAuth

MCP servers that access user-specific resources (files, issues, repositories) require OAuth authorization. Examples:
  • Notion MCP Server (https://mcp.notion.com/) - Access user’s Notion pages
  • Box MCP Server (https://mcp.box.com/) - Access user’s Box files
  • Linear MCP Server (https://mcp.linear.app/sse) - Access user’s Linear issues
  • GitHub Copilot MCP - Access GitHub repositories

User Flow

  1. Install - User initiates MCP server installation in frontend
  2. Authorize - Backend redirects to OAuth provider’s authorization page
  3. Callback - OAuth provider redirects back with authorization code
  4. Token Storage - Backend exchanges code for tokens, encrypts and stores them
  5. Use - Satellite injects tokens when connecting to MCP server

Architecture Overview

The OAuth implementation includes:
  • OAuth Discovery Service - Detects OAuth requirement and discovers endpoints using RFC 8414/9728
  • Authorization Endpoint - Initiates OAuth flow with PKCE, state parameter, and resource parameter
  • Callback Endpoint - Exchanges authorization code for tokens
  • Re-Authentication Endpoint - User-initiated token refresh when automatic refresh fails
  • Token Service - Handles token exchange and refresh operations
  • Client Registration Service - Implements RFC 7591 Dynamic Client Registration (DCR)
  • Encryption Service - AES-256-GCM encryption for tokens at rest
  • Token Refresh Job - Background cron job refreshing expiring tokens

Database Tables

  • mcpOauthProviders - Pre-registered OAuth providers (for non-DCR auth servers)
  • oauthPendingFlows - Temporary storage during OAuth flow (10-minute expiry)
  • mcpServerInstallations - MCP server installations
  • mcpOauthTokens - Encrypted access and refresh tokens

Implementation Components

OAuthDiscoveryService

File: services/backend/src/services/OAuthDiscoveryService.ts Purpose: Detects if an MCP server requires OAuth and discovers OAuth endpoints using RFC 8414 and RFC 9728.

When OAuth Detection Runs

OAuth detection occurs automatically in two scenarios:
  1. Server Creation (POST /mcp/servers/global) - When global admin creates new MCP server
  2. Server Updates (PUT /mcp/servers/global/:id) - When global admin updates existing MCP server
Why re-check on updates?
  • Server’s OAuth configuration might have changed
  • URL might be updated to a different endpoint
  • Transport type might change from stdio → http/sse or vice versa
  • Security verification ensures requires_oauth flag stays accurate

OAuth Detection

The service uses a hybrid detection approach to handle different server configurations: Detection Strategy:
  1. Try GET first (fast path for most servers like Notion, Box, Linear)
  2. Try POST with MCP protocol request if GET returns non-401 (handles servers like Harmonic AI that only protect POST endpoints)
Detection criteria:
  • HTTP 401 Unauthorized response (on GET or POST)
  • WWW-Authenticate: Bearer header present
Why two methods?
  • Standard servers (Notion, Box, Linear): Return 401 on GET for public health check endpoints
  • Harmonic-style servers: Return 200 on GET (public endpoint), but 401 on POST (protected MCP protocol endpoint)

OAuth Metadata Discovery

Once OAuth is detected, the service discovers endpoints using multiple methods with priority order: Priority 1: WWW-Authenticate Header Discovery URL Some servers provide a direct discovery URL in the WWW-Authenticate header:
Priority 2: RFC 8414 - Authorization Server Metadata
Priority 3: OpenID Connect Discovery (Final fallback):
Discovery Priority Chain:
  1. Discovery URL from WWW-Authenticate header (if provided)
  2. RFC 8414 Authorization Server Metadata
  3. OpenID Connect Discovery
  4. Give up - OAuth configuration cannot be determined

Metadata Structure

Pre-registered Provider Matching

If the discovered authorization server matches a pre-registered provider pattern, the service returns the provider configuration:

OAuth Detection on Server Updates

File: services/backend/src/routes/mcp/servers/update-global.ts Endpoint: PUT /api/mcp/servers/global/:id Purpose: Re-checks OAuth requirements whenever a global admin updates an MCP server configuration.

Update Detection Logic

When updating a global MCP server, the backend determines “effective values” for OAuth detection:
Effective Values Logic:
  • If update includes new transport_type → Use new value
  • If update doesn’t include transport_type → Use existing server’s value
  • Same logic applies to remotes field
Why effective values? Allows partial updates while still running OAuth detection correctly.

Update Scenarios

Scenario 1: URL Change
  • OAuth detection runs with new URL
  • requires_oauth updated based on new server’s response
Scenario 2: Transport Type Change (stdio → http)
  • OAuth detection runs (now HTTP transport)
  • requires_oauth set based on detection result
Scenario 3: Transport Type Change (http → stdio)
  • OAuth detection skipped (stdio doesn’t use OAuth)
  • requires_oauth explicitly set to false
Scenario 4: No Transport/URL Change
  • OAuth detection still runs with existing URL (security re-check)
  • Ensures requires_oauth reflects current server state

Error Handling

OAuth detection failures during updates are non-blocking:
Why non-blocking? Server updates should not fail due to temporary OAuth discovery issues. The update succeeds with requires_oauth=false as safe default.

Authorization Endpoint

File: services/backend/src/routes/mcp/installations/authorize.ts Endpoint: POST /api/teams/:teamId/mcp/installations/authorize Purpose: Initiates the OAuth 2.1 authorization flow with PKCE for MCP server installation.

Request Body

Authorization Flow Steps

1

Verify OAuth Requirement

Check that the MCP server has requires_oauth: true in the catalog.
2

Extract Server URL

Retrieve MCP server URL from remotes (HTTP/SSE) or packages (stdio) configuration.
3

Discover OAuth Endpoints

Call OAuthDiscoveryService.detectAndDiscoverOAuth() to get authorization endpoints.
4

Dynamic Client Registration or Provider Match

  • If registration_endpoint exists: Register new client via RFC 7591
  • Else if pre-registered provider matches: Use provider credentials
  • Else: Return error (cannot proceed)
5

Generate PKCE Pair

Create code verifier (128 random bytes) and code challenge (SHA256 hash).
6

Generate State Parameter

Create cryptographically secure random state for CSRF protection.
7

Generate Resource Parameter

Create resource parameter (RFC 8707) for token audience binding.
8

Create Pending Flow Record

Store temporary OAuth flow data in oauthPendingFlows table (expires in 10 minutes).
9

Build Authorization URL

Construct OAuth authorization URL with all parameters.
10

Return Authorization URL

Frontend opens this URL in a popup window for user authorization.

Callback Endpoint

File: services/backend/src/routes/mcp/installations/callback.ts Endpoint: GET /api/teams/:teamId/mcp/oauth/callback/:flowId Purpose: Receives authorization code from OAuth provider, exchanges it for tokens, and completes installation.

Callback Flow Steps

1

Validate OAuth Callback

Check for errors and validate required parameters.
2

Find Pending Flow

Retrieve pending flow by flowId, teamId, and state parameter.
3

Check Flow Expiration

Ensure flow hasn’t expired (10-minute window).
4

Exchange Code for Tokens

Use PKCE verifier to exchange authorization code for access/refresh tokens.
5

Create Installation

Create the MCP server installation record (not pending anymore).
Per-User Instance Creation: OAuth callback creates the user’s instance with status=‘connecting’. For multi-user teams:
  • Installing user’s instance created with their OAuth credentials
  • Other team members’ instances created with status=‘awaiting_user_config’ (they must authenticate separately)
  • Each user authenticates independently with their own OAuth account
For instance lifecycle details, see Instance Lifecycle.
6

Encrypt and Store Tokens

Encrypt access and refresh tokens using AES-256-GCM before storing.
7

Delete Pending Flow

Remove temporary flow record to prevent reuse.
8

Notify Satellites

Create satellite commands for immediate configuration update.
9

Return Success Page

Render HTML page that posts message to opener window and closes popup.

OAuthTokenService

File: services/backend/src/services/OAuthTokenService.ts Purpose: Handles token exchange and refresh operations with OAuth servers.

Token Exchange with PKCE

Exchanges authorization code for access/refresh tokens using PKCE verification:
Token endpoint authentication methods:
  • none - Public client (PKCE only, no client secret)
  • client_secret_post - Client secret in request body (GitHub, most OAuth providers)
  • client_secret_basic - HTTP Basic Auth header (enterprise providers)

Token Refresh

Refreshes expired access tokens using refresh token:

Update Refreshed Tokens

Updates database with newly refreshed encrypted tokens:
Note: Some OAuth providers rotate refresh tokens (issue new refresh token with each refresh). The service handles this by conditionally updating the refresh token field.

OAuthClientRegistrationService

File: services/backend/src/services/OAuthClientRegistrationService.ts Purpose: Implements RFC 7591 (OAuth 2.0 Dynamic Client Registration Protocol).

Dynamic Client Registration

Registers a new OAuth client with MCP server’s registration endpoint:
Registration response:
When DCR is used:
  • MCP server supports registration_endpoint in OAuth metadata
  • Client ID is generated dynamically per installation
  • No pre-registration required with OAuth provider
When Pre-registered Provider is used:
  • MCP server does NOT support registration_endpoint
  • Pre-registered provider configured in mcpOauthProviders table
  • Uses fixed client ID and client secret
  • Example: GitHub OAuth Apps for GitHub MCP server

Database Schema

mcpOauthProviders Table

Pre-registered OAuth providers for MCP servers that don’t support Dynamic Client Registration.
Example provider record:

oauthPendingFlows Table

Temporary storage for OAuth flows during authorization (expires in 10 minutes).
Important: This table is cleaned up automatically after OAuth flow completes or expires. Records should never exist for more than 10 minutes.

mcpOauthTokens Table

Encrypted OAuth tokens for MCP server installations.
Encryption format: iv:authTag:encryptedData (all hex-encoded)
  • IV: 16 bytes (128 bits)
  • Auth Tag: 16 bytes (128 bits)
  • Encrypted Data: Variable length
Index: (installation_id, user_id, team_id) for fast token lookups by satellite.

Token Lifecycle

Token Issuance

  1. User authorizes application at OAuth provider
  2. OAuth provider redirects to callback with authorization code
  3. Backend exchanges code for access/refresh tokens using PKCE
  4. Tokens encrypted using AES-256-GCM
  5. Encrypted tokens stored in mcpOauthTokens table
  6. Installation status set to connecting

Automatic Token Refresh

File: services/backend/src/jobs/refresh-oauth-tokens.ts Cron Schedule: Every 5 minutes Refresh Criteria:
  • Token has refresh_token (NOT NULL)
  • Token has expires_at timestamp (NOT NULL)
  • Token expires within next 10 minutes
  • Token not already expired
Refresh Process:
1

Find Expiring Tokens

Query tokens expiring in the next 10 minutes.
2

Discover OAuth Endpoints

Re-discover OAuth endpoints for each MCP server (ensures current endpoints).
3

Decrypt Refresh Token

Decrypt stored refresh token using AES-256-GCM.
4

Call Token Endpoint

Exchange refresh token for new access token.
5

Encrypt and Update

Encrypt new tokens and update database.
6

Handle Failures

If refresh fails, set installation status to requires_reauth.
Per-User Instance Impact: Token refresh failures only affect the specific user’s instance. Other team members’ instances remain unaffected even if one user’s OAuth token expires.
Logging:

Token Expiration Handling

When automatic token refresh fails (server offline, invalid refresh token, token revoked), the installation enters requires_reauth status. Users can recover through self-service re-authentication. Automatic Refresh Failure:
  • Token refresh job attempts refresh
  • Refresh fails (network error, invalid_grant, server offline)
  • Installation status → requires_reauth
  • Status message explains why re-auth is needed
User-Initiated Re-Authentication: Users can re-authenticate existing installations without reinstalling: Endpoint: POST /api/teams/:teamId/mcp/installations/:installationId/reauth Permission: mcp.installations.view (both team_admin and team_user) Why Both Roles: OAuth tokens are per-user credentials, not team-level configuration. Each user authenticates with their own account. Flow:
  1. Frontend detects requires_reauth status and shows “Re-authenticate” button
  2. User clicks button → Backend starts OAuth flow (same as initial authorization)
  3. OAuth provider redirects to authorization page
  4. User authorizes → Callback exchanges code for new tokens
  5. Backend updates existing token record (doesn’t create new installation)
  6. Installation status → connectingonline
  7. Satellite receives updated tokens via configuration sync
Key Difference from Initial Authorization:
  • Initial: Creates new installation + new token record
  • Re-auth: Updates existing installation + existing token record
Database Impact:
  • Pending flow created with installation_id reference (links to existing installation)
  • Callback detects installation_id and performs UPDATE instead of INSERT
  • Preserves team configuration (env vars, args, headers)
Security: Same PKCE flow, state validation, and token encryption as initial authorization

Token Revocation

When user deletes an MCP server installation:
  1. Installation deleted from mcpServerInstallations (CASCADE)
  2. Tokens automatically deleted from mcpOauthTokens (CASCADE foreign key)
  3. Future enhancement: Call OAuth provider’s revocation endpoint
  4. Satellite receives configuration update removing the installation

Security Implementation

PKCE (Proof Key for Code Exchange)

Required for all OAuth flows to prevent authorization code interception attacks. PKCE Generation:
Authorization request:
Token exchange:
Security: OAuth server verifies SHA256(code_verifier) == code_challenge before issuing tokens.

State Parameter

Purpose: CSRF protection during OAuth flow. Generation:
Flow:
  1. Backend generates random state before redirecting to OAuth provider
  2. State stored in oauthPendingFlows table
  3. OAuth provider includes state in callback URL
  4. Backend verifies state matches stored value
  5. If mismatch → Reject callback (potential CSRF attack)

Resource Parameter

Purpose: Token audience binding (RFC 8707) to prevent token misuse. Generation:
Benefits:
  • Tokens bound to specific MCP server and team
  • Prevents token reuse across different installations
  • OAuth provider includes resource in issued token

Token Encryption

Algorithm: AES-256-GCM (Authenticated Encryption with Associated Data) Encryption:
Decryption:
Key Derivation:
Security Features:
  • AES-256: Industry-standard symmetric encryption
  • GCM mode: Authenticated encryption prevents tampering
  • Random IV: Each encryption uses unique initialization vector
  • AAD: Additional authenticated data binds encryption context
  • Scrypt: Key derivation function resistant to brute-force attacks
Environment Variable:
Production requirement: Must be at least 32 characters for security.

HTTPS Requirements

All OAuth endpoints require HTTPS:
  • Authorization endpoint
  • Token endpoint
  • Callback endpoint (redirect URI)
Why: OAuth flows transmit sensitive data (authorization codes, tokens) that must be protected from interception. Local development exception: http://localhost allowed for testing.

OAuth Discovery Process

Step-by-Step Discovery Flow

1

Try GET Request (Fast Path)

Send GET request to MCP server URL to detect OAuth requirement.
2

Check GET Response for OAuth

Look for 401 status and WWW-Authenticate header.
3

Try POST Request (Harmonic-style Servers)

If GET returns non-401, try POST with MCP protocol request.
Why POST? Servers like Harmonic AI allow GET for public health checks but require OAuth on POST endpoints.
4

Try Discovery URL from Header (Priority 1)

If WWW-Authenticate header includes discovery URL, try it first.
5

Fetch Authorization Server Metadata (Priority 2 - RFC 8414)

If no discovery URL or it failed, try RFC 8414.
6

Fallback to OpenID Connect Discovery (Priority 3)

If RFC 8414 fails, try OpenID Connect as final fallback.
7

Validate Metadata

Ensure required endpoints are present.
8

Check PKCE Support

Verify server supports S256 code challenge method.
9

Match Pre-registered Provider (Optional)

Check if authorization server matches known provider.
10

Return Discovery Result

Complete discovery with metadata and optional provider.

Detection Methods by Server Type

Different MCP servers implement OAuth protection at different endpoint levels. DeployStack’s hybrid detection approach handles all configurations:

Standard Servers (Notion, Box, Linear)

Behavior: Return 401 on GET requests to base URL
Detection: Immediate OAuth detection via GET request (fast path)

Harmonic-style Servers (Harmonic AI)

Behavior: Allow GET for public health checks, require OAuth on POST endpoints
Detection: GET returns 200, POST returns 401 - requires hybrid detection Why this pattern? Public health check endpoints let monitoring systems verify server status without authentication, while actual MCP protocol operations require OAuth.

Servers with Discovery URL

Behavior: Include oauth_authorization_server in WWW-Authenticate header
Detection: Discovery URL extracted from header and used as first priority for metadata discovery Benefit: Faster discovery - no need to guess or try multiple well-known endpoint patterns

Error Handling

Discovery failures:
  • GET and POST both return non-401: Server does not require OAuth
  • Discovery URL from header fails: Try RFC 8414 Authorization Server Metadata
  • Authorization server metadata not found: Try OpenID Connect discovery
  • All discovery methods fail: Return error to user - OAuth configuration cannot be determined
  • Network timeout: Retry with exponential backoff (3 attempts)
  • Invalid JSON: Log error and return OAuth not supported
Complete Fallback Chain:
  1. Discovery URL from WWW-Authenticate header (if provided)
  2. RFC 8414 Authorization Server Metadata
  3. OpenID Connect Discovery
  4. Give up and return error

Integration Points

Backend → Database

OAuth detection triggers:
  1. Server Creation: POST /mcp/servers/global
    • OAuth detection runs on initial creation
    • requires_oauth stored in mcpServers table
  2. Server Updates: PUT /mcp/servers/global/:id
    • OAuth detection re-runs on every update
    • requires_oauth updated to reflect current state
    • Ensures accuracy even if server’s OAuth config changes
Why re-detect on updates?
  • Remote servers might enable/disable OAuth
  • URLs might change to different endpoints
  • Transport types might switch between stdio/http/sse
  • Security verification ensures flag accuracy

Frontend → Backend

Installation initiation:

Backend → Satellite

Satellite retrieves OAuth tokens during configuration fetch: The satellite calls /api/satellites/config which includes OAuth tokens for installations:
Important: Backend decrypts tokens before sending to satellite over HTTPS. Satellite never stores encrypted tokens. Satellite implementation: See OAuth Token Injection documentation.

Satellite → MCP Server

Token injection in HTTP/SSE requests: Satellite adds Authorization header when connecting to OAuth-enabled MCP servers:
Header priority: OAuth Authorization header added last to prevent override by team headers.

Testing and Debugging

Manual Testing with Real MCP Servers

Notion MCP Server (Standard Detection):
  1. Add Notion to catalog: https://mcp.notion.com/
  2. Backend detects OAuth requirement via GET request (fast path)
  3. Install as user → Opens Notion OAuth page
  4. Authorize → Callback completes installation
  5. Check mcpOauthTokens table for encrypted tokens
  6. Verify satellite receives decrypted token in config
Harmonic AI MCP Server (Hybrid Detection):
  1. Add Harmonic to catalog: https://mcp.api.harmonic.ai
  2. Backend tries GET first (returns 200 - public endpoint)
  3. Backend tries POST with MCP protocol request (returns 401 - OAuth required)
  4. Extracts discovery URL from WWW-Authenticate header
  5. Install as user → Opens Harmonic OAuth page
  6. Authorize → Callback completes installation
  7. Verify logs show “OAuth detected via POST”
Box MCP Server:
  1. Add Box to catalog: https://mcp.box.com/
  2. Follow same flow as Notion
  3. Verify PKCE S256 is used (check logs)
  4. Test token refresh by manually updating expires_at to past

Testing Dynamic Client Registration

MCP servers with DCR support:
  • Notion: ✅ Supports RFC 7591 registration endpoint
  • Box: ✅ Supports RFC 7591 registration endpoint
  • Linear: ✅ Supports RFC 7591 registration endpoint
Testing DCR flow:
  1. Ensure no pre-registered provider matches
  2. Check logs for “Registering dynamic OAuth client”
  3. Verify oauth_client_id is dynamically generated
  4. Check that client can refresh tokens using generated client ID

Testing Pre-registered Providers

Setup test provider:
Test flow:
  1. Add GitHub MCP server requiring OAuth
  2. Install → Should match provider by auth server pattern
  3. Check logs for “Using pre-registered OAuth provider: GitHub (Test)”
  4. Verify client_id from provider is used instead of DCR

Testing OAuth Detection on Updates

Test Case 1: Update URL to OAuth-enabled server
Test Case 2: Security re-check on metadata updates
Test Case 3: Transport type change

Common Issues

Issue: “OAuth not detected” for Harmonic-style servers
  • Cause: Server returns 200 on GET, only protects POST endpoints
  • Solution: Fixed in commit 2d3bf3d7 - Now tries POST with MCP protocol request if GET returns non-401
  • Verify: Check logs for “Server returned non-401 on GET, trying POST with MCP protocol request”
Issue: “OAuth provider not configured” error
  • Cause: MCP server doesn’t support DCR and no pre-registered provider matches
  • Fix: Add provider to mcpOauthProviders table with matching auth server pattern
Issue: Discovery URL in header not being used
  • Cause: WWW-Authenticate header parsing failed
  • Debug: Check logs for “OAuth discovery URL found in WWW-Authenticate header”
  • Format: Header must contain oauth_authorization_server="https://..."
Issue: Tokens not refreshing automatically
  • Cause: Cron job not running or refresh token missing
  • Fix: Check refreshExpiringOAuthTokens cron job logs, verify refresh_token field is not NULL
Issue: “Flow expired” error during callback
  • Cause: User took more than 10 minutes to authorize
  • Fix: Increase expiry in authorize.ts or inform user to complete authorization faster
Issue: Installation stuck in “connecting” status after OAuth
  • Cause: Satellite hasn’t polled configuration yet
  • Fix: Check satellite logs, verify satellite commands created, wait for next config poll
Issue: Token decryption error
  • Cause: DEPLOYSTACK_ENCRYPTION_SECRET changed between encryption and decryption
  • Fix: Ensure encryption secret is consistent across deployments

Log Analysis

Successful OAuth flow with GET detection (Standard servers):
Successful OAuth flow with POST detection (Harmonic-style servers):
Failed token refresh: