Hi There,
I have encountered an issue that whenever I tried to change the "32-byte-long-auth-key" key. It always shows "Forbidden - CSRF token invalid". It was working fine until the "32-byte-long-auth-key" key is changed.
Basically, I have two buttons. One button is for getting the CSRF token. The other button is for submitting data through AJAX. The CSRF token is embedded in the header when submitted the data through AJAX. The issue can be reproduced by the following steps:
- Click on "get token" button.
- Click on "submit" button.
- Repeat Step 1 and Step 2 several times. It's all working fine.
- modify "test_form.go" and change "32-byte-long-auth-key" to something else say "38-byte-long-auth-key".
- stop and start the Go web server by running: go run github.com/hello/test_form.go
- At this point, when I tried to repeat Step 1 and Step 2. It keeps showing "Forbidden - CSRF token invalid".
Note: I've turned on my Chrome developer tools to observer the information in network and console tabs.
I have tried to reset the _gorilla_csrf cookie (value, path, expired date, secure, httponly) on both client and server side without any lucks. The only way to make it working again is to clear the cookies manually in Chrome's setting.
I have observed one thing. When it's getting the 403 forbidden CSRF token error message, the cookie contains multiple _gorilla_csrf cookie names in the requested header. For example:
Cookie:_gorilla_csrf=MTQ1OTIxOTcxM3xJbEZDZVU1dlduaFBhek5TVDNkTU1FNXZka1kwYkZGNVdEbEtTM3AyVWtzeWJrNVRRMFZST1ZJd1FtTTlJZ289fCA3jchCObZSAf3CiVXbDTht4lYPAWGd_BIKKq53BHJK; _gorilla_csrf=MTQ1OTIxOTcxNnxJa0oyWm5wQ1NEUXdhMVphTUhSblVGWmtkMnhVZFVwUlJXdGpaV1ZxY1V4VVkyMU1PRFJFZEVkb1JFRTlJZ289fNWkquJkXj0qW4_VvLwMmBlfBGu7uSTlH5F-w_NDPKYL
Note: I have reproduced this issue several times and it has the same issue in both HTTP and HTTPS. And also take care of csrf.Secure(false) when switch between HTTP/HTTPS.
I would like to know the proper way to deal with this issue. Because I do not want to bother the client to clear the cookie manually each time the site isn't working.
server side script - github.com/hello/test_form.go:
package main
import (
"fmt"
"github.com/gorilla/csrf"
"github.com/gorilla/mux"
"net/http"
"encoding/json"
"strconv"
)
const (
HTTP_PORT = ":80"
CSRF_AUTH_KEY = "31-byte-long-auth-key"
STATIC_DIR = "/static/"
)
type RetJson struct {
Status bool
}
type User struct {
Uid int
Name string
Email string
}
func RootHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte("<script src='/static/scripts/jquery-2.2.2.min.js'></script>" +
"<script src='/static/scripts/test_form.js'></script>"))
}
func SubmitSignupForm(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
// We can trust that requests making it this far have satisfied
// our CSRF protection requirements.
retjson := RetJson {}
retjson.Status = true
b, err := json.Marshal(retjson)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
w.Write(b)
}
func GetUser(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
uid := mux.Vars(r)["uid"] // variable name is case sensitive
uid2, err := strconv.Atoi(uid)
fmt.Printf("main(): %s\n", err)
// Authenticate the request, get the id from the route params,
// and fetch the user from the DB, etc.
user := User {Uid: uid2, Name: "Test Name", Email: "[email protected]"}
// Get the token and pass it in the CSRF header. Our JSON-speaking client
// or JavaScript framework can now read the header and return the token in
// in its own "X-CSRF-Token" request header on the subsequent POST.
w.Header().Set("X-CSRF-Token", csrf.Token(r))
b, err := json.Marshal(user)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
w.Write(b)
}
func main() {
r := mux.NewRouter()
r.PathPrefix(STATIC_DIR).Handler(http.StripPrefix(STATIC_DIR, http.FileServer(http.Dir("." + STATIC_DIR))))
r.HandleFunc("/", RootHandler)
api := r.PathPrefix("/api").Subrouter()
api.HandleFunc("/user/{uid}", GetUser).Methods("GET")
// All POST requests without a valid token will return HTTP 403 Forbidden.
api.HandleFunc("/signup/post", SubmitSignupForm).Methods("POST")
err := http.ListenAndServe(HTTP_PORT, csrf.Protect([]byte(CSRF_AUTH_KEY), csrf.Secure(false))(r))
if err != nil {
fmt.Printf("main(): %s\n", err)
}
}
client side script - static/scripts/test_form.js:
$( document ).ready(function() {
$('body').append('user id: <input id="user_id" value="10"/>');
$('body').append('<br><br>Token: <input id="token" size="160" />');
$('body').append('<br><br>ajax-return-data: <textarea id="ajax-return-data" cols="160" rows="5"></textarea>');
$('body').append('<br><br><button id="getToken">get token</button>');
$('body').append(' <button id="submitBtn">submit</button>');
$('#getToken').on('click', function(event){
var _user_id = $('#user_id').val();
$.ajax({
url: "/api/user/" + _user_id,
}).done(function(data, status, xhr) {
var _token = xhr.getResponseHeader('X-Csrf-Token');
$('#token').val(_token);
$('#ajax-return-data').val(JSON.stringify(data));
}).fail(function(data, status, xhr) {
console.log(data);
console.log(status);
});
});
$('#submitBtn').on('click', function(event){
var _token = $('#token').val();
$.ajax({
url: "/api/signup/post",
method: 'post',
headers: {'X-CSRF-Token': _token},
data: {name: 'test', pass: 'test'},
dataType: 'json',
}).done(function(data, status, xhr) {
console.log(data);
//console.log(xhr.getAllResponseHeaders());
}).fail(function(data, status, xhr) {
console.log(data);
console.log(status);
});
});
});