oauth-architecture.md•15.9 kB
# OAuth Architecture: MCP OAuth vs Sentry OAuth
## Two Separate OAuth Systems
The Sentry MCP implementation involves **two completely separate OAuth providers**:
### 1. MCP OAuth Provider (Our Server)
- **What it is**: Our own OAuth 2.0 server built with `@cloudflare/workers-oauth-provider`
- **Purpose**: Authenticates MCP clients (like Cursor, VS Code, etc.)
- **Tokens issued**: MCP access tokens and MCP refresh tokens
- **Storage**: Uses Cloudflare KV to store encrypted tokens
- **Endpoints**: `/oauth/register`, `/oauth/authorize`, `/oauth/token`
### 2. Sentry OAuth Provider (Sentry's Server)
- **What it is**: Sentry's official OAuth 2.0 server at `sentry.io`
- **Purpose**: Authenticates users and grants API access to Sentry
- **Tokens issued**: Sentry access tokens and Sentry refresh tokens
- **Storage**: Tokens are stored encrypted within MCP's token props
- **Endpoints**: `https://sentry.io/oauth/authorize/`, `https://sentry.io/oauth/token/`
## High-Level Flow
The system uses a dual-token approach:
1. **MCP clients** authenticate with **MCP OAuth** to get MCP tokens
2. **MCP OAuth** authenticates with **Sentry OAuth** to get Sentry tokens
3. **MCP tokens** contain encrypted **Sentry tokens** in their payload
4. When serving MCP requests, the server uses Sentry tokens to call Sentry's API
### Complete Flow Diagram
```mermaid
sequenceDiagram
participant Client as MCP Client (Cursor)
participant MCPOAuth as MCP OAuth Provider<br/>(Our Server)
participant MCP as MCP Server<br/>(Durable Object)
participant SentryOAuth as Sentry OAuth Provider<br/>(sentry.io)
participant SentryAPI as Sentry API
participant User as User
Note over Client,SentryAPI: Initial Client Registration
Client->>MCPOAuth: Register as OAuth client
MCPOAuth-->>Client: MCP Client ID & Secret
Note over Client,SentryAPI: User Authorization Flow
Client->>MCPOAuth: Request authorization
MCPOAuth->>User: Show MCP consent screen
User->>MCPOAuth: Approve MCP permissions
MCPOAuth->>SentryOAuth: Redirect to Sentry OAuth
SentryOAuth->>User: Sentry login page
User->>SentryOAuth: Authenticate with Sentry
SentryOAuth-->>MCPOAuth: Sentry auth code
MCPOAuth->>SentryOAuth: Exchange code for tokens
SentryOAuth-->>MCPOAuth: Sentry access + refresh tokens
MCPOAuth-->>Client: MCP access token<br/>(contains encrypted Sentry tokens)
Note over Client,SentryAPI: Using MCP Protocol
Client->>MCP: MCP request with MCP Bearer token
MCP->>MCPOAuth: Validate MCP token
MCPOAuth-->>MCP: Decrypted props<br/>(includes Sentry tokens)
MCP->>SentryAPI: API call with Sentry Bearer token
SentryAPI-->>MCP: API response
MCP-->>Client: MCP response
Note over Client,SentryAPI: Token Refresh
Client->>MCPOAuth: POST /oauth/token<br/>(MCP refresh_token)
MCPOAuth->>MCPOAuth: Check Sentry token expiry
alt Sentry token still valid
MCPOAuth-->>Client: New MCP token<br/>(reusing cached Sentry token)
else Sentry token expired
MCPOAuth->>SentryOAuth: Refresh Sentry token
SentryOAuth-->>MCPOAuth: New Sentry tokens
MCPOAuth-->>Client: New MCP token<br/>(with new Sentry tokens)
end
```
## Key Concepts
### Token Types
| Token Type | Issued By | Used By | Contains | Purpose |
|------------|-----------|---------|----------|----------|
| **MCP Access Token** | MCP OAuth Provider | MCP Clients | Encrypted Sentry tokens | Authenticate to MCP Server |
| **MCP Refresh Token** | MCP OAuth Provider | MCP Clients | Grant reference | Refresh MCP access tokens |
| **Sentry Access Token** | Sentry OAuth | MCP Server | User credentials | Call Sentry API |
| **Sentry Refresh Token** | Sentry OAuth | MCP OAuth Provider | Refresh credentials | Refresh Sentry tokens |
### Not a Simple Proxy
**Important**: MCP is NOT an HTTP proxy that forwards requests. Instead:
- MCP implements the **Model Context Protocol** (tools, prompts, resources)
- Clients send MCP protocol messages, not HTTP requests
- MCP Server executes these commands using Sentry's API
- Responses are MCP protocol messages, not raw HTTP responses
## Technical Implementation
### MCP OAuth Provider Details
The MCP OAuth Provider is built with `@cloudflare/workers-oauth-provider` and provides:
1. **Dynamic client registration** - MCP clients can register on-demand
2. **PKCE support** - Secure authorization code flow
3. **Token management** - Issues and validates MCP tokens
4. **Consent UI** - Custom approval screen for permissions
5. **Token encryption** - Stores Sentry tokens encrypted in MCP token props
### Sentry OAuth Integration
The integration with Sentry OAuth happens through:
1. **Authorization redirect** - After MCP consent, redirect to Sentry OAuth
2. **Code exchange** - Exchange Sentry auth code for tokens
3. **Token storage** - Store Sentry tokens in MCP token props
4. **Token refresh** - Use Sentry refresh tokens to get new access tokens
## Key Concepts
### How the MCP OAuth Provider Works
```mermaid
sequenceDiagram
participant Agent as AI Agent
participant MCPOAuth as MCP OAuth Provider
participant KV as Cloudflare KV
participant User as User
participant MCP as MCP Server
Agent->>MCPOAuth: Register as client
MCPOAuth->>KV: Store client registration
MCPOAuth-->>Agent: MCP Client ID & Secret
Agent->>MCPOAuth: Request authorization
MCPOAuth->>User: Show MCP consent screen
User->>MCPOAuth: Approve
MCPOAuth->>KV: Store grant
MCPOAuth-->>Agent: Authorization code
Agent->>MCPOAuth: Exchange code for MCP token
MCPOAuth->>KV: Validate grant
MCPOAuth->>KV: Store encrypted MCP token
MCPOAuth-->>Agent: MCP access token
Agent->>MCP: MCP protocol request with MCP token
MCP->>MCPOAuth: Validate MCP token
MCPOAuth->>KV: Lookup MCP token
MCPOAuth-->>MCP: Decrypted props (includes Sentry tokens)
MCP-->>Agent: MCP protocol response
```
## Implementation Details
### 1. MCP OAuth Provider Configuration
The MCP OAuth Provider is configured in `src/server/index.ts`:
```typescript
const oAuthProvider = new OAuthProvider({
apiHandlers: {
"/sse": createMcpHandler("/sse", true),
"/mcp": createMcpHandler("/mcp", false),
},
defaultHandler: app, // Hono app for non-OAuth routes
authorizeEndpoint: "/oauth/authorize",
tokenEndpoint: "/oauth/token",
clientRegistrationEndpoint: "/oauth/register",
scopesSupported: Object.keys(SCOPES),
});
```
### 2. API Handlers
The `apiHandlers` are protected endpoints that require valid OAuth tokens:
- `/mcp/*` - MCP protocol endpoints
- `/sse/*` - Server-sent events for MCP
These handlers receive:
- `request`: The incoming request
- `env`: Cloudflare environment bindings
- `ctx`: Execution context with `ctx.props` containing decrypted user data
### 3. Token Structure
MCP tokens contain encrypted properties including Sentry tokens:
```typescript
interface WorkerProps {
id: string; // Sentry user ID
name: string; // User name
accessToken: string; // Sentry access token
refreshToken?: string; // Sentry refresh token
accessTokenExpiresAt?: number; // Sentry token expiry timestamp
scope: string; // MCP permissions granted
grantedScopes?: string[]; // Sentry API scopes
}
```
### 4. URL Constraints Challenge
#### The Problem
The MCP server needs to support URL-based constraints like `/mcp/sentry/javascript` to limit agent access to specific organizations/projects. However:
1. OAuth Provider only does prefix matching (`/mcp` matches `/mcp/*`)
2. The agents library rewrites URLs to `/streamable-http` before reaching the Durable Object
3. URL path parameters are lost in this rewrite
#### The Solution
We use HTTP headers to preserve constraints through the URL rewriting:
```typescript
const createMcpHandler = (basePath: string, isSSE = false) => {
const handler = isSSE ? SentryMCP.serveSSE("/*") : SentryMCP.serve("/*");
return {
fetch: (request: Request, env: unknown, ctx: ExecutionContext) => {
const url = new URL(request.url);
// Extract constraints from URL
const pathMatch = url.pathname.match(
/^\/(mcp|sse)(?:\/([a-z0-9._-]+))?(?:\/([a-z0-9._-]+))?/i
);
// Pass constraints via headers (preserved through URL rewriting)
const headers = new Headers(request.headers);
if (pathMatch?.[2]) {
headers.set("X-Sentry-Org-Slug", pathMatch[2]);
}
if (pathMatch?.[3]) {
headers.set("X-Sentry-Project-Slug", pathMatch[3]);
}
const modifiedRequest = new Request(request, { headers });
return handler.fetch(modifiedRequest, env, ctx);
},
};
};
```
## Storage (KV Namespace)
The MCP OAuth Provider uses `OAUTH_KV` namespace to store:
1. **MCP Client registrations**: `client:{clientId}` - MCP OAuth client details
2. **MCP Authorization grants**: `grant:{userId}:{grantId}` - User consent records for MCP
3. **MCP Access tokens**: `token:{userId}:{grantId}:{tokenId}` - Encrypted MCP tokens (contains Sentry tokens)
4. **MCP Refresh tokens**: `refresh:{userId}:{grantId}:{refreshId}` - For MCP token renewal
### Token Storage Structure
When a user completes the full OAuth flow, the MCP OAuth Provider stores Sentry tokens inside MCP token props:
```typescript
// In /oauth/callback after exchanging code with Sentry
const { redirectTo } = await c.env.OAUTH_PROVIDER.completeAuthorization({
// ... other params
props: {
id: payload.user.id, // From Sentry
name: payload.user.name, // From Sentry
accessToken: payload.access_token, // Sentry's access token
refreshToken: payload.refresh_token, // Sentry's refresh token
accessTokenExpiresAt: Date.now() + payload.expires_in * 1000,
scope: oauthReqInfo.scope.join(" "), // MCP scopes
grantedScopes: Array.from(grantedScopes), // Sentry API scopes
// ... other fields
}
});
```
## Token Refresh Implementation
### Dual Refresh Token System
The system maintains two separate refresh flows:
1. **MCP Token Refresh**: When MCP clients need new MCP access tokens
2. **Sentry Token Refresh**: When Sentry access tokens expire (handled internally)
### MCP Token Refresh Flow
When an MCP client's token expires:
1. Client sends refresh request to MCP OAuth: `POST /oauth/token` with MCP refresh token
2. MCP OAuth invokes `tokenExchangeCallback` function
3. Callback checks if cached Sentry token is still valid (with 2-minute safety window)
4. If Sentry token is valid, returns new MCP token with cached Sentry token
5. If Sentry token expired, refreshes with Sentry OAuth and updates storage
### Token Exchange Callback Implementation
```typescript
// tokenExchangeCallback in src/server/oauth/helpers.ts
export async function tokenExchangeCallback(options, env) {
// Only handle MCP refresh_token requests
if (options.grantType !== "refresh_token") {
return undefined;
}
// Extract Sentry refresh token from MCP token props
const sentryRefreshToken = options.props.refreshToken;
if (!sentryRefreshToken) {
throw new Error("No Sentry refresh token available in stored props");
}
// Smart caching: Check if Sentry token is still valid
const sentryTokenExpiresAt = props.accessTokenExpiresAt;
if (sentryTokenExpiresAt && Number.isFinite(sentryTokenExpiresAt)) {
const remainingMs = sentryTokenExpiresAt - Date.now();
const SAFE_WINDOW_MS = 2 * 60 * 1000; // 2 minutes safety
if (remainingMs > SAFE_WINDOW_MS) {
// Sentry token still valid - return new MCP token with cached Sentry token
return {
newProps: { ...options.props },
accessTokenTTL: Math.floor(remainingMs / 1000),
};
}
}
// Sentry token expired - refresh with Sentry OAuth
const [sentryTokens, errorResponse] = await refreshAccessToken({
client_id: env.SENTRY_CLIENT_ID,
client_secret: env.SENTRY_CLIENT_SECRET,
refresh_token: sentryRefreshToken,
upstream_url: "https://sentry.io/oauth/token/",
});
// Update MCP token props with new Sentry tokens
return {
newProps: {
...options.props,
accessToken: sentryTokens.access_token, // New Sentry access token
refreshToken: sentryTokens.refresh_token, // New Sentry refresh token
accessTokenExpiresAt: Date.now() + sentryTokens.expires_in * 1000,
},
accessTokenTTL: sentryTokens.expires_in,
};
}
```
### Error Scenarios
1. **Missing Sentry Refresh Token**:
- Error: "No Sentry refresh token available in stored props"
- Resolution: Client must re-authenticate through full OAuth flow
2. **Sentry Refresh Token Invalid**:
- Error: Sentry OAuth returns 401/400
- Resolution: Client must re-authenticate with both MCP and Sentry
3. **Network Failures**:
- Error: Cannot reach Sentry OAuth endpoint
- Resolution: Retry with exponential backoff or re-authenticate
The 2-minute safety window prevents edge cases with clock skew and processing delays between MCP and Sentry.
## Security Features
1. **PKCE**: MCP OAuth uses PKCE to prevent authorization code interception
2. **Token encryption**: Sentry tokens encrypted within MCP tokens using WebCrypto
3. **Dual consent**: Users approve both MCP permissions and Sentry access
4. **Scope enforcement**: Both MCP and Sentry scopes limit access
5. **Token expiration**: Both MCP and Sentry tokens have expiry times
6. **Refresh token rotation**: Sentry issues new refresh tokens on each refresh
## Discovery Endpoints
The MCP OAuth Provider automatically provides:
- `/.well-known/oauth-authorization-server` - MCP OAuth server metadata
- `/.well-known/oauth-protected-resource` - MCP resource server info
Note: These describe the MCP OAuth server, not Sentry's OAuth endpoints.
## Integration Between MCP OAuth and MCP Server
The MCP Server (Durable Object `SentryMCP`) receives:
1. **Props via constructor**: Decrypted data from MCP token (includes Sentry tokens)
2. **Constraints via headers**: Organization/project limits from URL path
3. **Both stored**: In Durable Object storage for session persistence
The MCP Server then uses the Sentry access token from props to make Sentry API calls.
## Limitations
1. **No direct Hono integration**: OAuth Provider expects specific handler signatures
2. **URL rewriting**: Requires header-based constraint passing
3. **Props architecture mismatch**: OAuth passes props per-request, agents library expects them in constructor
## Why Use Two OAuth Systems?
### Benefits of the Dual OAuth Approach
1. **Security isolation**: MCP clients never see Sentry tokens directly
2. **Token management**: MCP can refresh Sentry tokens transparently
3. **Permission layering**: MCP permissions separate from Sentry API scopes
4. **Client flexibility**: MCP clients don't need to understand Sentry OAuth
### Why Not Direct Sentry OAuth?
If MCP clients used Sentry OAuth directly:
- Clients would need to manage Sentry token refresh
- No way to add MCP-specific permissions
- Clients would have raw Sentry API access (security risk)
- No centralized token management
### Implementation Complexity
The MCP OAuth Provider (via `@cloudflare/workers-oauth-provider`) provides:
- OAuth 2.0 authorization flows
- Dynamic client registration
- Token issuance and validation
- PKCE support
- Consent UI
- Token encryption
- KV storage
- Discovery endpoints
Reimplementing this would be complex and error-prone.
## Related Documentation
- [Cloudflare OAuth Provider](https://github.com/cloudflare/workers-oauth-provider)
- [OAuth 2.0 Specification](https://oauth.net/2/)
- [Dynamic Client Registration](https://www.rfc-editor.org/rfc/rfc7591.html)
- [PKCE](https://www.rfc-editor.org/rfc/rfc7636)