Files
cg_api_secure-webshare/docs/AUTH_FLOW.md

7.1 KiB
Raw Blame History

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:

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).


  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 clients 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

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:

Set-Cookie: cgcx_pw=<value>; 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 below).

The same cookie is also set by the POST /api/content/:cxid/verify-password endpoint after a successful JSON password verification.


  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.

The cookie value is a Base64-encoded string of the form:

base64(cxid + ":" + hmac_sha256(cxid, secret))
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)
}
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()
}

The HMAC key (cookie_secret) is derived from the master encryption key at startup:

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