From fc9ad06afb62840a7b7254e0539a7808994819d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Z=C3=ADpek?= Date: Sun, 21 Aug 2022 20:51:14 +0200 Subject: [PATCH] Working on auth rework --- server/.env.example | 1 + server/config/config.go | 14 ++-- server/main.go | 25 ++++--- server/middleware/auth.go | 34 ++++++++++ server/routes/auth.go | 57 ++++++++++++++++ server/services/auth_service.go | 28 +++++++- server/services/services.go | 2 - server/services/sessions_service.go | 100 ++++++++++++++++++++++++++++ 8 files changed, 239 insertions(+), 22 deletions(-) create mode 100644 server/middleware/auth.go create mode 100644 server/routes/auth.go create mode 100644 server/services/sessions_service.go diff --git a/server/.env.example b/server/.env.example index 25cbd40..3b5e2d1 100644 --- a/server/.env.example +++ b/server/.env.example @@ -4,3 +4,4 @@ PORT=8083 BIND_IP=localhost AUTH_USERNAME=admin AUTH_PASSWORD=password +AUTH_KEY=password diff --git a/server/config/config.go b/server/config/config.go index ee2eaf2..acfbace 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -6,12 +6,13 @@ import ( ) type Config struct { - Mode string `env:"GIN_MODE"` - DatabaseUrl string `env:"DATABASE_URL"` - Port int `env:"PORT"` - Ip string `env:"BIND_IP"` - AuthUsername string `env:"AUTH_USERNAME"` - AuthPassword string `env:"AUTH_PASSWORD"` + Mode string + DatabaseUrl string + Port int + Ip string + AuthUsername string + AuthPassword string + AuthKey string } func LoadConfig() *Config { @@ -28,6 +29,7 @@ func LoadConfig() *Config { Ip: os.Getenv("BIND_IP"), AuthUsername: os.Getenv("AUTH_USERNAME"), AuthPassword: os.Getenv("AUTH_PASSWORD"), + AuthKey: os.Getenv("SENSOR_AUTH_KEY"), } return &config diff --git a/server/main.go b/server/main.go index 1a2440d..e1b38d6 100644 --- a/server/main.go +++ b/server/main.go @@ -2,6 +2,7 @@ package main import ( "basic-sensor-receiver/app" + "basic-sensor-receiver/middleware" "basic-sensor-receiver/routes" "fmt" "log" @@ -23,19 +24,21 @@ func main() { router := gin.Default() - protected := router.Group("/", gin.BasicAuth(gin.Accounts{ - server.Config.AuthUsername: server.Config.AuthPassword, - })) + router.StaticFile("/", "client/index.html") + router.Static("/js", "client/js") + router.Static("/css", "client/css") - protected.StaticFile("/", "client/index.html") - protected.Static("/js", "client/js") - protected.Static("/css", "client/css") + router.POST("/api/login", routes.Login(server)) - protected.GET("/api/sensors", routes.GetSensors(server)) - protected.GET("/api/sensors/:sensor/values", routes.HandleGetSensorValues(server)) - protected.POST("/api/sensors/:sensor/values", routes.HandlePostSensorValues(server)) - protected.GET("/api/sensors/:sensor/config", routes.GetSensorConfig(server)) - protected.PUT("/api/sensors/:sensor/config/:key", routes.HandlePutSensorConfig(server)) + loginProtected := router.Group("/", middleware.LoginAuthMiddleware(server)) + loginProtected.GET("/api/sensors", routes.GetSensors(server)) + loginProtected.GET("/api/sensors/:sensor/values", routes.HandleGetSensorValues(server)) + loginProtected.GET("/api/sensors/:sensor/config", routes.GetSensorConfig(server)) + loginProtected.PUT("/api/sensors/:sensor/config/:key", routes.HandlePutSensorConfig(server)) + loginProtected.POST("/api/logout", routes.Logout(server)) + + keyProtected := router.Group("/", middleware.KeyAuthMiddleware(server)) + keyProtected.POST("/api/sensors/:sensor/values", routes.HandlePostSensorValues(server)) router.Run(fmt.Sprintf("%s:%d", server.Config.Ip, server.Config.Port)) } diff --git a/server/middleware/auth.go b/server/middleware/auth.go new file mode 100644 index 0000000..89dd8bf --- /dev/null +++ b/server/middleware/auth.go @@ -0,0 +1,34 @@ +package middleware + +import ( + "basic-sensor-receiver/app" + "net/http" + + "github.com/gin-gonic/gin" +) + +func LoginAuthMiddleware(server *app.Server) gin.HandlerFunc { + return func(c *gin.Context) { + _, err := server.Services.Auth.FromContext(c) + + if err != nil { + c.AbortWithError(http.StatusUnauthorized, err) + + return + } + + c.Next() + } +} + +func KeyAuthMiddleware(server *app.Server) gin.HandlerFunc { + return func(c *gin.Context) { + if c.GetHeader("authorization") != server.Config.AuthKey { + c.AbortWithStatus(http.StatusUnauthorized) + + return + } + + c.Next() + } +} diff --git a/server/routes/auth.go b/server/routes/auth.go new file mode 100644 index 0000000..71e198c --- /dev/null +++ b/server/routes/auth.go @@ -0,0 +1,57 @@ +package routes + +import ( + "basic-sensor-receiver/app" + "net/http" + + "github.com/gin-gonic/gin" +) + +type postLoginBody struct { + Username string `json:"username"` + Password string `json:"password"` +} + +func Login(s *app.Server) gin.HandlerFunc { + return func(c *gin.Context) { + body := postLoginBody{} + + if err := c.BindJSON(&body); err != nil { + c.AbortWithError(400, err) + return + } + + if body.Password != s.Config.AuthPassword || body.Username != s.Config.AuthUsername { + c.AbortWithStatus(401) + return + } + + if err := s.Services.Auth.Login(c); err != nil { + c.AbortWithError(500, err) + } + + c.Writer.WriteHeader(http.StatusOK) + } +} + +func Logout(s *app.Server) gin.HandlerFunc { + return func(c *gin.Context) { + body := postLoginBody{} + + if err := c.BindJSON(&body); err != nil { + c.AbortWithError(400, err) + return + } + + if body.Password != s.Config.AuthPassword || body.Username != s.Config.AuthUsername { + c.AbortWithStatus(401) + return + } + + if err := s.Services.Auth.Login(c); err != nil { + c.AbortWithError(500, err) + } + + c.Writer.WriteHeader(http.StatusOK) + } +} diff --git a/server/services/auth_service.go b/server/services/auth_service.go index 76694a9..81ed1ae 100644 --- a/server/services/auth_service.go +++ b/server/services/auth_service.go @@ -6,12 +6,34 @@ type AuthService struct { ctx *Context } -func (s *AuthService) FromContext(ctx *gin.Context) (*User, error) { +func (s *AuthService) FromContext(ctx *gin.Context) (*SessionItem, error) { + return s.ctx.Services.Sessions.FromContext(ctx) +} + +func (s *AuthService) Login(ctx *gin.Context) error { + session, err := s.ctx.Services.Sessions.Create() + + if err != nil { + return err + } + + s.ctx.Services.Sessions.ToContext(ctx, session) + + return nil +} + +func (s *AuthService) Logout(ctx *gin.Context) error { session, err := s.ctx.Services.Sessions.FromContext(ctx) if err != nil { - return nil, err + return err } - return s.ctx.Services.Users.GetById(session.UserId) + if err := s.ctx.Services.Sessions.Destroy(session.Id); err != nil { + return err + } + + s.ctx.Services.Sessions.ClearContext(ctx) + + return nil } diff --git a/server/services/services.go b/server/services/services.go index db81dd4..56f8953 100644 --- a/server/services/services.go +++ b/server/services/services.go @@ -10,7 +10,6 @@ type Services struct { SensorValues *SensorValuesService Sensors *SensorsService Sessions *SessionsService - Users *UsersService Auth *AuthService } @@ -30,7 +29,6 @@ func InitializeServices(ctx *Context) *Services { services.Sensors = &SensorsService{ctx: ctx} services.Sessions = &SessionsService{ctx: ctx} services.Auth = &AuthService{ctx: ctx} - services.Users = &UsersService{ctx: ctx} return &services } diff --git a/server/services/sessions_service.go b/server/services/sessions_service.go new file mode 100644 index 0000000..cf036b5 --- /dev/null +++ b/server/services/sessions_service.go @@ -0,0 +1,100 @@ +package services + +import ( + "crypto/rand" + "encoding/hex" + "io" + "time" + + "github.com/gin-gonic/gin" +) + +type SessionsService struct { + ctx *Context +} + +type SessionItem struct { + Id string `json:"id"` + ExpiresAt int64 `json:"expiresAt"` +} + +func (s *SessionsService) FromContext(ctx *gin.Context) (*SessionItem, error) { + cookie, err := ctx.Cookie("session.id") + + if err != nil { + return nil, err + } + + session, err := s.GetById(cookie) + + if err != nil { + return nil, err + } + + s.Extend(session) + + return session, nil +} + +func (s *SessionsService) ToContext(ctx *gin.Context, session *SessionItem) { + ctx.SetCookie("session.id", session.Id, int(time.Duration(time.Hour*24).Seconds()), "/", "", true, true) +} + +func (s *SessionsService) ClearContext(ctx *gin.Context) { + ctx.SetCookie("session.id", "", int(time.Duration(time.Hour*24).Seconds()), "/", "", true, true) +} + +func (s *SessionsService) GetById(id string) (*SessionItem, error) { + item := SessionItem{} + + row := s.ctx.DB.QueryRow("SELECT id, expires_at FROM sessions WHERE id = ? AND expires_at > ?", id, time.Now().Unix()) + + err := row.Scan(&item.Id, &item.ExpiresAt) + if err != nil { + return nil, err + } + + return &item, nil +} + +func (s *SessionsService) Create() (*SessionItem, error) { + item := SessionItem{ + // TODO: The key is not guaranteed to be unique, how do we guarantee that? + Id: hex.EncodeToString(generateRandomKey(128)), + ExpiresAt: generateExpiryDate().Unix(), + } + + _, err := s.ctx.DB.Exec("INSERT INTO sessions (id, expires_at) VALUES (?, ?, ?)", item.Id, item.ExpiresAt) + + if err != nil { + return nil, err + } + + return &item, nil +} + +func (s *SessionsService) Destroy(sessionId string) error { + _, err := s.ctx.DB.Exec("DELETE FROM sessions WHERE id = ?", sessionId) + + return err +} + +func (s *SessionsService) Extend(session *SessionItem) error { + session.ExpiresAt = generateExpiryDate().Unix() + + _, err := s.ctx.DB.Exec("UPDATE sessions SET expires_at = ? WHERE id = ?", session.ExpiresAt, session.Id) + + return err +} + +func generateExpiryDate() time.Time { + return time.Now().Add(time.Hour * 24) +} + +func generateRandomKey(length int) []byte { + k := make([]byte, length) + if _, err := io.ReadFull(rand.Reader, k); err != nil { + return nil + } + return k +}