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:
- Dynamic Client Registration - Register an OAuth client with Seed
- Authorization Flow - Obtain user consent and authorization code
- Token Exchange - Exchange code for access token
- MCP Session - Initialize an MCP session
- 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
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
{
"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?
- Seed generated a unique
client_idwithseed-prefix - Client metadata was stored in Redis with 30-day TTL
- No client secret required (public client using PKCE)
- 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:
# 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
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
- Open the URL in your browser
- Redirect to OIDC provider - You'll be redirected to your configured OIDC provider
- Login - Enter your credentials
- Grant consent - Approve the requested permissions
- Receive authorization code - You'll be redirected back to your
redirect_uriwith a code parameter
Example redirect:
http://localhost:8080/callback?code=def502004e9c4a...&state=abc123...Extract the code:
# 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
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
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtleTEifQ...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "def502004e9c4a3e2f1b0c8d7a6e5f4d3c2b1a0...",
"scope": "openid"
}Save your tokens:
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:
echo "$ACCESS_TOKEN" | cut -d. -f2 | base64 -d | jqExample payload:
{
"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
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
{
"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:
curl -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-d '{ ... }' \
-i | grep mcp-session-idExample:
mcp-session-id: 550e8400-e29b-41d4-a716-446655440000Understanding Sessions
- Session ID: UUID assigned to each initialized session
- Session Storage: Transport instances stored in memory map
- Session Header: Include
mcp-session-idheader 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
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
{
"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
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
{
"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:
{
"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
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
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtleTEifQ...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "ghi789012k3l4m5n6o7p8q9r0s1t2u3v4w5x6...",
"scope": "openid"
}Update your tokens:
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:
#!/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:
chmod +x test-seed-flow.sh
./test-seed-flow.shUnderstanding User Context
When tools are called, they receive user context extracted from the JWT:
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:
{
"error": "invalid_client",
"error_description": "Client not found"
}Solutions:
- Check
client_idis correct - Client may have expired (30-day TTL)
- Re-register the client
Issue: "Invalid authorization code"
Error:
{
"error": "invalid_grant",
"error_description": "Invalid authorization code"
}Solutions:
- Authorization codes are single-use - don't reuse them
- Codes expire quickly (typically 10 minutes)
- Ensure
redirect_urimatches exactly - Verify
code_verifiermatches the original challenge
Issue: "Token expired"
Error:
{
"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:
{
"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:
- API Reference - Explore all available endpoints
- Architecture: OAuth - Deep dive into OAuth implementation
- Architecture: MCP Server - Understanding MCP architecture
- 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
Related Documentation
- Quick Start - Getting Seed running
- Configuration - OIDC provider setup
- API: OAuth - OAuth endpoint reference
- API: MCP - MCP endpoint reference