CSRF
CSRF module: Cross-Site Request Forgery protection for Bastion web apps.
Generates a per-session CSRF token stored in the signed session under "_csrf_token", so it persists across requests (the form rendered on a GET and the token validated on the POST share one session-backed value). The middleware validates the token on every mutating request (POST, PUT, PATCH, DELETE) that is not a JSON API call.
Typical usage in an endpoint pipeline:
conn
|> CSRF.ensure_token() -- assign token to session (call early)
|> CSRF.protect() -- validate on mutating requestsIn ~H templates, <form method="post"> auto-injects the hidden field via the desugar pass. For manually built forms use CSRF.tag(conn).
Token format: 32 cryptographically random bytes, base64-encoded. Rotation: Per-session (not per-request) to support back button / multi-tab usage.
Functions
Ensure the Conn has a CSRF token, generating one if absent.
conn: CSRF.ensure_token(conn)
Returns an updated Conn with the token in assigns. Idempotent: if the
token already exists this is a no-op and the existing token is kept.
Call this early in the endpoint pipeline, after session loading.
conn
|> CSRF.ensure_token()
|> CSRF.protect()
|> MyApp.Router.route()Generate a fresh cryptographically random CSRF token.
CSRF.generate_token() -- e.g. "dGhpcyBpcyBhIHRlc3Q..."
Uses 32 bytes from the OS CSPRNG, URL-safe base64-encoded (no padding).CSRF protection middleware. Validates the token on mutating requests.
conn |> CSRF.protect()
Safe methods (GET, HEAD, OPTIONS) pass through unchanged.
JSON API requests (Content-Type: application/json) are exempt — they are
protected by the browser's same-origin policy for cross-origin fetches.
All other mutating requests (POST, PUT, PATCH, DELETE) must include a
valid `_csrf_token` field in the form body. On mismatch the request is
halted with a `403 Forbidden` response.
Routes that legitimately receive cross-origin POSTs (e.g., Stripe
webhooks) should call `CSRF.skip/1` before routing.Skip CSRF validation for this Conn.
conn |> CSRF.skip() |> MyApp.WebhookHandler.stripe()
Marks the Conn so that `CSRF.protect/1` and `CSRF.validate/1` will pass
through without checking the token. Use for incoming webhooks or other
legitimate cross-origin POST endpoints.Return an IOList containing a hidden CSRF input tag for manual form building.
Html.tag("form", [("method", "post")], CSRF.tag(conn))
Produces:
<input type="hidden" name="_csrf_token" value="...">
The token value is HTML-escaped so it is safe to embed in any HTML context.
For ~H sigil templates, the compiler auto-injects this tag into any
`<form method="post/put/patch/delete">` — you do not need to call this
manually unless you are building forms outside ~H.Return the CSRF hidden input tag as a plain String.
Same as `CSRF.tag/1` but returns `String` rather than `IOList`. Used
internally by the ~H compiler desugar pass which builds a `List(String)`
before calling `IOList.from_strings/1`.
CSRF.tag_string(conn)
-- "<input type=\"hidden\" name=\"_csrf_token\" value=\"tok\">"Get the current CSRF token from the Conn assigns.
let tok = CSRF.token(conn)
Returns the stored token string, or an empty string if the token has not
yet been assigned (i.e., `ensure_token/1` was not called upstream).
Call `ensure_token/1` in the middleware pipeline to guarantee a token is
present before calling `token/1` in a template or handler.Validate the CSRF token in the request against the session token.
if CSRF.validate(conn) do
handle_form(conn)
else
HttpServer.send_resp(conn, 403, "Invalid CSRF token")
end
Reads `_csrf_token` from the form body (URL-encoded) and compares it to
the token stored in the signed session. Returns `false` if either token is
absent or if they do not match.
The `protect/1` middleware calls this automatically; use `validate/1`
directly only when you need custom error handling.