diff --git a/app/rest/auth/auth.go b/app/rest/auth/auth.go index 8251e616..558849b5 100644 --- a/app/rest/auth/auth.go +++ b/app/rest/auth/auth.go @@ -3,6 +3,7 @@ package auth import ( "encoding/base64" + "log" "net/http" "strings" @@ -59,6 +60,30 @@ func (a *Authenticator) Auth(reqAuth bool) func(http.Handler) http.Handler { return } + xsrfStatus := func() bool { + xsrfToken := r.Header.Get("X-XSRF-TOKEN") + sessionToken, headerOk := session.Values["xsrf_token"] + if (xsrfToken == "" || sessionToken == nil || !headerOk) && reqAuth { + log.Print("[WARN] no xsrf_token in session") + return false + } + + if xsrfToken != sessionToken { + log.Printf("[WARN] xsrf header not matched session token, %q != %q", xsrfToken, sessionToken) + return false + } + return true + }() + + if xsrfStatus { + if reqAuth { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + h.ServeHTTP(w, r) // in anonymous mode just pass it to next handler + return + } + if ok { // if uinfo in session, populate to context user := uinfoData.(store.User) for _, admin := range a.Admins { diff --git a/app/rest/auth/provider.go b/app/rest/auth/provider.go index 5d683ec2..f091944a 100644 --- a/app/rest/auth/provider.go +++ b/app/rest/auth/provider.go @@ -10,6 +10,7 @@ import ( "io/ioutil" "log" "net/http" + "time" "github.com/go-chi/chi" "github.com/go-chi/render" @@ -173,10 +174,19 @@ func (p Provider) authHandler(w http.ResponseWriter, r *http.Request) { } session.Values["uinfo"] = u + xsrfToken := p.randToken() + session.Values["xsrf_token"] = xsrfToken + + xsrfCookie := http.Cookie{Name: "XSRF-TOKEN", Value: p.randToken(), HttpOnly: false, Path: "/", + MaxAge: 3600 * 24 * 365, Secure: true, + } + http.SetCookie(w, &xsrfCookie) + if err = session.Save(r, w); err != nil { rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to save user info") return } + p.sendXsrfCookie(w) log.Printf("[DEBUG] user info %+v", session.Values["uinfo"]) @@ -185,7 +195,6 @@ func (p Provider) authHandler(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, fromURL.(string), http.StatusTemporaryRedirect) return } - render.JSON(w, r, jData) } @@ -201,6 +210,10 @@ func (p Provider) LogoutHandler(w http.ResponseWriter, r *http.Request) { delete(session.Values, "uinfo") delete(session.Values, "from") delete(session.Values, "state") + delete(session.Values, "xsrf_token") + xsrfCookie := http.Cookie{Name: "XSRF-TOKEN", Value: "", HttpOnly: false, Path: "/", + MaxAge: -1, Expires: time.Unix(0, 0), Secure: true} + http.SetCookie(w, &xsrfCookie) if err = session.Save(r, w); err != nil { rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to reset user info") @@ -209,6 +222,18 @@ func (p Provider) LogoutHandler(w http.ResponseWriter, r *http.Request) { log.Printf("[DEBUG] logout, %+v", session.Values) } +func (p Provider) sendXsrfCookie(w http.ResponseWriter) { + xsrfCookie := http.Cookie{ + Name: "XSRF-TOKEN", + Value: p.randToken(), + HttpOnly: false, + Path: "/", + MaxAge: 3600 * 24 * 365, + Secure: true, + } + http.SetCookie(w, &xsrfCookie) +} + func (p Provider) randToken() string { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { diff --git a/app/rest/auth/provider_test.go b/app/rest/auth/provider_test.go index b5c522b7..c3168b9b 100644 --- a/app/rest/auth/provider_test.go +++ b/app/rest/auth/provider_test.go @@ -32,6 +32,7 @@ func TestLogin(t *testing.T) { body, err := ioutil.ReadAll(resp.Body) assert.Nil(t, err) t.Logf("resp %s", string(body)) + t.Logf("headers: %+v", resp.Header) u := store.User{} err = json.Unmarshal(body, &u) assert.Nil(t, err) @@ -58,6 +59,7 @@ func TestLogout(t *testing.T) { s, err := sessionStore.Get(nil, "remark") assert.Nil(t, err) + t.Log(s.Values) assert.Equal(t, 0, len(s.Values)) }