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_key in the provider block; it handles the header.

When a user logs in through the UI, the backend sets three cookies and returns the CSRF token in the response body:

CookieFlagsPurpose
cap_accessHttpOnly, Secure, SameSite=StrictJWT access token (15 min default).
cap_refreshHttpOnly, Secure, SameSite=Strict, Path=/api/v1/authRefresh token (7 days default).
cap_csrfSecure, 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

  1. Client hits an endpoint; access token expired → 401.
  2. Client calls POST /api/v1/auth/refresh with the refresh cookie.
  3. On success: new cookies set in the response. Client retries the original request.
  4. 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

StatusCodeMeaning
401no_authNo credentials.
401expired_tokenAccess token expired; client should refresh.
401invalid_tokenSignature / format wrong.
401mfa_requiredLogin needs OTP.
403insufficient_roleAuthenticated but role too low.
403project_scope_violationAPI key scoped to project X, endpoint is org-wide or a different project.
403csrf_validation_failedCookie request missing / wrong X-CSRF-Token.
429auth_rate_limitedToo 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.

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.