123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522 |
- package auth
- /*
- ArOZ Online Authentication Module
- author: tobychui
- This 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 key
- auth/login/{username}/passhash => hashed password
- auth/login/{username}/permission => permission level
- Other system variables related to auth
- auth/users/usercount => Number of users in the system
- Pre-requirement: imuslab.com/arozos/mod/database
- */
- import (
- "crypto/sha512"
- "errors"
- "net/http"
- "strings"
- "sync"
- "encoding/hex"
- "log"
- "time"
- "github.com/gorilla/sessions"
- "imuslab.com/arozos/mod/auth/accesscontrol/blacklist"
- "imuslab.com/arozos/mod/auth/accesscontrol/whitelist"
- "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
- //IPLists manager
- WhitelistManager *whitelist.WhiteList
- BlacklistManager *blacklist.BlackList
- //Logger
- Logger *authlogger.Logger
- }
- type AuthEndpoints struct {
- Login string
- Logout string
- Register string
- CheckLoggedIn string
- Autologin string
- }
- //Constructor
- func 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 whitelist manager
- thisWhitelistManager := whitelist.NewWhitelistManager(sysdb)
- //Create a new blacklist manager
- thisBlacklistManager := blacklist.NewBlacklistManager(sysdb)
- //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{},
- //Auto login management
- AllowAutoLogin: false,
- autoLoginTokens: []*AutoLoginToken{},
- //Blacklist management
- WhitelistManager: thisWhitelistManager,
- BlacklistManager: thisBlacklistManager,
- 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 listener
- func (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 in
- func (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)
- }
- }
- //Handle login request, require POST username and password
- func (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, rejectionReason := a.ValidateUsernameAndPasswordWithReason(username, password)
- //The database contain this user information. Check its password if it is correct
- if passwordCorrect {
- //Password correct
- //Check if this request origin is allowed to access
- ok, reasons := a.ValidateLoginRequest(w, r)
- if !ok {
- sendErrorResponse(w, reasons.Error())
- return
- }
- // 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 + " login request rejected: " + rejectionReason)
- sendErrorResponse(w, rejectionReason)
- a.Logger.LogAuth(r, false)
- return
- }
- }
- func (a *AuthAgent) ValidateUsernameAndPassword(username string, password string) bool {
- succ, _ := a.ValidateUsernameAndPasswordWithReason(username, password)
- return succ
- }
- //validate the username and password, return reasons if the auth failed
- func (a *AuthAgent) ValidateUsernameAndPasswordWithReason(username string, password string) (bool, string) {
- 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, "Invalid username or password"
- }
- if passwordInDB == hashedPassword {
- return true, ""
- } else {
- return false, "Invalid username or password"
- }
- }
- //Validate the user request for login
- func (a *AuthAgent) ValidateLoginRequest(w http.ResponseWriter, r *http.Request) (bool, error) {
- //Check if the account is whitelisted
- if a.WhitelistManager.Enabled && !a.WhitelistManager.CheckIsWhitelistedByRequest(r) {
- //Whitelist enabled but this IP is not whitelisted
- return false, errors.New("Your IP is not whitelisted on this host")
- }
- //Check if the account is banned
- if a.BlacklistManager.Enabled && a.BlacklistManager.CheckIsBannedByRequest(r) {
- //This user is banned
- return false, errors.New("Your IP is banned by this host")
- }
- return true, nil
- }
- //Login the user by creating a valid session for this user
- 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 REDIRECTION
- func (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 request
- func (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 JSON
- func (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 value
- func (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 HANDLER
- func (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 system
- func (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 system
- func (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 exists
- func (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 account
- func (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 hash
- func Hash(raw string) string {
- h := sha512.New()
- h.Write([]byte(raw))
- return hex.EncodeToString(h.Sum(nil))
- }
|