|
| 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 | +``` |
0 commit comments