func authenticate(c *bitmonster.Context) (err error) { // Get the socket. s := c.Socket() // Reset the session authentication values if an error occurs. defer func() { if err == nil { return } // Reset the socket authentication values. resetAuthSocketValue(s) }() // Obtain the authentication data from the context. authData := struct { Token string `json:"token"` Fingerprint string `json:"fingerprint"` }{} err = c.Decode(&authData) if err != nil { return err } if len(authData.Token) == 0 || len(authData.Fingerprint) == 0 { return fmt.Errorf("invalid authentication data") } // Parse the authentication data. userID, key, token, err := parseAuthToken(authData.Token) if err != nil { return err } // Obtain the user. user, err := GetUser(userID) if err != nil { return err } // Debug log. log.L.WithFields(logrus.Fields{ "remoteAddress": s.RemoteAddr(), "user": user.Username, "userID": user.ID, }).Debugf("auth: authentication request") // Check if the authenticated sessions map is nil. if user.AuthSessions == nil { return fmt.Errorf("invalid authentication data") } // Obtain the authenticated session value. as, ok := user.AuthSessions[key] if !ok { return fmt.Errorf("invalid authentication data") } // Get the current time. timeNow := time.Now() // Check if the maximum age is reached. if as.Created.Add(authSessionMaxAge).Before(timeNow) { return fmt.Errorf("authenticated user session expired: max age reached") } // Compare the tokens in a constant time span. if subtle.ConstantTimeCompare([]byte(as.Token), []byte(token)) != 1 { return fmt.Errorf("invalid authentication token") } // Compare the fingerprints. err = comparePasswordHash(as.Fingerprint, authData.Fingerprint) if err != nil { return fmt.Errorf("invalid fingerprint: %v", err) } // Hint: Authentication success. // ----------------------------- // Update the timestamps. user.LastLogin = timeNow as.LastAuth = timeNow // If the token is expired, then create a new token. // This ensures an authentication token rotation. var authToken string if as.TokenCreated.Add(authSessionTokenLifetime).Before(timeNow) { // Create a new random token. as.Token = utils.RandomString(authSessionTokenLength) as.TokenCreated = timeNow // Create a new encrypted authentication token. authToken, err = newAuthToken(user.ID, key, as.Token) if err != nil { return err } // Debug log. log.L.WithFields(logrus.Fields{ "remoteAddress": s.RemoteAddr(), "user": user.Username, "userID": user.ID, }).Debugf("auth: authentication token rotated") } // Update the user in the database. err = UpdateUser(user) if err != nil { return err } // Get or create the auth socket value. av := getAuthSocketValue(s) if av == nil { return fmt.Errorf("failed to create auth socket value for socket") } // Stop the logout timer if present. if av.reauthTimer != nil { av.reauthTimer.Stop() } // Update the auth socket value. av.isAuth = true av.authSessionKey = key av.userID = user.ID // Clear the cache. clearCache(s) // Create the respond data. data := struct { Token string `json:"token"` User *User `json:"user"` }{ Token: authToken, User: user, } // Set it. c.Data(data) // Debug log. log.L.WithFields(logrus.Fields{ "remoteAddress": s.RemoteAddr(), "user": user.Username, "userID": user.ID, }).Debugf("auth: authentication success") return nil }
func login(c *bitmonster.Context) error { // Obtain the authentication data from the context. loginData := struct { Username string `json:"username"` Password string `json:"password"` Fingerprint string `json:"fingerprint"` }{} err := c.Decode(&loginData) if err != nil { return err } if len(loginData.Username) == 0 || len(loginData.Password) == 0 || len(loginData.Fingerprint) == 0 { c.Error("invalid login credentials") return nil } // Get the user by the username. user, err := GetUserByUsername(loginData.Username) if err != nil { c.Error("invalid login credentials") return nil } // Compare the password. if match := user.ComparePasswords(loginData.Password); !match { c.Error("invalid login credentials") return nil } // Update the last login timestamp. timeNow := time.Now() user.LastLogin = timeNow // Handle the fingerprint like a password. fingerprint, err := hashPassword(loginData.Fingerprint) if err != nil { return err } // Create the map if nil. if user.AuthSessions == nil { user.AuthSessions = make(AuthSessions) } // Remove the oldest sessions if the maximum count of sessions is reached. // Included endless-loop prevention. Just to be sure. for i := 0; i < 100 && len(user.AuthSessions) >= maxAuthSessions; i++ { minTime := timeNow var minKey string for key, as := range user.AuthSessions { if as.LastAuth.Before(minTime) { minTime = as.LastAuth minKey = key } } delete(user.AuthSessions, minKey) } // Create a new authenticated session. as := &AuthSession{ Fingerprint: fingerprint, Token: utils.RandomString(authSessionTokenLength), TokenCreated: timeNow, Created: timeNow, LastAuth: timeNow, } // Create a new unique key for it. key := utils.RandomString(authSessionKeyLength) for { if _, ok := user.AuthSessions[key]; !ok { break } key = utils.RandomString(authSessionKeyLength) } // Add it to the map with the key. user.AuthSessions[key] = as // Create a new encrypted authentication token. authToken, err := newAuthToken(user.ID, key, as.Token) if err != nil { return err } // Update the user in the database. err = UpdateUser(user) if err != nil { return err } // Create the respond data. data := struct { Token string `json:"token"` }{ Token: authToken, } // Set it. c.Data(data) return nil }