ocre/runtime/security.rs
1use crate::{Ctx, Error, Result};
2
3/// Counts one request for `key` against the Workers Rate Limiting binding `binding`; 429 when over the limit.
4///
5/// Rails' `rate_limit to: 10, within: 1.minute, by: ...`: the limit and the
6/// period are set on the binding in `cloudflare.config.ts` (`period` is 10 or 60
7/// seconds), the key is chosen per call, e.g. `login:<client ip>` or
8/// `api:<user id>`. `ocre g auth` adds an `AUTH_RATE_LIMITER` binding and
9/// calls this (through the generated `throttle`) in every route that checks
10/// a password or sends an email: login, sign-up, magic link, password reset,
11/// email confirmation, token and account deletion.
12///
13/// ```ts
14/// // worker.env; `namespace`: any integer unique in your account
15/// AUTH_RATE_LIMITER: bindings.rateLimit({ namespace: "1001", simple: { limit: 10, period: 60 } }),
16/// ```
17///
18/// Counters are per Cloudflare location and eventually consistent: a limit,
19/// not an exact count. `ocre dev` simulates the binding locally.
20///
21/// # Free plan
22///
23/// The Rate Limiting binding is available on the free plan and costs no
24/// D1, KV or Durable Object operation; the call does not wait on the
25/// network (counters are cached on the machine running the Worker).
26///
27/// # Errors
28///
29/// - [`Error::TooManyRequests`] (429) when `key` is over the limit.
30/// - [`Error::Internal`] when the binding is missing from `cloudflare.config.ts`
31/// (message names the fix) or the call fails.
32///
33/// # Examples
34///
35/// ```no_run
36/// use axum::{extract::State, http::HeaderMap};
37/// use ocre::{Ctx, Result, security::rate_limit};
38///
39/// async fn login(State(ctx): State<Ctx>, headers: HeaderMap) -> Result<&'static str> {
40/// let ip = headers.get("cf-connecting-ip").and_then(|ip| ip.to_str().ok()).unwrap_or("unknown");
41/// rate_limit(&ctx, "AUTH_RATE_LIMITER", &format!("login:{ip}")).await?; // 429 when over
42/// Ok("checked the password")
43/// }
44/// # let _ = login;
45/// ```
46pub async fn rate_limit(ctx: &Ctx, binding: &str, key: &str) -> Result<()> {
47 let limiter = ctx.env().rate_limiter(binding).map_err(|err| {
48 Error::internal(format!(
49 "rate limiting binding `{binding}` is missing ({err}). Fix: add to worker.env in cloudflare.config.ts\n\
50 {binding}: bindings.rateLimit({{ namespace: \"1001\", simple: {{ limit: 10, period: 60 }} }}),"
51 ))
52 })?;
53 let outcome = limiter.limit(key.to_owned()).await?;
54 if outcome.success { Ok(()) } else { Err(Error::TooManyRequests) }
55}