Files
dsh-web/internal/api/AUTH_DESIGN.md
T

53 lines
3.9 KiB
Markdown

# Authentication boundary (§13)
- Real logins retain `dsh_session` as a database session binding and require a
five-minute HS256 access JWT in an additional HttpOnly cookie. JWT verification
fixes the algorithm and binds subject/session/issuer/audience/expiry. Bare
session IDs never authenticate. Internal callers and test fixtures must also
attach a signed access cookie. Database session expiry or revocation rejects
access even if the JWT's five-minute expiry has not yet elapsed.
- The initial refresh token is random 256-bit data in an HttpOnly cookie, scoped to `/api`, with only its
SHA-256 hash stored in SQLite. The session has an absolute lifetime configured
by SESSION_TTL (default 24h), applied only to new logins. Values use Go duration
syntax (e.g. 168h, not 7d), must be at least one second and a whole number of
seconds; invalid values prevent startup when the store is opened.
Session, refresh and CSRF cookies persist across browser restarts with explicit
Max-Age/Expires bounded by that database expiry; rotation does not extend it.
Transactional rotation stores current and previous token hashes. A keyed,
session-bound successor derivation makes retries of the previous token return
the same current token, including after response loss or server restart. Only
one previous generation is accepted (until the next rotation or session expiry);
this intentionally permits that generation to be replayed with the required
session/Origin/CSRF checks. Older and unknown tokens are rejected. Plaintext
refresh credentials are never stored.
Logout deletes the database session and its refresh binding. Both refresh and
logout retain mandatory exact Origin and session-bound CSRF checks, even when
access has expired, but require an unexpired database session. Logout does not
require the refresh cookie. Other protected operations require valid access.
Self-service password changes and administrator password resets revoke all of
that user's sessions and refresh bindings, requiring a new login.
- Signing key is generated once under the configured data root, private mode
0600, and reused across restarts. Initialization errors fail authentication closed.
Session/access/refresh credentials are issued as cookies, not JSON or URLs,
and are not included in authentication audit detail or application logs. The
CSRF token is intentionally returned as `csrf_token` by login, refresh and
`/api/me`, and is also in the JavaScript-readable `dsh_csrf` cookie. All four
cookies use SameSite=Lax; the other three are HttpOnly. Secure is enabled by
direct TLS or an HTTPS PANEL_PUBLIC_URL, not forwarded protocol headers.
Origin checks compare against the configured public origin, or direct request
scheme/Host if it is unset; forwarded Host/Proto headers are not used.
- Known accounts lock for 15 minutes after five failures; the counter/lock survive
restart. Successful login resets the counter; expired locks reset the counter.
Current-password reauthentication failures share this account lock; successful
reauthentication alone does not reset the counter. Additional login protection
allows 30 attempts per source per minute, at most 1024 source buckets, and eight
admitted concurrent login requests (credential checks are serialized). Source
identity uses the direct peer unless it matches TRUSTED_PROXY_CIDRS, a
comma-separated CIDR list; then X-Forwarded-For is scanned right-to-left for
the first untrusted address. These in-memory limits reset on process restart.
The server runs expired-session/refresh and elapsed-lock cleanup immediately
and every ten minutes; expiry checks do not wait for cleanup.
- Tests cover missing/tampered/expired access, database binding, refresh rotation
and replay, CSRF/Origin rejection, logout revocation, key reuse/permissions,
account isolation, restart persistence, lock expiry, and runtime compatibility.