Skip to content

OAuth 2.1 Implementation

Seed implements a complete OAuth 2.1 authorization server with PKCE support, token exchange, token revocation, 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)
  • Token Revocation (RFC 7009) - ✅ IMPLEMENTED (2026-01-06)
  • 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.

Token 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:

http
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_VERIFIER

Required Parameters:

  • grant_type: Must be "authorization_code"
  • code: Authorization code from /oauth/authorize
  • redirect_uri: Must match registered redirect URI
  • client_id: Client identifier from registration
  • code_verifier: PKCE verifier (plain text that hashes to code_challenge)

Response (Success):

json
{
  "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:

http
POST /oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&refresh_token=REFRESH_TOKEN
&client_id=seed-abc123

Required Parameters:

  • grant_type: Must be "refresh_token"
  • refresh_token: Refresh token from previous token response
  • client_id: Client identifier

Response (Success):

json
{
  "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-):

  1. Lookup client in Redis:

    typescript
    const client = await clientStore.get(params.client_id);
    if (!client) {
      return res.status(401).json({
        error: "invalid_client",
        error_description: "Client not found or expired"
      });
    }
  2. Validate redirect_uri (authorization code grant only):

    typescript
    if (!client.redirect_uris.includes(params.redirect_uri)) {
      return res.status(400).json({
        error: "invalid_request",
        error_description: "redirect_uri does not match registered URIs"
      });
    }
  3. Replace client_id with static IdP client:

    typescript
    proxyParams.client_id = config.oidc.audience; // Static IdP client ID
  4. Proxy to upstream token endpoint:

    typescript
    const response = await fetch(config.oidc.tokenUrl, {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams(proxyParams),
    });
  5. Return upstream response with same status code

Error Responses

OAuth error responses follow RFC 6749 Section 5.2:

json
{
  "error": "invalid_request",
  "error_description": "Missing required parameter: code"
}

Error Codes:

  • invalid_request: Missing or invalid parameters
  • invalid_client: Client authentication failed
  • invalid_grant: Authorization code or refresh token invalid
  • unsupported_grant_type: Grant type not supported
  • server_error: Internal server error

Authorization Endpoint

Endpoint: GET /oauth/authorize

File: src/routes/oauth-authorize.ts

Public endpoint (no authentication required)

Request Parameters

http
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=S256

Required Parameters:

  • response_type: Must be "code"
  • client_id: Client identifier
  • redirect_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

  1. Validate client (for DCR clients):

    typescript
    const 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}`);
    }
  2. Replace client_id (for DCR clients):

    typescript
    params.client_id = config.oidc.audience; // Static IdP client
  3. Redirect to upstream authorization endpoint:

    typescript
    const 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
HTTP/1.1 302 Found
Location: https://app.example.com/callback?code=AUTHORIZATION_CODE&state=random-csrf-token

Dynamic 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

http
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"]
  • response_types: Array of response types (default: ["code"])
    • Allowed: ["code"]
  • token_endpoint_auth_method: Auth method (default: "none")
    • Allowed: ["none", "client_secret_post", "client_secret_basic"]
  • software_id: Software identifier
  • software_version: Software version

Redirect URI Validation

Security Rules:

  1. Must be a valid URL
  2. Must use https:// (exception: localhost can use http://)
  3. Must not contain a fragment (#)
  4. Maximum 10 redirect URIs per client

Examples:

typescript
// 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 format

Client ID Generation

Client IDs are generated using cryptographically secure random characters:

typescript
function generateClientId(): string {
  const prefix = config.dcr.clientIdPrefix; // "seed-"
  const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
  const length = 12;

  const randomPart = Array.from({ length }, () =>
    chars[Math.floor(Math.random() * chars.length)]
  ).join("");

  return `${prefix}${randomPart}`;
}

// Example: "seed-a1b2c3d4e5f6"

Properties:

  • Prefix: Configurable via DCR_CLIENT_ID_PREFIX (default: "seed-")
  • Length: 12 random characters (lowercase alphanumeric)
  • Character set: a-z and 0-9 (36 characters)
  • Entropy: ~62 bits (36^12 combinations)
  • Collision resistance: Extremely low probability with 30-day TTL

Response (Success)

Status: 201 Created

json
{
  "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)

json
{
  "error": "invalid_redirect_uri",
  "error_description": "redirect_uri must use https (except for localhost)"
}

Error Codes:

  • invalid_redirect_uri: Redirect URI validation failed
  • invalid_client_metadata: Other validation failures
  • server_error: Storage or processing error

Rate Limiting

The Dynamic Client Registration endpoint is protected by distributed rate limiting (10 registrations/hour per IP, 1,000 globally).

See Also: Rate Limiting for comprehensive documentation including configuration, sliding window implementation, and observability.

Token Revocation

IMPLEMENTED (2026-01-06) - RFC 7009 compliant token revocation endpoint for access and refresh tokens.

Endpoint: POST /oauth/revoke

File: src/routes/oauth-revoke.ts

Public endpoint (no authentication required)

Request

http
POST /oauth/revoke
Content-Type: application/x-www-form-urlencoded

token=REFRESH_TOKEN_OR_ACCESS_TOKEN
&client_id=seed-abc123
&token_type_hint=refresh_token

Required Parameters:

  • token: The token to revoke (access token or refresh token)
  • client_id: Client identifier

Optional Parameters:

  • token_type_hint: Hint about token type (access_token or refresh_token)

Behavior

Refresh Token Revocation:

  • Token proxied to upstream IdP's revocation endpoint
  • IdP immediately invalidates the refresh token
  • Cannot be used to obtain new access tokens

Access Token Revocation:

  • Token added to revocation cache in Redis (5-minute TTL)
  • Authentication middleware checks revocation cache before validating token
  • Rejected with 401 if found in revocation cache
  • Does not affect upstream IdP (access tokens are typically short-lived)

Implementation Details:

typescript
// src/routes/oauth-revoke.ts
export const revocationCache = {
  keyPrefix: "revoked:token:",
  ttlSeconds: 300,  // 5 minutes (covers typical access token lifetime)
};

// Store revoked access token
await redis.setex(
  `${revocationCache.keyPrefix}${tokenHash}`,
  revocationCache.ttlSeconds,
  "1"
);

Response

Success (200 OK):

json
{}

RFC 7009 requires successful revocation to return 200 OK with empty response, regardless of whether the token was valid.

Error (400 Bad Request):

json
{
  "error": "invalid_request",
  "error_description": "Missing required parameter: token"
}

Security Considerations

Token Hashing:

  • Access tokens hashed (SHA-256) before storage in revocation cache
  • Prevents token leakage from cache inspection

TTL-Based Cleanup:

  • Revoked access tokens auto-expire after 5 minutes
  • Matches typical access token lifetime
  • Prevents unbounded cache growth

Upstream Delegation:

  • Refresh token revocation delegated to IdP
  • IdP handles actual invalidation and security
  • Seed acts as transparent proxy

Use Cases

  1. User Logout: Revoke refresh token to prevent new access tokens
  2. Security Incident: Immediately revoke compromised tokens
  3. Token Rotation: Revoke old tokens during rotation flows
  4. Session Termination: Revoke tokens when closing MCP session

See Also: API Documentation - Token Revocation for request examples and integration guidance.

Observability & Monitoring

OAuth endpoints include comprehensive logging and metrics for monitoring and debugging.

See Also: Observability for comprehensive documentation including structured logging, Prometheus metrics, and monitoring best practices.

Configuration

Environment Variables

bash
# 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.com

Security 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 code and refresh token grants
  • Authorization: src/routes/oauth-authorize.ts - Authorization endpoint with PKCE
  • Token Revocation: src/routes/oauth-revoke.ts - RFC 7009 token revocation endpoint
  • Registration: src/routes/oauth-register.ts - Dynamic Client Registration with rate limiting
  • Client Store: src/services/client-store.ts - Redis-backed client metadata storage
  • Logger Service: src/services/logger.js - Winston logger with structured logging
  • Metrics Service: src/services/metrics.ts - Prometheus metrics definitions
  • Rate Limiting: src/middleware/distributed-rate-limit.ts - Redis-backed rate limiter
  • Config: src/config/oidc.ts, src/config/dcr.ts, src/config/rate-limit.ts

Released under the MIT License.