diff --git a/auth/auth.go b/auth/auth.go index 0233d79..2760665 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -6,24 +6,34 @@ import ( "fmt" "os" "strings" + "time" + "github.com/gorilla/securecookie" "golang.org/x/crypto/bcrypt" ) type AuthStore interface { Register(user, pass string) error - Authenticate(user, pass string) error + Login(user, pass string) (token string, err error) + Verify(token string) (session Session, err error) Remove(user string) error } +type Session struct { + User string + Expiry time.Time +} + type Htpasswd struct { accounts map[string]string filePath string + cookie *securecookie.SecureCookie } -func NewHtpasswd(path string) (AuthStore, error) { +func New(path string, hashKey []byte) (AuthStore, error) { s := Htpasswd{ filePath: path, + cookie: securecookie.New(hashKey, nil), } err := s.read() return s, err @@ -40,12 +50,29 @@ func (s Htpasswd) Register(user, pass string) (err error) { return s.write() } -func (s Htpasswd) Authenticate(user, pass string) (err error) { +func (s Htpasswd) Login(user, pass string) (token string, err error) { hashed, ok := s.accounts[user] if !ok { - return errors.New("user not found") + return "", errors.New("user not found") } - return bcrypt.CompareHashAndPassword([]byte(hashed), []byte(pass)) + err = bcrypt.CompareHashAndPassword([]byte(hashed), []byte(pass)) + if err != nil { + return "", errors.New("wrong password") + } + session := Session{ + User: user, + Expiry: time.Now().AddDate(0, 0, 7), + } + token, err = s.cookie.Encode("session", session) + if err != nil { + panic(err) + } + return +} + +func (s Htpasswd) Verify(token string) (session Session, err error) { + err = s.cookie.Decode("session", token, &session) + return } func (s Htpasswd) Remove(user string) (err error) { diff --git a/auth/auth_test.go b/auth/auth_test.go index fd19fe8..afc8f4d 100644 --- a/auth/auth_test.go +++ b/auth/auth_test.go @@ -5,6 +5,8 @@ import ( "io/ioutil" "strings" "testing" + + "github.com/gorilla/securecookie" ) type User struct { @@ -14,6 +16,7 @@ type User struct { } func TestHtpasswdSuccess(t *testing.T) { + hashKey := securecookie.GenerateRandomKey(32) path := "/tmp/.htpasswd" user1 := User{ user: "user", @@ -25,15 +28,23 @@ func TestHtpasswdSuccess(t *testing.T) { if err != nil { t.Fatal(err) } - store, err := NewHtpasswd(path) + store, err := New(path, hashKey) if err != nil { t.Fatal(err) } - err = store.Authenticate(user1.user, user1.pass) + token, err := store.Login(user1.user, user1.pass) if err != nil { t.Error(err) } + session, err := store.Verify(token) + if err != nil { + t.Error(err) + } + if session.User != user1.user { + t.Fatalf("expected %s, got %s", user1.user, session.User) + } + user2 := User{ user: "foo", pass: "bar", diff --git a/go.mod b/go.mod index 73246c7..2f15bfb 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-playground/validator/v10 v10.10.0 // indirect github.com/goccy/go-json v0.9.7 // indirect + github.com/gorilla/securecookie v1.1.1 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/leodido/go-urn v1.2.1 // indirect github.com/mattn/go-isatty v0.0.14 // indirect diff --git a/go.sum b/go.sum index 195834a..9b82db5 100644 --- a/go.sum +++ b/go.sum @@ -19,6 +19,8 @@ github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGF github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= diff --git a/main.go b/main.go index 547c351..6f2745f 100644 --- a/main.go +++ b/main.go @@ -1,12 +1,13 @@ package main import ( - "errors" "flag" "log" + "net/http" "text/template" "github.com/gin-gonic/gin" + "github.com/gorilla/securecookie" "github.com/lancatlin/ledger-quicknote/auth" ) @@ -33,9 +34,16 @@ func init() { flag.StringVar(&LEDGER_INIT, "i", "", "ledger initiation file") flag.StringVar(&WORKING_DIR, "w", "", "ledger working directory") flag.StringVar(&HOST, "b", "127.0.0.1:8000", "binding address") + var hashKey string + flag.StringVar(&hashKey, "s", "", "session secret") flag.Parse() + + if hashKey == "" { + hashKey = string(securecookie.GenerateRandomKey(32)) + log.Printf("Generate random session key: %s", hashKey) + } var err error - store, err = auth.NewHtpasswd(HTPASSWD_FILE) + store, err = auth.New(HTPASSWD_FILE, []byte(hashKey)) if err != nil { panic(err) } @@ -49,20 +57,14 @@ func main() { c.HTML(200, "signup.html", nil) }) - r.POST("/signup", func(c *gin.Context) { - var user UserLogin - if err := c.ShouldBind(&user); err != nil { - c.HTML(400, "signup.html", err) - return - } - if err := store.Register(user.Email, user.Password); err != nil { - c.HTML(400, "signup.html", err) - return - } - c.Request.SetBasicAuth(user.Email, user.Password) - c.Redirect(303, "/dashboard") + r.GET("/signin", func(c *gin.Context) { + c.HTML(200, "signin.html", nil) }) + r.POST("/signup", signup) + + r.POST("/signin", signin) + authZone := r.Group("", basicAuth) authZone.GET("/dashboard", func(c *gin.Context) { @@ -117,18 +119,18 @@ func main() { } func basicAuth(c *gin.Context) { - var user UserLogin - var ok bool - user.Email, user.Password, ok = c.Request.BasicAuth() - if !ok { - c.Header("WWW-Authenticate", "basic realm=\"Login to continue\"") - c.AbortWithError(401, errors.New("login required")) + cookie, err := c.Cookie("session") + if err == http.ErrNoCookie { + c.Redirect(303, "/signin") return } - if err := store.Authenticate(user.Email, user.Password); err != nil { - c.AbortWithError(401, err) + session, err := store.Verify(cookie) + if err != nil { + c.Redirect(303, "/signin") return } - c.Set("user", user) + c.Set("user", UserLogin{ + Email: session.User, + }) c.Next() } diff --git a/session.go b/session.go new file mode 100644 index 0000000..86b36e7 --- /dev/null +++ b/session.go @@ -0,0 +1,31 @@ +package main + +import "github.com/gin-gonic/gin" + +func signup(c *gin.Context) { + var user UserLogin + if err := c.ShouldBind(&user); err != nil { + c.HTML(400, "signup.html", err) + return + } + if err := store.Register(user.Email, user.Password); err != nil { + c.HTML(400, "signup.html", err) + return + } + signin(c) +} + +func signin(c *gin.Context) { + var user UserLogin + if err := c.ShouldBind(&user); err != nil { + c.HTML(400, "signin.html", err) + return + } + token, err := store.Login(user.Email, user.Password) + if err != nil { + c.HTML(401, "signin.html", err) + return + } + c.SetCookie("session", token, 60*60*24*7, "", "", false, false) + c.Redirect(303, "/dashboard") +} diff --git a/templates/signin.html b/templates/signin.html new file mode 100644 index 0000000..e9ff082 --- /dev/null +++ b/templates/signin.html @@ -0,0 +1,10 @@ +{{ define "title" }}Sign Up{{ end }} +{{ define "main" }} +

Sign In

+
+
+
+ {{ with .Error }}

{{ . }}

{{ end }} + +
+{{ end }}