feat(api): user jwt authentication

This commit is contained in:
Leandro Antônio Farias Machado 2023-05-02 23:55:21 -03:00
parent 5daf95c99c
commit bfb2acf0fa
8 changed files with 198 additions and 13 deletions

View File

@ -9,6 +9,7 @@ require (
) )
require ( require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
github.com/golang/snappy v0.0.1 // indirect github.com/golang/snappy v0.0.1 // indirect
github.com/google/uuid v1.3.0 // indirect github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/mux v1.8.0 // indirect github.com/gorilla/mux v1.8.0 // indirect

View File

@ -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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:oUGPjRwWcZQRgDD9wVDV7y7i7yBSxts3vcvcNJo8B4Q=
github.com/eclipse/paho.golang v0.10.0/go.mod h1:rhrV37IEwauUyx8FHrvmXOKo+QRKng5ncoN1vJiJMcs= 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= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=

View File

@ -3,6 +3,7 @@ package api
import ( import (
"encoding/json" "encoding/json"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/leandrofars/oktopus/internal/api/auth"
"github.com/leandrofars/oktopus/internal/api/middleware" "github.com/leandrofars/oktopus/internal/api/middleware"
"github.com/leandrofars/oktopus/internal/db" "github.com/leandrofars/oktopus/internal/db"
"github.com/leandrofars/oktopus/internal/mtp" "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) { func StartApi(a Api) {
r := mux.NewRouter() 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) { iot.Use(func(handler http.Handler) http.Handler {
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 {
return middleware.Middleware(handler) return middleware.Middleware(handler)
}) })
@ -274,3 +277,61 @@ func (a *Api) deviceExists(sn string, w http.ResponseWriter) {
return 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
}

View File

@ -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
}

View File

@ -1,12 +1,28 @@
package middleware 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 { func Middleware(next http.Handler) http.Handler {
return http.HandlerFunc( return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") 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))
}, },
) )
} }

View File

@ -9,6 +9,7 @@ import (
type Database struct { type Database struct {
devices *mongo.Collection devices *mongo.Collection
users *mongo.Collection
ctx context.Context ctx context.Context
} }
@ -22,7 +23,9 @@ func NewDatabase(ctx context.Context, mongoUri string) Database {
log.Println("Connected to MongoDB-->", mongoUri) log.Println("Connected to MongoDB-->", mongoUri)
devices := client.Database("oktopus").Collection("devices") devices := client.Database("oktopus").Collection("devices")
users := client.Database("oktopus").Collection("users")
db.devices = devices db.devices = devices
db.users = users
db.ctx = ctx db.ctx = ctx
return db return db
} }

View File

@ -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
}

View File

@ -252,7 +252,6 @@ func (m *Mqtt) handleApiRequest(api []byte) {
log.Println(err) log.Println(err)
} }
//TODO: verify record operation type
var msg usp_msg.Msg var msg usp_msg.Msg
err = proto.Unmarshal(record.GetNoSessionContext().Payload, &msg) err = proto.Unmarshal(record.GetNoSessionContext().Payload, &msg)
if err != nil { if err != nil {