API authentication
Two auth paths: API keys for programmatic callers and JWT cookies for browsers. They can't be mixed on one request. Cookie auth requires CSRF; API-key auth does not. This page shows the exact wire shape for each.
01API key (recommended for programmatic)
GET /api/v1/projects/{projectId}/certificates
Host: cap.example.com
Authorization: Bearer cap_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Accept: application/json
- Prefix:
cap_+ 52 base32 chars. - Scope: org-wide or single-project — set at creation time.
- Role: viewer, operator, or admin (never owner).
- CSRF: not required.
Full details: API keys.
curl
export CAP_KEY=cap_...
curl -H "Authorization: Bearer $CAP_KEY" \
https://cap.example.com/api/v1/projects/$PID/certificates
HTTP client libraries
- Go
net/http:req.Header.Set("Authorization", "Bearer "+key). - Python
requests:headers={"Authorization": f"Bearer {key}"}. - TypeScript
ky:ky.create({ headers: { Authorization: `Bearer ${key}` }}). - Terraform provider: pass
api_keyin the provider block; it handles the header.
02Cookie (browser)
When a user logs in through the UI, the backend sets three cookies and returns the CSRF token in the response body:
| Cookie | Flags | Purpose |
|---|---|---|
cap_access | HttpOnly, Secure, SameSite=Strict | JWT access token (15 min default). |
cap_refresh | HttpOnly, Secure, SameSite=Strict, Path=/api/v1/auth | Refresh token (7 days default). |
cap_csrf | Secure, SameSite=Strict (readable by JS) | CSRF token to mirror into the header. |
CSRF double-submit
Every mutation (POST/PUT/PATCH/DELETE) must include the CSRF value in X-CSRF-Token:
POST /api/v1/projects/{projectId}/certificates
Cookie: cap_access=...; cap_csrf=XXXX
X-CSRF-Token: XXXX
Content-Type: application/json
{ ... }
The backend compares header vs cookie on every mutation. Mismatch → 403 csrf_validation_failed.
GET requests don't need CSRF. API-key requests never need CSRF.
Refresh flow
- Client hits an endpoint; access token expired → 401.
- Client calls
POST /api/v1/auth/refreshwith the refresh cookie. - On success: new cookies set in the response. Client retries the original request.
- On failure (reuse detected or expired): 401. Client redirects to login.
Reuse detection: using a refresh token that's already been used invalidates the whole token family. Protects against stolen refresh cookies — see Sessions.
03Login endpoint
POST /api/v1/auth/login
Content-Type: application/json
{ "email": "alice@example.com", "password": "...", "otp": "123456" }
# → 200 OK
# Set-Cookie: cap_access=...
# Set-Cookie: cap_refresh=...
# Set-Cookie: cap_csrf=...
{
"user": { "id":"...", "email":"alice@example.com", "org_role":"admin" },
"csrf_token": "XXXX"
}
Include otp when the user has TOTP enrolled; omit otherwise. 401 mfa_required if OTP is required but absent.
04Logout
POST /api/v1/auth/logout
X-CSRF-Token: XXXX
Revokes the current refresh token and clears all cookies.
05Who am I
GET /api/v1/auth/me
# → current user + role + org / project memberships
GET /api/v1/auth/me/profile
# → profile preferences (timezone, language, notification prefs)
Both are read-only and accept either cookie or API-key auth.
06CORS
By default, no CORS — the UI and API share the same origin.
Cross-origin browser apps need explicit allowlist; set
CERTAUTOPILOT_SERVER_CORS_ALLOWED_ORIGINS to a
comma-separated list. The backend always disallows
Authorization header from cross-origin requests
(forces cookie + CSRF for browsers).
07Auth error codes
| Status | Code | Meaning |
|---|---|---|
| 401 | no_auth | No credentials. |
| 401 | expired_token | Access token expired; client should refresh. |
| 401 | invalid_token | Signature / format wrong. |
| 401 | mfa_required | Login needs OTP. |
| 403 | insufficient_role | Authenticated but role too low. |
| 403 | project_scope_violation | API key scoped to project X, endpoint is org-wide or a different project. |
| 403 | csrf_validation_failed | Cookie request missing / wrong X-CSRF-Token. |
| 429 | auth_rate_limited | Too many failed attempts per IP. |
08Troubleshooting
"CSRF validation failed" on an API-key call
You're sending both cookies and an Authorization header. Drop the cookies; API-key auth should be headers-only.
Cookie authentication: "expired_token" immediately after login
System clock skew > access token TTL (15 min). NTP the backend; browser clocks don't matter for this (cookies are set by the server).
Browser CORS error from a test harness
Tests call from a different origin. Add it to CERTAUTOPILOT_SERVER_CORS_ALLOWED_ORIGINS, or proxy the test traffic through the same origin.