Danh sách bài viết

Bài 34: Cookie Extractor: Plain, Signed, Private

Bài 34 của series Rust RESTful API — chi tiết axum_extra::CookieJar với 3 loại jar tách bạch theo mức độ bảo vệ cho mọi use case cookie Shop API: CookieJar plain (cookie không bảo vệ, client đọc/ghi tự do qua DevTools — dùng cho preference UI như dark mode, language), SignedCookieJar (HMAC SHA256 ký phía sau giá trị <value>.<signature>, client ĐỌC được nội dung nhưng KHÔNG sửa được vì server verify signature integrity — dùng cho CSRF token cần client đọc và echo lại), PrivateCookieJar (AES-256-GCM authenticated encryption + sign, client KHÔNG đọc được nội dung — DevTools chỉ thấy bytes random base64 — dùng cho session ID admin dashboard và data nhạy cảm). API set cookie qua builder Cookie::build((name, value)) chain attribute method .path()/.max_age()/.secure(true)/.http_only(true)/.same_site(SameSite::Lax), return type handler CookieJar (modified) — axum tự render Set-Cookie header response. 4 attribute quan trọng lock vĩnh viễn theo RFC 6265bis: Secure (chỉ gửi qua HTTPS — production MANDATORY, dev local HTTP skip để test), HttpOnly (JavaScript document.cookie KHÔNG đọc được — chống XSS đánh cắp session cookie, session cookie LUÔN bật), SameSite 3 mode (Strict chỉ gửi cùng site chống CSRF mạnh nhất / Lax gửi với top-level navigation mặc định modern browser / None gửi cross-site require Secure), Max-Age seconds preferred hơn Expires UTC date (RFC 6265bis khuyến nghị Max-Age vì không phụ thuộc clock skew client). Shop API decision lock B19 confirm B34: cookie CHỈ cho admin dashboard nội bộ — B105 admin session dùng PrivateCookieJar encrypt session ID + B107 CSRF protection dùng SignedCookieJar double-submit pattern; API client /api/v1/* (mobile, SPA, partner) dùng thuần Bearer JWT (lock B4) KHÔNG dùng cookie. Key management lock vĩnh viễn: 64 bytes random (cookie crate yêu cầu cho AES-256-GCM key + HMAC key derive HKDF), dev sinh qua Key::generate() lúc bootstrap process (ephemeral acceptable cho test vì restart sinh key mới làm invalidate session cũ là OK trong dev), production đọc từ env variable COOKIE_KEY base64-decoded qua Key::from(&base64::decode(env)?) (share cross-instance qua secret manager Vault/AWS Secrets Manager/K8s Secret); CẤM hard-code key trong source code (commit vào git = security breach), CẤM Key::generate() ở production (mỗi pod sẽ có key khác → session encrypted ở pod A không decrypt được ở pod B → session loss khi load balancer route khác pod); rotation theo lịch B289 deep dive (dual-key window grace period 7-30 ngày). Attribute lock Shop API: Session cookie HttpOnly + Secure (prod) + SameSite=Lax + Max-Age=24h, CSRF cookie Secure (prod) + SameSite=Strict + Max-Age=1h, preference cookie plain SameSite=Lax + Max-Age=1y. B105 AppState extend thêm cookie_key: Key field + impl FromRef<AppState> for Key cho sub-state extract chuẩn bị wire PrivateCookieJar/SignedCookieJar vào handler. B34 conceptual + preview pattern, KHÔNG tạo file thực tế ở Shop API — B105 implement đầy đủ PrivateCookieJar admin login flow, B107 implement SignedCookieJar CSRF token double-submit, B289 implement Key rotation strategy.

14/06/2026
10 phút đọc
1 lượt xem
1

Mục Tiêu Bài Học

Sau bài học, bạn sẽ:

  • Phân biệt 3 loại CookieJar của axum-extra: CookieJar plain (không bảo vệ, client đọc/ghi tự do), SignedCookieJar (HMAC SHA256 tamper-proof — client đọc được nhưng không sửa được), PrivateCookieJar (AES-256-GCM encrypt + sign — client không đọc được).
  • Hiểu use case mỗi loại jar cho Shop API (decision đã lock từ B19): plain cho preference UI public, Signed cho CSRF token client cần echo lại, Private cho session ID admin dashboard.
  • Biết set/get cookie với axum-extra: Cookie::build((name, value)) builder chain attribute + return type handler CookieJar để axum tự render Set-Cookie header response.
  • Nắm 4 attribute quan trọng theo RFC 6265bis: Secure (HTTPS-only prod mandatory), HttpOnly (chống XSS đánh cắp), SameSite (Strict/Lax/None chống CSRF), Max-Age (seconds preferred hơn Expires UTC date).
  • Hiểu pattern key management 64 bytes: dev Key::generate() ephemeral acceptable, production env COOKIE_KEY base64 share cross-instance qua secret manager; cấm hard-code, cấm Key::generate() ở prod.
  • Foundation cho B105 admin session (PrivateCookieJar + Redis session store) + B107 CSRF protection (SignedCookieJar double-submit pattern); B105 sẽ extend AppState thêm cookie_key: Key field + impl FromRef<AppState> for Key.
2

Cookie Là Gì? Server Set, Client Send Lại

Cookie (bánh quy) là cơ chế HTTP stateful theo RFC 6265bis cho phép server lưu trữ state nhỏ phía client browser. Server response set header Set-Cookie, browser tự động lưu, mọi request sau gửi lại qua header Cookie:

┌──────────────────────────────────────────────────────────────────────┐
│ Flow 1 — Server SET cookie qua response header                       │
├──────────────────────────────────────────────────────────────────────┤
│  HTTP/1.1 200 OK                                                     │
│  Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax;         │
│              Max-Age=86400; Path=/                                   │
│  Content-Type: application/json                                      │
└──────────────────────────────────────────────────────────────────────┘
                                ↓
                Browser lưu cookie vào cookie jar
                                ↓
┌──────────────────────────────────────────────────────────────────────┐
│ Flow 2 — Browser SEND lại cookie ở mọi request kế tiếp               │
├──────────────────────────────────────────────────────────────────────┤
│  GET /admin/dashboard HTTP/1.1                                       │
│  Cookie: session=abc123; theme=dark                                  │
│  Host: admin.shop.com                                                │
└──────────────────────────────────────────────────────────────────────┘

Use case điển hình:

  • Session ID — admin dashboard login → server tạo session UUID → set cookie HttpOnly + Secure → request sau browser gửi lại → server lookup Redis session:{id} → biết user.
  • CSRF token — server set cookie chứa token random → form HTML embed token hidden field → server verify cookie token == form token (double-submit pattern B107).
  • Preference UI — client chọn dark mode → server set cookie theme=dark Max-Age 1 năm → mọi page reload load theme đúng.

Anti-pattern cần tránh: dùng cookie cho API stateless. Mobile app/SPA/partner integration nên dùng Bearer JWT (Authorization: Bearer <JWT> lock B4) — JWT self-contained, không cần server lookup, không có CSRF risk (không tự động gửi), debug dễ hơn. Cookie sinh ra cho browser context với origin-based security model, không phù hợp non-browser client.

Shop API decision lock B19 confirm B34: cookie CHỈ cho admin dashboard nội bộ (HTML form login → POST /admin/login → set session cookie → render server-side dashboard). API client truy cập /api/v1/* dùng thuần Bearer JWT, KHÔNG dùng cookie. Quyết định này giúp tách bạch concern: browser session (cookie + CSRF) cho admin, API stateless (JWT) cho client app.

3

3 Loại CookieJar Của axum-extra

axum-extra cung cấp 3 jar tách bạch theo mức độ bảo vệ (lock B19 với 3 feature flag cookie + cookie-signed + cookie-private trong workspace dependencies):

┌───────────────────────┬────────────────┬──────────────┬──────────────────────┐
│ Jar                   │ Bảo vệ         │ Client đọc?  │ Client sửa?          │
├───────────────────────┼────────────────┼──────────────┼──────────────────────┤
│ CookieJar (plain)     │ Không có       │ Có (free)    │ Có (free)            │
│ SignedCookieJar       │ HMAC SHA256    │ Có           │ KHÔNG (sig fail)     │
│ PrivateCookieJar      │ AES-256-GCM    │ KHÔNG        │ KHÔNG                │
│                       │ + HMAC sign    │ (bytes mã)   │ (decrypt fail)       │
└───────────────────────┴────────────────┴──────────────┴──────────────────────┘

CookieJar plain — cookie không bảo vệ, value lưu thẳng dạng text. Client mở DevTools đọc thoải mái, sửa value bằng JS hoặc tab Application → Cookies → edit. Không cần Key. Use case: preference UI không nhạy cảm (theme, language, sidebar collapse state). Anti-use: KHÔNG bao giờ lưu thông tin trust (user ID, role, permission) — client sửa được tức là escalation.

SignedCookieJar — HMAC SHA256 ký phía sau value, dạng wire <value>.<hmac>. Client ĐỌC được nội dung value (không encrypt), nhưng KHÔNG sửa được vì sửa value sẽ làm signature không match → server verify fail → reject cookie. Cần Key ≥ 64 bytes random. Use case: CSRF token (client cần đọc token để echo lại trong form/header).

PrivateCookieJar — AES-256-GCM authenticated encryption + sign, dạng wire là bytes encrypted base64 client không thể decode. DevTools chỉ thấy rác kiểu aGVsbG8gd29ybGQK.... Cần Key ≥ 64 bytes. Use case: session ID admin dashboard, data nhạy cảm (user email lưu cookie tránh DB query mỗi request).

Shop API mapping (lock B19):

  • Plain: preference UI public (chỉ khi admin dashboard có UI customization riêng — chưa scope Group 4).
  • SignedCookieJar: CSRF token (B107) — token random server sinh, set cookie Signed, form HTML embed cùng giá trị qua hidden field, server verify cookie == form (double-submit). Client đọc cookie để echo, nhưng không sửa được.
  • PrivateCookieJar: session ID admin dashboard (B105) — UUID session sinh sau login thành công, set cookie Private, Redis store session:{id} chứa user info; mọi request kế server decrypt cookie → lookup Redis.
4

CookieJar Plain — Cơ Bản

Plain CookieJar dùng cho cookie không cần bảo vệ — preference UI. Handler extract CookieJar ở arg, đọc cookie hiện có qua .get(name), thêm cookie mới qua .add(cookie), return jar (modified) — axum tự render Set-Cookie response:

// Pattern preview — admin preference (KHÔNG implement B34, demo concept)
use axum_extra::extract::CookieJar;
use axum_extra::extract::cookie::Cookie;

// GET /admin/preference/theme — đọc theme từ cookie, default light
async fn get_theme(jar: CookieJar) -> String {
    match jar.get("theme") {
        Some(cookie) => cookie.value().to_string(),
        None => "light".to_string(),
    }
}

// POST /admin/preference/theme/:value — set theme cookie
async fn set_theme(jar: CookieJar, axum::extract::Path(value): axum::extract::Path<String>) -> CookieJar {
    let cookie = Cookie::build(("theme", value))
        .path("/")
        .max_age(time::Duration::days(365))
        .same_site(axum_extra::extract::cookie::SameSite::Lax)
        .build();
    jar.add(cookie)
}

Điểm chú ý code:

  • Extract CookieJar ở arg handler — axum-extra impl FromRequestParts sẵn cho jar, không phải custom extractor. Jar đọc Cookie header request parse thành map name→value.
  • Builder Cookie::build((name, value)) — tuple (&str, String), chain attribute method .path()/.max_age()/.same_site() rồi .build() trả Cookie<'static>.
  • Return type CookieJar — handler trả jar đã modified, axum render Set-Cookie header tự động cho mỗi cookie thêm/sửa/xóa. Pattern này tránh phải build response manual.
  • KHÔNG cần Key — plain jar không encrypt/sign, value đi thẳng vào header. AppState không phải store key cho plain.

Plain jar có .add(cookie), .remove(cookie) (set Max-Age=0 để browser xóa), .get(name) trả Option<&Cookie>, .iter() loop tất cả cookie. Đủ cho mọi use case preference UI public.

5

SignedCookieJar — HMAC Tamper-Proof

SignedCookieJar ký value bằng HMAC SHA256 với Key server-side. Client đọc được value (vd csrf=tk_abc123.signature_xyz), nhưng nếu sửa value thì signature không khớp → jar.get() trả None như cookie không tồn tại. Use case chính Shop API: CSRF token cho admin dashboard (B107):

// File: crates/shop-api/src/routes/admin/csrf.rs (preview B107 — KHÔNG implement B34)
use axum::extract::State;
use axum_extra::extract::SignedCookieJar;
use axum_extra::extract::cookie::{Cookie, SameSite};
use shop_common::error::{AppError, AppResult};

// GET /admin/csrf — sinh CSRF token mới, set cookie Signed
async fn generate_csrf(jar: SignedCookieJar) -> SignedCookieJar {
    let token = generate_random_token();  // 32 bytes random base64
    let cookie = Cookie::build(("csrf_token", token))
        .secure(true)              // HTTPS only (prod)
        .same_site(SameSite::Strict)  // chống CSRF mạnh nhất
        .max_age(time::Duration::hours(1))
        .path("/admin")
        .build();
    jar.add(cookie)
}

// POST /admin/products — verify CSRF token trước khi tạo product
async fn verify_csrf(jar: SignedCookieJar) -> AppResult<()> {
    match jar.get("csrf_token") {
        Some(_cookie) => {
            // Signature valid → cookie chưa bị tamper
            // Đối chiếu với form field cho double-submit (B107 chi tiết)
            Ok(())
        }
        None => Err(AppError::BadRequest(
            "CSRF token missing or tampered".to_string(),
        )),
    }
}

Điểm chú ý:

  • Extract SignedCookieJar đòi hỏi AppState provide Key qua FromRef<AppState> impl — chi tiết Step 8 (key management). B105 sẽ wire vào AppState.
  • Client đọc được token — DevTools thấy csrf=tk_abc123.signature_xyz. Form HTML render server-side embed token vào hidden field; JS client cũng đọc cookie được để gửi header X-CSRF-Token.
  • Client KHÔNG sửa được — sửa tk_abc123 thành tk_evil → signature signature_xyz không match → server verify fail → jar.get() trả None, handler reject.
  • SameSite::Strict cho CSRF token cứng nhất — cookie KHÔNG gửi cross-site, browser tự động block CSRF attack ở tầng dưới. Step 7 chi tiết.

B107 sẽ implement đầy đủ double-submit pattern: cookie chứa token Signed + form field hidden chứa cùng token + server verify cookie value == form value. Pattern này chống được CSRF attack ngay cả khi attacker biết format token, vì attacker không thể đọc cross-origin cookie để echo lại form.

6

PrivateCookieJar — AES-256-GCM Encrypt

PrivateCookieJar dùng AES-256-GCM authenticated encryption + HMAC sign. Value cookie phía wire là bytes encrypted base64, client KHÔNG đọc được nội dung và KHÔNG sửa được (authenticated encryption phát hiện cả tampering). Cần Key ≥ 64 bytes. Use case Shop API: session ID admin dashboard (B105):

// File: crates/shop-api/src/routes/admin/auth.rs (preview B105 — KHÔNG implement B34)
use axum::{extract::State, response::Redirect, Form};
use axum_extra::extract::PrivateCookieJar;
use axum_extra::extract::cookie::{Cookie, SameSite};
use shop_common::error::AppResult;

// POST /admin/login — verify credentials, set session cookie encrypted
async fn admin_login(
    State(state): State<AppState>,
    jar: PrivateCookieJar,
    Form(form): Form<LoginForm>,
) -> AppResult<(PrivateCookieJar, Redirect)> {
    let user = verify_credentials(&state, &form).await?;
    let session_id = create_session(&state, user.id).await?;  // Redis SET session:{id}
    let cookie = Cookie::build(("session", session_id))
        .secure(true)                    // HTTPS only (prod)
        .http_only(true)                 // JS không đọc được
        .same_site(SameSite::Lax)        // Lax cho login flow OK
        .max_age(time::Duration::hours(24))
        .path("/")
        .build();
    Ok((jar.add(cookie), Redirect::to("/admin")))
}

// POST /admin/logout — xóa session cookie + xóa Redis
async fn admin_logout(
    State(state): State<AppState>,
    jar: PrivateCookieJar,
) -> (PrivateCookieJar, Redirect) {
    if let Some(cookie) = jar.get("session") {
        let _ = delete_session(&state, cookie.value()).await;
    }
    (
        jar.remove(Cookie::from("session")),
        Redirect::to("/admin/login"),
    )
}

Điểm chú ý:

  • Return type tuple (PrivateCookieJar, Redirect) — handler vừa modify jar vừa redirect; axum impl IntoResponse cho tuple, render Set-Cookie header trước rồi 302 redirect.
  • jar.remove(Cookie::from("session")) — set Max-Age=0 + empty value để browser xóa cookie phía client. Pattern logout chuẩn: xóa cả Redis session + browser cookie.
  • Bộ ba secure + http_only + same_site=Lax cho session cookie là lock vĩnh viễn (xem Step 7). SameSite::Lax thay Strict vì login flow cần cookie gửi với top-level navigation (redirect từ /admin/login → /admin sau POST).
  • Client KHÔNG bao giờ thấy session_id raw — DevTools chỉ thấy bytes encrypted; ngay cả browser extension XSS đọc cookie cũng chỉ lấy được ciphertext không decrypt được.

B105 sẽ implement đầy đủ flow login + session middleware (extract PrivateCookieJar → lookup Redis session:{id} → load user vào Extension<AdminUser> cho handler downstream). Pattern PrivateCookieJar tách bạch transport security (encrypt value) khỏi storage logic (Redis lưu user info), đúng layering chuẩn web security.

7

Attribute Quan Trọng: Secure, HttpOnly, SameSite

RFC 6265bis định nghĩa nhiều attribute kiểm soát hành vi cookie. 4 attribute quan trọng nhất cho Shop API:

┌──────────────┬────────────────────────────────────────────────────────┐
│ Attribute    │ Ý nghĩa                                                │
├──────────────┼────────────────────────────────────────────────────────┤
│ Secure       │ Chỉ gửi qua HTTPS — production MANDATORY               │
│              │ Dev local HTTP: skip để test                           │
├──────────────┼────────────────────────────────────────────────────────┤
│ HttpOnly     │ JS document.cookie KHÔNG đọc được                      │
│              │ Chống XSS đánh cắp session cookie                      │
│              │ Session cookie LUÔN bật, CSRF cookie có thể tắt        │
├──────────────┼────────────────────────────────────────────────────────┤
│ SameSite     │ Kiểm soát cross-site request có gửi cookie không       │
│   Strict     │   Chỉ gửi same-site → chống CSRF mạnh nhất             │
│   Lax        │   Gửi với top-level navigation (mặc định modern)       │
│   None       │   Gửi cross-site (require Secure)                      │
├──────────────┼────────────────────────────────────────────────────────┤
│ Max-Age      │ Sống bao lâu (seconds) — preferred hơn Expires         │
│              │ Không phụ thuộc clock skew client                      │
└──────────────┴────────────────────────────────────────────────────────┘

Secure — cookie chỉ gửi qua HTTPS connection. Production MANDATORY: HTTP plain text exposed session cookie qua mạng → MITM attack đánh cắp. Dev local HTTP skip để test ở localhost (browser modern cho phép cookie không Secure trên localhost). Pattern config theo env: .secure(state.config.app_env.is_production()).

HttpOnly — JavaScript document.cookie không đọc được cookie này, browser chỉ tự động gửi qua HTTP header. Mục tiêu: chống XSS attack lấy session cookie (attacker inject JS qua XSS lỗ hổng cũng không đọc được session). Session cookie LUÔN bật HttpOnly. CSRF cookie có thể tắt nếu cần JS đọc để gửi header X-CSRF-Token (pattern double-submit thay form field).

SameSite — RFC 6265bis định nghĩa 3 mode kiểm soát cross-site behavior:

  • Strict: cookie chỉ gửi với request cùng site origin. Chống CSRF mạnh nhất nhưng break flow OAuth redirect (cookie không gửi khi user click link từ email về site). Dùng cho CSRF token và cookie sensitive đặc biệt.
  • Lax: cookie gửi với top-level navigation (user click link, browser GET URL trực tiếp), không gửi với cross-site POST/iframe/img. Mặc định modern browser từ 2020. Cân bằng UX vs security — dùng cho session cookie admin login.
  • None: cookie gửi cross-site mọi context. Bắt buộc kèm Secure. Dùng cho widget embed third-party, hiếm khi áp dụng cho Shop API admin.

Max-Age vs Expires: cả hai set lifetime cookie, nhưng Max-Age preferred (RFC 6265bis):

  • Max-Age=86400 — seconds count từ thời điểm browser nhận response, không phụ thuộc clock client (clock skew không ảnh hưởng).
  • Expires=Wed, 14 Jun 2026 12:00:00 GMT — UTC date cụ thể, browser tính dựa system clock; nếu clock sai thì cookie expire sai.
  • Modern browser hiểu cả hai; Max-Age override Expires nếu có cùng cookie.

Shop API attribute lock vĩnh viễn:

  • Session cookie (B105): HttpOnly + Secure (prod) + SameSite=Lax + Max-Age=24h + Path=/ — đủ cho admin login flow.
  • CSRF cookie (B107): Secure (prod) + SameSite=Strict + Max-Age=1h + Path=/admin — không HttpOnly (JS cần đọc để echo header), Strict cho chống CSRF mạnh, TTL ngắn 1h refresh per session.
  • Preference cookie plain: SameSite=Lax + Max-Age=1y + Path=/ — không nhạy cảm, không cần Secure/HttpOnly, TTL dài.
8

Key Management — Dev vs Prod

SignedCookieJarPrivateCookieJar cần cookie::Key 64 bytes random để HMAC sign + AES-256-GCM encrypt. Cookie crate (dep của axum-extra) derive HKDF 2 subkey từ master key 64 bytes: 32 bytes cho AES, 32 bytes cho HMAC.

Pattern dev:

use axum_extra::extract::cookie::Key;

// Dev — sinh key random mỗi lần bootstrap process
let key = Key::generate();
// → 64 bytes random từ OsRng; restart process sinh key mới
// → session cũ encrypted với key cũ sẽ KHÔNG decrypt được
// → acceptable cho dev: session ephemeral, restart frequent OK

Pattern production:

use axum_extra::extract::cookie::Key;
use base64::{engine::general_purpose::STANDARD, Engine as _};

// Prod — đọc key từ env, base64 decode
let key_b64 = std::env::var("COOKIE_KEY")
    .context("COOKIE_KEY env required in production")?;
let key_bytes = STANDARD.decode(&key_b64)
    .context("COOKIE_KEY must be valid base64")?;
if key_bytes.len() < 64 {
    anyhow::bail!("COOKIE_KEY must be >= 64 bytes");
}
let key = Key::from(&key_bytes);
// → key share cross-pod qua secret manager (Vault, AWS Secrets Manager, K8s Secret)
// → mọi pod cùng key → session encrypted ở pod A decrypt được ở pod B

Sinh key 64 bytes một lần cho prod (CLI):

# Generate key một lần, lưu vào secret manager
openssl rand -base64 64 | tr -d '\n'
# → vd: "k3JZ4mPx9...random64bytes...base64encoded=="
# → store vào Vault path secret/shop/cookie-key
# → K8s Secret: kubectl create secret generic shop-cookie --from-literal=key="$KEY"

CẤM:

  • Hard-code key trong source code — commit vào git = security breach. Lịch sử git lưu vĩnh viễn, attacker pull repo public/internal lấy key decrypt được mọi session.
  • Dùng Key::generate() ở production — mỗi pod sẽ có key khác nhau. Session encrypted ở pod A khi load balancer route request tiếp theo qua pod B sẽ không decrypt được → user bị logout liên tục. Đặc biệt nguy hiểm khi rolling deploy: pod cũ key A, pod mới key B, session lifespan cross deploy = invalidate.
  • Reuse key cũ sau rotation — rotation đúng cách (B289): grace period dual-key window 7-30 ngày verify cả key cũ + key mới, sau đó retire key cũ. KHÔNG reuse key cũ về sau (compromised once = compromised forever).

Setup pattern AppState extend (B105 implement):

// File: crates/shop-api/src/state.rs (B105 extend từ B17 + B28)
use axum::extract::FromRef;
use axum_extra::extract::cookie::Key;
use shop_common::config::AppConfig;
use std::sync::Arc;

#[derive(Clone)]
pub struct AppState {
    pub config: Arc<AppConfig>,
    pub cookie_key: Key,  // ← Add khi B105
    // pub pool: PgPool,         // G6
    // pub redis: RedisPool,     // G18
}

// SignedCookieJar / PrivateCookieJar extract Key qua FromRef sub-state pattern
impl FromRef<AppState> for Key {
    fn from_ref(state: &AppState) -> Self {
        state.cookie_key.clone()  // Key có impl Clone cheap
    }
}

Pattern FromRef cho phép extractor con (SignedCookieJar, PrivateCookieJar) extract sub-state Key từ AppState mà không cần handler nhận State<AppState> trực tiếp. B105 sẽ load cookie_key trong AppConfig::from_env (đọc env COOKIE_KEY hoặc Key::generate() theo app_env), wire vào AppState::new.

Rotation strategy (B289 deep dive): cookie crate hỗ trợ multi-key verify qua wrapper. Khi rotation: thêm Key mới, giữ Key cũ trong list verify; cookie cũ vẫn decrypt được bằng key cũ trong grace period; cookie mới sign bằng key mới; sau N tháng (typical 30 ngày) retire key cũ. Monitoring metric "decrypt fail rate" để biết khi nào safe cut-over hoàn toàn.

9

Tổng Kết

  • 3 loại CookieJar của axum-extra: Plain (không bảo vệ, client đọc/ghi tự do), Signed (HMAC SHA256 — client đọc được nhưng không sửa được), Private (AES-256-GCM encrypt + sign — client không đọc được nội dung).
  • Use case: Plain cho preference UI (theme, language), Signed cho CSRF token client cần echo lại (B107 double-submit), Private cho session ID admin dashboard (B105).
  • Shop API decision lock B19 confirm B34: cookie CHỈ cho admin dashboard nội bộ (B105 session + B107 CSRF); API client /api/v1/* (mobile, SPA, partner) dùng thuần Bearer JWT (lock B4) KHÔNG dùng cookie.
  • 4 attribute quan trọng theo RFC 6265bis: Secure (HTTPS-only prod mandatory), HttpOnly (chống XSS đánh cắp), SameSite 3 mode (Strict chống CSRF mạnh nhất / Lax mặc định modern browser / None cross-site require Secure), Max-Age seconds preferred hơn Expires UTC date (không phụ thuộc clock skew client).
  • Attribute lock Shop API vĩnh viễn: Session cookie HttpOnly + Secure (prod) + SameSite=Lax + Max-Age=24h; CSRF cookie Secure (prod) + SameSite=Strict + Max-Age=1h (không HttpOnly vì JS cần đọc); Preference cookie plain SameSite=Lax + Max-Age=1y.
  • Key management: 64 bytes random — dev Key::generate() ephemeral acceptable (restart sinh key mới invalidate session OK), production đọc từ env COOKIE_KEY base64-decoded qua Key::from(&decoded_bytes), share cross-instance qua secret manager (Vault / AWS Secrets Manager / K8s Secret).
  • CẤM: hard-code key trong source code (commit git = security breach), Key::generate() ở production (mỗi pod khác key → session invalidate cross-pod khi load balancer route khác pod), reuse key cũ sau rotation. Rotation chi tiết B289 (dual-key window grace period 7-30 ngày).
  • File path lock: B105 sẽ extend crates/shop-api/src/state.rs thêm cookie_key: Key field vào AppState + impl FromRef<AppState> for Key cho sub-state extract; SignedCookieJar/PrivateCookieJar extractor đọc Key qua FromRef pattern không cần handler nhận State<AppState> trực tiếp.
  • Foundation cho B105 admin session (PrivateCookieJar + Redis session:{id} store) + B107 CSRF protection (SignedCookieJar double-submit pattern); B34 conceptual + preview, KHÔNG tạo file thực tế Shop API — B105/B107 implement đầy đủ khi infra Redis + jsonwebtoken wire vào AppState.
10

Bài Tập Củng Cố

Tự trả lời, đáp án ở cuối:

  1. 3 loại CookieJar khác nhau ở điểm gì (bảo vệ, đọc/sửa client, key requirement)? Khi nào dùng cái nào cho Shop API?
  2. Bộ ba attribute Secure + HttpOnly + SameSite=Strict phù hợp cho cookie nào? Tại sao session cookie lại dùng SameSite=Lax thay Strict?
  3. Shop API API client (mobile, frontend SPA, partner integration) dùng cookie hay Bearer JWT cho auth? Lý do tách bạch cookie cho admin và JWT cho API client?
  4. PrivateCookieJar cần Key bao nhiêu bytes và để làm gì? Quản lý key ở dev vs prod khác nhau ra sao? Pattern FromRef<AppState> for Key giải quyết vấn đề gì?
  5. Anti-pattern hard-code Key trong source code có risk gì cụ thể? Tại sao Key::generate() không dùng được ở prod? Pattern đúng để sinh, share, rotate key prod?
Đáp án
  1. 3 loại CookieJar tách bạch theo mức độ bảo vệ. CookieJar plain (feature cookie): không bảo vệ — value lưu thẳng dạng text, client mở DevTools đọc thoải mái và sửa bằng JS/UI; KHÔNG cần Key; dùng cho preference UI không nhạy cảm (theme, language, sidebar collapse). Anti-use: KHÔNG bao giờ lưu trust info (user_id, role) vì client sửa được = privilege escalation. SignedCookieJar (feature cookie-signed): HMAC SHA256 ký phía sau value dạng wire <value>.<hmac>; client ĐỌC được nội dung value (không encrypt), KHÔNG sửa được vì sửa value sẽ làm signature không khớp → server verify fail → jar.get() trả None; CẦN Key ≥ 64 bytes; dùng cho CSRF token client cần echo lại trong form/header (B107 double-submit pattern). PrivateCookieJar (feature cookie-private): AES-256-GCM authenticated encryption + HMAC sign; value phía wire là bytes encrypted base64 client không decode được; client KHÔNG đọc nội dung và KHÔNG sửa được (authenticated encryption phát hiện cả tampering); CẦN Key ≥ 64 bytes; dùng cho session ID admin dashboard (B105) và data nhạy cảm. Shop API mapping lock B19: Plain cho preference UI public (chỉ nếu admin dashboard có customization riêng — chưa scope Group 4); SignedCookieJar cho CSRF token (B107) — token random server sinh, set cookie Signed, form HTML embed cùng giá trị qua hidden field, server verify cookie == form (double-submit); PrivateCookieJar cho session ID admin dashboard (B105) — UUID session sinh sau login, set cookie Private, Redis store session:{id} chứa user info, mọi request kế server decrypt cookie → lookup Redis.
  2. Bộ ba Secure + HttpOnly + SameSite=Strict phù hợp cho cookie sensitive cao cần chống cả XSS + CSRF + MITM: ví dụ admin password reset token, payment confirmation token, banking transaction token. Secure chỉ gửi qua HTTPS (chống MITM đánh cắp qua mạng plaintext); HttpOnly JS không đọc được (chống XSS exfiltration); SameSite=Strict chỉ gửi same-site request (chống CSRF mạnh nhất, không gửi cross-site bất kỳ context nào). Tại sao session cookie dùng SameSite=Lax thay Strict: SameSite=Strict phá UX login flow — khi user click link từ email (vd "Vào dashboard") browser navigate sang admin.shop.com, cookie session NOT gửi → user bị logout giả → frustrating. Lax cho phép cookie gửi với top-level navigation (user click link, browser GET URL trực tiếp) nhưng vẫn block cross-site POST/iframe/img → vẫn chống được CSRF main vector (POST từ malicious site qua hidden form). Cân bằng UX vs security — modern browser từ 2020 mặc định Lax nếu không khai báo. CSRF cookie thì cứng nhắc hơn dùng Strict vì cookie này chỉ dùng nội bộ form admin, không cần gửi với navigation từ ngoài. Shop API lock: session cookie HttpOnly + Secure + SameSite=Lax + Max-Age=24h, CSRF cookie Secure + SameSite=Strict + Max-Age=1h (không HttpOnly vì JS cần đọc echo header X-CSRF-Token trong AJAX request).
  3. Shop API API client (mobile app, frontend SPA, partner integration) dùng thuần Bearer JWT (lock B4 header convention Authorization: Bearer <JWT>), KHÔNG dùng cookie. Endpoint /api/v1/* stateless 100%, mỗi request kèm JWT trong header, server verify signature + expiry không cần lookup session storage. Lý do tách bạch cookie cho admin và JWT cho API client: (a) Cookie sinh ra cho browser context với origin-based security model (SameSite, Secure, HttpOnly, browser tự động gửi) — không phù hợp non-browser client như mobile app (iOS/Android không có cookie jar native, phải implement manual = bug-prone) hoặc partner integration server-to-server (không có browser, cookie ko có ý nghĩa). (b) JWT self-contained — claims chứa user_id + role + exp + scope, server không cần DB/Redis lookup cho mỗi request → scale tốt hơn (Shop API có 60 endpoint, nếu mỗi request lookup session = 60 lần Redis call per page). (c) CSRF risk — cookie tự động gửi với mọi request từ origin → vector CSRF attack; JWT KHÔNG tự động gửi, client phải explicit add vào header → CSRF không apply (không có "cross-site JWT" attack). (d) Debug dễ hơn — JWT trong header thấy ngay qua DevTools/curl, cookie ẩn trong jar phải mở tab Application; replay request từ Postman với JWT trivial, với cookie phải copy paste cookie string. (e) Mobile native support — mobile có thư viện JWT well-established (jose-swift, JJWT Android), không có cookie jar native. (f) Multi-platform consistency — frontend SPA, mobile, partner cùng dùng JWT → 1 auth scheme cross-client, server logic đơn giản. Admin dashboard ngược lại render server-side template HTML form login → browser flow tự nhiên với cookie (HttpOnly chống XSS, SameSite chống CSRF, Secure chống MITM); session ID nhỏ (UUID 36 ký tự) trong PrivateCookieJar + Redis store cho user info detail. Tách bạch concern: browser session (cookie + CSRF + render server-side) cho admin, API stateless (JWT + JSON) cho client app — không trộn lẫn để tránh complexity và security pitfall.
  4. PrivateCookieJar cần Key 64 bytes random vì cookie crate (dep của axum-extra) derive HKDF 2 subkey từ master key 64 bytes: 32 bytes cho AES-256-GCM (authenticated encryption — encrypt value + tag auth), 32 bytes cho HMAC SHA256 (sign integrity check secondary, defense in depth). Master key 64 bytes đủ entropy cho cả hai subkey không correlate. SignedCookieJar cùng kích thước key (64 bytes) để consistent API và sẵn sàng nâng cấp Signed → Private không phải re-key. Quản lý dev vs prod: (a) Dev: sinh Key::generate() lúc bootstrap process, key ổn định trong vòng đời process — restart sinh key mới làm invalidate session cũ; chấp nhận được cho dev vì session ephemeral, dev restart frequent, mỗi developer 1 instance riêng. Code: let key = Key::generate(); ngay trong AppConfig::from_env khi app_env == Dev. (b) Prod: đọc từ env variable COOKIE_KEY base64-decoded qua Key::from(&base64::decode(env)?); key sinh 1 lần qua openssl rand -base64 64, lưu vào secret manager (HashiCorp Vault, AWS Secrets Manager, K8s Secret), inject vào pod qua env variable lúc deploy; mọi pod cùng key → session encrypted ở pod A decrypt được ở pod B (load balancer route khác pod OK). Code: validate COOKIE_KEY tồn tại + base64 valid + bytes >= 64, fail-fast nếu missing/invalid (bootstrap không lên được, prevent silent misconfig). Pattern FromRef<AppState> for Key giải quyết vấn đề: SignedCookieJarPrivateCookieJar impl FromRequestParts<S> where S: Send + Sync, Key: FromRef<S> — extract sub-state Key từ outer state AppState mà không cần handler nhận State<AppState> trực tiếp. Implement: impl FromRef<AppState> for Key { fn from_ref(state: &AppState) -> Self { state.cookie_key.clone() } }Key có impl Clone cheap (Arc internal). Handler signature gọn: async fn admin_login(jar: PrivateCookieJar, Form(form): Form<LoginForm>) không cần State<AppState> trừ khi truy cập field khác (Redis pool, config). Pattern này áp dụng cho mọi sub-state Shop API tương lai (pool, redis) khi sub-extractor cần thay vì handler phải receive full AppState. B105 sẽ extend AppState thêm cookie_key: Key field + impl FromRef.
  5. Anti-pattern hard-code Key trong source code risk cụ thể: (a) Commit vào git = security breach vĩnh viễn — git history lưu mọi commit không xóa được (force push xóa được nhưng nếu repo public hoặc team đông đã pull/fork = compromised); attacker pull repo lấy key decrypt được mọi session encrypt bằng key đó, đọc nội dung cookie sensitive (user email, role), tạo session giả mạo. (b) Cross-environment leak — dev có thể chạy với cùng key như staging/prod nếu hard-code, phát triển local accidentally test với prod data. (c) Rotation không feasible — đổi key phải deploy code mới, downtime + risk khi rollback. (d) Code review không bắt được nếu encode dạng base64 trong const, người review không nhận ra là key thật. Tại sao Key::generate() không dùng được ở prod: Key::generate() sinh random từ OsRng mỗi lần process start — mỗi pod sẽ có key khác nhau. Hậu quả runtime: (a) Session encrypted ở pod A khi load balancer route request tiếp theo qua pod B sẽ không decrypt được (key B khác key A) → cookie verify fail → user bị logout ngay sau khi login → support flood. (b) Rolling deploy cực kỳ nguy hiểm: pod cũ key A, pod mới key B chạy parallel 5-10 phút; user nhận cookie từ pod cũ rồi request tiếp routing pod mới = session loss; kết quả ALL user logout khi deploy = catastrophic UX. (c) Horizontal pod autoscaler scale lên thêm pod = key mới = ảnh hưởng user. (d) Restart pod (OOM kill, health check fail, manual restart) cũng làm key mới. Pattern đúng để sinh, share, rotate key prod: (1) Sinh: 1 lần duy nhất qua CLI openssl rand -base64 64 | tr -d '\\n' trên máy admin secure (không phải laptop developer), output base64 string 88 ký tự. (2) Share: lưu vào secret manager tier 1 (HashiCorp Vault với policy admin-only read, AWS Secrets Manager với IAM role pod-specific, K8s Secret với RBAC role binding); CẤM commit git, CẤM dán Slack/email, CẤM gửi qua chat plaintext. (3) Inject: deployment template (Helm chart, K8s manifest) reference secret qua envFrom.secretRef hoặc init-container fetch từ Vault; pod khởi động đọc env COOKIE_KEY base64 decode validate >= 64 bytes, fail-fast nếu missing/invalid prevent silent misconfig. (4) Rotate (B289 deep dive): cookie crate hỗ trợ multi-key verify qua wrapper; thêm Key mới vào AppState (vd cookie_keys: Vec<Key>); cookie mới sign bằng key đầu tiên (key mới nhất), verify thử với từng key trong list — cookie cũ vẫn decrypt được bằng key cũ trong grace period 7-30 ngày; sau grace period retire key cũ (remove khỏi list); monitor metric "decrypt fail rate" qua Prometheus để biết khi nào safe cut-over hoàn toàn. (5) Audit: log mỗi access key (Vault audit log), alert nếu key access bất thường (off-hours, từ IP unknown); rotate ngay nếu nghi ngờ compromise.
11

Bài Tiếp Theo

— chi tiết Form<T> extractor cho application/x-www-form-urlencoded (admin login page B105, Stripe webhook B197), MultiPart preview B36 cho file upload, integrate với template engine (askama, maud) cho server-side render.