package usermanager import ( "errors" "time" "math/rand" "strconv" "git.mmnx.de/Moe/databaseutils" "git.mmnx.de/Moe/configutils" "git.mmnx.de/Moe/errorhelpers" "github.com/dgrijalva/jwt-go" "github.com/kataras/iris" "golang.org/x/crypto/bcrypt" "fmt" ) var ( Users *[]User // stores all currently logged in users ) type User struct { // User ID string Username string Password string Admin string TokenUsed string } func (user *User) Login(username string, password string) (string, error) { hmacSampleSecret := []byte(configutils.Conf.CryptoKey) // crypto key for JWT encryption row, err := databaseutils.DBUtil.GetRow("*", "users", "username", username) // get user from db if err != nil { if err.Error() == databaseutils.ERR_EMPTY_RESULT { // empty result -> user not found return "", errors.New(errorhelpers.ERR_USER_NOT_FOUND) } else { return "", errors.New("Unknown error") } } err = bcrypt.CompareHashAndPassword([]byte(row[2]), []byte(password)) if err == nil { // if sent' pw hash == stored pw hash expire, _ := time.ParseDuration("168h") // 7 days token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "username": username, "userid": row[0], "nbf": time.Now().Unix(), "exp": time.Now().Add(expire).Unix(), "token": "nigger", // TODO db based tokens }) tokenString, _ := token.SignedString(hmacSampleSecret) user.ID = row[0] user.Username = row[1] user.Password = string(row[2]) user.Admin = string(row[3]) user.TokenUsed = string(row[4]) if err != nil { fmt.Printf("Error: ", err.Error()) } *Users = append(*Users, *user) // store user in logged-in-users list return tokenString, nil // return tokenString (Cookie) } else { return "", errors.New(errorhelpers.ERR_PASSWORD_MISMATCH) // wrong password } } func (user *User) Logout(userID string) { userArrayID := SearchUser(userID) // get logged in users list index user.ID = "" // empty user.Username = "" user.Password = "" user.Admin = "" user.TokenUsed = "" (*Users)[userArrayID] = *user return } func LogoutHandler(ctx *iris.Context) { userID := ctx.GetString("userID") user, err := GetUserFromDB(userID) errorhelpers.HandleError(err, ctx) user.Logout(userID); ctx.SetCookieKV("token", "") err = errors.New(errorhelpers.SUCCESS_LOGOUT) errorhelpers.HandleError(err, ctx) } func (user *User) Update() error { colsVals := make([][]string, 2) colsVals[0] = []string{"username", user.Username} colsVals[1] = []string{"password", user.Password} err := databaseutils.DBUtil.UpdateRow("users", "id", string(user.ID), colsVals) if err != nil { fmt.Println("ERROOR UPDATING: " + err.Error()) } return nil } func SearchUser(userID string) int { for i := range *Users { if (*Users)[i].ID == userID { return i } } return -1 } func SearchUserByUsername(username string) int { for i := range *Users { if (*Users)[i].Username == username { return i } } return -1 } func VerifyUserLoggedIn(tokenString string) (bool, string, error) { // TODO renew JWT from time to time preventing expiry if tokenString == "" { // if no tokenString("Cookie") exists fail return false, "-1", errors.New(errorhelpers.ERR_INVALID_TOKEN) } hmacSampleSecret := []byte(configutils.Conf.CryptoKey) // crypto key for JWT encryption token, _ := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) } return hmacSampleSecret, nil }) if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { // if token is valid if userID, ok := claims["userid"].(string); ok { // extract userID sliceID := SearchUser(userID) // verify that user has a session on the server if sliceID != -1 { // searchUser returns -1 if there's no such user return true, userID, nil // logged in, TODO: "0" template comparision dynamic } else { return false, "-1", errors.New(errorhelpers.ERR_SESSION_TIMED_OUT) // Session probably expired - may also be faked? TODO more checks? } } else { return false, "-1", errors.New("Unknown error") // This should never happen, prolly can't convert something in claims then.. } } else { return false, "-1", errors.New(errorhelpers.ERR_INVALID_TOKEN) // Token is invalid, expired or whatever, TODO switch with ERR_SESSION_TIMED_OUT when database based session system } } func AuthHandler(ctx *iris.Context) { tokenString := ctx.GetCookie("token") isAuthed, userID, err := VerifyUserLoggedIn(tokenString) if isAuthed { ctx.Set("userID", userID) // save userID for in-context use userArrayID := SearchUser(userID) params := ctx.Get("params").(map[string]string) params["username"] = (*Users)[userArrayID].Username params["admin"] = (*Users)[userArrayID].Admin // TODO rename to isAdmin ? ctx.Set("params", params) } errorhelpers.HandleError(err, ctx) // if error, show error, otherwise next middleware } func CanBeAuthedHandler(ctx *iris.Context) { tokenString := ctx.GetCookie("token") isAuthed, userID, err := VerifyUserLoggedIn(tokenString) if isAuthed { ctx.Set("userID", userID) // save userID for in-context use } else if err != nil { if !((err.Error() != "ERR_SESSION_TIMED_OUT") || (err.Error() != "ERR_INVALID_TOKEN")) { // ignore ERR_SESSION_TIMED_OUT and ERR_INVALID_TOKEN errorhelpers.HandleError(err, ctx) return } } ctx.Next() // authed users can now use their accounts, next handler } func AdminHandler(ctx *iris.Context) { userID := ctx.GetString("userID") user, err := GetUser(userID) if user.Admin != "1" { // check if user is admin err = errors.New("User no Admin: " + userID) fmt.Println(err.Error()) ctx.Redirect("/") return } else { ctx.Next() } } func GenerateTokens(numTokens int) ([]string, error) { const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" tokens := make([]string, 0) dbTokens := make([][]string, 0) for i := 0; i < numTokens; i++ { b := make([]byte, 16) for i := range b { b[i] = letterBytes[rand.Intn(len(letterBytes))] } tokens = append(tokens, string(b)) dbTokens = [][]string{[]string{"value", string(b)}, []string{"used", "0"}} err := databaseutils.DBUtil.InsertRow("tokens", dbTokens) if err != nil { return []string{""}, err } } return tokens, nil } func GetTokens(used bool) []string { dbTokens, err := databaseutils.DBUtil.GetRows("*", "tokens", "used", "0") // get unused tokens if used { dbTokens, err = databaseutils.DBUtil.GetRows("*", "tokens", "used", "1") // get used tokens } if err != nil { fmt.Println(err.Error()) // TODO: nicer / outsource } tokens := make([]string, 0) for i, _ := range dbTokens { tokens = append(tokens, dbTokens[i][1]) } return tokens } func GetTokensAsString(used bool) string { tokens := GetTokens(used) ret := "" for i := range tokens { ret += fmt.Sprintf("%s\n", tokens[i]) } return ret } func GetUser(userID string) (User, error) { usersArrayID := SearchUser(userID) if usersArrayID == -1 { // TODO check if unneccessary (AuthHandler) (Ddepends on where used: TODO CHECK) return User{}, errors.New("User not logged in") } user := (*Users)[usersArrayID] // user must be logged in to do this -> get from users list return user, nil } func GetUserFromDB(userID string) (User, error) { row, err := databaseutils.DBUtil.GetRow("*", "users", "id", userID) // get user from db if err != nil { return User{}, err } return User{row[0], row[1], string(row[2]), string(row[3]), string(row[4])}, nil } func SearchUserByUsernameInDB(username string) int { user, err := databaseutils.DBUtil.GetRow("*", "users", "username", username) if err != nil { if err.Error() != "ERR_EMPTY_RESULT" { fmt.Println(err.Error()) } return -1 } userID, err := strconv.Atoi(user[0]) if err != nil { fmt.Println(err.Error()) } return userID } func SearchUserByTokenInDB(token string) (int, error) { user, err := databaseutils.DBUtil.GetRowsDoubleCond("users", "tokens", "`token-id` = `tokens`.`id`", "`tokens`.`value` = '" + token + "'") if err != nil { return -1, err } userID, err := strconv.Atoi(user[0][0]) if err != nil { return -1, err } return userID, nil } func RegisterUserWithToken(username string, password string, token string) error { tokenID := databaseutils.DBUtil.GetString("id", "tokens", "value", token) user := [][]string{[]string{"username", username}, []string{"password", password}, []string{"admin", "0"}, []string{"token-id", tokenID}} err := databaseutils.DBUtil.InsertRow("users", user) if err != nil { fmt.Println(err.Error()) return err } err = databaseutils.DBUtil.UpdateRow("tokens", "value", token, [][]string{[]string{"used", "1"}}) if err != nil { fmt.Println(err.Error()) return err } return nil } func VerifyUserUpdate(username string, password string, userID string) error { tmpUser, err := GetUserFromDB(userID) if err != nil { return err } if SearchUserByUsernameInDB(username) != -1 && username != tmpUser.Username { // username can't be changed as there already exists a user with that name or it's the old name return errors.New(errorhelpers.ERR_USERNAME_TAKEN) } if username == "" { // if not left empty change return errors.New(errorhelpers.ERR_INVALID_PARAM) } if password == "" { // if not left empty we change it return errors.New(errorhelpers.ERR_INVALID_PARAM) } return nil } // Processes the update of an user, username and password are the new "wanted" values, can also be empty string ("") func UserUpdateProcessor(username string, password string, userID string) error { user, err := GetUserFromDB(userID) hashedPassword := "" if err != nil { return err } if username == "" { username = user.Username } if password == "" { password = user.Password hashedPassword = user.Password // we dont need to / _can't_ re-hash the hash } if err = VerifyUserUpdate(username, password, userID); err != nil { return err } if hashedPassword == "" { hashedPassword, err = func (hashedPassword []byte, err error) (string, error) { // hash password, we use an anonymous function to convert int to string if err != nil { // should never happen return "", err } return string(hashedPassword), nil }(bcrypt.GenerateFromPassword([]byte(password), 15)) // this is the actual hashing call } user.Username = username user.Password = hashedPassword if err = user.Update(); err != nil { return err } return nil } func IsTokenUsed(tokens []string, token string) bool { usedToken := false for i, _ := range tokens { if token == tokens[i] { usedToken = true break } } return usedToken } func RegisterHandler(ctx *iris.Context) { token := ctx.FormValueString("token") // POST values from login form username := ctx.FormValueString("username") password := ctx.FormValueString("password") unusedTokens := GetTokens(false) // get all unused tokens usedTokens := GetTokens(true) // get all used tokens unusedToken := IsTokenUsed(unusedTokens, token) // check if token is unused usedToken := IsTokenUsed(usedTokens, token) // check if token is used if !unusedToken && !usedToken { // token doesnt exist err := errors.New(errorhelpers.ERR_INVALID_TOKEN) // TODO rename this to differ from cookie-token? errorhelpers.HandleError(err, ctx) return } tokenUserID, err := SearchUserByTokenInDB(token) if err != nil { // id of user, we're going to change if exists if err.Error() != "ERR_EMPTY_RESULT" { // if no user found for that token let them register errorhelpers.HandleError(err, ctx) return } } tokenUserIDStr := strconv.FormatInt(int64(tokenUserID), 10) user := User{} // new user if tokenUserIDStr == "-1" { // register a new account passwordBin, _ := bcrypt.GenerateFromPassword([]byte(password), 15) // hash password err := RegisterUserWithToken(username, string(passwordBin), token) // register user if err != nil { errorhelpers.HandleError(err, ctx) return } tokenString, err := user.Login(username, password) // try to login if err != nil { errorhelpers.HandleError(err, ctx) } else { ctx.SetCookieKV("token", tokenString) // set tokenString as cookie err = errors.New(errorhelpers.SUCCESS_REGISTER) errorhelpers.HandleError(err, ctx) } } else { // used token -> update if err := UserUpdateProcessor(username, password, tokenUserIDStr); err != nil { // simply try to update errorhelpers.HandleError(err, ctx) return } else { user.Logout(tokenUserIDStr) // log user out from system err = errors.New(errorhelpers.SUCCESS_UPDATE) errorhelpers.HandleError(err, ctx) } } }