testing-api-authentication
Test FastAPI endpoints with Clerk JWT authentication while avoiding common environment, token expiry, and claim validation pitfalls. Use when tester-agent needs to validate API authentication, run end-to-end tests with real tokens, or troubleshoot JWT verification issues.
When & Why to Use This Skill
This Claude skill provides a battle-tested framework for validating FastAPI endpoints secured with Clerk JWT authentication. It focuses on overcoming common integration hurdles such as environment variable synchronization, RS256 token verification, and handling the strict 60-second expiry of Clerk session tokens. By implementing this protocol, developers can ensure robust API security, troubleshoot 401 Unauthorized errors effectively, and maintain ALCOA+ compliant audit logs for high-stakes production environments.
Use Cases
- End-to-end testing of FastAPI applications using real Clerk session tokens to validate authentication middleware.
- Troubleshooting JWT verification failures related to missing 'aud' (audience) claims or incorrect RS256 public key configurations.
- Automating short-lived token generation and immediate API testing to handle Clerk's 60-second session security window.
- Verifying security audit trails to ensure user attribution and contemporaneous logging meet regulatory compliance standards.
- Debugging environment variable loading issues in FastAPI to prevent 'Authentication system not configured' 500 errors.
| name | testing-api-authentication |
|---|---|
| description | Test FastAPI endpoints with Clerk JWT authentication while avoiding common environment, token expiry, and claim validation pitfalls. Use when tester-agent needs to validate API authentication, run end-to-end tests with real tokens, or troubleshoot JWT verification issues. |
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:
CLERK_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):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)Critical: Must load BEFORE importing dependencies that use environment variables.
Token generation script:
main/scripts/create_clerk_session.pyTesting script:
main/scripts/test_clerk_auth.pyClerk 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 tokensmain/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 loadingmain/api/dependencies.py- JWT verification logicmain/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