Clerk authentication proxy for custom domains (ngrok, tunnels, etc.)
Project description
clerkway
Clerk authentication proxy for custom domains (ngrok, Cloudflare Tunnel, etc.)
Problem
When using Clerk with a custom domain proxy (like ngrok), OAuth flows break because:
- Cookie domains - Clerk sets cookies for
frontend-api.clerk.dev, not your domain - Redirect URLs - OAuth callbacks redirect to Clerk's domain, not your proxy
- Multiple cookies - Standard header handling loses cookies (only keeps one)
- JWKS auth - Clerk's Backend API requires
Authorization+User-Agentheaders
This library solves all of these issues.
Installation
pip install clerkway
Quick Start
1. Add the proxy router to your FastAPI app
import os
from fastapi import FastAPI
from clerkway import create_clerk_proxy_router, ClerkProxyConfig
app = FastAPI()
# Configure the proxy
config = ClerkProxyConfig(
secret_key=os.environ["CLERK_SECRET_KEY"],
)
# Add the router - this creates /__clerk/* routes
app.include_router(create_clerk_proxy_router(config))
2. Configure your frontend
In your Clerk provider configuration, set the proxy URL:
// React example
<ClerkProvider
publishableKey={import.meta.env.VITE_CLERK_PUBLISHABLE_KEY}
proxyUrl="/__clerk"
>
3. Set up JWT verification
import os
import jwt
from clerkway import get_jwks_client
# Use Clerk's Backend API for fast JWKS fetching
jwks_client = get_jwks_client(
jwks_url="https://api.clerk.com/v1/jwks",
secret_key=os.environ["CLERK_SECRET_KEY"],
)
def verify_token(token: str) -> dict:
signing_key = jwks_client.get_signing_key_from_jwt(token)
return jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
# Add issuer verification for your domain
issuer=f"https://{os.environ['YOUR_DOMAIN']}/__clerk",
)
Environment Variables
| Variable | Description |
|---|---|
CLERK_SECRET_KEY |
Your Clerk secret key (from Dashboard) |
CLERK_PUBLISHABLE_KEY |
Your Clerk publishable key (frontend) |
Configuration Options
@dataclass
class ClerkProxyConfig:
secret_key: str # Required: Clerk secret key
route_prefix: str = "/__clerk" # URL prefix for proxy routes
timeout: float = 30.0 # HTTP request timeout
log_oauth_callbacks: bool = True # Log OAuth flow for debugging
How It Works
Cookie Rewriting
Clerk sets cookies with domain=frontend-api.clerk.dev. The proxy rewrites these to your domain so the browser stores them correctly.
Location Header Rewriting
OAuth callbacks redirect to Clerk's domain. The proxy rewrites Location headers to keep users on your domain:
/v1/oauth_callback?...→/__clerk/v1/oauth_callback?...https://frontend-api.clerk.dev/...→https://yourdomain.com/__clerk/...
Multi-Cookie Handling
HTTP allows multiple Set-Cookie headers. Standard Python dict handling loses all but one. This library uses multi_items() and headers.append() to preserve all cookies.
JWKS Authentication
Clerk's Backend API (api.clerk.com/v1/jwks) requires:
Authorization: Bearer <secret_key>headerUser-Agentheader (urllib omits this by default)
The ClerkJWKClient class handles both automatically.
Vite Configuration
If using Vite, add your ngrok domain to allowedHosts:
// vite.config.ts
export default defineConfig({
server: {
allowedHosts: [
"localhost",
"your-app.ngrok.dev",
],
},
});
Troubleshooting
403 Forbidden on JWKS fetch
You're missing the Authorization or User-Agent header. Use get_jwks_client() which handles this automatically.
Cookies not being set
Check that:
- Your domain matches what's in the rewritten cookies
- You're using HTTPS (required for
Securecookies) - The proxy is correctly rewriting
domain=in Set-Cookie headers
OAuth redirect loop
The Location header isn't being rewritten. Check that:
- The proxy is handling the OAuth callback path
follow_redirects=Falseis set on the HTTP client
Usage Example (Complete FastAPI App)
"""Example FastAPI application with Clerk authentication via ngrok proxy."""
import os
from dataclasses import dataclass
from typing import Annotated
import jwt
from fastapi import Depends, FastAPI, HTTPException, Request, status
from clerkway import (
ClerkProxyConfig,
create_clerk_proxy_router,
get_jwks_client,
)
app = FastAPI(title="My App")
# --- Clerk Proxy Setup ---
clerk_config = ClerkProxyConfig(
secret_key=os.environ["CLERK_SECRET_KEY"],
)
app.include_router(create_clerk_proxy_router(clerk_config))
# --- JWT Verification ---
CLERK_JWKS_URL = "https://api.clerk.com/v1/jwks"
CLERK_ISSUER = f"https://{os.environ['APP_DOMAIN']}/__clerk"
@dataclass
class ClerkUser:
clerk_id: str
email: str | None = None
name: str | None = None
def get_jwks_client_instance():
return get_jwks_client(
jwks_url=CLERK_JWKS_URL,
secret_key=os.environ["CLERK_SECRET_KEY"],
)
async def get_current_user(request: Request) -> ClerkUser:
"""FastAPI dependency to get the authenticated user."""
auth_header = request.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
)
token = auth_header[7:]
try:
jwks_client = get_jwks_client_instance()
signing_key = jwks_client.get_signing_key_from_jwt(token)
payload = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
issuer=CLERK_ISSUER,
)
return ClerkUser(
clerk_id=payload.get("sub", ""),
email=payload.get("email"),
name=payload.get("name"),
)
except jwt.InvalidTokenError as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token",
) from e
CurrentUser = Annotated[ClerkUser, Depends(get_current_user)]
# --- Routes ---
@app.get("/api/me")
async def get_me(user: CurrentUser):
return {"clerk_id": user.clerk_id, "email": user.email, "name": user.name}
@app.get("/api/public")
async def public_endpoint():
return {"message": "This is public"}
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file clerkway-0.1.1.tar.gz.
File metadata
- Download URL: clerkway-0.1.1.tar.gz
- Upload date:
- Size: 10.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
533b178d16d899e93185b1706d7eee74ee8826e2f6bd0e4de5b38f0d5bd7d8ed
|
|
| MD5 |
551bfcdd46ceeb065633f9690d9bb7bd
|
|
| BLAKE2b-256 |
fc4f5beabdfc532c8d2463517f359484dc4ebdbf60203d52a0bb5236eb9557c3
|
File details
Details for the file clerkway-0.1.1-py3-none-any.whl.
File metadata
- Download URL: clerkway-0.1.1-py3-none-any.whl
- Upload date:
- Size: 9.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5bc0fbae9b22ca0bb9aa30a2515b00caf13d1493d43827d6fded7ccb85377d01
|
|
| MD5 |
621e7de5a9d3b62e0f8f4f2d15403883
|
|
| BLAKE2b-256 |
1e81663da9c9a9b207eb078d73a6f35defc861e52358409356302922d115f487
|