Skip to content

Commit e14c32e

Browse files
authored
Add auth server design proposals (#19)
MCP servers need to authenticate users via OAuth/OIDC and obtain upstream identity tokens to call external APIs (GitHub, Google, Atlassian) on behalf of users. OAuth 2.0 requires pre-registered redirect URIs and client credentials at each Identity Provider, making it impractical for individual user clients to register directly. A centralized Authorization Server solves this by acting as the single registered OAuth client with upstream IDPs, enabling Dynamic Client Registration for MCP clients while managing the complexity of upstream token storage and retrieval. The solution implements an OAuth Authorization Server built on Fosite that authenticates users against upstream IDPs, stores their tokens, and issues its own JWTs with a tsid claim linking to the stored credentials. The server exposes standard OAuth 2.0/OIDC endpoints including authorization with PKCE, token exchange, dynamic client registration (RFC 7591), and JWKS for token validation. MCP servers and vMCP can then validate incoming JWTs and retrieve the upstream IDP tokens via the tsid to make authenticated API calls on behalf of users. The design documents in this commit outline the solution in detail.
1 parent 9beccbc commit e14c32e

2 files changed

Lines changed: 384 additions & 0 deletions

File tree

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
# OAuth Authorization Server Design
2+
3+
This document covers the implementation of the ToolHive OAuth Authorization
4+
Server. A separate design document covers the overall backend authentication
5+
approach and rationale.
6+
7+
## Problem Statement
8+
9+
MCP servers need to authenticate users and obtain their upstream identity for API access. The auth server must implement OAuth 2.0 flows, integrate with upstream IDPs, issue JWTs, and manage sessions with upstream tokens for later retrieval by vMCP.
10+
11+
## Goals
12+
13+
- Implement OAuth 2.0 Authorization Code Flow with PKCE (RFC 7636)
14+
- Integrate with upstream OIDC Identity Providers
15+
- Issue signed JWTs for authenticated sessions
16+
- Provide dynamic client registration (RFC 7591)
17+
- Expose OIDC discovery and JWKS endpoints
18+
- Abstract storage for horizontal scaling (with practical implementations being in-memory or Redis)
19+
20+
## Non-Goals
21+
22+
- Full OIDC Provider certification (we implement the subset needed for MCP use case)
23+
24+
## Architecture Overview
25+
26+
The auth server is built on Fosite, a mature OAuth 2.0/OIDC library, with
27+
custom handlers for upstream IDP integration and session management.
28+
29+
```mermaid
30+
flowchart TB
31+
subgraph AuthServer["Auth Server"]
32+
subgraph Endpoints["HTTP Endpoints"]
33+
authorize["/authorize"]
34+
token["/token"]
35+
callback["/callback"]
36+
end
37+
38+
subgraph Fosite["Fosite OAuth Provider"]
39+
pkce["PKCE validation"]
40+
tokissue["Token issuance"]
41+
session["Session management"]
42+
clientauth["Client authentication"]
43+
end
44+
45+
subgraph Backend["Backend Services"]
46+
storage["Storage<br/>(mem/Redis)"]
47+
idpclient["Upstream<br/>IDP Client"]
48+
signing["Signing<br/>Keys"]
49+
end
50+
51+
authorize --> Fosite
52+
token --> Fosite
53+
callback --> Fosite
54+
Fosite --> storage
55+
Fosite --> idpclient
56+
Fosite --> signing
57+
end
58+
```
59+
60+
## Detailed Design
61+
62+
### 1. OAuth 2.0 Endpoints
63+
64+
The auth server exposes standard OAuth 2.0 and OIDC endpoints for client interaction and token management.
65+
66+
| Endpoint | Method | Description |
67+
|----------|--------|-------------|
68+
| `/oauth/authorize` | GET | Authorization endpoint - initiates flow, generates PKCE challenge |
69+
| `/oauth/callback` | GET | Callback from upstream IDP - exchanges upstream code for tokens |
70+
| `/oauth/token` | POST | Token endpoint - exchanges authorization code for access/refresh tokens |
71+
| `/oauth/register` | POST | Dynamic client registration (RFC 7591) |
72+
| `/.well-known/openid-configuration` | GET | OIDC discovery document |
73+
| `/.well-known/jwks.json` | GET | JSON Web Key Set for token validation |
74+
75+
### 2. Authorization Code Flow with PKCE
76+
77+
All authorization flows require PKCE (S256 method) for security. The flow begins when a client redirects to `/oauth/authorize` with a code challenge.
78+
79+
```mermaid
80+
sequenceDiagram
81+
participant Client
82+
participant AS as Auth Server
83+
participant IDP as Upstream IDP
84+
85+
Client->>AS: GET /oauth/authorize<br/>code_challenge, redirect_uri, state
86+
AS-->>Client: 302 Redirect to IDP
87+
88+
Client->>IDP: User authenticates
89+
IDP-->>Client: 302 to Auth Server /callback
90+
91+
Client->>AS: GET /oauth/callback<br/>code, state
92+
AS->>IDP: POST /token
93+
IDP-->>AS: IDP tokens<br/>(access, refresh, id_token)
94+
AS-->>Client: 302 to client with auth code
95+
96+
Client->>AS: POST /oauth/token<br/>code, code_verifier
97+
AS-->>Client: JWT tokens (with tsid claim)
98+
```
99+
100+
### 3. Upstream IDP Integration
101+
102+
The auth server acts as a client to upstream identity providers such as Google, GitHub, and others. It fetches the IDP's discovery document, constructs authorization URLs, and exchanges codes for tokens. Each IDP is configured with an issuer URL, client ID, client secret, and requested scopes. When the user authenticates, the auth server receives the access token, refresh token, and ID token from the upstream IDP and stores them for later retrieval by vMCP or backend MCP servers.
103+
104+
Upstream ID tokens must be cryptographically verified before trusting their claims. The auth server fetches and caches the upstream IDP's JWKS, retrieved from the `jwks_uri` specified in the IDP's discovery document. The standard OIDC claims (`iss`, `aud`, `exp`, `iat`, `nonce`) are validated according to the OIDC specification to ensure token integrity and prevent replay attacks.
105+
106+
### 4. JWT Token Issuance
107+
108+
The auth server issues its own JWTs for client authentication. Clients present
109+
these JWTs to ToolHive, which validates them and uses the `tsid` claim to
110+
retrieve stored upstream IDP tokens.
111+
112+
**Token claims:**
113+
```json
114+
{
115+
"iss": "https://auth.toolhive.example.com",
116+
"sub": "user@example.com",
117+
"aud": "mcp-server",
118+
"exp": 1234567890,
119+
"iat": 1234567890,
120+
"tsid": "session-uuid-here"
121+
}
122+
```
123+
124+
JWTs are signed using ECDSA (ES256, preferred) or RSA (RS256) keys. The algorithm is auto-derived from the key type. ES256 is recommended for new deployments due to smaller signatures and faster operations; RS256 is supported for OIDC compliance per [Section 15.1](https://openid.net/specs/openid-connect-core-1_0.html#ServerMTI). Public keys are exposed via the JWKS endpoint for verification, with the OIDC discovery document dynamically advertising the configured algorithm.
125+
126+
### 5. Session Management
127+
128+
Sessions store the complete authentication state including upstream IDP tokens. The `tsid` (token session ID) in issued JWTs references these sessions.
129+
130+
The session data includes:
131+
- User subject and claims from upstream ID token
132+
- Client ID and redirect URI
133+
- Upstream access token, refresh token, ID token
134+
- Token expiration timestamps
135+
- Refresh token family ID for rotation tracking
136+
137+
### 6. Refresh Token Rotation
138+
139+
Refresh tokens are rotated on each use to detect token replay attacks. When a refresh token is used, a new one is issued and the old one is invalidated. All refresh tokens in a session share a family ID, allowing the auth server to detect replay attempts—if an old refresh token is reused, the entire family is revoked, invalidating all tokens in that session. When a client refreshes its tokens, the auth server also refreshes the upstream IDP tokens if they have expired.
140+
141+
### 7. Token Revocation (RFC 7009) - Future
142+
143+
The `/oauth/revoke` endpoint will accept both access and refresh tokens and invalidate them. Revoking a refresh token cascades to invalidate all related access tokens. Optionally, revocation can also cascade to delete the stored upstream IDP tokens associated with the session.
144+
145+
### 8. Dynamic Client Registration (RFC 7591)
146+
147+
Clients can register dynamically via POST to `/oauth/register`. This returns a client ID and optional client secret.
148+
149+
**Registration metadata:**
150+
- `redirect_uris`: Allowed callback URLs
151+
- `grant_types`: Supported grant types (default: `authorization_code`, `refresh_token`)
152+
- `response_types`: Supported response types (default: `code`)
153+
- `client_name`: Human-readable name
154+
155+
### 9. Storage Abstraction
156+
157+
Storage is abstracted behind interfaces to support different backends. In-memory storage is suitable for development; Redis is required for production multi-replica deployments.
158+
159+
**Storage interfaces:**
160+
- `AuthorizationCodeStorage`: Stores authorization codes with PKCE verifiers
161+
- `AccessTokenStorage`: Stores access token metadata
162+
- `RefreshTokenStorage`: Stores refresh tokens with family tracking
163+
- `SessionStorage`: Stores complete session state including IDP tokens
164+
- `ClientStorage`: Stores registered client configurations
165+
166+
## Integration
167+
168+
The auth server is implemented as `pkg/authserver` and can be embedded into `thv` or run standalone.
169+
170+
**Embedded Mode** (MVP): The auth server exposes an `http.Handler` that serves all OAuth/OIDC endpoints. This handler is mounted in the `thv` HTTP server under `/oauth/`.
171+
172+
**Standalone Mode** (future): The auth server can run as a separate service with its own HTTP server.
173+
174+
## Data Model
175+
176+
### Session
177+
178+
```go
179+
type Session struct {
180+
ID string
181+
Subject string
182+
ClientID string
183+
IDPAccessToken string
184+
IDPRefreshToken string
185+
IDPIDToken string
186+
IDPTokenExpiry time.Time
187+
RefreshFamily string
188+
CreatedAt time.Time
189+
ExpiresAt time.Time
190+
}
191+
```
192+
193+
### Client
194+
195+
```go
196+
type Client struct {
197+
ID string
198+
SecretHash []byte
199+
RedirectURIs []string
200+
GrantTypes []string
201+
ResponseTypes []string
202+
Metadata map[string]string
203+
}
204+
```
205+
206+
## Security Considerations
207+
208+
- **PKCE required**: No implicit flow; all authorization requests must include PKCE
209+
- **State validation**: CSRF protection via state parameter
210+
- **ID token verification**: Upstream ID tokens verified against JWKS before trusting
211+
- **Nonce validation**: Replay protection for ID tokens
212+
- **Secret management**: Signing keys and client secrets stored securely
213+
- **Rate limiting**: Authentication endpoints protected against brute force
214+
215+
## Configuration Example
216+
217+
```yaml
218+
auth_server:
219+
issuer: "https://auth.toolhive.example.com"
220+
221+
upstream_idp:
222+
issuer: "https://accounts.google.com"
223+
client_id: "your-client-id"
224+
client_secret_env: "IDP_CLIENT_SECRET"
225+
scopes: ["openid", "profile", "email"]
226+
227+
signing:
228+
# Optional: if omitted, a key is auto-generated and persisted to storage
229+
key_file: "/etc/toolhive/signing-key.pem"
230+
# Optional: auto-derived from key type (ES256 for EC, RS256 for RSA)
231+
algorithm: "ES256"
232+
233+
storage:
234+
type: "redis" # or "memory"
235+
redis:
236+
address: "redis:6379"
237+
238+
tokens:
239+
access_token_lifetime: "1h"
240+
refresh_token_lifetime: "24h"
241+
authorization_code_lifetime: "10m"
242+
```
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# OAuth Authorization Server for ToolHive - Overview
2+
3+
## Problem Statement
4+
5+
ToolHive MCP servers need to authenticate users via OAuth/OIDC and obtain
6+
their upstream identity for API access. Users authenticate with upstream
7+
Identity Providers (Google, GitHub, Atlassian, etc.) and MCP servers need to
8+
call those upstream APIs on behalf of users.
9+
10+
Currently, there is no implemented way to handle user authentication to
11+
upstream IDPs or upstream IDP token pass-through across MCP servers and vMCP.
12+
13+
## Why an Authorization Server?
14+
15+
### The Redirect URI and Client ID/Client Secret Problem
16+
17+
OAuth 2.0 requires pre-registered redirect URIs at each Identity Provider. Moreover
18+
many IDPs (Google, GitHub, etc.) require manual registration of OAuth clients in order
19+
to obtain Client ID and Client Secret.
20+
21+
If each user client (e.g. an IDE) were its own OAuth client, the registration
22+
burden would be too high. Moreover, each client comes with its own redirect URI.
23+
24+
### How a Centralized AS Solves This
25+
26+
With ToolHive's Authorization Server:
27+
28+
```mermaid
29+
flowchart LR
30+
User --> AS[ToolHive AS]
31+
AS --> IDP[Upstream IDP<br/>GitHub, Google, Atlassian]
32+
IDP --> AS
33+
AS --> Store[(Tokens stored)]
34+
Store --> MCP[MCP Server]
35+
AS -->|JWT with tsid claim| User
36+
```
37+
38+
### Dynamic Client Registration (RFC 7591)
39+
40+
MCP clients can **self-register** with ToolHive's AS via Dynamic Client Registration. This
41+
enables self-service onboarding of clients without admin intervention.
42+
43+
**Note**: DCR works for clients → ToolHive AS, but not for ToolHive → upstream
44+
IDPs (Google, GitHub, etc. require manual OAuth client registration).
45+
46+
**Future Enhancement**: The MCP 2025-11-25 specification recommends Client ID
47+
Metadata Documents as the preferred client registration method over DCR. In this
48+
approach, clients use HTTPS URLs as client identifiers (e.g.,
49+
`https://app.example.com/oauth/client-metadata.json`) that the authorization
50+
server fetches and validates dynamically. We will implement DCR first for the
51+
MVP, then migrate to Client ID Metadata Documents in a later phase to align with
52+
the MCP specification's recommended priority order.
53+
54+
## Use Cases
55+
56+
### Google Docs MCP Server
57+
> As a user, I want to authenticate to a Google Docs MCP server and only be able to access my own documents. The MCP server should effectively impersonate me when calling Google APIs.
58+
59+
### GitHub MCP Server
60+
> As a developer, I want to authenticate to a GitHub MCP server so I can create PRs, manage issues, and access repositories using my own GitHub identity and permissions.
61+
62+
### Multi-Backend Workflow (vMCP)
63+
> As a platform engineer, I want to deploy vMCP so that users authenticate once and vMCP retrieves their upstream IDP tokens to pass through to multiple backend MCP servers (GitHub, Jira, Slack).
64+
65+
## Backend Authentication Approach
66+
67+
The MCP authorization spec covers authorization **to** the MCP server but not how MCP servers authenticate **to upstream services**. While RFC 8693 token exchange and federated IDPs (e.g., Google Workforce Identity Federation) work when the upstream service is in the same trust domain, many MCP servers need to access external services like GitHub, Google, or Atlassian APIs where no federation exists.
68+
69+
This design implements an **OAuth Authorization Server** that:
70+
1. Authenticates users against the upstream IDP (GitHub, Google, Atlassian, etc.)
71+
2. Stores the upstream IDP tokens
72+
3. Issues its own JWT with a `tsid` claim linking to the stored tokens
73+
4. Enables vMCP and MCP servers to retrieve and use the upstream tokens
74+
75+
**Note**: A future enhancement may add an internal company IDP hop before the external IDP, enabling enterprise identity linking. The current implementation goes directly to the external IDP.
76+
77+
## Goals
78+
79+
- Provide OAuth 2.0 for authentication to MCP servers
80+
- Integrate with upstream OIDC Identity Providers (Google, GitHub, Atlassian, Okta, Azure AD)
81+
- Issue signed JWTs to clients
82+
- Integrate with Kubernetes operator via CRDs
83+
- Enable vMCP to retrieve and pass through upstream IDP tokens
84+
- Support Dynamic Client Registration (RFC 7591) for MCP clients
85+
- Support both embedded and centralized deployment modes, starting with embedded
86+
87+
## Non-Goals
88+
89+
- Full OIDC Provider certification
90+
- Acting as a general-purpose OAuth server for non-MCP use cases
91+
92+
## Architecture Overview
93+
94+
The auth server acts as an intermediary between clients and upstream IDPs, issuing its own JWTs while storing upstream tokens for later retrieval.
95+
96+
```mermaid
97+
flowchart TB
98+
subgraph ToolHive["ToolHive Ecosystem"]
99+
Client[Client]
100+
AS[Auth Server]
101+
Store[(Token Store)]
102+
MCP["MCP Server / vMCP<br/>• Validates JWT via JWKS<br/>• Retrieves IDP tokens via tsid<br/>• Passes tokens to upstream APIs"]
103+
end
104+
105+
IDP[Upstream IDP<br/>GitHub, Google, Atlassian]
106+
107+
Client <--> AS
108+
AS <--> IDP
109+
AS -->|Store IDP tokens| Store
110+
Client -->|JWT| MCP
111+
Store -->|Retrieve tokens| MCP
112+
```
113+
114+
## Token Flow Summary
115+
116+
1. **Client initiates OAuth flow** - Client redirects to auth server's `/oauth/authorize` with PKCE challenge
117+
2. **Upstream IDP authentication** - Auth server redirects to upstream IDP (GitHub, Google, etc.); user authenticates
118+
3. **Callback and token exchange** - Auth server receives callback, exchanges code with upstream IDP
119+
4. **JWT issuance** - Auth server issues its own JWT with `tsid` claim linking to stored IDP tokens
120+
5. **MCP server access** - Client presents JWT to MCP server; server validates via JWKS
121+
6. **vMCP token pass-through** - vMCP extracts `tsid`, retrieves IDP tokens, passes to backend APIs
122+
123+
## Deployment Modes
124+
125+
In the interest of ease of installation and operation, we will initially
126+
support an embedded deployment mode where the auth server runs in the same process
127+
as the MCP server proxy. This mode is suitable for simple MCP server deployments.
128+
129+
However, in the interest of scalability and security, we'll move to a centralized
130+
or distributed deployment mode where a standalone auth server services multiple MCP
131+
servers. This will allow for better separation of concerns and allow to secure the AS better
132+
as well as scale it independently.
133+
134+
## Implementation Steps
135+
136+
| Step | Scope |
137+
|----------------------------------------------|----------------------------------------------------------|
138+
| **1: MVP** | Core OAuth flow, in-memory storage, K8s integration, DCR |
139+
| **2: Persistence & Security** | Redis storage, refresh token utilization rotation |
140+
| **3: vMCP Support** | Token pass-through middleware, session lookup |
141+
| **4: Productization and operationalization** | Rate limiting, Observability, Auditing |
142+
| **5: Centralized Deployment** | Standalone mode, multi-replica support |

0 commit comments

Comments
 (0)