diff --git a/README.md b/README.md index 85a5344..2a82d41 100644 --- a/README.md +++ b/README.md @@ -36,12 +36,12 @@ Features may be added here at any time as things are in early stages right now: ### etc -* [ ] middleware for easing auth flow: +* [x] middleware for easing auth flow: - [x] `Protected`: require login - [x] `Authorize`: login and redirect - - [ ] `Bunt`: logout and redirect - - [ ] `Fortify`: setup CSRF protection (use on the form) - - [ ] `Defend`: enact CSRF protection (use on the endpoint) + - [x] `Bunt`: logout and redirect + - [x] `Fortify`: setup CSRF protection (use on the form) + - [x] `Defend`: enact CSRF protection (use on the endpoint) * [ ] generic DAL wrapper? might be unneccessary ## license diff --git a/auth/auth.go b/auth/auth.go index ccb5573..c7a21e1 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -23,6 +23,8 @@ type UserStore interface { AddUser(user string, password string) error DeleteUser(user string) error ChangePassword(user string, oldPassword string, newPassword string) error + GetLastLoginTime(user string) (time.Time, error) + GetLastTimeSeen(user string) (time.Time, error) SetData(user string, key string, value interface{}) error GetData(user string, key string) (interface{}, error) } @@ -32,6 +34,9 @@ func Login(user string, password string, userStore UserStore, w http.ResponseWri if loginErr == nil { cookie.StoreToken("user", user, w, t) cookie.StoreToken("session", session, w, t) + csrfToken := cookie.GenToken(64) + cookie.StoreToken("csrfToken", csrfToken, w, t) + userStore.SetData(user, "csrfToken", csrfToken) return nil } return loginErr @@ -42,6 +47,8 @@ func Logout(user string, userStore UserStore, w http.ResponseWriter) error { if logoutErr == nil { cookie.StoreToken("user", "", w, 0) cookie.StoreToken("session", "", w, 0) + cookie.StoreToken("csrfToken", "", w, 0) + userStore.SetData(user, "csrfToken", "") return nil } return logoutErr diff --git a/indentalUserDB/indentalUserDB.go b/indentalUserDB/indentalUserDB.go index f79d314..971d113 100644 --- a/indentalUserDB/indentalUserDB.go +++ b/indentalUserDB/indentalUserDB.go @@ -117,6 +117,20 @@ func (self *IndentalUserDB) AddUser(user string, password string) error { return nil } +func (self *IndentalUserDB) GetLastLoginTime(user string) (time.Time, error) { + if usr, exists := self.Users[user]; exists { + return usr.LoginTime, nil + } + return time.UnixMicro(0), errors.New("User not in DB") +} + +func (self *IndentalUserDB) GetLastTimeSeen(user string) (time.Time, error) { + if usr, exists := self.Users[user]; exists { + return usr.LastSeen, nil + } + return time.UnixMicro(0), errors.New("User not in DB") +} + func (self *IndentalUserDB) SetData(user string, key string, value interface{}) error { if _, exists := self.Users[user]; !exists { return errors.New("User not in DB") diff --git a/middleware/middleware.go b/middleware/middleware.go index cbd1998..d83f4b1 100644 --- a/middleware/middleware.go +++ b/middleware/middleware.go @@ -8,7 +8,7 @@ import ( "nilfm.cc/git/quartzgun/cookie" ) -func Protected(next http.Handler, method string, userStore auth.UserStore) http.Handler { +func Protected(next http.Handler, method string, userStore auth.UserStore, login string) http.Handler { handlerFunc := func(w http.ResponseWriter, req *http.Request) { user, err := cookie.GetToken("user", req) if err == nil { @@ -26,13 +26,34 @@ func Protected(next http.Handler, method string, userStore auth.UserStore) http. } fmt.Printf("unauthorized...\n") req.Method = http.MethodGet - http.Redirect(w, req, "/login", http.StatusSeeOther) + http.Redirect(w, req, login, http.StatusSeeOther) } return http.HandlerFunc(handlerFunc) } -func Authorize(next string, userStore auth.UserStore) http.Handler { +func Bunt(next string, userStore auth.UserStore, denied string) http.Handler { + handlerFunc := func(w http.ResponseWriter, req *http.Request) { + user, err := cookie.GetToken("user", req) + if err == nil { + err := auth.Logout( + user, + userStore, + w) + if err == nil { + req.Method = http.MethodGet + http.Redirect(w, req, next, http.StatusSeeOther) + return + } + } + req.Method = http.MethodGet + http.Redirect(w, req, denied, http.StatusUnauthorized) + } + + return http.HandlerFunc(handlerFunc) +} + +func Authorize(next string, userStore auth.UserStore, denied string) http.Handler { handlerFunc := func(w http.ResponseWriter, req *http.Request) { err := auth.Login( req.FormValue("user"), @@ -45,16 +66,49 @@ func Authorize(next string, userStore auth.UserStore) http.Handler { fmt.Printf("logged in as %s\n", req.FormValue("user")) http.Redirect(w, req, next, http.StatusSeeOther) } else { - *req = *req.WithContext( - context.WithValue( - req.Context(), - "message", - "Incorrect credentials")) fmt.Printf("login failed!\n") req.Method = http.MethodGet - http.Redirect(w, req, "/login", http.StatusSeeOther) + http.Redirect(w, req, denied, http.StatusSeeOther) } } return http.HandlerFunc(handlerFunc) } + +func Fortify(next http.Handler) http.Handler { + handlerFunc := func(w http.ResponseWriter, req *http.Request) { + token, err := cookie.GetToken("csrfToken", req) + if err == nil { + *req = *req.WithContext( + context.WithValue( + req.Context(), + "csrfToken", + token)) + } + next.ServeHTTP(w, req) + } + + return http.HandlerFunc(handlerFunc) +} + +func Defend(next http.Handler, userStore auth.UserStore, denied string) http.Handler { + handlerFunc := func(w http.ResponseWriter, req *http.Request) { + user, err := cookie.GetToken("user", req) + if err == nil { + masterToken, err := userStore.GetData(user, "csrfToken") + if err == nil { + cookieToken, err := cookie.GetToken("csrfToken", req) + if err == nil { + formToken := req.FormValue("csrfToken") + if formToken == cookieToken && formToken == masterToken.(string) { + next.ServeHTTP(w, req) + return + } + } + } + } + http.Redirect(w, req, denied, http.StatusUnauthorized) + } + + return http.HandlerFunc(handlerFunc) +} diff --git a/quartzgun_test.go b/quartzgun_test.go index 3c9d099..cc99b83 100644 --- a/quartzgun_test.go +++ b/quartzgun_test.go @@ -47,11 +47,11 @@ func TestMain(m *testing.M) { rtr.Get("/login", renderer.Template( "testData/templates/login.html")) - rtr.Post("/login", middleware.Authorize("/", udb)) + rtr.Post("/login", middleware.Authorize("/", udb, "/login?tryagain=1")) rtr.Get("/", middleware.Protected( renderer.Template( - "testData/templates/test.html"), http.MethodGet, udb)) + "testData/templates/test.html"), http.MethodGet, udb, "/login")) rtr.Get("/json", ApiSomething(renderer.JSON("apiData"))) diff --git a/renderer/renderer.go b/renderer/renderer.go index 00288a8..3d912eb 100644 --- a/renderer/renderer.go +++ b/renderer/renderer.go @@ -3,6 +3,7 @@ package renderer import ( "encoding/json" "encoding/xml" + "fmt" "html/template" "net/http" ) @@ -11,7 +12,10 @@ func Template(t ...string) http.Handler { tmpl := template.Must(template.ParseFiles(t...)) handlerFunc := func(w http.ResponseWriter, req *http.Request) { - tmpl.Execute(w, req) + err := tmpl.Execute(w, req) + if err != nil { + fmt.Printf(err.Error()) + } } return http.HandlerFunc(handlerFunc) diff --git a/testData/templates/login.html b/testData/templates/login.html index f3f740e..d9e28d2 100644 --- a/testData/templates/login.html +++ b/testData/templates/login.html @@ -1,4 +1,4 @@ -{{ $errorMsg := (.Context).Value "message" }} +{{ $tryagain := .FormValue "tryagain" }} @@ -9,8 +9,8 @@ Nirvash — Login - {{ if $errorMsg }} -
{{ $errorMsg }}
+ {{ if $tryagain }} +
Incorrect credentials; please try again.
{{ end }}