Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Authentication

Pierre supports multiple authentication methods for different use cases.

Authentication Methods

methoduse caseheaderendpoints
jwt tokensmcp clients, web appsAuthorization: Bearer <token>all authenticated endpoints
api keysa2a systemsX-API-Key: <key>a2a endpoints
oauth2provider integrationvariesfitness provider apis

JWT Authentication

Registration

curl -X POST http://localhost:8081/api/auth/register \
  -H "Authorization: Bearer <admin_jwt_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "password": "SecurePass123!",
    "display_name": "User Name"
  }'

Response:

{
  "user_id": "uuid",
  "email": "user@example.com",
  "token": "jwt_token",
  "expires_at": "2024-01-01T00:00:00Z"
}

Login

Uses OAuth2 Resource Owner Password Credentials (ROPC) flow:

curl -X POST http://localhost:8081/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=password&username=user@example.com&password=SecurePass123!"

Response includes jwt_token. Store securely.

Using JWT Tokens

Include in authorization header:

curl -H "Authorization: Bearer <jwt_token>" \
  http://localhost:8081/mcp

Token Expiry

Default: 24 hours (configurable via JWT_EXPIRY_HOURS)

Refresh before expiry:

curl -X POST http://localhost:8081/api/auth/refresh \
  -H "Authorization: Bearer <current_token>"

API Key Authentication

For a2a systems and service-to-service communication.

Creating API Keys

Requires admin or user jwt:

curl -X POST http://localhost:8081/api/keys \
  -H "Authorization: Bearer <jwt_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My A2A System",
    "tier": "professional"
  }'

Response:

{
  "api_key": "generated_key",
  "name": "My A2A System",
  "tier": "professional",
  "created_at": "2024-01-01T00:00:00Z"
}

Save api key - cannot be retrieved later.

Using API Keys

curl -H "X-API-Key: <api_key>" \
  http://localhost:8081/a2a/tools

API Key Tiers

  • trial: 1,000 requests/month (auto-expires after 14 days)
  • starter: 10,000 requests/month
  • professional: 100,000 requests/month
  • enterprise: unlimited (no fixed monthly cap)

Rate limits are enforced per API key over a rolling 30-day window.

OAuth2 (MCP Client Authentication)

Pierre acts as oauth2 authorization server for mcp clients.

OAuth2 vs OAuth (Terminology)

Pierre implements two oauth systems:

  1. oauth2_server module (src/oauth2_server/): pierre AS oauth2 server

    • mcp clients authenticate TO pierre
    • rfc 7591 dynamic client registration
    • issues jwt access tokens
  2. oauth2_client module (src/oauth2_client/): pierre AS oauth2 client

    • pierre authenticates TO fitness providers (strava, garmin, fitbit, whoop)
    • manages provider tokens
    • handles token refresh

OAuth2 Flow (MCP Clients)

Sdk handles automatically. Manual flow:

  1. register client:
curl -X POST http://localhost:8081/oauth2/register \
  -H "Content-Type: application/json" \
  -d '{
    "redirect_uris": ["http://localhost:35535/oauth/callback"],
    "client_name": "My MCP Client",
    "grant_types": ["authorization_code"]
  }'
  1. authorization (browser):
http://localhost:8081/oauth2/authorize?
  client_id=<client_id>&
  redirect_uri=<redirect_uri>&
  response_type=code&
  code_challenge=<sha256_base64url(verifier)>&
  code_challenge_method=S256
  1. token exchange:
curl -X POST http://localhost:8081/oauth2/token \
  -d "grant_type=authorization_code&\
      code=<code>&\
      client_id=<client_id>&\
      client_secret=<client_secret>&\
      code_verifier=<verifier>"

Receives jwt access token.

PKCE Enforcement

Pierre requires pkce (rfc 7636) for security:

  • code verifier: 43-128 random characters
  • code challenge: base64url(sha256(verifier))
  • challenge method: S256 only

No plain text challenge methods allowed.

MCP Client Integration (Claude Code, VS Code, etc.)

mcp clients (claude code, vs code with cline/continue, cursor, etc.) connect to pierre via http-based mcp protocol.

Authentication Flow

  1. user registration and login:
# create user account
curl -X POST http://localhost:8081/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "password": "SecurePass123!"
  }'

