mirror of
https://github.com/ngoduykhanh/wireguard-ui.git
synced 2025-05-04 22:12:20 +03:00
Further session protections and fixes
Use MaxAge instead of Expires Verify if the cookie is not too old and not from the future Verify if the user exists and unchanged Refresh not sooner than 24h Do not refresh temporary sessions Delete cookies on logout
This commit is contained in:
parent
91427427f2
commit
bee5c54127
5 changed files with 156 additions and 7 deletions
|
@ -94,10 +94,8 @@ func Login(db store.IStore) echo.HandlerFunc {
|
||||||
|
|
||||||
if userCorrect && passwordCorrect {
|
if userCorrect && passwordCorrect {
|
||||||
ageMax := 0
|
ageMax := 0
|
||||||
expiration := time.Now().Add(24 * time.Hour)
|
|
||||||
if rememberMe {
|
if rememberMe {
|
||||||
ageMax = 86400 * 7
|
ageMax = 86400 * 7
|
||||||
expiration = time.Now().Add(time.Duration(ageMax) * time.Second)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cookiePath := util.BasePath
|
cookiePath := util.BasePath
|
||||||
|
@ -116,8 +114,11 @@ func Login(db store.IStore) echo.HandlerFunc {
|
||||||
// set session_token
|
// set session_token
|
||||||
tokenUID := xid.New().String()
|
tokenUID := xid.New().String()
|
||||||
sess.Values["username"] = dbuser.Username
|
sess.Values["username"] = dbuser.Username
|
||||||
|
sess.Values["user_hash"] = util.GetDBUserCRC32(dbuser)
|
||||||
sess.Values["admin"] = dbuser.Admin
|
sess.Values["admin"] = dbuser.Admin
|
||||||
sess.Values["session_token"] = tokenUID
|
sess.Values["session_token"] = tokenUID
|
||||||
|
sess.Values["max_age"] = ageMax
|
||||||
|
sess.Values["last_update"] = time.Now().UTC().Unix()
|
||||||
sess.Save(c.Request(), c.Response())
|
sess.Save(c.Request(), c.Response())
|
||||||
|
|
||||||
// set session_token in cookie
|
// set session_token in cookie
|
||||||
|
@ -125,7 +126,7 @@ func Login(db store.IStore) echo.HandlerFunc {
|
||||||
cookie.Name = "session_token"
|
cookie.Name = "session_token"
|
||||||
cookie.Path = cookiePath
|
cookie.Path = cookiePath
|
||||||
cookie.Value = tokenUID
|
cookie.Value = tokenUID
|
||||||
cookie.Expires = expiration
|
cookie.MaxAge = ageMax
|
||||||
cookie.HttpOnly = true
|
cookie.HttpOnly = true
|
||||||
cookie.SameSite = http.SameSiteLaxMode
|
cookie.SameSite = http.SameSiteLaxMode
|
||||||
c.SetCookie(cookie)
|
c.SetCookie(cookie)
|
||||||
|
@ -266,7 +267,7 @@ func UpdateUser(db store.IStore) echo.HandlerFunc {
|
||||||
log.Infof("Updated user information successfully")
|
log.Infof("Updated user information successfully")
|
||||||
|
|
||||||
if previousUsername == currentUser(c) {
|
if previousUsername == currentUser(c) {
|
||||||
setUser(c, user.Username, user.Admin)
|
setUser(c, user.Username, user.Admin, util.GetDBUserCRC32(user))
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Updated user information successfully"})
|
return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Updated user information successfully"})
|
||||||
|
|
|
@ -25,6 +25,7 @@ func ValidSession(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidSession middleware must be used before RefreshSession
|
||||||
func RefreshSession(next echo.HandlerFunc) echo.HandlerFunc {
|
func RefreshSession(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
doRefreshSession(c)
|
doRefreshSession(c)
|
||||||
|
@ -50,28 +51,67 @@ func isValidSession(c echo.Context) bool {
|
||||||
if err != nil || sess.Values["session_token"] != cookie.Value {
|
if err != nil || sess.Values["session_token"] != cookie.Value {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check time bounds
|
||||||
|
lastUpdate := getLastUpdate(sess)
|
||||||
|
maxAge := getMaxAge(sess)
|
||||||
|
// Temporary session is considered valid within 24h if browser is not closed before
|
||||||
|
// This value is not saved and is used as virtual expiration
|
||||||
|
if maxAge == 0 {
|
||||||
|
maxAge = 86400
|
||||||
|
}
|
||||||
|
expiration := lastUpdate + int64(maxAge)
|
||||||
|
now := time.Now().UTC().Unix()
|
||||||
|
if lastUpdate > now || expiration < now {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user still exists and unchanged
|
||||||
|
username := fmt.Sprintf("%s", sess.Values["username"])
|
||||||
|
userHash := getUserHash(sess)
|
||||||
|
if uHash, ok := util.DBUsersToCRC32[username]; !ok || userHash != uHash {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Refreshes a "remember me" session when the user visits web pages (not API)
|
||||||
|
// Session must be valid before calling this function
|
||||||
|
// Refresh is performet at most once per 24h
|
||||||
func doRefreshSession(c echo.Context) {
|
func doRefreshSession(c echo.Context) {
|
||||||
if util.DisableLogin {
|
if util.DisableLogin {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sess, _ := session.Get("session", c)
|
sess, _ := session.Get("session", c)
|
||||||
|
maxAge := getMaxAge(sess)
|
||||||
|
if maxAge <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
oldCookie, err := c.Cookie("session_token")
|
oldCookie, err := c.Cookie("session_token")
|
||||||
if err != nil || sess.Values["session_token"] != oldCookie.Value {
|
if err != nil || sess.Values["session_token"] != oldCookie.Value {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Refresh no sooner than 24h
|
||||||
|
lastUpdate := getLastUpdate(sess)
|
||||||
|
expiration := lastUpdate + int64(getMaxAge(sess))
|
||||||
|
now := time.Now().UTC().Unix()
|
||||||
|
if expiration < now || now-lastUpdate < 86400 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
cookiePath := util.BasePath
|
cookiePath := util.BasePath
|
||||||
if cookiePath == "" {
|
if cookiePath == "" {
|
||||||
cookiePath = "/"
|
cookiePath = "/"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sess.Values["last_update"] = now
|
||||||
sess.Options = &sessions.Options{
|
sess.Options = &sessions.Options{
|
||||||
Path: cookiePath,
|
Path: cookiePath,
|
||||||
MaxAge: sess.Options.MaxAge,
|
MaxAge: maxAge,
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
SameSite: http.SameSiteLaxMode,
|
SameSite: http.SameSiteLaxMode,
|
||||||
}
|
}
|
||||||
|
@ -81,12 +121,61 @@ func doRefreshSession(c echo.Context) {
|
||||||
cookie.Name = "session_token"
|
cookie.Name = "session_token"
|
||||||
cookie.Path = cookiePath
|
cookie.Path = cookiePath
|
||||||
cookie.Value = oldCookie.Value
|
cookie.Value = oldCookie.Value
|
||||||
cookie.Expires = time.Now().Add(time.Duration(sess.Options.MaxAge) * time.Second)
|
cookie.MaxAge = maxAge
|
||||||
cookie.HttpOnly = true
|
cookie.HttpOnly = true
|
||||||
cookie.SameSite = http.SameSiteLaxMode
|
cookie.SameSite = http.SameSiteLaxMode
|
||||||
c.SetCookie(cookie)
|
c.SetCookie(cookie)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get time in seconds this session is valid without updating
|
||||||
|
func getMaxAge(sess *sessions.Session) int {
|
||||||
|
if util.DisableLogin {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
maxAge := sess.Values["max_age"]
|
||||||
|
|
||||||
|
switch typedMaxAge := maxAge.(type) {
|
||||||
|
case int:
|
||||||
|
return typedMaxAge
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a timestamp in seconds of the last session update
|
||||||
|
func getLastUpdate(sess *sessions.Session) int64 {
|
||||||
|
if util.DisableLogin {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
lastUpdate := sess.Values["last_update"]
|
||||||
|
|
||||||
|
switch typedLastUpdate := lastUpdate.(type) {
|
||||||
|
case int64:
|
||||||
|
return typedLastUpdate
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get CRC32 of a user at the moment of log in
|
||||||
|
// Any changes to user will result in logout of other (not updated) sessions
|
||||||
|
func getUserHash(sess *sessions.Session) uint32 {
|
||||||
|
if util.DisableLogin {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
userHash := sess.Values["user_hash"]
|
||||||
|
|
||||||
|
switch typedUserHash := userHash.(type) {
|
||||||
|
case uint32:
|
||||||
|
return typedUserHash
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// currentUser to get username of logged in user
|
// currentUser to get username of logged in user
|
||||||
func currentUser(c echo.Context) string {
|
func currentUser(c echo.Context) string {
|
||||||
if util.DisableLogin {
|
if util.DisableLogin {
|
||||||
|
@ -109,9 +198,10 @@ func isAdmin(c echo.Context) bool {
|
||||||
return admin == "true"
|
return admin == "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
func setUser(c echo.Context, username string, admin bool) {
|
func setUser(c echo.Context, username string, admin bool, userCRC32 uint32) {
|
||||||
sess, _ := session.Get("session", c)
|
sess, _ := session.Get("session", c)
|
||||||
sess.Values["username"] = username
|
sess.Values["username"] = username
|
||||||
|
sess.Values["user_hash"] = userCRC32
|
||||||
sess.Values["admin"] = admin
|
sess.Values["admin"] = admin
|
||||||
sess.Save(c.Request(), c.Response())
|
sess.Save(c.Request(), c.Response())
|
||||||
}
|
}
|
||||||
|
@ -120,7 +210,27 @@ func setUser(c echo.Context, username string, admin bool) {
|
||||||
func clearSession(c echo.Context) {
|
func clearSession(c echo.Context) {
|
||||||
sess, _ := session.Get("session", c)
|
sess, _ := session.Get("session", c)
|
||||||
sess.Values["username"] = ""
|
sess.Values["username"] = ""
|
||||||
|
sess.Values["user_hash"] = 0
|
||||||
sess.Values["admin"] = false
|
sess.Values["admin"] = false
|
||||||
sess.Values["session_token"] = ""
|
sess.Values["session_token"] = ""
|
||||||
|
sess.Values["max_age"] = -1
|
||||||
|
sess.Options.MaxAge = -1
|
||||||
sess.Save(c.Request(), c.Response())
|
sess.Save(c.Request(), c.Response())
|
||||||
|
|
||||||
|
cookiePath := util.BasePath
|
||||||
|
if cookiePath == "" {
|
||||||
|
cookiePath = "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
cookie, err := c.Cookie("session_token")
|
||||||
|
if err != nil {
|
||||||
|
cookie = new(http.Cookie)
|
||||||
|
}
|
||||||
|
|
||||||
|
cookie.Name = "session_token"
|
||||||
|
cookie.Path = cookiePath
|
||||||
|
cookie.MaxAge = -1
|
||||||
|
cookie.HttpOnly = true
|
||||||
|
cookie.SameSite = http.SameSiteLaxMode
|
||||||
|
c.SetCookie(cookie)
|
||||||
}
|
}
|
||||||
|
|
|
@ -163,6 +163,14 @@ func (o *JsonDB) Init() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// init cache
|
// init cache
|
||||||
|
for _, i := range results {
|
||||||
|
user := model.User{}
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(i), &user); err == nil {
|
||||||
|
util.DBUsersToCRC32[user.Username] = util.GetDBUserCRC32(user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
clients, err := o.GetClients(false)
|
clients, err := o.GetClients(false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
|
@ -217,11 +225,13 @@ func (o *JsonDB) SaveUser(user model.User) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
util.DBUsersToCRC32[user.Username] = util.GetDBUserCRC32(user)
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteUser func to remove user from the database
|
// DeleteUser func to remove user from the database
|
||||||
func (o *JsonDB) DeleteUser(username string) error {
|
func (o *JsonDB) DeleteUser(username string) error {
|
||||||
|
delete(util.DBUsersToCRC32, username)
|
||||||
return o.conn.Delete("users", username)
|
return o.conn.Delete("users", username)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,3 +5,4 @@ import "sync"
|
||||||
var IPToSubnetRange = map[string]uint16{}
|
var IPToSubnetRange = map[string]uint16{}
|
||||||
var TgUseridToClientID = map[int64][]string{}
|
var TgUseridToClientID = map[int64][]string{}
|
||||||
var TgUseridToClientIDMutex sync.RWMutex
|
var TgUseridToClientIDMutex sync.RWMutex
|
||||||
|
var DBUsersToCRC32 = map[string]uint32{}
|
||||||
|
|
27
util/util.go
27
util/util.go
|
@ -5,6 +5,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"hash/crc32"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
@ -827,3 +828,29 @@ func filterStringSlice(s []string, excludedStr string) []string {
|
||||||
}
|
}
|
||||||
return filtered
|
return filtered
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetDBUserCRC32(dbuser model.User) uint32 {
|
||||||
|
var isAdmin byte = 0
|
||||||
|
if dbuser.Admin {
|
||||||
|
isAdmin = 1
|
||||||
|
}
|
||||||
|
return crc32.ChecksumIEEE(ConcatMultipleSlices([]byte(dbuser.Username), []byte{isAdmin}, []byte(dbuser.PasswordHash), []byte(dbuser.Password)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConcatMultipleSlices(slices ...[]byte) []byte {
|
||||||
|
var totalLen int
|
||||||
|
|
||||||
|
for _, s := range slices {
|
||||||
|
totalLen += len(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]byte, totalLen)
|
||||||
|
|
||||||
|
var i int
|
||||||
|
|
||||||
|
for _, s := range slices {
|
||||||
|
i += copy(result[i:], s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue