- Suitable for distributed deployments
- Simple to understand, and implement
- Easy secret rotation
- No datastore required
This is a, kind of, reverse HMAC header. Using a random token as the 'data'.
The server side generates a random token, which is sent as a hidden form field. The server uses its secret to create a SHA256 hash of the random token, as a second hidden form field. When the form is submitted, the server re-computes the hash of the submitted token and compares.
The server secrets can be rotated, regularly, without breaking in-flight form requests. Server secrets are picked up from the env var CSRF_SALTS, a list in csv format. Inject these into the Docker container, or the local env.
$ echo $CSRF_SALTS
veClIlI6DN5qBeFifxodUt08PEAFXDvb,YuN8WJPWyW8MBG9csENe0SCvMYmKIfzH,0PS0eLn9TqF093fR3pQ4gow9giMLperw
This makes it simple to sync any number of secret rotations across many containers/instances of your web app. Deploy a cron job to all containers ahead of time, which updates the env var.
We always generate new tokens using the left-most secret. And we walk left to right when validating incoming hashes. Logging which secret (index) was used, means you can learn how long you need to leave right-most secrets active.
$ go get github.com/idthings/csrf
$ export CSRF_SALTS='myreallylongsaltthatshouldberandom'
Once imported, generating a token:
csrfToken, csrfHash, err := csrf.Generate()
if err != nil {
// do something
}
var templateData = struct{
CSRFToken string
CSRFHash string
}{
CSRFToken: csrfToken,
CSRFHash: csrfHash,
}
And using the token/hash pair in a Go html template:
<input type="hidden" name="_token" value="{{ .CSRFToken }}" />
<input type="hidden" name="_hash" value="{{ .CSRFHash }}" />
Here's a snippet example of usage in a Go web handler, when validating form input requests. Capturing and logging the keyUsed to validate, means you can keep track of when it's safe to retire old keys.
func FormRequestHandler(w http.ResponseWriter, r *http.Request) {
token := r.FormValue("_token")
hash := r.FormValue("_hash")
valid, keyUsed := csrf.Validate(token, hash)
if valid && keyUsed > 0 {
log.Info("CSRF: valid use of CRSF_SALT %d", keyUsed)
}
log.Info("CSRF: token %s, hash %s", token, hash)
log.Info("CSRF: validated %t", valid)
}
Output:
2021/05/01 15:02:56 CSRF: valid use of CSRF_SALT 3
2021/05/01 15:02:56 CSRF: token veClIlI6DN5qBeFifxodUt08PEAFXDvb, hash UhAD9N3wO/lVFojcM23WJCeiyAnkXTTe51n8seVoZac=
2021/05/01 15:02:56 CSRF: validated true