From 20e23ef9c167004872f0933fa66df5dbfe7db927 Mon Sep 17 00:00:00 2001 From: darius Date: Sun, 19 May 2024 17:49:20 +0200 Subject: [PATCH] Added jwt to cookie + database change --- api/apiRoutes.go | 6 +-- api/handlers/authHandler.go | 39 +++++++++++-------- api/handlers/errorHandlers.go | 25 ++++++++++-- api/handlers/mainHandler.go | 2 +- api/handlers/userHandler.go | 9 ++++- {service => api/service}/bcrypt/password.go | 0 api/service/jwt/create.go | 33 ++++++++++++++++ api/service/jwt/verify.go | 24 ++++++++++++ .../service}/validate/validateUser.go | 3 +- database/ent/schema/team.go | 3 +- database/ent/schema/user.go | 5 ++- database/query/authQuery.go | 5 +-- database/query/userQuery.go | 8 ++-- go.mod | 1 + go.sum | 2 + 15 files changed, 130 insertions(+), 35 deletions(-) rename {service => api/service}/bcrypt/password.go (100%) create mode 100644 api/service/jwt/create.go create mode 100644 api/service/jwt/verify.go rename {service => api/service}/validate/validateUser.go (76%) diff --git a/api/apiRoutes.go b/api/apiRoutes.go index 2b24215..5c6512b 100644 --- a/api/apiRoutes.go +++ b/api/apiRoutes.go @@ -12,9 +12,9 @@ func ApiRoutes() *http.ServeMux { // Register the routes and webHandler mux.HandleFunc("/", handlers.CatchAllHandler) - mux.HandleFunc("POST /api/user", handlers.CreateUser) - mux.HandleFunc("GET /api/user/{id}", handlers.GetUser) - mux.HandleFunc("POST /api/login", handlers.Login) + mux.HandleFunc("POST /register", handlers.CreateUser) + mux.HandleFunc("GET /user/{id}", handlers.GetUser) + mux.HandleFunc("POST /login", handlers.Login) return mux } diff --git a/api/handlers/authHandler.go b/api/handlers/authHandler.go index 045d023..09d6c3f 100644 --- a/api/handlers/authHandler.go +++ b/api/handlers/authHandler.go @@ -3,16 +3,14 @@ package handlers import ( "context" "encoding/json" - "fmt" "net/http" + "portfolio/api/service/bcrypt" + "portfolio/api/service/jwt" "portfolio/database/ent" - "portfolio/database/ent/user" "portfolio/database/query" - "portfolio/service/bcrypt" ) func Login(w http.ResponseWriter, r *http.Request) { - fmt.Println("test") var u *ent.User isHtmx := r.Header.Get("HX-Request") @@ -21,31 +19,40 @@ func Login(w http.ResponseWriter, r *http.Request) { u = &ent.User{ Name: r.PostFormValue("name"), Password: r.PostFormValue("password"), - Role: user.Role(r.PostFormValue("role")), } } else { err := json.NewDecoder(r.Body).Decode(&u) if err != nil { - InternalServerErrorHandler(w) + InternalServerErrorHandler(w, err) } } - User, err := query.GetLogin(context.Background(), u.Name) + User, err := query.GetLogin(context.Background(), u) if err != nil { + UnprocessableEntityHandler(w, err) return } if bcrypt.CheckPasswordHash(u.Password, User.Password) { - w.Header().Set("HX-Location", "/") - return + + jwtToken := jwt.CreateUserJWT(u.Name, u.ID, string(u.Role)) + + if jwtToken != "" { + w.Header().Set("HX-Location", "/") + + cookie := &http.Cookie{Name: "jwt", Value: jwtToken, HttpOnly: true, Secure: true, SameSite: http.SameSiteStrictMode} + http.SetCookie(w, cookie) + + w.WriteHeader(http.StatusOK) + _, err = w.Write([]byte("login success")) + return + } else { + InternalServerErrorHandler(w, err) + return + } + } else { - - } - - w.Header().Set("Content-Type", "application/json") - - err = json.NewEncoder(w).Encode(User) - if err != nil { + UnauthorizedHandler(w) return } } diff --git a/api/handlers/errorHandlers.go b/api/handlers/errorHandlers.go index 2833b69..0e982a9 100644 --- a/api/handlers/errorHandlers.go +++ b/api/handlers/errorHandlers.go @@ -2,12 +2,13 @@ package handlers import "net/http" -func InternalServerErrorHandler(w http.ResponseWriter) { - w.WriteHeader(http.StatusInternalServerError) - _, err := w.Write([]byte("500 Internal Server Error")) +func InternalServerErrorHandler(w http.ResponseWriter, err error) { + w.WriteHeader(http.StatusInternalServerError) //set http status + _, err = w.Write([]byte(err.Error())) //set response message if err != nil { return } + return } func NotFoundHandler(w http.ResponseWriter) { @@ -25,3 +26,21 @@ func BadRequestHandler(w http.ResponseWriter) { return } } + +func UnprocessableEntityHandler(w http.ResponseWriter, err error) { + w.WriteHeader(http.StatusUnprocessableEntity) //set http status + _, err = w.Write([]byte(err.Error())) //set response message + if err != nil { + return + } + return +} + +func UnauthorizedHandler(w http.ResponseWriter) { + w.WriteHeader(http.StatusUnauthorized) //set http status + _, err := w.Write([]byte("Unauthorized")) //set response message + if err != nil { + return + } + return +} diff --git a/api/handlers/mainHandler.go b/api/handlers/mainHandler.go index 93135e3..1fb157e 100644 --- a/api/handlers/mainHandler.go +++ b/api/handlers/mainHandler.go @@ -6,6 +6,6 @@ func CatchAllHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusGone) _, err := w.Write([]byte("Bad endpoint")) if err != nil { - InternalServerErrorHandler(w) + InternalServerErrorHandler(w, err) } } diff --git a/api/handlers/userHandler.go b/api/handlers/userHandler.go index 22c0620..100ae5d 100644 --- a/api/handlers/userHandler.go +++ b/api/handlers/userHandler.go @@ -4,10 +4,11 @@ import ( "context" "encoding/json" "net/http" + "portfolio/api/service/bcrypt" + "portfolio/api/service/validate" "portfolio/database/ent" "portfolio/database/ent/user" "portfolio/database/query" - "portfolio/service/validate" "strconv" ) @@ -25,7 +26,7 @@ func CreateUser(w http.ResponseWriter, r *http.Request) { } else { err := json.NewDecoder(r.Body).Decode(&u) if err != nil { - InternalServerErrorHandler(w) + InternalServerErrorHandler(w, err) } } @@ -34,8 +35,12 @@ func CreateUser(w http.ResponseWriter, r *http.Request) { return } + //hash password + u.Password, _ = bcrypt.HashPassword(u.Password) + err := query.CreateUser(context.Background(), *u) if err != nil { + UnprocessableEntityHandler(w, err) return } w.WriteHeader(http.StatusCreated) diff --git a/service/bcrypt/password.go b/api/service/bcrypt/password.go similarity index 100% rename from service/bcrypt/password.go rename to api/service/bcrypt/password.go diff --git a/api/service/jwt/create.go b/api/service/jwt/create.go new file mode 100644 index 0000000..0346463 --- /dev/null +++ b/api/service/jwt/create.go @@ -0,0 +1,33 @@ +package jwt + +import ( + "github.com/golang-jwt/jwt/v5" + "os" + "strconv" + "time" +) + +func CreateUserJWT(name string, uid int, role string) string { + + //create claims for jwt + claims := jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), + IssuedAt: jwt.NewNumericDate(time.Now()), + NotBefore: jwt.NewNumericDate(time.Now()), + Issuer: "portfolio.dariusklein.nl", + Subject: name, + ID: strconv.Itoa(uid), + Audience: []string{role}, + } + return SignJWT(claims) +} + +func SignJWT(claims jwt.Claims) string { + //Build jwt with claims + t := jwt.NewWithClaims(jwt.SigningMethodHS512, claims) + //get jwt secret from environment + secret := os.Getenv("JWT_SECRET") + //sign jwt token with secret + token, _ := t.SignedString([]byte(secret)) + return token +} diff --git a/api/service/jwt/verify.go b/api/service/jwt/verify.go new file mode 100644 index 0000000..8e1c365 --- /dev/null +++ b/api/service/jwt/verify.go @@ -0,0 +1,24 @@ +package jwt + +import ( + _ "context" + "github.com/golang-jwt/jwt/v5" + "os" + "strconv" +) + +// VerifyJWT verify JWT token and returns user object +func VerifyJWT(authToken string) (int, string, error) { + //get jwt secret from environment + secret := os.Getenv("JWT_SECRET") + //parse jwt token + token, err := jwt.ParseWithClaims(authToken, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) { + return []byte(secret), nil + }) + if err != nil { + return 0, "", err + } + uid, err := strconv.Atoi(token.Claims.(*jwt.RegisteredClaims).ID) + audience := token.Claims.(*jwt.RegisteredClaims).Audience[0] + return uid, audience, err +} diff --git a/service/validate/validateUser.go b/api/service/validate/validateUser.go similarity index 76% rename from service/validate/validateUser.go rename to api/service/validate/validateUser.go index e994f5c..f338446 100644 --- a/service/validate/validateUser.go +++ b/api/service/validate/validateUser.go @@ -6,7 +6,8 @@ import ( func UserIsValid(u *ent.User) bool { if len(u.Name) > 0 && - len(u.Role) > 0 { + len(u.Email) > 0 && + len(u.Password) > 0 { return true } return false diff --git a/database/ent/schema/team.go b/database/ent/schema/team.go index 5314a3d..7216b66 100644 --- a/database/ent/schema/team.go +++ b/database/ent/schema/team.go @@ -14,7 +14,8 @@ type Team struct { // Fields of the Team. func (Team) Fields() []ent.Field { return []ent.Field{ - field.String("name"), + field.String("name"). + Unique(), } } diff --git a/database/ent/schema/user.go b/database/ent/schema/user.go index f490834..c602ec6 100644 --- a/database/ent/schema/user.go +++ b/database/ent/schema/user.go @@ -16,10 +16,11 @@ func (User) Fields() []ent.Field { return []ent.Field{ field.String("name"). Unique(), - field.String("email"), + field.String("email"). + Unique(), field.String("password"), field.Enum("role"). - Values("admin", "user", "visitor"), + Values("owner", "admin", "user", "visitor"), } } diff --git a/database/query/authQuery.go b/database/query/authQuery.go index d6b5b55..a459a38 100644 --- a/database/query/authQuery.go +++ b/database/query/authQuery.go @@ -9,11 +9,10 @@ import ( "portfolio/database/ent/user" ) -func GetLogin(ctx context.Context, name string) (*ent.User, error) { - +func GetLogin(ctx context.Context, U *ent.User) (*ent.User, error) { u, err := database.Client.User. Query(). - Where(user.Name(name)). + Where(user.Name(U.Name)). Only(ctx) if err != nil { return nil, fmt.Errorf("failed querying user: %w", err) diff --git a/database/query/userQuery.go b/database/query/userQuery.go index 813e0be..28b0d89 100644 --- a/database/query/userQuery.go +++ b/database/query/userQuery.go @@ -22,12 +22,14 @@ func GetUser(ctx context.Context, id int) (*ent.User, error) { return u, nil } -func CreateUser(ctx context.Context, User ent.User) error { +func CreateUser(ctx context.Context, user ent.User) error { _, err := database.Client.User. Create(). - SetName(User.Name). - SetRole(User.Role). + SetName(user.Name). + SetEmail(user.Email). + SetPassword(user.Password). + SetRole("visitor"). Save(ctx) if err != nil { return fmt.Errorf("failed to create user: %w", err) diff --git a/go.mod b/go.mod index cf1c9db..66ffc30 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.22.2 require ( entgo.io/ent v0.13.1 github.com/delaneyj/gomponents-iconify v0.0.20231025 + github.com/golang-jwt/jwt/v5 v5.2.1 github.com/joho/godotenv v1.5.1 github.com/lib/pq v1.10.9 github.com/maragudk/gomponents v0.20.2 diff --git a/go.sum b/go.sum index 472ccd1..0e6d193 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNP github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=