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
Create Application
Create a new application in the Clerk dashboard
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
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
)
user.updated
Updates local user record when user profile changes in Clerk
user.deleted
Deactivates local user record when user is deleted in Clerk
Navigate to Webhooks
Go to Webhooks in your Clerk dashboard
Add Endpoint
Click Add Endpoint and enter your webhook URL:https://your-domain.com/auth/clerk/webhook
Subscribe to Events
Enable these events:
user.created
user.updated
user.deleted
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:
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
Use HTTPS in Production
Always use HTTPS for API endpoints and webhook URLs
Verify Webhook Signatures
Never skip webhook signature verification in production
Rotate Secrets Regularly
Rotate your CLERK_SECRET_KEY and CLERK_WEBHOOK_SECRET periodically
Limit Token Lifetime
Configure appropriate token expiration in Clerk dashboard
Validate User Permissions
Check is_admin, is_active, and is_approved flags before granting access
Troubleshooting
Common Issues:
- Invalid JWT signature: Ensure
CLERK_PUBLISHABLE_KEY matches your Clerk app
- Webhook signature mismatch: Verify
CLERK_WEBHOOK_SECRET is correct and includes whsec_ prefix
- User not created: Check webhook endpoint is publicly accessible (use ngrok for local testing)
- CORS errors: Add your frontend URL to
CORS_ORIGINS in .env
Next Steps