# login to get jwt token (OAuth2 ROPC flow)
curl -X POST http://localhost:8081/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=password&username=user@example.com&password=SecurePass123!"

response includes jwt token:

{
  "jwt_token": "eyJ0eXAiOiJKV1Qi...",
  "expires_at": "2025-11-05T18:00:00Z",
  "user": {
    "id": "75059e8b-1f56-4fcf-a14e-860966783c93",
    "email": "user@example.com"
  }
}
  1. configure mcp client:

option a: claude code - using /mcp command (interactive):

# in claude code session
/mcp add pierre-production \
  --url http://localhost:8081/mcp \
  --transport http \
  --header "Authorization: Bearer eyJ0eXAiOiJKV1Qi..."

manual configuration (~/.config/claude-code/mcp_config.json):

{
  "mcpServers": {
    "pierre-production": {
      "url": "http://localhost:8081/mcp",
      "transport": "http",
      "headers": {
        "Authorization": "Bearer eyJ0eXAiOiJKV1Qi..."
      }
    }
  }
}

option b: vs code (cline, continue, cursor) - edit settings:

for cline extension (~/.vscode/settings.json or workspace settings):

{
  "cline.mcpServers": {
    "pierre-production": {
      "url": "http://localhost:8081/mcp",
      "transport": "http",
      "headers": {
        "Authorization": "Bearer eyJ0eXAiOiJKV1Qi..."
      }
    }
  }
}

for continue extension:

{
  "continue.mcpServers": [{
    "url": "http://localhost:8081/mcp",
    "headers": {
      "Authorization": "Bearer eyJ0eXAiOiJKV1Qi..."
    }
  }]
}
  1. automatic authentication:

mcp clients include jwt token in all mcp requests:

POST /mcp HTTP/1.1
Host: localhost:8081
Authorization: Bearer eyJ0eXAiOiJKV1Qi...
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "connect_provider",
    "arguments": {"provider": "strava"}
  }
}

pierre’s mcp server validates jwt on every request:

  • extracts user_id from token
  • validates signature using jwks
  • checks expiration
  • enforces rate limits per tenant

MCP Endpoint Authentication Requirements

endpointauth requirednotes
POST /mcp (initialize)nodiscovery only
POST /mcp (tools/list)nounauthenticated tool listing
POST /mcp (tools/call)yesrequires valid jwt
POST /mcp (prompts/list)nodiscovery only
POST /mcp (resources/list)nodiscovery only

implementation: src/mcp/mcp_request_processor.rs:95-106

Token Expiry and Refresh

jwt tokens expire after 24 hours (default, configurable via JWT_EXPIRY_HOURS).

when token expires, user must:

  1. login again to get new jwt token
  2. update claude code configuration with new token

automatic refresh not implemented in most mcp clients (requires manual re-login).

Connecting to Fitness Providers

once authenticated to pierre, connect to fitness providers:

  1. using mcp tool (recommended):
user: "connect to strava"

mcp client calls connect_provider tool with jwt authentication:

  • pierre validates jwt, extracts user_id
  • generates oauth authorization url for that user_id
  • opens browser for strava authorization
  • callback stores strava token for user_id
  • no pierre login required - user already authenticated via jwt!
  1. via rest api:
curl -H "Authorization: Bearer <jwt>" \
  http://localhost:8081/api/oauth/auth/strava/<user_id>

Why No Pierre Login During Strava OAuth?

common question: “why don’t i need to log into pierre when connecting to strava?”

answer: you’re already authenticated!

sequence:

  1. you logged into pierre (got jwt token)
  2. configured your mcp client (claude code, vs code, cursor, etc.) with jwt token
  3. mcp client includes jwt in every mcp request
  4. when you say “connect to strava”:
    • mcp client sends tools/call with jwt
    • pierre extracts user_id from jwt (e.g., 75059e8b-1f56-4fcf-a14e-860966783c93)
    • generates oauth url: http://localhost:8081/api/oauth/auth/strava/75059e8b-1f56-4fcf-a14e-860966783c93
    • state parameter includes user_id: 75059e8b-1f56-4fcf-a14e-860966783c93:random_nonce
  5. browser opens strava authorization (you prove you own the strava account)
  6. strava redirects to callback with code
  7. pierre validates state, exchanges code for token
  8. stores strava token for your user_id (from jwt)

