package db import ( "JISQueueing/common" "crypto/rand" "database/sql" "encoding/base64" "errors" "log" "golang.org/x/crypto/bcrypt" ) //LoginFailReason specifies either the reason that a login request had failed or a value to indicate that it had succeeded. type LoginFailReason uint8 //RegisterFailReason specifies either the reason that a registration request had failed or a value to indicate that it had succeeded. type RegisterFailReason uint8 //BcryptCost is the cost of the password hash we use. A larger value means that it is harder to break but takes longer to compute on the server too. const BcryptCost = 10 var ErrTokenNotFound = errors.New("the provided token does not belong to any staff member") const ( //InvalidUser indicates that the provided username could not be found in the database. InvalidUser LoginFailReason = iota //InvalidPassword indicates that the provided password was incorrect. InvalidPassword //InvalidInternalPasswordHash indicates that the validation of password hash that was stored in database had failed. InvalidInternalPasswordHash //ReloginRequest indicates that the login was successful but the user is already logged in. ReloginRequest //LoginSuccessful indicates that the login was successful. LoginSuccessful ) const ( //UsernameAlreadyExists indicates that the provided username was already registered. UsernameAlreadyExists RegisterFailReason = iota //RegisterSuccessful indicates that the registration was successful. RegisterSuccessful ) var ( staffToHash = make(map[common.Staff]string) hashToStaff = make(map[string]common.Staff) encoder = base64.StdEncoding ) //Login attempts to log a user in. It takes in username and password, processes the request and returns LoginFailReason. func Login(username string, password string) (string, LoginFailReason) { results, _ := db.Query("SELECT * FROM staff WHERE username=? COLLATE NOCASE", username) defer results.Close() //No results? if !results.Next() { return "", InvalidUser } staff := convertIntoStaff(results) result := bcrypt.CompareHashAndPassword([]byte(staff.Hash), []byte(password)) if result == bcrypt.ErrMismatchedHashAndPassword { return "", InvalidPassword } if result != nil { log.Printf("[WARN] An internal password hash is invalid: user=%s, hash=%s", username, staff.Hash) return "", InvalidInternalPasswordHash } token, loggedInAlready := newToken(staff) if loggedInAlready { return token, ReloginRequest } return token, LoginSuccessful } func Register(staff common.Staff, password string) RegisterFailReason { token, loginFailReason := Login(staff.Username, password) //Attempt to login as the user to make sure it doesn't exist. if loginFailReason == LoginSuccessful || loginFailReason == ReloginRequest { if loginFailReason == LoginSuccessful { Logout(token) } return UsernameAlreadyExists } if loginFailReason != InvalidUser { //Oops. The user already exists! return UsernameAlreadyExists } hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), BcryptCost) staff.Hash = string(hashedPassword) db.Exec("INSERT INTO staff (username, password, name, email, admin) VALUES (?, ?, ?, ?, ?)", staff.Username, staff.Hash, staff.Name, staff.Email, staff.Admin) return RegisterSuccessful } //Logout logs out the user with the token. func Logout(token string) bool { if hashToStaff[token] == (common.Staff{}) { return false } delete(staffToHash, hashToStaff[token]) delete(hashToStaff, token) return true } //GetTokenOwner returns the owner of a given token. func GetTokenOwner(token string) (common.Staff, error) { if _, ok := hashToStaff[token]; ok { return hashToStaff[token], nil } return common.Staff{}, ErrTokenNotFound } //VerifyToken checks if a given token is valid. func VerifyToken(token string) bool { _, ok := hashToStaff[token] return ok } //newToken creates or returns a previously cached token for the given staff member. The second return value is a bool that is true if the member had already been logged in previously. func newToken(staff common.Staff) (string, bool) { if staffToHash[staff] != "" { return staffToHash[staff], true } tokenString := "" ok := true //Keep looping if the token already exists. Shouldn't happen too often. for ok { token := make([]byte, 18) //18 bytes is 24 characters of base64 _, err := rand.Read(token) if err != nil { panic(err) //Having no longer random bits is worse than the service not working. } tokenString = encoder.EncodeToString(token) _, ok = hashToStaff[tokenString] } staffToHash[staff] = tokenString hashToStaff[tokenString] = staff return tokenString, false } func convertIntoStaff(rows *sql.Rows) common.Staff { var username string var hash string var name string var email string var admin bool rows.Scan(&username, &hash, &name, &email, &admin) return common.Staff{ Username: username, Name: name, Hash: hash, Admin: admin, } }