Skip to content

First Steps

Walk through your first complete OAuth 2.1 flow with Seed, from client registration to making authenticated MCP requests.

Prerequisites

Before starting:

  • [ ] Seed is running (see Quick Start)
  • [ ] Authentication is enabled (AUTH_REQUIRED=true)
  • [ ] OIDC provider is configured
  • [ ] You have access to your OIDC provider's user credentials

Overview

This guide walks through the complete authentication and MCP workflow:

  1. Dynamic Client Registration - Register an OAuth client with Seed
  2. Authorization Flow - Obtain user consent and authorization code
  3. Token Exchange - Exchange code for access token
  4. MCP Session - Initialize an MCP session
  5. Tool Execution - Call MCP tools with authentication

Step 1: Register an OAuth Client

Seed supports Dynamic Client Registration (DCR) per RFC 7591. This allows clients to automatically register without manual configuration.

Register Client

bash
curl -X POST http://localhost:3000/oauth/register \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "My Test Client",
    "redirect_uris": ["http://localhost:8080/callback"],
    "grant_types": ["authorization_code", "refresh_token"],
    "response_types": ["code"],
    "token_endpoint_auth_method": "none"
  }'

Response

json
{
  "client_id": "seed-abc123xyz",
  "client_name": "My Test Client",
  "redirect_uris": ["http://localhost:8080/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none",
  "client_id_issued_at": 1702080000,
  "client_secret_expires_at": 0
}

Important: Save your client_id (e.g., seed-abc123xyz) - you'll need it for the next steps.

What Just Happened?

  1. Seed generated a unique client_id with seed- prefix
  2. Client metadata was stored in Redis with 30-day TTL
  3. No client secret required (public client using PKCE)
  4. Client is now authorized to use Seed's OAuth endpoints

Client Lifecycle

  • TTL: Clients expire after 30 days (configurable via DCR_CLIENT_TTL)
  • Storage: Clients are stored in Redis at key dcr:client:{client_id}
  • Rate Limiting: Default 10 registrations per hour per IP

Step 2: Start Authorization Flow

The authorization flow redirects users to your OIDC provider for authentication and consent.

Generate PKCE Challenge

First, generate a PKCE (Proof Key for Code Exchange) code verifier and challenge:

bash
# Generate code verifier (43-128 characters)
CODE_VERIFIER=$(openssl rand -base64 32 | tr -d '=' | tr '+/' '-_')
echo "Code Verifier: $CODE_VERIFIER"

# Generate code challenge (SHA256 hash)
CODE_CHALLENGE=$(echo -n "$CODE_VERIFIER" | openssl dgst -binary -sha256 | openssl base64 | tr -d '=' | tr '+/' '-_')
echo "Code Challenge: $CODE_CHALLENGE"

Important: Save both values - you'll need CODE_VERIFIER in Step 3.

Build Authorization URL

bash
CLIENT_ID="seed-abc123xyz"  # Your client_id from Step 1
REDIRECT_URI="http://localhost:8080/callback"
STATE=$(openssl rand -hex 16)  # Random state for CSRF protection

AUTH_URL="http://localhost:3000/oauth/authorize?response_type=code&client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&code_challenge=${CODE_CHALLENGE}&code_challenge_method=S256&state=${STATE}&scope=openid"

echo "Visit this URL in your browser:"
echo "$AUTH_URL"

Complete Authorization

  1. Open the URL in your browser
  2. Redirect to OIDC provider - You'll be redirected to your configured OIDC provider
  3. Login - Enter your credentials
  4. Grant consent - Approve the requested permissions
  5. Receive authorization code - You'll be redirected back to your redirect_uri with a code parameter

Example redirect:

http://localhost:8080/callback?code=def502004e9c4a...&state=abc123...

Extract the code:

bash
# From the redirect URL
AUTHORIZATION_CODE="def502004e9c4a..."

Step 3: Exchange Code for Token

Exchange the authorization code for an access token using your PKCE verifier.

Token Request

bash
curl -X POST http://localhost:3000/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=${AUTHORIZATION_CODE}" \
  -d "client_id=${CLIENT_ID}" \
  -d "redirect_uri=${REDIRECT_URI}" \
  -d "code_verifier=${CODE_VERIFIER}"

Response

json
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtleTEifQ...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "def502004e9c4a3e2f1b0c8d7a6e5f4d3c2b1a0...",
  "scope": "openid"
}

Save your tokens:

bash
ACCESS_TOKEN="eyJhbGci..."
REFRESH_TOKEN="def50200..."

Token Details

  • access_token: JWT bearer token for authentication
  • expires_in: Token lifetime in seconds (typically 3600 = 1 hour)
  • refresh_token: Use to obtain new access tokens when current expires
  • scope: Granted scopes (minimum: openid)

Inspect Your Token

Decode the JWT to see claims:

bash
echo "$ACCESS_TOKEN" | cut -d. -f2 | base64 -d | jq

Example payload:

