Skip to main content

Overview

Finance Agent uses Clerk for authentication. Clerk provides:
  • Secure user authentication with JWT tokens
  • Pre-built UI components for login/signup
  • User management dashboard
  • Webhook integration for syncing user data

Prerequisites

1

Create Clerk Account

2

Create Application

Create a new application in the Clerk dashboard
3

Get API Keys

Copy your API keys from the Clerk dashboard

Environment Variables

Add Clerk credentials to your .env file:
# ========================================
# Clerk Authentication (Primary)
# ========================================
# Get these from your Clerk Dashboard: https://dashboard.clerk.com

# Backend secret key
CLERK_SECRET_KEY=sk_test_your-clerk-secret-key

# Publishable key (used by backend and frontend)
CLERK_PUBLISHABLE_KEY=pk_test_your-clerk-publishable-key

# Webhook secret for verifying webhook signatures
CLERK_WEBHOOK_SECRET=whsec_your-webhook-secret

# Frontend (Vite requires VITE_ prefix to expose vars to browser)
VITE_CLERK_PUBLISHABLE_KEY=pk_test_your-clerk-publishable-key
Never commit .env to version control! Keep your Clerk secret keys secure.

Backend Setup

1. Clerk Configuration

Clerk is configured in config.py:
@dataclass
class ClerkConfig:
    """Clerk authentication configuration."""

    # Clerk API keys (loaded from environment)
    SECRET_KEY: Optional[str] = field(default_factory=lambda: os.getenv("CLERK_SECRET_KEY"))
    PUBLISHABLE_KEY: Optional[str] = field(default_factory=lambda: os.getenv("CLERK_PUBLISHABLE_KEY"))
    WEBHOOK_SECRET: Optional[str] = field(default_factory=lambda: os.getenv("CLERK_WEBHOOK_SECRET"))

    # Clerk JWKS configuration
    JWKS_CACHE_TTL_SECONDS: int = 3600  # Cache JWKS for 1 hour

    @property
    def is_configured(self) -> bool:
        """Check if Clerk is properly configured."""
        return bool(self.SECRET_KEY and self.PUBLISHABLE_KEY)

2. JWT Token Verification

Clerk JWTs are verified using JWKS (JSON Web Key Sets):
from app.auth.jwt_config import verify_clerk_token

# Verify a Clerk token
token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
payload = await verify_clerk_token(token)

if payload:
    user_id = payload.get('sub')  # Clerk user ID
    print(f"Authenticated user: {user_id}")

3. Protected Routes

Use the get_current_user dependency to protect API endpoints:
from fastapi import APIRouter, Depends
from app.auth.auth_utils import get_current_user

router = APIRouter()

@router.get("/protected")
async def protected_route(current_user: dict = Depends(get_current_user)):
    """Only accessible to authenticated users."""
    return {
        "message": "You are authenticated",
        "user_id": current_user['id'],
        "email": current_user['email']
    }

Webhook Setup

1. Create Webhook Endpoint

The webhook endpoint is defined in app/auth/auth.py:
@router.post("/clerk/webhook")
async def clerk_webhook(
    request: Request,
    svix_id: Optional[str] = Header(None, alias="svix-id"),
    svix_timestamp: Optional[str] = Header(None, alias="svix-timestamp"),
    svix_signature: Optional[str] = Header(None, alias="svix-signature")
):
    """Handle Clerk webhook events."""
    # Signature verification
    # Event handling

2. Supported Events

1

user.created

Creates a local user record when a user signs up via Clerk:
async def handle_user_created(data: Dict[str, Any], db: asyncpg.Connection):
    clerk_user_id = data.get("id")
    email = extract_primary_email(data)
    
    # Create user in local database
    user_id = await db.fetchval(
        """INSERT INTO users (
            clerk_user_id, username, email, full_name,
            is_active, is_approved
        ) VALUES ($1, $2, $3, $4, TRUE, TRUE)
        RETURNING id""",
        clerk_user_id, username, email, full_name
    )
2

user.updated

Updates local user record when user profile changes in Clerk
3

user.deleted

Deactivates local user record when user is deleted in Clerk

3. Configure Webhook in Clerk Dashboard

1

Navigate to Webhooks

Go to Webhooks in your Clerk dashboard
2

Add Endpoint

Click Add Endpoint and enter your webhook URL:
https://your-domain.com/auth/clerk/webhook
3

Subscribe to Events

Enable these events:
  • user.created
  • user.updated
  • user.deleted
4

Copy Webhook Secret

Copy the webhook signing secret and add it to your .env:
CLERK_WEBHOOK_SECRET=whsec_...

4. Webhook Signature Verification

