OAuth 2.1 Implementation
Seed implements a complete OAuth 2.1 authorization server with PKCE support, token exchange, and dynamic client registration (DCR). It acts as an OAuth proxy, allowing dynamically registered clients while delegating actual authentication to an upstream identity provider.
Overview
The OAuth implementation provides:
- Authorization Code Flow with PKCE (RFC 7636)
- Refresh Token Grant (RFC 6749 Section 6)
- Dynamic Client Registration (RFC 7591)
- OAuth 2.0 Discovery (RFC 8414)
- Protected Resource Metadata (RFC 9728)
Architecture Pattern
Seed uses an OAuth Proxy Pattern:
- Seed exposes OAuth endpoints to Claude clients
- Dynamically registered clients are stored in Redis
- Client IDs are mapped to a static upstream IdP client
- Actual authentication is delegated to the upstream IdP (e.g., Authentik)
This pattern gives Seed control over the OAuth flow while the IdP handles authentication.
sequenceDiagram
participant Client as Claude Client
participant Seed as Seed OAuth Proxy
participant Redis as Redis (DCR Store)
participant IdP as Identity Provider
Client->>Seed: POST /oauth/register
Seed->>Redis: Store client metadata
Redis-->>Seed: OK
Seed-->>Client: client_id: seed-abc123
Client->>Seed: GET /oauth/authorize?client_id=seed-abc123
Seed->>Redis: Lookup client
Redis-->>Seed: Client metadata
Seed->>IdP: Redirect with static client_id
IdP-->>Client: Authorization code
Client->>Seed: POST /oauth/token (code + code_verifier)
Seed->>Redis: Lookup client
Seed->>IdP: Exchange code (with static client_id)
IdP-->>Seed: Access token + refresh token
Seed-->>Client: Access token + refresh tokenToken Exchange Flow
Endpoint: POST /oauth/token
File: src/routes/oauth-token.ts
Public endpoint (no authentication required)
Supported Grant Types
1. Authorization Code Grant
Exchange an authorization code for access and refresh tokens.
Request:
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=AUTHORIZATION_CODE
&redirect_uri=https://app.example.com/callback
&client_id=seed-abc123
&code_verifier=PKCE_VERIFIERRequired Parameters:
grant_type: Must be"authorization_code"code: Authorization code from /oauth/authorizeredirect_uri: Must match registered redirect URIclient_id: Client identifier from registrationcode_verifier: PKCE verifier (plain text that hashes to code_challenge)
Response (Success):
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"scope": "openid profile email"
}2. Refresh Token Grant
Exchange a refresh token for a new access token.
Request:
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&refresh_token=REFRESH_TOKEN
&client_id=seed-abc123Required Parameters:
grant_type: Must be"refresh_token"refresh_token: Refresh token from previous token responseclient_id: Client identifier
Response (Success):
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"scope": "openid profile email"
}Dynamic Client Registration Flow
For clients with IDs starting with the configured prefix (default: seed-):
Lookup client in Redis:
typescriptconst client = await clientStore.get(params.client_id); if (!client) { return res.status(401).json({ error: "invalid_client", error_description: "Client not found or expired" }); }Validate redirect_uri (authorization code grant only):
typescriptif (!client.redirect_uris.includes(params.redirect_uri)) { return res.status(400).json({ error: "invalid_request", error_description: "redirect_uri does not match registered URIs" }); }Replace client_id with static IdP client:
typescriptproxyParams.client_id = config.oidc.audience; // Static IdP client IDProxy to upstream token endpoint:
typescriptconst response = await fetch(config.oidc.tokenUrl, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams(proxyParams), });Return upstream response with same status code
Error Responses
OAuth error responses follow RFC 6749 Section 5.2:
{
"error": "invalid_request",
"error_description": "Missing required parameter: code"
}Error Codes:
invalid_request: Missing or invalid parametersinvalid_client: Client authentication failedinvalid_grant: Authorization code or refresh token invalidunsupported_grant_type: Grant type not supportedserver_error: Internal server error
Authorization Endpoint
Endpoint: GET /oauth/authorize
File: src/routes/oauth-authorize.ts
Public endpoint (no authentication required)
Request Parameters
GET /oauth/authorize
?response_type=code
&client_id=seed-abc123
&redirect_uri=https://app.example.com/callback
&scope=openid%20profile%20email
&state=random-csrf-token
&code_challenge=BASE64URL(SHA256(code_verifier))
&code_challenge_method=S256Required Parameters:
response_type: Must be"code"client_id: Client identifierredirect_uri: Callback URL (must match registered URI for DCR clients)scope: Requested scopes (e.g., "openid profile")state: CSRF protection token (recommended)code_challenge: PKCE challenge (base64url-encoded SHA-256 hash)code_challenge_method: Must be"S256"
Flow
Validate client (for DCR clients):
typescriptconst client = await clientStore.get(clientId); if (!client) { return res.redirect(`${redirectUri}?error=invalid_client&state=${state}`); } if (!client.redirect_uris.includes(redirectUri)) { return res.redirect(`${redirectUri}?error=invalid_request&error_description=invalid_redirect_uri&state=${state}`); }Replace client_id (for DCR clients):
typescriptparams.client_id = config.oidc.audience; // Static IdP clientRedirect to upstream authorization endpoint:
typescriptconst authUrl = `${config.oidc.authorizationUrl}?${new URLSearchParams(params)}`; res.redirect(302, authUrl);
Response
The endpoint returns a 302 redirect to the upstream IdP's authorization page. After authentication, the IdP redirects back to the redirect_uri with an authorization code:
HTTP/1.1 302 Found
Location: https://app.example.com/callback?code=AUTHORIZATION_CODE&state=random-csrf-tokenDynamic Client Registration
Endpoint: POST /oauth/register
File: src/routes/oauth-register.ts
Public endpoint (no authentication required)
RFC 7591 Compliance
Implements OAuth 2.0 Dynamic Client Registration Protocol (RFC 7591) with:
- Client metadata validation
- Redirect URI security checks
- Client ID generation
- TTL-based storage in Redis
Request
POST /oauth/register
Content-Type: application/json
{
"redirect_uris": [
"https://app.example.com/callback",
"http://localhost:3000/callback"
],
"client_name": "My Application",
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none",
"software_id": "my-app",
"software_version": "1.0.0"
}Required Fields:
redirect_uris: Array of callback URLs (1-10 URIs)
Optional Fields:
client_name: Human-readable name (default: "OAuth Client")grant_types: Array of grant types (default:["authorization_code"])- Allowed:
["authorization_code", "refresh_token"]
- Allowed:
response_types: Array of response types (default:["code"])- Allowed:
["code"]
- Allowed:
token_endpoint_auth_method: Auth method (default:"none")- Allowed:
["none", "client_secret_post", "client_secret_basic"]
- Allowed:
software_id: Software identifiersoftware_version: Software version
Redirect URI Validation
Security Rules:
- Must be a valid URL
- Must use
https://(exception:localhostcan usehttp://) - Must not contain a fragment (
#) - Maximum 10 redirect URIs per client
Examples:
// Valid
"https://app.example.com/callback"
"http://localhost:3000/callback"
"http://127.0.0.1:8080/auth"
// Invalid
"http://app.example.com/callback" // Not localhost, must use https
"https://app.example.com#fragment" // Contains fragment
"not-a-url" // Invalid URL formatClient ID Generation
function generateClientId(): string {
const prefix = config.dcr.clientIdPrefix; // "seed-"
const randomPart = randomBytes(6).toString("base64url"); // 12 chars
return `${prefix}${randomPart}`;
}
// Example: "seed-a1b2c3d4e5f6"Response (Success)
Status: 201 Created
{
"client_id": "seed-a1b2c3d4e5f6",
"client_name": "My Application",
"redirect_uris": [
"https://app.example.com/callback",
"http://localhost:3000/callback"
],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none",
"client_id_issued_at": 1702857600,
"client_secret_expires_at": 0,
"software_id": "my-app",
"software_version": "1.0.0"
}Notes:
client_secret_expires_at: Always 0 (public client, no secret)client_id_issued_at: Unix timestamp of registration- Client stored in Redis with 30-day TTL (configurable)
Response (Error)
{
"error": "invalid_redirect_uri",
"error_description": "redirect_uri must use https (except for localhost)"
}Error Codes:
invalid_redirect_uri: Redirect URI validation failedinvalid_client_metadata: Other validation failuresserver_error: Storage or processing error
Configuration
Environment Variables
# OAuth Endpoints
OAUTH_AUTHORIZATION_URL=https://auth.example.com/application/o/authorize/
OAUTH_TOKEN_URL=https://auth.example.com/application/o/token/
# OIDC Configuration
OIDC_ISSUER=https://auth.example.com/application/o/my-app/
OIDC_AUDIENCE=my-static-client-id
# Dynamic Client Registration
REDIS_URL=redis://redis:6379
DCR_CLIENT_TTL=2592000 # 30 days
DCR_RATE_LIMIT_WINDOW_MS=3600000 # 1 hour
DCR_RATE_LIMIT_MAX=10 # 10 requests per window
# Server
BASE_URL=https://seed.example.comSecurity Considerations
Redirect URI Security
- HTTPS required (except localhost)
- No fragments allowed
- Exact match validation
- Maximum 10 URIs per client
Client ID Security
- Cryptographically random generation
- Prefix-based identification (
seed-) - TTL-based expiration
- Redis storage with expiry
PKCE Requirements
- S256 method required (SHA-256)
- Code verifier: 43-128 characters
- Code challenge: Base64url-encoded SHA-256
Implementation Files
- Token Exchange:
src/routes/oauth-token.ts - Authorization:
src/routes/oauth-authorize.ts - Registration:
src/routes/oauth-register.ts - Client Store:
src/services/client-store.ts - Config:
src/config/oidc.ts,src/config/dcr.ts
Related Documentation
- Authentication Flow - JWT validation
- Session Management - MCP session tracking
- Configuration System - Environment configuration