json
{
  "sub": "user123",
  "email": "user@example.com",
  "name": "John Doe",
  "iss": "https://auth.example.com/application/o/my-app/",
  "aud": "my-client-id",
  "exp": 1702083600,
  "iat": 1702080000
}

Step 4: Initialize MCP Session

Now use your access token to initialize an MCP session.

Send Initialize Request

bash
curl -X POST http://localhost:3000/mcp \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "initialize",
    "params": {
      "protocolVersion": "2024-11-05",
      "capabilities": {},
      "clientInfo": {
        "name": "my-test-client",
        "version": "1.0.0"
      }
    }
  }'

Response

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2024-11-05",
    "serverInfo": {
      "name": "seed",
      "version": "0.1.3"
    },
    "capabilities": {
      "tools": {}
    }
  }
}

Check response headers for session ID:

bash
curl -X POST http://localhost:3000/mcp \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -d '{ ... }' \
  -i | grep mcp-session-id

Example:

mcp-session-id: 550e8400-e29b-41d4-a716-446655440000

Understanding Sessions

  • Session ID: UUID assigned to each initialized session
  • Session Storage: Transport instances stored in memory map
  • Session Header: Include mcp-session-id header in subsequent requests to reuse session
  • Session Lifecycle: Sessions exist until server restart or explicit cleanup

Step 5: List Available Tools

Discover what MCP tools are available.

List Tools Request

bash
curl -X POST http://localhost:3000/mcp \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "mcp-session-id: 550e8400-e29b-41d4-a716-446655440000" \
  -d '{
    "jsonrpc": "2.0",
    "id": 2,
    "method": "tools/list",
    "params": {}
  }'

Response

json
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "tools": [
      {
        "name": "seed_ping",
        "description": "Health check tool - returns server status",
        "inputSchema": {
          "type": "object",
          "properties": {},
          "additionalProperties": false
        }
      }
    ]
  }
}

Step 6: Call MCP Tools

Execute an MCP tool with your authenticated session.

Call seed_ping Tool

bash
curl -X POST http://localhost:3000/mcp \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "mcp-session-id: 550e8400-e29b-41d4-a716-446655440000" \
  -d '{
    "jsonrpc": "2.0",
    "id": 3,
    "method": "tools/call",
    "params": {
      "name": "seed_ping",
      "arguments": {}
    }
  }'

Response

json
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\"status\":\"ok\",\"version\":\"0.1.3\",\"timestamp\":\"2025-12-09T10:30:00.000Z\"}"
      }
    ]
  }
}

Tool Response Format

MCP tool responses follow this structure:

typescript
{
  "content": [
    {
      "type": "text" | "image" | "resource",
      "text"?: string,           // For type: text
      "data"?: string,           // For type: image (base64)
      "mimeType"?: string,       // For type: image
      "uri"?: string,            // For type: resource
      "mimeType"?: string        // For type: resource
    }
  ],
  "isError"?: boolean
}

Step 7: Refresh Access Token

When your access token expires, use the refresh token to obtain a new one.

Refresh Token Request

bash
curl -X POST http://localhost:3000/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=${REFRESH_TOKEN}" \
  -d "client_id=${CLIENT_ID}"

Response

json
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtleTEifQ...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "ghi789012k3l4m5n6o7p8q9r0s1t2u3v4w5x6...",
  "scope": "openid"
}

Update your tokens:

bash
ACCESS_TOKEN="<new_access_token>"
REFRESH_TOKEN="<new_refresh_token>"

Token Rotation

Some OIDC providers implement refresh token rotation:

  • Each refresh issues a new refresh token
  • Old refresh token becomes invalid
  • Always save the new refresh token from responses

Complete Workflow Script

Here's a complete bash script demonstrating the full flow:

bash
#!/bin/bash

set -e

# Configuration
BASE_URL="http://localhost:3000"
REDIRECT_URI="http://localhost:8080/callback"

echo "=== Step 1: Register Client ==="
REGISTER_RESPONSE=$(curl -s -X POST "$BASE_URL/oauth/register" \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "Test Client",
    "redirect_uris": ["'$REDIRECT_URI'"],
    "grant_types": ["authorization_code", "refresh_token"],
    "response_types": ["code"],
    "token_endpoint_auth_method": "none"
  }')

CLIENT_ID=$(echo "$REGISTER_RESPONSE" | jq -r '.client_id')
echo "Client ID: $CLIENT_ID"

echo ""
echo "=== Step 2: Generate PKCE ==="
CODE_VERIFIER=$(openssl rand -base64 32 | tr -d '=' | tr '+/' '-_')
CODE_CHALLENGE=$(echo -n "$CODE_VERIFIER" | openssl dgst -binary -sha256 | openssl base64 | tr -d '=' | tr '+/' '-_')
STATE=$(openssl rand -hex 16)

echo "Code Verifier: $CODE_VERIFIER"
echo "Code Challenge: $CODE_CHALLENGE"
echo "State: $STATE"

