package users import ( "encoding/json" "net/http" "os" "time" "github.com/dustinpianalto/errors" "github.com/dustinpianalto/quartermaster" "github.com/dustinpianalto/quartermaster/pkg/services" "github.com/golang-jwt/jwt" "golang.org/x/crypto/bcrypt" ) var jwtKey = []byte(os.Getenv("JWT_KEY")) func loginHandler(w http.ResponseWriter, r *http.Request) error { const method errors.Method = "users/login" var userReq quartermaster.User err := json.NewDecoder(r.Body).Decode(&userReq) if err != nil { return errors.E(method, errors.Malformed, "failed to decode user request", err) } user, err := services.UserService.User(userReq.Username) if err != nil { return errors.E(method, errors.Username(userReq.Username), errors.Incorrect, "user not found", err) } if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(userReq.Password)); err != nil { return errors.E(method, errors.Username(userReq.Username), errors.Incorrect, "incorrect password", err) } expires := time.Now().Add(10 * time.Minute) claims := &quartermaster.Claims{ ID: user.ID, Username: user.Username, StandardClaims: jwt.StandardClaims{ ExpiresAt: expires.Unix(), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS512, claims) tokenString, err := token.SignedString(jwtKey) if err != nil { return errors.E(method, errors.Username(user.Username), errors.Internal, "error signing token", err) } http.SetCookie(w, &http.Cookie{ Name: "token", Value: tokenString, Expires: expires, }) return nil } func registerHandler(w http.ResponseWriter, r *http.Request) error { const method errors.Method = "users/register" var user *quartermaster.User err := json.NewDecoder(r.Body).Decode(&user) if err != nil { return errors.E(method, errors.Malformed, "failed to decode user request", err) } p, err := bcrypt.GenerateFromPassword([]byte(user.Password), 5) if err != nil { return errors.E(method, errors.Username(user.Username), errors.Internal, "error hashing password", err) } user.Password = string(p) user, err = services.UserService.AddUser(user) if err != nil { return errors.E(method, errors.Username(user.Username), errors.Conflict, "user already exists", err) } return nil } func refreshHandler(w http.ResponseWriter, r *http.Request) error { const method errors.Method = "users/refresh" c, err := r.Cookie("token") if err != nil { if err == http.ErrNoCookie { return errors.E(method, errors.Incorrect, "cookie not found", err) } return errors.E(method, errors.Malformed, "failed to get cookie data", err) } tknStr := c.Value claims := &quartermaster.Claims{} tkn, err := jwt.ParseWithClaims(tknStr, claims, func(token *jwt.Token) (interface{}, error) { return jwtKey, nil }) if err != nil { if err == jwt.ErrSignatureInvalid { return errors.E(method, errors.Incorrect, "cookie is invalid", err) } e, _ := err.(*jwt.ValidationError) if e.Inner == jwt.ErrInvalidKeyType { return errors.E(method, errors.Internal, err) } else if e.Inner == jwt.ErrHashUnavailable { return errors.E(method, errors.Internal, err) } return errors.E(method, errors.Malformed, "failed to parse cookie", err) } if !tkn.Valid { return errors.E(method, errors.Incorrect, "cookie is invalid", err) } // We ensure that a new token is not issued until enough time has elapsed // In this case, a new token will only be issued if the old token is within // 2 minutes of expiry. Otherwise, return a bad request status if time.Until(time.Unix(claims.StandardClaims.ExpiresAt, 0)) >= 2*time.Minute { return errors.E(method, errors.Malformed, "not enough time passed") } // Now, create a new token for the current use, with a renewed expiration time expirationTime := time.Now().Add(10 * time.Minute) claims.StandardClaims.ExpiresAt = expirationTime.Unix() token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenString, err := token.SignedString(jwtKey) if err != nil { return errors.E(method, errors.Internal, "error signing token", err) } // Set the new token as the users `token` cookie http.SetCookie(w, &http.Cookie{ Name: "token", Value: tokenString, Expires: expirationTime, }) return nil }