You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
136 lines
4.1 KiB
136 lines
4.1 KiB
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
|
|
}
|