March Docs

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 requests

In ~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

fnensure_tokenensure_token(conn)#

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()
fngenerate_tokengenerate_token() : String#

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).
fnprotectprotect(conn)#

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.
fnskipskip(conn)#

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.
fntagtag(conn) : IOList#

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.
fntag_stringtag_string(conn) : String#

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\">"
fntokentoken(conn) : String#

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.
fnvalidatevalidate(conn) : Bool#

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.