key insight: jwt token proves your identity to pierre. strava oauth proves you own the fitness account. no duplicate login needed.

Security Considerations

jwt token storage: mcp clients store jwt tokens in configuration files:

  • claude code: ~/.config/claude-code/mcp_config.json
  • vs code extensions: .vscode/settings.json or user settings

these files should have restricted permissions (chmod 600 for config files).

token exposure: jwt tokens in config files are sensitive. treat like passwords:

  • don’t commit to version control
  • don’t share tokens
  • rotate regularly (re-login to get new token)
  • revoke if compromised

oauth state validation: pierre validates oauth state parameters to prevent:

  • csrf attacks (random nonce verified)
  • user_id spoofing (state must match authenticated user)
  • replay attacks (state used once)

implementation: src/routes/auth.rs, src/mcp/multitenant.rs

Troubleshooting

“authentication required” error:

  • check jwt token in your mcp client’s configuration file
    • claude code: ~/.config/claude-code/mcp_config.json
    • vs code: .vscode/settings.json
  • verify token not expired (24h default)
  • confirm token format: Bearer eyJ0eXAi...

“invalid token” error:

  • token may be expired - login again
  • token signature invalid - check PIERRE_MASTER_ENCRYPTION_KEY
  • user account may be disabled - check user status

fitness provider connection fails:

  • check oauth credentials (client_id, client_secret) at server startup
  • verify redirect_uri matches provider registration
  • see oauth credential validation logs for fingerprint debugging

oauth credential debugging:

pierre validates oauth credentials at startup and logs fingerprints:

OAuth provider strava: enabled=true, client_id=163846,
  secret_length=40, secret_fingerprint=f3c0d77f

use fingerprints to compare secrets without exposing actual values:

# check correct secret
echo -n "0f2b184c076e60a35e8ced43db9c3c20c5fcf4f3" | \
  sha256sum | cut -c1-8
# output: f3c0d77f ← correct

# check wrong secret
echo -n "1dfc45ad0a1f6983b835e4495aa9473d111d03bc" | \
  sha256sum | cut -c1-8
# output: 79092abb ← wrong!

if fingerprints don’t match, you’re using wrong credentials.

Provider OAuth (Fitness Data)

Pierre acts as oauth client to fitness providers.

Supported Providers

  • strava (oauth2)
  • garmin (oauth1 + oauth2)
  • fitbit (oauth2)

Configuration

Set environment variables:

# strava (local development)
export STRAVA_CLIENT_ID=your_id
export STRAVA_CLIENT_SECRET=your_secret
export STRAVA_REDIRECT_URI=http://localhost:8081/api/oauth/callback/strava  # local dev only

# strava (production)
export STRAVA_REDIRECT_URI=https://api.example.com/api/oauth/callback/strava  # required

# garmin (local development)
export GARMIN_CLIENT_ID=your_key
export GARMIN_CLIENT_SECRET=your_secret
export GARMIN_REDIRECT_URI=http://localhost:8081/api/oauth/callback/garmin  # local dev only

# garmin (production)
export GARMIN_REDIRECT_URI=https://api.example.com/api/oauth/callback/garmin  # required

callback url security requirements:

  • http urls: local development only (localhost/127.0.0.1)
  • https urls: required for production deployments
  • failure to use https in production:
    • authorization codes transmitted unencrypted
    • vulnerable to token interception
    • most providers reject http callbacks in production

Connecting Providers

Via mcp tool:

user: "connect to strava"

Or via rest api:

curl -H "Authorization: Bearer <jwt>" \
  http://localhost:8081/api/oauth/auth/strava/<user_id>

Opens browser for provider authentication. After approval, redirected to callback:

# local development
http://localhost:8081/api/oauth/callback/strava?code=<auth_code>

# production (https required)
https://api.example.com/api/oauth/callback/strava?code=<auth_code>

Pierre exchanges code for access/refresh tokens, stores encrypted.

security: authorization codes in callback urls must be protected with tls in production. Http callbacks leak codes to network observers.

Token Storage

Provider tokens stored encrypted in database:

  • encryption key: tenant-specific key (derived from master key)
  • algorithm: aes-256-gcm
  • rotation: automatic refresh before expiry

