Files
cg_api_secure-webshare/docs/AUTH_FLOW.md

195 lines
7.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 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
```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=<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](#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
```