diff --git a/cmd/login.go b/cmd/login.go new file mode 100644 index 0000000..d288c4f --- /dev/null +++ b/cmd/login.go @@ -0,0 +1,21 @@ +package main + +import ( + "net/http" + + "github.com/labstack/echo/v4" + logins "gitlab.g3e.fr/h6n/users/internal" +) + +func login(c echo.Context) error { + var request logins.RequestLogin + if err := c.Bind(&request); err != nil { + return c.JSON(http.StatusBadRequest, err) + } + + if response, err := logins.CreateLogin(db, request); err != nil { + return c.JSON(http.StatusBadRequest, "Not a valide user username or password") + } else { + return c.JSON(http.StatusOK, response) + } +} diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..7c639bc --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,64 @@ +package main + +import ( + "fmt" + "net/http" + + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "gitlab.g3e.fr/h6n/users/lib" + + "database/sql" + + _ "github.com/lib/pq" +) + +var db *sql.DB = nil + +func logout(c echo.Context) error { + return c.NoContent(http.StatusNoContent) +} + +func isLoggedIn(c echo.Context) error { + return c.NoContent(http.StatusNoContent) +} + +func init_database() { + var err error = nil + + connStr := "postgres://acc:totor@postgres:5432/accounts?sslmode=disable" + + db, err = sql.Open("postgres", connStr) + if err != nil { + fmt.Println(err) + } +} + +func skip_auth(c echo.Context) bool { + if c.Request().Method == "POST" && c.Path() == "/" { + return true + } + return false +} + +func main() { + init_database() + if err := lib.InitLoginBiscuit(); err != nil { + return + } + e := echo.New() + + // Middleware + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + e.Use(middleware.CORS()) + e.Use(lib.AuthMiddleware(skip_auth)) + + // Routes + e.GET("/", isLoggedIn) + e.POST("/", login) + e.DELETE("/", logout) + + // Start server + e.Logger.Fatal(e.Start(":1222")) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2eff39b --- /dev/null +++ b/go.mod @@ -0,0 +1,22 @@ +module gitlab.g3e.fr/h6n/users + +go 1.21.0 + +require ( + github.com/alecthomas/participle/v2 v2.0.0 // indirect + github.com/biscuit-auth/biscuit-go/v2 v2.2.0 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/labstack/echo/v4 v4.11.4 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + golang.org/x/crypto v0.17.0 // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/time v0.5.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0b19315 --- /dev/null +++ b/go.sum @@ -0,0 +1,39 @@ +github.com/alecthomas/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g= +github.com/alecthomas/participle/v2 v2.0.0/go.mod h1:rAKZdJldHu8084ojcWevWAL8KmEU+AT+Olodb+WoN2Y= +github.com/biscuit-auth/biscuit-go/v2 v2.2.0 h1:1zBfZ0ZCbxJbhtAhou6Fa07lBlJ+wcphBEPV/sENBvY= +github.com/biscuit-auth/biscuit-go/v2 v2.2.0/go.mod h1:c7AsMdr816vPd/4Psb3z6Xv2EKciKxSEayI8ueBaJio= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +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/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8= +github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= diff --git a/internal/createLogin.go b/internal/createLogin.go new file mode 100644 index 0000000..75efd85 --- /dev/null +++ b/internal/createLogin.go @@ -0,0 +1,70 @@ +package logins + +import ( + "database/sql" + "encoding/base64" + "encoding/hex" + "errors" + "time" + + "github.com/biscuit-auth/biscuit-go/v2" + "github.com/biscuit-auth/biscuit-go/v2/parser" + "gitlab.g3e.fr/h6n/users/lib" + "golang.org/x/crypto/bcrypt" +) + +func CheckPasswordHash(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} + +func CreateLogin(db *sql.DB, request RequestLogin) (ResponseLogin, error) { + type ( + User struct { + Username string + EncryptedPassword string + } + ) + var user User + var response ResponseLogin + + if err := db.QueryRow("SELECT username, password FROM users WHERE email = $1", request.Username).Scan(&user.Username, &user.EncryptedPassword); err != nil { + return response, err + } + if !CheckPasswordHash(request.Password, user.EncryptedPassword) { + return response, errors.ErrUnsupported + } + now := time.Now() + ValidateTime := 14 + expire := now.AddDate(0, 0, ValidateTime) + + user_id := string(user.Username) + + auth := `user("` + user_id + `"); + check if time($time), $time <= ` + expire.Format("2006-01-02T15:04:05Z") + `; + ` + + authority, err := parser.FromStringBlockWithParams(auth, map[string]biscuit.Term{"read": biscuit.String("read"), "write": biscuit.String("write")}) + + if err != nil { + return response, err + } + + builder := biscuit.NewBuilder(lib.PrivateKey) + builder.AddBlock(authority) + + b, err := builder.Build() + if err != nil { + return response, err + } + + token, err := b.Serialize() + if err != nil { + return response, err + } + + response.Token = base64.URLEncoding.EncodeToString(token) + response.PublicKey = hex.EncodeToString(lib.PublicKey) + + return response, nil +} diff --git a/internal/struct.go b/internal/struct.go new file mode 100644 index 0000000..751ee07 --- /dev/null +++ b/internal/struct.go @@ -0,0 +1,13 @@ +package logins + +type ( + RequestLogin struct { + Username string `json:"username"` + Password string `json:"password"` + } + + ResponseLogin struct { + Token string `json:"token"` + PublicKey string `json:"publickey"` + } +) diff --git a/lib/authMiddleware.go b/lib/authMiddleware.go new file mode 100644 index 0000000..fd8e92b --- /dev/null +++ b/lib/authMiddleware.go @@ -0,0 +1,73 @@ +package lib + +import ( + "encoding/base64" + "fmt" + "net/http" + "strings" + "time" + + "github.com/biscuit-auth/biscuit-go/v2" + "github.com/biscuit-auth/biscuit-go/v2/parser" + "github.com/labstack/echo/v4" +) + +func queryUser(authorizer biscuit.Authorizer) (biscuit.FactSet, error) { + rule, err := parser.FromStringRule(`data($name) <- user($name)`) + if err != nil { + return nil, fmt.Errorf("failed to parse check: %v", err) + } + + return authorizer.Query(rule) +} + +func AuthMiddleware(skipper_auth func(echo.Context) bool) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if skipper_auth(c) { + return next(c) + } + tokens := strings.Split(c.Request().Header.Get("Authorization"), " ") + + if len(tokens) != 2 { + return c.String(http.StatusUnauthorized, "Header d'authentification manquant") + } + + c.Set("token", tokens[1]) + byteToken, _ := base64.URLEncoding.DecodeString(tokens[1]) + b, err := biscuit.Unmarshal(byteToken) + + if err != nil { + return c.JSON(http.StatusUnauthorized, err) + } + + authorizer, err := b.Authorizer(PublicKey) + + if err != nil { + return c.JSON(http.StatusUnauthorized, err) + } + + now := time.Now() + authorizerContents, err := parser.FromStringAuthorizerWithParams(` + time(`+now.Format("2006-01-02T15:04:05Z")+`); + allow if time($time), $time <= `+now.Format("2006-01-02T15:04:05Z")+`; + `, map[string]biscuit.Term{}) + if err != nil { + return c.JSON(http.StatusUnauthorized, err) + } + authorizer.AddAuthorizer(authorizerContents) + + if err := authorizer.Authorize(); err != nil { + return c.JSON(http.StatusUnauthorized, err) + } + + fact, err := queryUser(authorizer) + if err != nil { + return c.JSON(http.StatusUnauthorized, err) + } + c.Set("username", strings.Split(fact[0].IDs[0].String(), "\"")[1]) + + return next(c) + } + } +} diff --git a/lib/initBiscuit.go b/lib/initBiscuit.go new file mode 100644 index 0000000..d350696 --- /dev/null +++ b/lib/initBiscuit.go @@ -0,0 +1,67 @@ +package lib + +import ( + "crypto/ed25519" + "crypto/rand" + "io/ioutil" + "os" + "time" +) + +var PrivateKey ed25519.PrivateKey +var PublicKey ed25519.PublicKey +var filePrivateKey string = "/keys/privatekey.pem" +var filePublicKey string = "/keys/publickey.pem" + +func InitLoginBiscuit() error { + + if _, err := os.Stat(filePrivateKey); os.IsNotExist(err) { + rng := rand.Reader + PublicKey, PrivateKey, _ = ed25519.GenerateKey(rng) + if err := ioutil.WriteFile(filePrivateKey, PrivateKey, 0600); err != nil { + return err + } + if err := ioutil.WriteFile(filePublicKey, PublicKey, 0600); err != nil { + return err + } + } else { + var err error = nil + PrivateKey, err = ioutil.ReadFile(filePrivateKey) + if err != nil { + return err + } + PublicKey, err = ioutil.ReadFile(filePublicKey) + if err != nil { + return err + } + } + return nil +} + +func InitGlobalBiscuit() error { + for { + time.Sleep(1 * time.Second) + _, err := os.Stat(filePublicKey) + if err == nil { + break + } + } + for { + time.Sleep(1 * time.Second) + _, err := os.Stat(filePrivateKey) + if err == nil { + break + } + } + var err error = nil + PrivateKey, err = ioutil.ReadFile(filePrivateKey) + if err != nil { + return err + } + PublicKey, err = ioutil.ReadFile(filePublicKey) + if err != nil { + return err + } + + return nil +}