Checking Connection Status

curl -H "Authorization: Bearer <jwt>" \
  http://localhost:8081/oauth/status

Response:

{
  "connected_providers": ["strava"],
  "strava": {
    "connected": true,
    "expires_at": "2024-01-01T00:00:00Z"
  },
  "garmin": {
    "connected": false
  }
}

Web Application Security

Pierre implements secure cookie-based authentication for web applications using httpOnly cookies with CSRF protection.

Security Model

httpOnly cookies prevent JavaScript access to JWT tokens, eliminating XSS-based token theft:

Set-Cookie: auth_token=<jwt>; HttpOnly; Secure; SameSite=Strict; Max-Age=86400

CSRF protection uses double-submit cookie pattern with cryptographic tokens:

Set-Cookie: csrf_token=<token>; Secure; SameSite=Strict; Max-Age=1800
X-CSRF-Token: <token>  (sent in request header)
flagvaluepurpose
HttpOnlytrueprevents JavaScript access (XSS protection)
Securetruerequires HTTPS (prevents sniffing)
SameSiteStrictprevents cross-origin requests (CSRF mitigation)
Max-Age86400 (auth), 1800 (csrf)automatic expiration

Authentication Flow

login (POST /oauth/token - OAuth2 ROPC flow):

curl -X POST http://localhost:8081/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=password&username=user@example.com&password=SecurePass123!"

response sets two cookies and returns csrf token:

{
  "jwt_token": "eyJ0eXAiOiJKV1Qi...",  // deprecated, for backward compatibility
  "csrf_token": "cryptographic_random_32bytes",
  "user": {"id": "uuid", "email": "user@example.com"},
  "expires_at": "2025-01-20T18:00:00Z"
}

cookies set automatically:

Set-Cookie: auth_token=eyJ0eXAiOiJKV1Qi...; HttpOnly; Secure; SameSite=Strict; Max-Age=86400
Set-Cookie: csrf_token=cryptographic_random_32bytes; Secure; SameSite=Strict; Max-Age=1800

authenticated requests:

browsers automatically include cookies. web apps must include csrf token header:

curl -X POST http://localhost:8081/api/something \
  -H "X-CSRF-Token: cryptographic_random_32bytes" \
  -H "Cookie: auth_token=...; csrf_token=..." \
  -d '{"data": "value"}'

server validates:

  1. jwt token from auth_token cookie
  2. csrf token from csrf_token cookie matches X-CSRF-Token header
  3. csrf token is valid for authenticated user
  4. csrf token not expired (30 minute lifetime)

logout (POST /api/auth/logout):

curl -X POST http://localhost:8081/api/auth/logout \
  -H "Cookie: auth_token=..."

server clears cookies:

Set-Cookie: auth_token=; Max-Age=0
Set-Cookie: csrf_token=; Max-Age=0

CSRF Protection Details

token generation:

  • 256-bit (32 byte) cryptographic randomness
  • user-scoped validation (token tied to specific user_id)
  • 30-minute expiration
  • stored in-memory (HashMap with automatic cleanup)

validation requirements:

  • csrf validation required for: POST, PUT, DELETE, PATCH
  • csrf validation skipped for: GET, HEAD, OPTIONS
  • validation extracts:
    1. user_id from jwt token (auth_token cookie)
    2. csrf token from X-CSRF-Token header
    3. verifies token valid for that user_id
    4. verifies token not expired

double-submit cookie pattern:

1. server generates csrf token
2. server sets csrf_token cookie (JavaScript readable)
3. server returns csrf_token in JSON response
4. client stores csrf_token in memory
5. client includes X-CSRF-Token header in state-changing requests
6. server validates:
   - csrf_token cookie matches X-CSRF-Token header
   - token is valid for authenticated user_id
   - token not expired

security benefits:

  • attacker cannot read csrf token (cross-origin restriction)
  • attacker cannot forge valid csrf token (cryptographic randomness)
  • attacker cannot reuse old token (user-scoped validation)
  • attacker cannot use expired token (30-minute lifetime)

Frontend Integration (React/TypeScript)

axios configuration:

// enable automatic cookie handling
axios.defaults.withCredentials = true;

