Security Guide¶
This project is a stateless OAuth2 resource server with ACL-controlled writes.
Authentication¶
Validate JWT signature against
OAUTH_JWKS_URL.Validate token expiration and audience (
OAUTH_AUDIENCE).Keep token validation centralized and identity-provider agnostic.
Attach authenticated principal to request context after validation.
Do not introduce session-based auth or server-side session state.
Browser Token Storage — Two-Store Design¶
The frontend stores the JWT in two places simultaneously, each serving a distinct audience:
Store |
Who reads it |
Purpose |
|---|---|---|
|
JavaScript |
|
|
The browser |
Attached automatically to every same-origin request, including |
Neither store can be dropped:
The
sessioncookie isHttpOnly, so JavaScript cannot read it. The SPA therefore needslocalStorageto hold the raw token for use inAuthorizationheaders.localStoragevalues are invisible to automatic browser credential attachment on navigations, so the cookie is still required for the iframe.
The exchange flow is:
User supplies a bearer token via the token dialog.
setToken(t)inauth.tswrites the token tolocalStorageand callsPOST /api/auth/sessionwith the token in theAuthorizationheader.The backend validates the JWT and responds with
Set-Cookie: session=<token>; HttpOnly; SameSite=Lax; Path=/.Subsequent iframe navigations and page refreshes carry the
sessioncookie automatically.On logout,
setToken(null)removes thelocalStorageentry and callsDELETE /api/auth/session, which clears the cookie viaMax-Age=0.On page refresh,
auth.tscallsPOST /api/auth/sessionimmediately if a token is already inlocalStorage, keeping the cookie current across hard reloads.
On the backend, get_optional_auth resolves credentials in
priority order: Authorization header → session cookie →
unauthenticated (None). No server-side state is created; the
cookie payload is the raw JWT and is validated identically to
the header path.
Upload Security¶
Accept ZIP uploads only when namespace/project authorization is valid.
Reject traversal paths and symlink entries in ZIP archives.
Require top-level
index.htmlin archive content.Enforce extraction size and file-count limits.
Ignore uploaded
metadata.toml; generate metadata server-side.Keep extraction and final write on same filesystem for atomic rename.
Filesystem and Immutability¶
/datais the authoritative data store.Never allow overwriting existing version+locale artifacts.
Perform all writes through storage abstraction.
Ensure atomic creation and ref symlink updates.
Resolution and Fallback Safety¶
Use resolver for every version lookup, including ref aliases.
Locale fallback order is fixed and deterministic: requested locale, then
en, then any existing locale, then404.If fallback occurs, UI should show a gentle notice.
nginx and Exposure Controls¶
nginx serves static documentation from
/data.nginx enforces upload request limits and max body size.
FastAPI should not stream large static files directly.
Every static documentation request MUST pass through the FastAPI
auth_requestgate (GET /api/auth) before nginx serves the file. nginx uses theauth_requestdirective to delegate authorization; FastAPI returns 200 (allow), 401 (unauthenticated), or 403 (forbidden) based on the namespace ACL.The internal auth location (
/internal/auth) is markedinternalin nginx and cannot be accessed by external clients.The
X-Original-URIheader carries the original request path toGET /api/authso it can extract the namespace and evaluate the correct ACL.When the API server is unreachable, nginx fails closed (returns 500) rather than open.
Security Review Checklist¶
Auth check runs before every write operation.
ACL write check is present for all mutating endpoints.
ZIP validation includes traversal and symlink rejection.
Filesystem writes are atomic and immutable.
Resolver is used for ref aliases and locale handling everywhere.
Static documentation routes are guarded by nginx
auth_requestdelegating toGET /api/auth.