| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485 | package auth/*ArOZ Online Authentication Moduleauthor: tobychuiThis system make use of sessions (similar to PHP SESSION) to remember the user login.See https://gowebexamples.com/sessions/ for detail.Auth database are stored as the following keyauth/login/{username}/passhash => hashed passwordauth/login/{username}/permission => permission level (wip)Other system variables related to authauth/users/usercount => Number of users in the systemPre-requirement: imuslab.com/arozos/mod/database*/import (	"crypto/sha512"	"errors"	"net/http"	"strings"	"sync"	//"encoding/json"	"encoding/hex"	"log"	"time"	"github.com/gorilla/sessions"	"imuslab.com/arozos/mod/auth/authlogger"	db "imuslab.com/arozos/mod/database")type AuthAgent struct {	//Session related	SessionName             string	SessionStore            *sessions.CookieStore	Database                *db.Database	LoginRedirectionHandler func(http.ResponseWriter, *http.Request)	//Token related	ExpireTime             int64 //Set this to 0 to disable token access	tokenStore             sync.Map	terminateTokenListener chan bool	mutex                  *sync.Mutex	//Autologin Related	AllowAutoLogin  bool	autoLoginTokens []AutoLoginToken	//Logger	Logger *authlogger.Logger}type AuthEndpoints struct {	Login         string	Logout        string	Register      string	CheckLoggedIn string	Autologin     string}//Constructorfunc NewAuthenticationAgent(sessionName string, key []byte, sysdb *db.Database, allowReg bool, loginRedirectionHandler func(http.ResponseWriter, *http.Request)) *AuthAgent {	store := sessions.NewCookieStore(key)	err := sysdb.NewTable("auth")	if err != nil {		log.Println("Failed to create auth database. Terminating.")		panic(err)	}	//Creat a ticker to clean out outdated token every 5 minutes	ticker := time.NewTicker(300 * time.Second)	done := make(chan bool)	//Create a new logger for logging all login request	newLogger, err := authlogger.NewLogger()	if err != nil {		panic(err)	}	//Create a new AuthAgent object	newAuthAgent := AuthAgent{		SessionName:             sessionName,		SessionStore:            store,		Database:                sysdb,		LoginRedirectionHandler: loginRedirectionHandler,		tokenStore:              sync.Map{},		ExpireTime:              120,		terminateTokenListener:  done,		mutex:                   &sync.Mutex{},		AllowAutoLogin:          false,		autoLoginTokens:         []AutoLoginToken{},		Logger:                  newLogger,	}	//Create a timer to listen to its token storage	go func(listeningAuthAgent *AuthAgent) {		for {			select {			case <-done:				return			case <-ticker.C:				listeningAuthAgent.ClearTokenStore()			}		}	}(&newAuthAgent)	//Return the authAgent	return &newAuthAgent}//Close the authAgent listenerfunc (a *AuthAgent) Close() {	//Stop the token listening	a.terminateTokenListener <- true	//Close the auth logger database	a.Logger.Close()}//This function will handle an http request and redirect to the given login address if not logged infunc (a *AuthAgent) HandleCheckAuth(w http.ResponseWriter, r *http.Request, handler func(http.ResponseWriter, *http.Request)) {	if a.CheckAuth(r) {		//User already logged in		handler(w, r)	} else {		//User not logged in		a.LoginRedirectionHandler(w, r)	}}//Register APIs that requires public accessfunc (a *AuthAgent) RegisterPublicAPIs(ep AuthEndpoints) {	http.HandleFunc(ep.Login, a.HandleLogin)	http.HandleFunc(ep.Logout, a.HandleLogout)	http.HandleFunc(ep.Register, a.HandleRegister)	http.HandleFunc(ep.CheckLoggedIn, a.CheckLogin)	http.HandleFunc(ep.Autologin, a.HandleAutologinTokenLogin)}//Handle login request, require POST username and passwordfunc (a *AuthAgent) HandleLogin(w http.ResponseWriter, r *http.Request) {	//Get username from request using POST mode	username, err := mv(r, "username", true)	if err != nil {		//Username not defined		log.Println("[System Auth] Someone trying to login with username: " + username)		//Write to log		a.Logger.LogAuth(r, false)		sendErrorResponse(w, "Username not defined or empty.")		return	}	//Get password from request using POST mode	password, err := mv(r, "password", true)	if err != nil {		//Password not defined		a.Logger.LogAuth(r, false)		sendErrorResponse(w, "Password not defined or empty.")		return	}	//Get rememberme settings	rememberme := false	rmbme, _ := mv(r, "rmbme", true)	if rmbme == "true" {		rememberme = true	}	//Check the database and see if this user is in the database	passwordCorrect := a.ValidateUsernameAndPassword(username, password)	//The database contain this user information. Check its password if it is correct	if passwordCorrect {		//Password correct		// Set user as authenticated		a.LoginUserByRequest(w, r, username, rememberme)		//Print the login message to console		log.Println(username + " logged in.")		a.Logger.LogAuth(r, true)		sendOK(w)	} else {		//Password incorrect		log.Println(username + " has entered an invalid username or password")		sendErrorResponse(w, "Invalid username or password")		a.Logger.LogAuth(r, false)		return	}}func (a *AuthAgent) ValidateUsernameAndPassword(username string, password string) bool {	hashedPassword := Hash(password)	var passwordInDB string	err := a.Database.Read("auth", "passhash/"+username, &passwordInDB)	if err != nil {		//User not found or db exception		//log.Println("[System Auth] " + username + " login with incorrect password")		return false	}	if passwordInDB == hashedPassword {		return true	} else {		return false	}}func (a *AuthAgent) LoginUserByRequest(w http.ResponseWriter, r *http.Request, username string, rememberme bool) {	session, _ := a.SessionStore.Get(r, a.SessionName)	session.Values["authenticated"] = true	session.Values["username"] = username	session.Values["rememberMe"] = rememberme	//Check if remember me is clicked. If yes, set the maxage to 1 week.	if rememberme == true {		session.Options = &sessions.Options{			MaxAge: 3600 * 24 * 7, //One week			Path:   "/",		}	} else {		session.Options = &sessions.Options{			MaxAge: 3600 * 1, //One hour			Path:   "/",		}	}	session.Save(r, w)}//Handle logout, reply OK after logged out. WILL NOT DO REDIRECTIONfunc (a *AuthAgent) HandleLogout(w http.ResponseWriter, r *http.Request) {	username, _ := a.GetUserName(w, r)	if username != "" {		log.Println(username + " logged out.")	}	// Revoke users authentication	err := a.Logout(w, r)	if err != nil {		sendErrorResponse(w, "Logout failed")		return	}	w.Write([]byte("OK"))}func (a *AuthAgent) Logout(w http.ResponseWriter, r *http.Request) error {	session, err := a.SessionStore.Get(r, a.SessionName)	if err != nil {		return err	}	session.Values["authenticated"] = false	session.Values["username"] = nil	session.Save(r, w)	return nil}//Get the current session username from requestfunc (a *AuthAgent) GetUserName(w http.ResponseWriter, r *http.Request) (string, error) {	if a.CheckAuth(r) {		//This user has logged in.		session, _ := a.SessionStore.Get(r, a.SessionName)		return session.Values["username"].(string), nil	} else {		//This user has not logged in.		return "", errors.New("User not logged in")	}}//Check if the user has logged in, return true / false in JSONfunc (a *AuthAgent) CheckLogin(w http.ResponseWriter, r *http.Request) {	if a.CheckAuth(r) != false {		sendJSONResponse(w, "true")	} else {		sendJSONResponse(w, "false")	}}//Handle new user register. Require POST username, password, group.func (a *AuthAgent) HandleRegister(w http.ResponseWriter, r *http.Request) {	userCount := a.GetUserCounts()	//Get username from request	newusername, err := mv(r, "username", true)	if err != nil {		sendTextResponse(w, "Error. Missing 'username' paramter")		return	}	//Get password from request	password, err := mv(r, "password", true)	if err != nil {		sendTextResponse(w, "Error. Missing 'password' paramter")		return	}	//Set permission group to default	group, err := mv(r, "group", true)	if err != nil {		sendTextResponse(w, "Error. Missing 'group' paramter")		return	}	//Check if the number of users in the system is == 0. If yes, there are no need to login before registering new user	if userCount > 0 {		//Require login to create new user		if a.CheckAuth(r) == false {			//System have more than one person and this user is not logged in			sendErrorResponse(w, "Login is needed to create new user")			return		}	}	//Ok to proceed create this user	err = a.CreateUserAccount(newusername, password, []string{group})	if err != nil {		sendErrorResponse(w, err.Error())		return	}	//Return to the client with OK	sendOK(w)	log.Println("[System Auth] New user " + newusername + " added to system.")	return}//Check authentication from request header's session valuefunc (a *AuthAgent) CheckAuth(r *http.Request) bool {	session, _ := a.SessionStore.Get(r, a.SessionName)	// Check if user is authenticated	if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {		return false	}	return true}//Handle de-register of users. Require POST username.//THIS FUNCTION WILL NOT CHECK FOR PERMISSION. PLEASE USE WITH PERMISSION HANDLERfunc (a *AuthAgent) HandleUnregister(w http.ResponseWriter, r *http.Request) {	//Check if the user is logged in	if a.CheckAuth(r) == false {		//This user has not logged in		sendErrorResponse(w, "Login required to remove user from the system.")		return	}	//Check for permission of this user.	/*		if !system_permission_checkUserIsAdmin(w,r){			//This user is not admin. No permission to access this function			sendErrorResponse(w, "Permission denied")		}	*/	//Get username from request	username, err := mv(r, "username", true)	if err != nil {		sendErrorResponse(w, "Missing 'username' paramter")		return	}	err = a.UnregisterUser(username)	if err != nil {		sendErrorResponse(w, err.Error())		return	}	//Return to the client with OK	sendOK(w)	log.Println("[system_auth] User " + username + " has been removed from the system.")	return}func (a *AuthAgent) UnregisterUser(username string) error {	//Check if the user exists in the system database.	if !a.Database.KeyExists("auth", "passhash/"+username) {		//This user do not exists.		return errors.New("This user does not exists.")	}	//OK! Remove the user from the database	a.Database.Delete("auth", "passhash/"+username)	a.Database.Delete("auth", "group/"+username)	a.Database.Delete("auth", "acstatus/"+username)	a.Database.Delete("auth", "profilepic/"+username)	//Remove the user's autologin tokens	a.RemoveAutologinTokenByUsername(username)	return nil}//Get the number of users in the systemfunc (a *AuthAgent) GetUserCounts() int {	entries, _ := a.Database.ListTable("auth")	usercount := 0	for _, keypairs := range entries {		if strings.Contains(string(keypairs[0]), "passhash/") {			//This is a user registry			usercount++		}	}	if usercount == 0 {		log.Println("There are no user in the database.")	}	return usercount}//List all username within the systemfunc (a *AuthAgent) ListUsers() []string {	entries, _ := a.Database.ListTable("auth")	results := []string{}	for _, keypairs := range entries {		if strings.Contains(string(keypairs[0]), "group/") {			username := strings.Split(string(keypairs[0]), "/")[1]			results = append(results, username)		}	}	return results}//Check if the given username existsfunc (a *AuthAgent) UserExists(username string) bool {	userpasswordhash := ""	err := a.Database.Read("auth", "passhash/"+username, &userpasswordhash)	if err != nil || userpasswordhash == "" {		return false	}	return true}//Update the session expire time given the request header.func (a *AuthAgent) UpdateSessionExpireTime(w http.ResponseWriter, r *http.Request) bool {	session, _ := a.SessionStore.Get(r, a.SessionName)	if session.Values["authenticated"].(bool) == true {		//User authenticated. Extend its expire time		rememberme := session.Values["rememberMe"].(bool)		//Extend the session expire time		if rememberme == true {			session.Options = &sessions.Options{				MaxAge: 3600 * 24 * 7, //One week				Path:   "/",			}		} else {			session.Options = &sessions.Options{				MaxAge: 3600 * 1, //One hour				Path:   "/",			}		}		session.Save(r, w)		return true	} else {		return false	}}//Create user accountfunc (a *AuthAgent) CreateUserAccount(newusername string, password string, group []string) error {	key := newusername	hashedPassword := Hash(password)	err := a.Database.Write("auth", "passhash/"+key, hashedPassword)	if err != nil {		return err	}	//Store this user's usergroup settings	err = a.Database.Write("auth", "group/"+newusername, group)	if err != nil {		return err	}	return nil}//Hash the given raw string into sha512 hashfunc Hash(raw string) string {	h := sha512.New()	h.Write([]byte(raw))	return hex.EncodeToString(h.Sum(nil))}
 |