Testing FastAPI API Authentication with Clerk JWT
Overview
This skill provides battle-tested guidance for testing FastAPI endpoints with Clerk JWT authentication based on real implementation lessons from Task 1.4 (Clerk Authentication Integration, 2025-11-11).
Verified Status: ✅ Production-ready (13/13 integration tests passing, end-to-end verified)
When to Use This Skill
- •Testing FastAPI endpoints that require Clerk JWT authentication
- •Validating JWT token verification logic (RS256 algorithm)
- •Running end-to-end API tests with real Clerk tokens
- •Troubleshooting authentication failures (401 errors)
- •Verifying ALCOA+ audit logging with user attribution
- •Debugging environment variable loading issues
- •Testing with Clerk session tokens (not Client API tokens)
Prerequisites
Before testing, verify these components exist:
- •
.env.local file with Clerk credentials:
bashCLERK_SECRET_KEY=sk_test_... CLERK_ISSUER=https://your-instance.clerk.accounts.dev CLERK_PEM_PUBLIC_KEY="-----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY-----" # CLERK_JWT_AUDIENCE - MUST be commented out (session tokens don't have 'aud')
- •
FastAPI app with python-dotenv loading (
main/api/app.py):pythonfrom dotenv import load_dotenv from pathlib import Path env_file = Path(__file__).parent.parent.parent / ".env.local" if env_file.exists(): load_dotenv(env_file)Critical: Must load BEFORE importing dependencies that use environment variables.
- •
Token generation script:
main/scripts/create_clerk_session.py - •
Testing script:
main/scripts/test_clerk_auth.py - •
Clerk test user: Configured in Clerk Dashboard
Step-by-Step Testing Protocol
Step 1: Start FastAPI Server
cd C:\Users\anteb\Desktop\Courses\Projects\thesis_project uv run uvicorn main.api.app:app --reload --port 8000
Important: After .env.local changes, RESTART server (not just reload). Environment variables only load at startup.
Step 2: Verify Environment Variables Loaded
Check server logs for:
Loaded environment variables from C:\Users\anteb\Desktop\Courses\Projects\thesis_project\.env.local
If missing, server won't have CLERK_PEM_PUBLIC_KEY → will return 500 error.
Step 3: Generate Fresh Clerk JWT Token
⚠️ CRITICAL: Clerk session tokens expire after 60 SECONDS (not 1 hour!).
uv run python main/scripts/create_clerk_session.py user_35KgiAcvIC0tdtFvJUN1vDkrNYc
Expected output:
Loaded environment variables from .env.local Creating session for user: user_35KgiAcvIC0tdtFvJUN1vDkrNYc Session created: sess_XXXXX JWT Token generated: eyJhbGciOiJSUzI1NiIs...
Step 4: Test Authentication IMMEDIATELY (< 60 seconds)
uv run python main/scripts/test_clerk_auth.py "<JWT_TOKEN>" test_urs.txt
Expected success output:
Status Code: 201
Response:
{"job_id":"...","status":"pending",...}
SUCCESS! Clerk authentication working!
Step 5: Verify Audit Logs
# Read latest audit log entry tail -n 1 logs/audit/jobs/audit_YYYYMMDD.jsonl
Verify ALCOA+ compliance:
- •✅
user_id: Clerk user ID captured - •✅
token_iat: JWT issued-at timestamp - •⚠️
user_email: May be null (session tokens don't always include email) - •✅
alcoa_attributable: User attribution present - •✅
alcoa_contemporaneous: Timestamp captured
Common Errors & Solutions
This section documents all errors encountered during Task 1.4 integration (2025-11-11) and their verified solutions.
Error 1: ModuleNotFoundError: No module named 'main.api'
Symptoms:
ModuleNotFoundError: No module named 'main.api'; 'main' is not a package
Root Cause: Missing main/__init__.py file.
Solution:
# Create package marker cat > main/__init__.py << 'EOF' """Main package for pharmaceutical test generation system.""" __version__ = "0.1.0" EOF
Verification: Server starts without ImportError.
Error 2: CRITICAL: Authentication system not configured (missing CLERK_PEM_PUBLIC_KEY)
Symptoms:
500 Internal Server Error
{"detail":"CRITICAL: Authentication system not configured (missing CLERK_PEM_PUBLIC_KEY)"}
Root Cause: FastAPI server not loading environment variables from .env.local.
Solution: Add python-dotenv loading to main/api/app.py BEFORE importing dependencies:
# MUST be at top of file, before other imports
from dotenv import load_dotenv
from pathlib import Path
env_file = Path(__file__).parent.parent.parent / ".env.local"
if env_file.exists():
load_dotenv(env_file)
logging.info(f"Loaded environment variables from {env_file}")
# NOW import dependencies that use environment variables
from .dependencies import CurrentUserDep
Why This Matters: Dependencies module imports os.getenv("CLERK_PEM_PUBLIC_KEY") at module load time. If .env not loaded first, variable will be None.
Verification: Server logs show "Loaded environment variables from .env.local" on startup.
Error 3: Token validation failed: Token is missing the "aud" claim
Symptoms:
401 Unauthorized
{"detail":"Token validation failed: Token is missing the \"aud\" claim"}
Root Cause: Clerk session tokens don't include audience ('aud') claim, but JWT decoder requires it by default.
Solution: Disable audience verification in main/api/dependencies.py:
def require_clerk_user(credentials):
# ... token extraction ...
verify_options = {
"verify_exp": True,
"verify_iat": True,
"verify_aud": False, # DISABLE for session tokens
"leeway": 10 # Clock skew tolerance
}
payload = jwt.decode(
token,
CLERK_PEM_PUBLIC_KEY,
algorithms=["RS256"],
issuer=CLERK_ISSUER,
options=verify_options # Pass verify_options!
)
Also: Comment out CLERK_JWT_AUDIENCE in .env.local:
# CLERK_JWT_AUDIENCE - Session tokens don't include 'aud' claim # CLERK_JWT_AUDIENCE=https://your-instance.clerk.accounts.dev
Verification: Token validates without audience errors.
Error 4: JWT missing 'email' claim
Symptoms:
WARNING: JWT missing 'email' claim for user user_35KgiAcvIC0tdtFvJUN1vDkrNYc
Root Cause: Clerk session tokens may not include email claim (optional field).
Solution 1: Make email optional in main/api/models.py:
class ClerkClaims(BaseModel):
sub: str # Required
email: str | None = Field(default=None, description="User email (optional in session tokens)")
# ... other fields ...
Solution 2: Change strict validation to warning in require_clerk_user():
user_claims = ClerkClaims(**payload)
# Warn if email missing (not an error)
if not user_claims.email:
logger.warning(f"JWT missing 'email' claim for user {user_claims.sub} - will fetch from Clerk API if needed")
Impact: Audit logs will show user_email: null. User ID alone is sufficient for ALCOA+ attribution.
Verification: Token validates successfully, audit logs capture user_id even without email.
Error 5: Token expired
Symptoms:
401 Unauthorized
{"detail":"Token expired"}
Root Cause: Clerk session tokens expire after 60 SECONDS (not 1 hour as documented).
Solution: Generate fresh token immediately before testing:
# Generate token uv run python main/scripts/create_clerk_session.py user_35KgiAcvIC0tdtFvJUN1vDkrNYc # Test IMMEDIATELY (within 60 seconds) uv run python main/scripts/test_clerk_auth.py "<TOKEN>" test_urs.txt
Best Practice: Automate token generation + testing in single script:
# Generate token token = create_clerk_session(user_id) # Test immediately result = test_authentication(token, urs_file)
Verification: Test completes before token expiry (< 60 seconds elapsed).
Error 6: Invalid token signature
Symptoms:
401 Unauthorized
{"detail":"Invalid token signature"}
Root Cause: CLERK_PEM_PUBLIC_KEY doesn't match Clerk's current public key (keys may rotate).
Solution: Fetch latest public key from Clerk JWKS endpoint:
# Fetch JWKS curl https://your-instance.clerk.accounts.dev/.well-known/jwks.json # Convert to PEM format (manual or script) # Update CLERK_PEM_PUBLIC_KEY in .env.local # RESTART server (reload not sufficient)
Verification: Token validates without signature errors.
Error 7: Environment variables not loading after .env.local changes
Symptoms: Changes to .env.local not reflected in running server.
Root Cause: Environment variables loaded at server startup, not on file watch reload.
Solution: RESTART server (not reload):
# Stop server (Ctrl+C) # Start fresh server uv run uvicorn main.api.app:app --reload --port 8000
Why Reload Doesn't Work: File watcher (WatchFiles) reloads Python modules, but environment variables are loaded once at process startup via load_dotenv().
Verification: Check server logs for "Loaded environment variables from .env.local" after restart.
Verification Checklist
Use this checklist to validate successful authentication integration:
Environment Configuration ✅
- •
.env.localexists with all required Clerk variables - •
CLERK_SECRET_KEYis valid (sk_test_... or sk_live_...) - •
CLERK_PEM_PUBLIC_KEYincludes BEGIN/END headers - •
CLERK_ISSUERmatches Clerk instance URL exactly - •
CLERK_JWT_AUDIENCEis commented out (for session tokens) - •
main/api/app.pyloads .env.local via python-dotenv - • .env loading happens BEFORE dependency imports
Server Configuration ✅
- • Server starts without import errors
- • Server logs show "Loaded environment variables from .env.local"
- • No "CRITICAL: Authentication system not configured" errors
- • FastAPI runs on http://localhost:8000
Token Generation ✅
- •
create_clerk_session.pyloads environment variables - • Script generates JWT token successfully
- • Token includes 'sub', 'iss', 'iat', 'exp' claims
- • Token may NOT include 'aud' or 'email' (expected)
Authentication Tests ✅
- • POST /jobs with valid token → Status 201 Created
- • POST /jobs without token → Status 401 Unauthorized
- • POST /jobs with expired token → Status 401 Unauthorized
- • POST /jobs with invalid signature → Status 401 Unauthorized
Audit Logging ✅
- • Audit log entries created in
logs/audit/jobs/audit_YYYYMMDD.jsonl - •
user_idcaptured from JWT 'sub' claim - •
token_iatcaptured from JWT 'iat' claim - •
alcoa_attributablefield present - •
alcoa_contemporaneoustimestamp present - •
user_emailmay be null (acceptable)
Integration Tests ✅
- •
test_api_jobs.py: 13/13 tests passing - •
test_api_auth.py: Production code tests passing (mock key issues OK) - • NO FALLBACK LOGIC violations: 0
- • Type checking: mypy passes
- • Linting: ruff passes
Anti-Patterns to Avoid
Based on Task 1.4 implementation experience:
❌ Don't: Import dependencies before loading .env
# WRONG - dependencies load before .env
from .dependencies import require_clerk_user
load_dotenv(".env.local")
✅ Do: Load .env BEFORE importing dependencies
# CORRECT - .env loads first
load_dotenv(".env.local")
from .dependencies import require_clerk_user
❌ Don't: Assume environment variables persist across file reloads
- •File watcher reloads Python modules, NOT environment variables
✅ Do: Restart server after .env.local changes
- •
Ctrl+C→uv run uvicorn main.api.app:app --reload
❌ Don't: Use stale JWT tokens (> 60 seconds old)
- •Clerk session tokens expire after 60 seconds
✅ Do: Generate fresh tokens immediately before testing
- •Run
create_clerk_session.py→ immediately test with token
❌ Don't: Enable audience verification for session tokens
- •Session tokens don't include 'aud' claim
✅ Do: Disable audience verification in JWT options
- •
options={"verify_aud": False}
❌ Don't: Make email claim required
- •Session tokens may not include 'email' claim
✅ Do: Make email optional in Pydantic model
- •
email: str | None = Field(default=None, ...)
❌ Don't: Use mock RSA keys in production code
- •Mock keys are for unit tests only
✅ Do: Use real Clerk PEM public key from JWKS
- •Fetch from
.well-known/jwks.jsonendpoint
Testing with Different Token Types
Clerk provides multiple token types - ensure you're using the right one:
Session Tokens (Recommended for FastAPI Backend)
Generated via: create_clerk_session.py (Clerk Backend API)
Characteristics:
- •✅ Expires after 60 seconds
- •✅ Contains: sub, iss, iat, exp, sid, sts
- •⚠️ May NOT contain: aud, email
- •✅ Used for backend-to-backend authentication
Use Case: Testing FastAPI endpoints directly without frontend
Client API Tokens (For Frontend-to-Backend)
Generated via: Clerk Frontend SDK (session.getToken())
Characteristics:
- •Expires after 1 hour
- •Contains: sub, iss, iat, exp, aud (if configured)
- •May contain email (if user profile includes it)
- •Used for authenticated frontend requests
Use Case: Full-stack testing with Next.js/React frontend
JWT Templates (Custom Claims)
Generated via: Clerk Dashboard → JWT Templates
Characteristics:
- •Configurable expiration
- •Custom claims support
- •Audience claim configurable
Use Case: Production with custom authorization logic
Quick Troubleshooting Decision Tree
Authentication test failing?
├─ Status 500: "Authentication system not configured"
│ └─ Check: Environment variables loaded?
│ ├─ No → Add python-dotenv loading before imports
│ └─ Yes → Check: CLERK_PEM_PUBLIC_KEY set?
│ ├─ No → Add to .env.local
│ └─ Yes → Restart server (reload not sufficient)
│
├─ Status 401: "Token is missing the 'aud' claim"
│ └─ Solution: Disable audience verification
│ ├─ Set verify_aud=False in jwt.decode options
│ └─ Comment out CLERK_JWT_AUDIENCE in .env.local
│
├─ Status 401: "Token expired"
│ └─ Solution: Generate fresh token (60-second expiry!)
│ └─ Run create_clerk_session.py → test immediately
│
├─ Status 401: "Invalid token signature"
│ └─ Check: CLERK_PEM_PUBLIC_KEY matches current key?
│ └─ Fetch from .well-known/jwks.json → update .env.local
│
└─ Status 401: "JWT missing 'email' claim" (warning only)
└─ Expected: Session tokens may not include email
└─ Verify: user_id still captured in audit logs
Resources
Scripts
- •
main/scripts/create_clerk_session.py- Generate fresh Clerk JWT tokens - •
main/scripts/test_clerk_auth.py- Test authentication with real tokens
Documentation
- •
main/docs/guides/CLERK_INTEGRATION_TESTING.md- Comprehensive testing guide - •
.env.example- Environment variable reference
Test Files
- •
main/tests/test_api_jobs.py- Integration tests (13/13 passing) - •
main/tests/test_api_auth.py- Authentication unit tests
Implementation Files
- •
main/api/app.py- FastAPI app with dotenv loading - •
main/api/dependencies.py- JWT verification logic - •
main/api/models.py- ClerkClaims Pydantic model
Success Criteria
Authentication integration is successful when:
- •✅ Server starts without errors
- •✅ Environment variables load on startup
- •✅ POST /jobs with valid token → Status 201 Created
- •✅ POST /jobs without token → Status 401 Unauthorized
- •✅ POST /jobs with expired token → Status 401 Unauthorized
- •✅ Audit logs capture user_id and token_iat
- •✅ Integration tests: 13/13 passing
- •✅ NO FALLBACK LOGIC: 0 violations
- •✅ ALCOA+ compliance verified
Version History
- •v1.0 (2025-11-11): Initial version based on Task 1.4 implementation
- •Documented 5 critical errors and solutions
- •Verified with production Clerk tokens
- •13/13 integration tests passing
- •End-to-end authentication successful
Skill Maintainer: Automatically generated from Task 1.4 lessons learned Last Updated: 2025-11-11 Status: ✅ Production-ready, battle-tested