// request interceptor for csrf token
axios.interceptors.request.use((config) => {
  if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(config.method?.toUpperCase() || '')) {
    const csrfToken = apiService.getCsrfToken();
    if (csrfToken && config.headers) {
      config.headers['X-CSRF-Token'] = csrfToken;
    }
  }
  return config;
});

// response interceptor for 401 errors
axios.interceptors.response.use(
  (response) => response,
  async (error) => {
    if (error.response?.status === 401) {
      // clear csrf token and redirect to login
      apiService.clearCsrfToken();
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

login flow (OAuth2 ROPC):

async function login(email: string, password: string) {
  const params = new URLSearchParams({
    grant_type: 'password',
    username: email,
    password: password
  });
  const response = await axios.post('/oauth/token', params, {
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
  });

  // store csrf token in memory (cookies set automatically)
  apiService.setCsrfToken(response.data.csrf_token);

  // store user info in localStorage (not sensitive)
  localStorage.setItem('user', JSON.stringify(response.data.user));

  return response.data;
}

logout flow:

async function logout() {
  try {
    // call backend to clear httpOnly cookies
    await axios.post('/api/auth/logout');
  } catch (error) {
    console.error('Logout failed:', error);
  } finally {
    // clear client-side state
    apiService.clearCsrfToken();
    localStorage.removeItem('user');
  }
}

Token Refresh

web apps can proactively refresh tokens using the refresh endpoint:

async function refreshToken() {
  const response = await axios.post('/api/auth/refresh');

  // server sets new auth_token and csrf_token cookies
  apiService.setCsrfToken(response.data.csrf_token);

  return response.data;
}

refresh generates:

  • new jwt token (24 hour expiry)
  • new csrf token (30 minute expiry)
  • both cookies updated automatically

when to refresh:

  • proactively before jwt expires (24h default)
  • after csrf token expires (30min default)
  • after receiving 401 response with expired token

Implementation References

backend:

  • csrf token manager: src/security/csrf.rs
  • secure cookie utilities: src/security/cookies.rs
  • csrf middleware: src/middleware/csrf.rs
  • authentication middleware: src/middleware/auth.rs (cookie-aware)
  • auth handlers: src/routes/auth.rs (login, refresh, logout)

frontend:

  • api service: frontend/src/services/api.ts
  • auth context: frontend/src/contexts/AuthContext.tsx

Backward Compatibility

pierre supports both cookie-based and bearer token authentication simultaneously:

  1. cookie-based (web apps): jwt from httpOnly cookie
  2. bearer token (api clients): Authorization: Bearer <token> header

middleware tries cookies first, falls back to authorization header.

API Key Authentication (Service-to-Service)

for a2a systems and service-to-service communication, api keys provide simpler authentication without cookies or csrf.

Security Features

Password Hashing

  • algorithm: argon2id (default) or bcrypt
  • configurable work factor
  • per-user salt

Token Encryption

  • jwt signing: rs256 asymmetric (rsa) or hs256 symmetric
    • rs256: 4096-bit rsa keys (production), 2048-bit (tests)
    • hs256: 64-byte secret (legacy)
  • provider tokens: aes-256-gcm
  • encryption keys: two-tier system
    • master key (env: PIERRE_MASTER_ENCRYPTION_KEY)
    • tenant keys (derived from master key)

RS256/JWKS

Asymmetric signing for distributed token verification.

Public keys available at /admin/jwks (legacy) and /oauth2/jwks (oauth2 clients):

curl http://localhost:8081/oauth2/jwks

Response (rfc 7517 compliant):

{
  "keys": [
    {
      "kty": "RSA",
      "use": "sig",
      "kid": "key_2024_01_01_123456",
      "n": "modulus_base64url",
      "e": "exponent_base64url"
    }
  ]
}

cache-control headers: jwks endpoint returns Cache-Control: public, max-age=3600 allowing browsers to cache public keys for 1 hour.

Clients verify tokens using public key. Pierre signs with private key.

Benefits:

  • private key never leaves server
  • clients verify without shared secret
  • supports key rotation with grace period
  • browser caching reduces jwks endpoint load

key rotation: when keys are rotated, old keys are retained during grace period to allow existing tokens to validate. New tokens are signed with the current key.

Rate Limiting

Token bucket algorithm per authentication method:

  • jwt tokens: per-tenant limits
  • api keys: per-tier limits (free: 100/day, professional: 10,000/day, enterprise: unlimited)
  • oauth2 endpoints: per-ip limits
    • /oauth2/authorize: 60 requests/minute
    • /oauth2/token: 30 requests/minute
    • /oauth2/register: 10 requests/minute

Oauth2 rate limit responses include:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
X-RateLimit-Reset: 1704067200
Retry-After: 42

Implementation: src/rate_limiting.rs, src/oauth2/rate_limiting.rs

CSRF Protection

pierre implements comprehensive csrf protection for web applications:

web application requests:

  • double-submit cookie pattern (see “Web Application Security” section above)
  • 256-bit cryptographic csrf tokens
  • user-scoped validation
  • 30-minute token expiration
  • automatic header validation for POST/PUT/DELETE/PATCH

oauth flows:

  • state parameter validation in oauth flows (prevents csrf in oauth redirects)
  • pkce for oauth2 authorization (code challenge verification)
  • origin validation for web requests

see “Web Application Security” section above for detailed csrf implementation.

Atomic Token Operations

Pierre prevents toctou (time-of-check to time-of-use) race conditions in token operations.

problem: token reuse attacks

Standard token validation flow vulnerable to race conditions:

thread 1: check token valid → ✓ valid
thread 2: check token valid → ✓ valid
thread 1: revoke token → success
thread 2: revoke token → success (token used twice!)

solution: atomic check-and-revoke

Pierre uses database-level atomic operations:

-- single atomic transaction
UPDATE oauth2_refresh_tokens
SET revoked_at = NOW()
WHERE token = ? AND revoked_at IS NULL
RETURNING *

Benefits:

  • race condition elimination: only one thread can consume token
  • database-level garantees: transaction isolation prevents concurrent access
  • zero-trust security: every token exchange verified atomically

vulnerable endpoints protected:

  • POST /oauth2/token (refresh token grant)
  • token refresh operations
  • authorization code exchange

implementation details:

Atomic operations in database plugins (src/database_plugins/):

#![allow(unused)]
fn main() {
/// atomically consume oauth2 refresh token (check-and-revoke in single operation)
async fn consume_refresh_token(&self, token: &str) -> Result<RefreshToken, DatabaseError>
}

Sqlite implementation uses RETURNING clause:

#![allow(unused)]
fn main() {
UPDATE oauth2_refresh_tokens
SET revoked_at = datetime('now')
WHERE token = ? AND revoked_at IS NULL
RETURNING *
}

Postgresql implementation uses same pattern with RETURNING:

#![allow(unused)]
fn main() {
UPDATE oauth2_refresh_tokens
SET revoked_at = NOW()
WHERE token = $1 AND revoked_at IS NULL
RETURNING *
}

If query returns no rows, token either:

  • doesn’t exist
  • already revoked (race condition detected)
  • expired

All three cases result in authentication failure, preventing token reuse.

Security guarantees:

  • serializability: database transactions prevent concurrent modifications
  • atomicity: check and revoke happen in single operation
  • consistency: no partial state changes possible
  • isolation: concurrent requests see consistent view

Implementation: src/database_plugins/sqlite.rs, src/database_plugins/postgres.rs, src/oauth2/endpoints.rs

Troubleshooting

“Invalid Token” Errors

  • check token expiry: jwt tokens expire after 24h (default)
  • verify token format: must be Bearer <token>
  • ensure token not revoked: check /oauth/status

OAuth2 Flow Fails

  • verify redirect uri exactly matches registration
  • check pkce challenge/verifier match
  • ensure code not expired (10 min lifetime)

Provider OAuth Fails

  • verify provider credentials (client_id, client_secret)
  • check redirect uri accessible from browser
  • ensure callback endpoint reachable

API Key Rejected

  • verify api key active: not deleted or expired
  • check rate limits: may be throttled
  • ensure correct header: X-API-Key (case-sensitive)

Implementation References

  • jwt authentication: src/auth.rs
  • api key management: src/api_keys.rs
  • oauth2 server: src/oauth2_server/
  • provider oauth: src/oauth2_client/
  • encryption: src/crypto/, src/key_management.rs
  • rate limiting: src/rate_limiting.rs