Clerk uses Svix for webhook delivery with HMAC-SHA256 signatures:
def verify_clerk_webhook_signature(
    payload: bytes,
    svix_id: str,
    svix_timestamp: str,
    svix_signature: str,
    webhook_secret: str
) -> bool:
    """Verify the Clerk webhook signature using Svix."""
    # Remove 'whsec_' prefix if present
    if webhook_secret.startswith("whsec_"):
        secret = webhook_secret[6:]
    else:
        secret = webhook_secret

    # Decode the base64 secret
    import base64
    secret_bytes = base64.b64decode(secret)

    # Create the signed content
    signed_content = f"{svix_id}.{svix_timestamp}.{payload.decode('utf-8')}"

    # Compute the expected signature
    expected_signature = hmac.new(
        secret_bytes,
        signed_content.encode('utf-8'),
        hashlib.sha256
    ).digest()
    expected_signature_b64 = base64.b64encode(expected_signature).decode('utf-8')

    # Compare signatures
    return hmac.compare_digest(sig_value, expected_signature_b64)

Frontend Integration

1. Install Clerk React

npm install @clerk/clerk-react

2. Wrap App with ClerkProvider

import { ClerkProvider } from '@clerk/clerk-react'

const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY

function App() {
  return (
    <ClerkProvider publishableKey={PUBLISHABLE_KEY}>
      {/* Your app components */}
    </ClerkProvider>
  )
}

3. Add Sign In/Sign Up Components

import { SignIn } from '@clerk/clerk-react'

function SignInPage() {
  return (
    <div className="flex justify-center items-center min-h-screen">
      <SignIn routing="path" path="/sign-in" />
    </div>
  )
}

4. Protect Routes

import { useAuth } from '@clerk/clerk-react'
import { Navigate } from 'react-router-dom'

function ProtectedRoute({ children }) {
  const { isSignedIn, isLoaded } = useAuth()

  if (!isLoaded) {
    return <div>Loading...</div>
  }

  if (!isSignedIn) {
    return <Navigate to="/sign-in" />
  }

  return children
}

5. Get User Token for API Calls

import { useAuth } from '@clerk/clerk-react'

function MyComponent() {
  const { getToken } = useAuth()

  const fetchData = async () => {
    const token = await getToken()
    
    const response = await fetch('https://api.yourapp.com/protected', {
      headers: {
        'Authorization': `Bearer ${token}`
      }
    })
    
    return response.json()
  }

  return <button onClick={fetchData}>Fetch Protected Data</button>
}

Auth Bypass (Development)

For development/testing, you can disable authentication:
# In .env
AUTH_DISABLED=true
NEVER use AUTH_DISABLED=true in production! This completely bypasses authentication and allows unauthenticated access to all endpoints.
When auth is disabled, the system creates a mock user for all requests:
if settings.APPLICATION.AUTH_DISABLED:
    # Return mock user for development
    return {
        'id': uuid.uuid4(),
        'clerk_user_id': 'dev_user',
        'username': 'dev',
        'email': '[email protected]',
        'is_admin': True
    }

User Database Schema

CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    clerk_user_id VARCHAR(255) UNIQUE,  -- Clerk's user ID
    username VARCHAR(255) UNIQUE NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    full_name VARCHAR(255),
    first_name VARCHAR(255),
    last_name VARCHAR(255),
    is_active BOOLEAN DEFAULT TRUE,
    is_approved BOOLEAN DEFAULT TRUE,
    is_admin BOOLEAN DEFAULT FALSE,
    onboarded_via_invitation BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

Testing Authentication

1. Test Webhook Locally

Use ngrok to expose your local server:
ngrok http 8000
Then configure the ngrok URL in Clerk dashboard:
https://abc123.ngrok.io/auth/clerk/webhook

2. Test JWT Verification

import asyncio
from app.auth.jwt_config import verify_clerk_token

async def test_token():
    token = "your-test-token-here"
    payload = await verify_clerk_token(token)
    print(f"Token payload: {payload}")

asyncio.run(test_token())

3. Test Protected Endpoint

# Get token from Clerk (in browser console)
const token = await window.Clerk.session.getToken()
console.log(token)

# Test API call
curl -H "Authorization: Bearer YOUR_TOKEN" \
  https://your-api.com/protected

Security Best Practices

1

Use HTTPS in Production

Always use HTTPS for API endpoints and webhook URLs
2

Verify Webhook Signatures

Never skip webhook signature verification in production
3

Rotate Secrets Regularly

Rotate your CLERK_SECRET_KEY and CLERK_WEBHOOK_SECRET periodically
4

Limit Token Lifetime

Configure appropriate token expiration in Clerk dashboard
5

Validate User Permissions

Check is_admin, is_active, and is_approved flags before granting access

Troubleshooting

Common Issues:
  1. Invalid JWT signature: Ensure CLERK_PUBLISHABLE_KEY matches your Clerk app
  2. Webhook signature mismatch: Verify CLERK_WEBHOOK_SECRET is correct and includes whsec_ prefix
  3. User not created: Check webhook endpoint is publicly accessible (use ngrok for local testing)
  4. CORS errors: Add your frontend URL to CORS_ORIGINS in .env

Next Steps

Build docs developers (and LLMs) love