echo ""
echo "=== Step 3: Authorization URL ==="
AUTH_URL="$BASE_URL/oauth/authorize?response_type=code&client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&code_challenge=${CODE_CHALLENGE}&code_challenge_method=S256&state=${STATE}&scope=openid"
echo "Visit this URL to authorize:"
echo "$AUTH_URL"
echo ""
echo "After authorization, paste the 'code' parameter from the redirect URL:"
read -r AUTHORIZATION_CODE

echo ""
echo "=== Step 4: Exchange Code for Token ==="
TOKEN_RESPONSE=$(curl -s -X POST "$BASE_URL/oauth/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=${AUTHORIZATION_CODE}" \
  -d "client_id=${CLIENT_ID}" \
  -d "redirect_uri=${REDIRECT_URI}" \
  -d "code_verifier=${CODE_VERIFIER}")

ACCESS_TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.access_token')
echo "Access Token: ${ACCESS_TOKEN:0:50}..."

echo ""
echo "=== Step 5: Initialize MCP Session ==="
INIT_RESPONSE=$(curl -s -X POST "$BASE_URL/mcp" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "initialize",
    "params": {
      "protocolVersion": "2024-11-05",
      "capabilities": {},
      "clientInfo": {"name": "test-client", "version": "1.0.0"}
    }
  }')

echo "$INIT_RESPONSE" | jq

echo ""
echo "=== Step 6: List Tools ==="
TOOLS_RESPONSE=$(curl -s -X POST "$BASE_URL/mcp" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -d '{
    "jsonrpc": "2.0",
    "id": 2,
    "method": "tools/list",
    "params": {}
  }')

echo "$TOOLS_RESPONSE" | jq

echo ""
echo "=== Step 7: Call seed_ping Tool ==="
PING_RESPONSE=$(curl -s -X POST "$BASE_URL/mcp" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -d '{
    "jsonrpc": "2.0",
    "id": 3,
    "method": "tools/call",
    "params": {
      "name": "seed_ping",
      "arguments": {}
    }
  }')

echo "$PING_RESPONSE" | jq

echo ""
echo "=== Workflow Complete ==="

Save as test-seed-flow.sh and run:

bash
chmod +x test-seed-flow.sh
./test-seed-flow.sh

Understanding User Context

When tools are called, they receive user context extracted from the JWT:

typescript
interface UserContext {
  sub: string;        // User ID (from JWT 'sub' claim)
  email?: string;     // User email (from JWT 'email' claim)
  name?: string;      // User name (from JWT 'name' claim)
  groups?: string[];  // User groups (from JWT 'groups' claim)
}

Tools can use this context to:

  • Filter data based on user permissions
  • Audit actions by user
  • Personalize responses
  • Implement authorization logic

Common Issues

Issue: "Client not found"

Error:

json
{
  "error": "invalid_client",
  "error_description": "Client not found"
}

Solutions:

  1. Check client_id is correct
  2. Client may have expired (30-day TTL)
  3. Re-register the client

Issue: "Invalid authorization code"

Error:

json
{
  "error": "invalid_grant",
  "error_description": "Invalid authorization code"
}

Solutions:

  1. Authorization codes are single-use - don't reuse them
  2. Codes expire quickly (typically 10 minutes)
  3. Ensure redirect_uri matches exactly
  4. Verify code_verifier matches the original challenge

Issue: "Token expired"

Error:

json
{
  "error": {
    "code": -32001,
    "message": "Unauthorized",
    "data": {
      "reason": "invalid_token",
      "details": "JWT expired"
    }
  }
}

Solution: Use the refresh token to obtain a new access token (Step 7).

Issue: "Invalid PKCE code verifier"

Error:

json
{
  "error": "invalid_grant",
  "error_description": "Invalid code verifier"
}

Solution: Ensure you're using the same code_verifier that was used to generate code_challenge in Step 2.

Next Steps

Now that you've completed the full OAuth and MCP workflow:

  1. API Reference - Explore all available endpoints
  2. Architecture: OAuth - Deep dive into OAuth implementation
  3. Architecture: MCP Server - Understanding MCP architecture
  4. Development: MCP Tools - Create custom MCP tools

Additional Resources

Token Management Best Practices

  • Store tokens securely - Never log or expose tokens
  • Handle expiration - Implement automatic refresh before expiration
  • Revoke on logout - Clear tokens when users log out
  • Use HTTPS - Always use TLS in production

PKCE Security

PKCE prevents authorization code interception attacks:

  • Code verifier - Random string (43-128 characters)
  • Code challenge - SHA256 hash of verifier
  • Verification - Server validates verifier matches challenge

OAuth 2.1 Features

Seed implements OAuth 2.1 security best practices:

  • PKCE required - All clients must use PKCE (no client secrets)
  • Short-lived codes - Authorization codes expire quickly
  • Token rotation - Refresh tokens may rotate on use
  • Exact redirect matching - No redirect URI wildcards

Released under the MIT License.