diff --git a/backend/services/controller/go.mod b/backend/services/controller/go.mod index 5507d84..4504b6f 100755 --- a/backend/services/controller/go.mod +++ b/backend/services/controller/go.mod @@ -9,6 +9,7 @@ require ( ) require ( + github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect github.com/golang/snappy v0.0.1 // indirect github.com/google/uuid v1.3.0 // indirect github.com/gorilla/mux v1.8.0 // indirect diff --git a/backend/services/controller/go.sum b/backend/services/controller/go.sum index 5d46421..f365626 100644 --- a/backend/services/controller/go.sum +++ b/backend/services/controller/go.sum @@ -1,6 +1,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/eclipse/paho.golang v0.10.0 h1:oUGPjRwWcZQRgDD9wVDV7y7i7yBSxts3vcvcNJo8B4Q= github.com/eclipse/paho.golang v0.10.0/go.mod h1:rhrV37IEwauUyx8FHrvmXOKo+QRKng5ncoN1vJiJMcs= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= diff --git a/backend/services/controller/internal/api/api.go b/backend/services/controller/internal/api/api.go index 14f9d7f..09cbb52 100644 --- a/backend/services/controller/internal/api/api.go +++ b/backend/services/controller/internal/api/api.go @@ -3,6 +3,7 @@ package api import ( "encoding/json" "github.com/gorilla/mux" + "github.com/leandrofars/oktopus/internal/api/auth" "github.com/leandrofars/oktopus/internal/api/middleware" "github.com/leandrofars/oktopus/internal/db" "github.com/leandrofars/oktopus/internal/mtp" @@ -36,17 +37,19 @@ func NewApi(port string, db db.Database, b mtp.Broker, msgQueue map[string](chan func StartApi(a Api) { r := mux.NewRouter() + authentication := r.PathPrefix("/auth").Subrouter() + authentication.HandleFunc("/login", a.generateToken).Methods("PUT") + authentication.HandleFunc("/register", a.registerUser).Methods("POST") + iot := r.PathPrefix("/device").Subrouter() + iot.HandleFunc("/", a.retrieveDevices).Methods("GET") + iot.HandleFunc("/{sn}/get", a.deviceGetMsg).Methods("PUT") + iot.HandleFunc("/{sn}/add", a.deviceCreateMsg).Methods("PUT") + iot.HandleFunc("/{sn}/del", a.deviceDeleteMsg).Methods("PUT") + iot.HandleFunc("/{sn}/set", a.deviceUpdateMsg).Methods("PUT") + //TODO: Create operation action handler + iot.HandleFunc("/device/{sn}/act", a.deviceUpdateMsg).Methods("PUT") - r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - return - }) - r.HandleFunc("/devices", a.retrieveDevices).Methods("GET") - r.HandleFunc("/device/{sn}/get", a.deviceGetMsg).Methods("PUT") - r.HandleFunc("/device/{sn}/add", a.deviceCreateMsg).Methods("PUT") - r.HandleFunc("/device/{sn}/del", a.deviceDeleteMsg).Methods("PUT") - r.HandleFunc("/device/{sn}/set", a.deviceUpdateMsg).Methods("PUT") - - r.Use(func(handler http.Handler) http.Handler { + iot.Use(func(handler http.Handler) http.Handler { return middleware.Middleware(handler) }) @@ -274,3 +277,61 @@ func (a *Api) deviceExists(sn string, w http.ResponseWriter) { return } } + +func (a *Api) registerUser(w http.ResponseWriter, r *http.Request) { + var user db.User + err := json.NewDecoder(r.Body).Decode(&user) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + if err := user.HashPassword(user.Password); err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + if err := a.Db.RegisterUser(user); err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } +} + +type TokenRequest struct { + Email string `json:"email"` + Password string `json:"password"` +} + +func (a *Api) generateToken(w http.ResponseWriter, r *http.Request) { + var tokenReq TokenRequest + + err := json.NewDecoder(r.Body).Decode(&tokenReq) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + user, err := a.Db.FindUser(tokenReq.Email) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode("Invalid Credentials") + return + } + + credentialError := user.CheckPassword(tokenReq.Password) + if credentialError != nil { + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode("Invalid Credentials") + return + } + + token, err := auth.GenerateJWT(user.Email, user.Name) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.Header().Add("Content-Type", "application/json") + json.NewEncoder(w).Encode(token) + return +} diff --git a/backend/services/controller/internal/api/auth/auth.go b/backend/services/controller/internal/api/auth/auth.go new file mode 100644 index 0000000..af2a12c --- /dev/null +++ b/backend/services/controller/internal/api/auth/auth.go @@ -0,0 +1,54 @@ +package auth + +import ( + "errors" + "github.com/dgrijalva/jwt-go" + "time" +) + +var jwtKey = []byte("supersecretkey") + +type JWTClaim struct { + Username string `json:"username"` + Email string `json:"email"` + jwt.StandardClaims +} + +func GenerateJWT(email string, username string) (tokenString string, err error) { + expirationTime := time.Now().Add(1 * time.Hour) + claims := &JWTClaim{ + Email: email, + Username: username, + StandardClaims: jwt.StandardClaims{ + ExpiresAt: expirationTime.Unix(), + }, + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err = token.SignedString(jwtKey) + return +} +func ValidateToken(signedToken string) (email string, err error) { + token, err := jwt.ParseWithClaims( + signedToken, + &JWTClaim{}, + func(token *jwt.Token) (interface{}, error) { + return []byte(jwtKey), nil + }, + ) + if err != nil { + return + } + claims, ok := token.Claims.(*JWTClaim) + if !ok { + err = errors.New("couldn't parse claims") + return + } + if claims.ExpiresAt < time.Now().Local().Unix() { + err = errors.New("token expired") + return + } + + email = claims.Email + + return +} diff --git a/backend/services/controller/internal/api/middleware/middleware.go b/backend/services/controller/internal/api/middleware/middleware.go index 97edbe4..fad3a59 100644 --- a/backend/services/controller/internal/api/middleware/middleware.go +++ b/backend/services/controller/internal/api/middleware/middleware.go @@ -1,12 +1,28 @@ package middleware -import "net/http" +import ( + "github.com/leandrofars/oktopus/internal/api/auth" + "golang.org/x/net/context" + "net/http" +) func Middleware(next http.Handler) http.Handler { return http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - next.ServeHTTP(w, r) + + tokenString := r.Header.Get("Authorization") + if tokenString == "" { + w.WriteHeader(http.StatusUnauthorized) + return + } + email, err := auth.ValidateToken(tokenString) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + return + } + ctx := context.WithValue(r.Context(), "email", email) + next.ServeHTTP(w, r.WithContext(ctx)) }, ) } diff --git a/backend/services/controller/internal/db/db.go b/backend/services/controller/internal/db/db.go index 5f217f7..7ff27ba 100644 --- a/backend/services/controller/internal/db/db.go +++ b/backend/services/controller/internal/db/db.go @@ -9,6 +9,7 @@ import ( type Database struct { devices *mongo.Collection + users *mongo.Collection ctx context.Context } @@ -22,7 +23,9 @@ func NewDatabase(ctx context.Context, mongoUri string) Database { log.Println("Connected to MongoDB-->", mongoUri) devices := client.Database("oktopus").Collection("devices") + users := client.Database("oktopus").Collection("users") db.devices = devices + db.users = users db.ctx = ctx return db } diff --git a/backend/services/controller/internal/db/user.go b/backend/services/controller/internal/db/user.go new file mode 100644 index 0000000..1552f0d --- /dev/null +++ b/backend/services/controller/internal/db/user.go @@ -0,0 +1,49 @@ +package db + +import ( + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "golang.org/x/crypto/bcrypt" + "log" +) + +type User struct { + Email string `json:"email"` + Name string `json:"name"` + Password string `json:"password"` +} + +func (d *Database) RegisterUser(user User) error { + err := d.users.FindOne(d.ctx, bson.D{{"email", user.Email}}).Err() + if err != nil { + if err == mongo.ErrNoDocuments { + _, err = d.users.InsertOne(d.ctx, user) + return err + } + log.Println(err) + } + return err +} + +func (d *Database) FindUser(email string) (User, error) { + var result User + err := d.users.FindOne(d.ctx, bson.D{{"email", email}}).Decode(&result) + return result, err +} + +func (user *User) HashPassword(password string) error { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) + if err != nil { + return err + } + user.Password = string(bytes) + return nil +} + +func (user *User) CheckPassword(providedPassword string) error { + err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(providedPassword)) + if err != nil { + return err + } + return nil +} diff --git a/backend/services/controller/internal/mqtt/mqtt.go b/backend/services/controller/internal/mqtt/mqtt.go index b1b3fa0..7a53378 100644 --- a/backend/services/controller/internal/mqtt/mqtt.go +++ b/backend/services/controller/internal/mqtt/mqtt.go @@ -252,7 +252,6 @@ func (m *Mqtt) handleApiRequest(api []byte) { log.Println(err) } - //TODO: verify record operation type var msg usp_msg.Msg err = proto.Unmarshal(record.GetNoSessionContext().Payload, &msg) if err != nil {