# Authentication Flow cg.cx uses a simple password-and-cookie authentication model. There is no user account system; access is granted per-content-item by knowing its password. --- ## AuthSource Enum The server tracks *how* a request was authenticated using the `AuthSource` enum: ```rust enum AuthSource { Cookie, // Request presented a valid cgcx_pw cookie QueryParam, // Request presented a correct ?sc=PASSWORD query param } ``` This distinction matters because the server only issues a **new** cookie when auth succeeds via `QueryParam`. Cookie-based auth does not re-issue the cookie (the browser already has it). --- ## How Direct Links with `?sc=PASSWORD` Work 1. A client requests a protected endpoint (e.g., `GET /api/content/:cxid` or `GET /api/content/:cxid/file/:file_idx`) with the password in the query string: ``` GET /api/content/abc123?sc=MySecretPassword ``` 2. The handler calls `password_from_request(...)`. 3. Inside `password_from_request`, the `sc` query parameter is checked **first**: - If `sc` is present and the content has a `password_hash`, the server verifies the supplied password against the stored Argon2 hash. - If verification succeeds, `password_from_request` returns `Some(AuthSource::QueryParam)`. 4. The handler proceeds with the request. 5. On the response path, the handler calls `add_auth_cookie(...)`. 6. Because the auth source is `QueryParam`, `add_auth_cookie` generates a new HMAC-signed `cgcx_pw` cookie and attaches it via a `Set-Cookie` header. 7. The client’s browser stores the cookie. Subsequent requests to the same domain automatically include it, so the `?sc=...` parameter is no longer needed. --- ## How the Server Validates Passwords (Argon2) Password verification happens in two places: `password_from_request` and the `verify_password` endpoint. ### Stored Hash Format Password hashes are stored in the database as Argon2id hashes in the standard PHC string format (e.g., `$argon2id$v=19$m=65536,t=3,p=4$...`). ### Verification ```rust use argon2::{Argon2, PasswordHash, PasswordVerifier}; let parsed_hash = PasswordHash::new(&hash)?; let valid = Argon2::default() .verify_password(password.as_bytes(), &parsed_hash) .is_ok(); ``` - Uses the default Argon2 parameters. - If parsing the stored hash fails, verification is treated as failed. - If verification fails, the server returns `401 Unauthorized`. --- ## How Cookies Are Set After Successful Query-Param Auth After a successful `?sc=...` authentication, the response includes: ```http Set-Cookie: cgcx_pw=; Max-Age=3600; SameSite=Strict; HttpOnly; Path=/ ``` - **Name:** `cgcx_pw` - **Max-Age:** 3600 seconds (1 hour) - **SameSite:** `Strict` - **HttpOnly:** `true` (not accessible to JavaScript) - **Path:** `/` The cookie value is generated by `make_cookie_value(cxid, cookie_secret)` (see [Cookie Format](#cookie-format-hmac-signed) below). The same cookie is also set by the `POST /api/content/:cxid/verify-password` endpoint after a successful JSON password verification. --- ## How Subsequent Requests Use the Cookie 1. The browser automatically sends the `cgcx_pw` cookie on every request to the same origin. 2. When `password_from_request` is called: - It first checks for `sc` query param auth. - If that fails or is absent, it scans **all** `Cookie` headers (there may be multiple). - For each cookie header, it splits on `;` and looks for a part starting with `cgcx_pw=`. - When found, it calls `verify_cookie(cxid, &part[8..], cookie_secret)`. - If `verify_cookie` returns `true`, `password_from_request` returns `Some(AuthSource::Cookie)`. 3. Because the auth source is `Cookie`, `add_auth_cookie` does **not** add a new `Set-Cookie` header. --- ## Cookie Format (HMAC-Signed) The cookie value is a Base64-encoded string of the form: ``` base64(cxid + ":" + hmac_sha256(cxid, secret)) ``` ### Building the Cookie (`make_cookie_value`) ```rust fn make_cookie_value(cxid: &str, secret: &[u8]) -> String { let mac = hmac_cookie(cxid, secret); // HMAC-SHA256 of cxid let mut raw = Vec::with_capacity(cxid.len() + 1 + mac.len()); raw.extend_from_slice(cxid.as_bytes()); raw.push(b':'); raw.extend_from_slice(&mac); base64::engine::general_purpose::STANDARD.encode(&raw) } ``` ### Verifying the Cookie (`verify_cookie`) ```rust fn verify_cookie(cxid: &str, cookie_value: &str, secret: &[u8]) -> bool { let decoded = base64_decode(cookie_value)?; let mut parts = decoded.splitn(2, |&b| b == b':'); let decoded_cxid = std::str::from_utf8(parts.next()?)?; let mac_bytes = parts.next()?; if decoded_cxid != cxid { return false; } let expected = hmac_cookie(cxid, secret); if mac_bytes.len() != expected.len() { return false; } // Constant-time comparison to prevent timing attacks mac_bytes.ct_eq(&expected).into() } ``` ### Cookie Secret The HMAC key (`cookie_secret`) is derived from the master encryption key at startup: ```rust let cookie_secret = blake3::hash(master_key.as_bytes()).as_bytes().to_vec(); ``` This means: - The cookie secret is deterministic for a given master key. - Cookies are bound to a specific server instance (or cluster sharing the same master key). - Changing the master key invalidates all existing auth cookies. ### Security Properties - **Binding:** The HMAC includes the `cxid`, so a cookie for one piece of content cannot be replayed against another. - **Tamper resistance:** Without the `cookie_secret`, an attacker cannot forge a valid `cgcx_pw` cookie. - **Timing safety:** Verification uses `subtle::ConstantTimeEq` to avoid leaking information via timing side channels. --- ## Summary Flow Diagram ``` Client Request │ ▼ ┌─────────────────────────────────────────┐ │ Does the content have a password? │ └─────────────────────────────────────────┘ │ ├─ No ──► Proceed (no auth needed) │ └─ Yes │ ▼ ┌─────────────────────────────────────────┐ │ Is ?sc=PASSWORD present and correct? │ │ └─ Argon2 verify against stored hash │ └─────────────────────────────────────────┘ │ ├─ Yes ──► AuthSource::QueryParam │ └─ Set cgcx_pw cookie on response │ └─ No │ ▼ ┌─────────────────────────────────────────┐ │ Is cgcx_pw cookie present and valid? │ │ └─ HMAC-SHA256 verify against secret │ └─────────────────────────────────────────┘ │ ├─ Yes ──► AuthSource::Cookie │ └─ Proceed without setting new cookie │ └─ No ──► 401 Unauthorized ```