CSRF protection using HMAC-signed state tokens
README.md

csrf #

Signed OAuth state parameters, against cross-site request forgery.

An OAuth client sends the user to the provider with a state parameter and checks, when the provider redirects back, that the same value returns. If the value is not bound to the client, an attacker can start a login flow of their own and send the victim the callback, logging the victim into the attacker's account. csrf binds the state to a server secret: Csrf.sign_state appends a dot and the hex HMAC-SHA256 tag of the state under a key derived from the secret with HKDF (RFC 5869), and Csrf.verify_state checks the tag in constant time and returns the original state. The tag proves only that this server minted the state; the attacker's own flow gets a validly signed state too. The binding to the victim's browser is the caller's: it also stores the state in a cookie on the browser that started the flow and requires the callback's state to equal that cookie, as auth does with its oauth_state cookie (RFC 6749, section 10.12). It refuses any signed state longer than 256 bytes before hashing it. The tag has no expiry and no nonce, so a caller that needs either puts it inside the state.

Install #

$ opam install csrf

If opam cannot find the package, add the overlay repository first:

$ opam repo add samoht https://tangled.org/gazagnaire.org/opam-overlay.git
$ opam update
$ opam install csrf

Usage #

Sign the state before the redirect, and verify it when the browser returns:

let signed = Csrf.sign_state ~secret:"server secret" "oauth-login"

let () =
  match Csrf.verify_state ~secret:"server secret" signed with
  | Some payload -> assert (payload = "oauth-login")
  | None -> failwith "tampered state"

License #

ISC. See LICENSE.md.