diff --git a/auth/auth.go b/auth/auth.go new file mode 100644 index 0000000..9c762a6 --- /dev/null +++ b/auth/auth.go @@ -0,0 +1,98 @@ +package auth + +import ( + "fmt" + "net/http" + + "github.com/gorilla/sessions" + "golang.org/x/crypto/bcrypt" +) + +var ( + // ErrInvalidPassword returned when password and hash don't match + ErrInvalidPassword = fmt.Errorf("Invalid password") + // ErrCorrupted returned when session have been corrupted + ErrCorrupted = fmt.Errorf("Corrupted session") +) + +// Authorizer handle sesssion +type Authorizer struct { + cookiejar *sessions.CookieStore + cookieName string + peeper string + cost int +} + +// New Authorizer peeper is like a salt but not stored in database, +// cost is the bcrypt cost for hashing the password +func New(peeper, cookieName, cookieKey string, cost int) *Authorizer { + return &Authorizer{ + cookiejar: sessions.NewCookieStore([]byte(cookieKey)), + cookieName: cookieName, + peeper: peeper, + cost: cost, + } +} + +// GenHash generates a new hash from a password +func (a *Authorizer) GenHash(password string) (string, error) { + b, err := bcrypt.GenerateFromPassword([]byte(password+a.peeper), a.cost) + if err != nil { + return "", err + } + return string(b), nil +} + +// Login cheks password and updates cookie info +func (a *Authorizer) Login(rw http.ResponseWriter, req *http.Request, username, hash, password string) error { + cookie, err := a.cookiejar.Get(req, a.cookieName) + if err != nil { + return err + } + + err = bcrypt.CompareHashAndPassword([]byte(hash), []byte(password+a.peeper)) + if err != nil { + return ErrInvalidPassword + } + + cookie.Values["username"] = username + err = cookie.Save(req, rw) + if err != nil { + return err + } + return nil +} + +// Logout remove cookie info +func (a *Authorizer) Logout(rw http.ResponseWriter, req *http.Request) error { + cookie, err := a.cookiejar.Get(req, a.cookieName) + if err != nil { + return err + } + cookie.Values["username"] = "" + cookie.Options.MaxAge = -1 // kill the cookie + err = cookie.Save(req, rw) + if err != nil { + return err + } + return nil +} + +// CurrentUser returns the logged in username from session +func (a *Authorizer) CurrentUser(rw http.ResponseWriter, req *http.Request) (string, error) { + cookie, err := a.cookiejar.Get(req, a.cookieName) + if err != nil { + return "", err + } + + username := cookie.Values["username"] + + if !cookie.IsNew && username != nil { + str, ok := username.(string) + if !ok { + return "", ErrCorrupted + } + return str, nil + } + return "", nil +} diff --git a/auth/auth_test.go b/auth/auth_test.go new file mode 100644 index 0000000..8f95d39 --- /dev/null +++ b/auth/auth_test.go @@ -0,0 +1,161 @@ +package auth + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/http/cookiejar" + "net/http/httptest" + "testing" + + "github.com/gorilla/mux" + "github.com/kr/pretty" +) + +const ( + peeper = "polp" + key = "plop" + cookie = "auth" + cost = 10 + + username = "plop" + password = "ploppwd" + hash = "$2a$10$eVye8xbs6nj4TWnlTmifRuBsAU3F2hkxEcFz9WXdYjUuE6uKLVuzK" +) + +func login(w http.ResponseWriter, r *http.Request) { + a := New(peeper, cookie, key, cost) + err := a.Login(w, r, username, hash, password) + if err != nil { + fmt.Fprintf(w, "%s", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) + +} +func logout(w http.ResponseWriter, r *http.Request) { + a := New(peeper, cookie, key, cost) + err := a.Logout(w, r) + if err != nil { + fmt.Fprintf(w, "%s", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) +} +func check(w http.ResponseWriter, r *http.Request) { + a := New(peeper, cookie, key, cost) + u, err := a.CurrentUser(w, r) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "%s", err) + return + } + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "%s", u) + +} + +func handlers() *mux.Router { + r := mux.NewRouter() + r.HandleFunc("/login", login).Methods("GET") + r.HandleFunc("/logout", logout).Methods("GET") + r.HandleFunc("/check", check).Methods("GET") + return r +} + +func TestAuth(t *testing.T) { + ts := httptest.NewServer(handlers()) + defer ts.Close() + + cookieJar, _ := cookiejar.New(nil) + client := &http.Client{ + Jar: cookieJar, + } + + // Check no user logged in = + res, err := client.Get(ts.URL + "/check") + if err != nil { + t.Fatal(err) + } + + body, err := ioutil.ReadAll(res.Body) + res.Body.Close() + if err != nil { + t.Fatal(err) + } + if res.StatusCode != http.StatusOK { + t.Fatal(body) + } + if string(body) != "" { + t.Fatalf("No user logged in expected but found: %s", body) + } + + // Login + res, err = client.Get(ts.URL + "/login") + if err != nil { + t.Fatal(err) + } + + body, err = ioutil.ReadAll(res.Body) + res.Body.Close() + if err != nil { + t.Fatal(err) + } + if res.StatusCode != http.StatusOK { + t.Fatal(string(body)) + } + + // Checks we are logged in + res, err = client.Get(ts.URL + "/check") + if err != nil { + t.Fatal(err) + } + + body, err = ioutil.ReadAll(res.Body) + res.Body.Close() + if err != nil { + t.Fatal(err) + } + if res.StatusCode != http.StatusOK { + pretty.Println(res.StatusCode) + t.Fatal(body) + } + if string(body) != username { + t.Fatalf("We expect be logged in as %s but we got: %s", username, body) + } + + // Logout + res, err = client.Get(ts.URL + "/logout") + if err != nil { + t.Fatal(err) + } + + body, err = ioutil.ReadAll(res.Body) + res.Body.Close() + if err != nil { + t.Fatal(err) + } + if res.StatusCode != http.StatusOK { + t.Fatal(string(body)) + } + + // Check no username logged in anymore + res, err = client.Get(ts.URL + "/check") + if err != nil { + t.Fatal(err) + } + + body, err = ioutil.ReadAll(res.Body) + res.Body.Close() + if err != nil { + t.Fatal(err) + } + if res.StatusCode != http.StatusOK { + t.Fatal(body) + } + if string(body) != "" { + t.Fatalf("No user logged in expected but found: %s", body) + } +}