gorilla/csrf

Background GET before POST request?

britishben opened this issue · 4 comments

Hi, I'm hoping you could help me with a problem I've run into when trying to use this library. I've been tasked with retrofitting CSRF protection into a static SPA application, with a JSON API, as in #40, but since it needs to be backwards-compatible I was hoping to not need any changes to my users' scripts.

The /login endpoint is a simple web form, but the /apilogin endpoint takes form values from a POST request, without requiring a GET first. Once logged in, users issue JSON requests to the /apidata endpoint, using their session cookie (from gorilla/sessions). Turning off CSRF protection for /apidata would obviously defeat the purpose, so I'd need to have the header/cookie at that point, regardless of which login method was used.

So, my plan was, if I get a POST request to the /apilogin endpoint, to make an additional GET (or possibly HEAD) request first to get the CSRF cookie, and then use that for the rest of the login process. I can see the GET being generated, and the cookie copied across, but I'm still getting a 403 on a POST to that endpoint.

I've made up a quick example to show what I mean (obviously, the authentication is a little bit stronger in the real program):

import (
        "github.com/gorilla/csrf"
        "net/http"
)

var mux = http.NewServeMux()
var CSRF = csrf.Protect(
        []byte("thirtytwo-bytes-of-fake-csrf-key"),
)

func loginHandle(h http.Handler) http.Handler {
    fn := func(w http.ResponseWriter, r *http.Request) {
        if r.RequestURI == "/apilogin" && r.Method == http.MethodPost {
            resp, _ := http.Get("http://127.0.0.1:8080/apilogin")
            for _, cookie := range resp.Cookies(){
                r.AddCookie(cookie)
            }
        }
        h.ServeHTTP(w, r)
    }
    return http.HandlerFunc(fn)
}

func main() {
        mux.HandleFunc("/", DoNothing)
        mux.HandleFunc("/apidata", DoNothing)
        mux.HandleFunc("/login", DoNothing)
        mux.HandleFunc("/apilogin", APILogin)

        http.ListenAndServe(":8080",
                loginHandle(CSRF(mux)))
}

func DoNothing(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Done"))
}

func APILogin(w http.ResponseWriter, r *http.Request) {
        if r.Method == http.MethodGet {
                w.Write([]byte("OK"))
                return
        }
        u := r.FormValue("username")
        p := r.FormValue("password")
        if u == "foo" && p == "bar" {
                w.Write([]byte("Success!"))
                return
        }
        http.Error(w, "Failure!", http.StatusUnauthorized)
}

Sorry if I've just not understood this enough - I'm still rather new to the language, but can you see a reason why that would fail? I did read your response in #40, where you recommended something similar, but I can't seem to get this working. If you're able to point me in the right direction, I'd appreciate it.

Hi Matt, did you manage to reproduce this?
As I said, it's entirely possible I've done something wrong here, but conceptually it makes sense to me.
Let me know if you've worked anything out.
-Benjamin

Closing due to inactivity. Re-open if any questions!