From baa8eb2b935bbe65faf872dff491107b6adcf5fe Mon Sep 17 00:00:00 2001 From: Maximilian Date: Tue, 28 Feb 2023 14:54:55 -0600 Subject: [PATCH] Move to a session based system for AuthTokens --- models/session.go | 103 ++++++++++++++++++++++++++++++++++ models/user.go | 137 +++++++++++----------------------------------- 2 files changed, 136 insertions(+), 104 deletions(-) create mode 100644 models/session.go diff --git a/models/session.go b/models/session.go new file mode 100644 index 0000000..6ae7e02 --- /dev/null +++ b/models/session.go @@ -0,0 +1,103 @@ +package models + +import ( + "GoWeb/app" + "crypto/rand" + "encoding/hex" + "log" + "net/http" + "time" +) + +type Session struct { + Id int64 + UserId int64 + AuthToken string + CreatedAt time.Time +} + +// CreateSession creates a new session for a user +func CreateSession(app *app.App, w http.ResponseWriter, userId int64) (Session, error) { + session := Session{} + session.UserId = userId + session.AuthToken = generateAuthToken(app) + session.CreatedAt = time.Now() + + // If the AuthToken column for any user matches the token, set existingAuthToken to true + var existingAuthToken bool + err := app.Db.QueryRow("SELECT EXISTS(SELECT 1 FROM public.\"Session\" WHERE \"AuthToken\" = $1)", session.AuthToken).Scan(&existingAuthToken) + if err != nil { + log.Println("Error checking for existing auth token") + log.Println(err) + return Session{}, err + } + + // If duplicate token found, recursively call function until unique token is generated + if existingAuthToken == true { + log.Println("Duplicate token found in sessions table, generating new token...") + return CreateSession(app, w, userId) + } + + // Insert session into database + err = app.Db.QueryRow("INSERT INTO public.\"Session\" (\"UserId\", \"AuthToken\", \"CreatedAt\") VALUES ($1, $2, $3) RETURNING \"Id\"", session.UserId, session.AuthToken, session.CreatedAt).Scan(&session.Id) + if err != nil { + log.Println("Error inserting session into database") + return Session{}, err + } + + createSessionCookie(app, w, session) + return session, nil +} + +// Generates a random 64-byte string +func generateAuthToken(app *app.App) string { + // Generate random bytes + b := make([]byte, 64) + _, err := rand.Read(b) + if err != nil { + log.Println("Error generating random bytes") + } + + // Convert random bytes to hex string + return hex.EncodeToString(b) +} + +// createSessionCookie creates a new session cookie +func createSessionCookie(app *app.App, w http.ResponseWriter, session Session) { + cookie := &http.Cookie{ + Name: "session", + Value: session.AuthToken, + Path: "/", + MaxAge: 86400, + HttpOnly: true, + Secure: true, + } + + http.SetCookie(w, cookie) +} + +// deleteSessionCookie deletes the session cookie +func deleteSessionCookie(app *app.App, w http.ResponseWriter) { + cookie := &http.Cookie{ + Name: "session", + Value: "", + Path: "/", + MaxAge: -1, + } + + http.SetCookie(w, cookie) +} + +// DeleteSessionByAuthToken deletes a session from the database by AuthToken +func DeleteSessionByAuthToken(app *app.App, w http.ResponseWriter, authToken string) error { + // Delete session from database + _, err := app.Db.Exec("DELETE FROM public.\"Session\" WHERE \"AuthToken\" = $1", authToken) + if err != nil { + log.Println("Error deleting session from database") + return err + } + + deleteSessionCookie(app, w) + + return nil +} diff --git a/models/user.go b/models/user.go index dbb7ae2..cce5356 100644 --- a/models/user.go +++ b/models/user.go @@ -2,11 +2,9 @@ package models import ( "GoWeb/app" - "crypto/rand" "database/sql" - "encoding/hex" + "fmt" "log" - "math" "net/http" "strconv" "time" @@ -18,7 +16,6 @@ type User struct { Id int64 Username string Password string - AuthToken string CreatedAt time.Time UpdatedAt time.Time } @@ -35,7 +32,7 @@ func GetCurrentUser(app *app.App, r *http.Request) (User, error) { var userId int64 // Query row by session cookie - err = app.Db.QueryRow("SELECT \"Id\" FROM public.\"User\" WHERE \"AuthToken\" = $1", cookie.Value).Scan(&userId) + err = app.Db.QueryRow("SELECT \"Id\" FROM public.\"Session\" WHERE \"AuthToken\" = $1", cookie.Value).Scan(&userId) if err != nil { log.Println("Error querying session row with session: " + cookie.Value) return User{}, err @@ -49,7 +46,7 @@ func GetUserById(app *app.App, id int64) (User, error) { user := User{} // Query row by id - row, err := app.Db.Query("SELECT \"Id\", \"Username\", \"Password\", \"AuthToken\", \"CreatedAt\", \"UpdatedAt\" FROM public.\"User\" WHERE \"Id\" = $1", id) + row, err := app.Db.Query("SELECT \"Id\", \"Username\", \"Password\", \"CreatedAt\", \"UpdatedAt\" FROM public.\"User\" WHERE \"Id\" = $1", id) if err != nil { log.Println("Error querying user row with id: " + strconv.FormatInt(id, 10)) return User{}, err @@ -63,22 +60,27 @@ func GetUserById(app *app.App, id int64) (User, error) { } }(row) - // Feed row data into user struct - row.Next() - var authToken sql.NullString - err = row.Scan(&user.Id, &user.Username, &user.Password, &authToken, &user.CreatedAt, &user.UpdatedAt) + return user, nil +} + +// GetUserByUsername finds a users table row in the database by username and returns a struct representing this row +func GetUserByUsername(app *app.App, username string) (User, error) { + user := User{} + + // Query row by username + row, err := app.Db.Query("SELECT \"Id\", \"Username\", \"Password\", \"CreatedAt\", \"UpdatedAt\" FROM public.\"User\" WHERE \"Username\" = $1", username) if err != nil { - log.Println("Error reading queried row from database") - log.Println(err) + log.Println("Error querying user row with username: " + username) return User{}, err } - // If the AuthToken column is null in the database it is handled here by setting user.authToken to an empty string - if authToken.Valid { - user.AuthToken = authToken.String - } else { - user.AuthToken = "" - } + defer func(row *sql.Rows) { + err := row.Close() + if err != nil { + log.Println("Error closing database row") + log.Println(err) + } + }(row) return user, nil } @@ -104,102 +106,30 @@ func CreateUser(app *app.App, username string, password string, createdAt time.T return GetUserById(app, lastInsertId) } -// AuthenticateUser validates the password for the specified user if it matches a session cookie is created and returned -func AuthenticateUser(app *app.App, w http.ResponseWriter, username string, password string) (string, error) { - var hashedPassword []byte +// AuthenticateUser validates the password for the specified user +func AuthenticateUser(app *app.App, w http.ResponseWriter, username string, password string) (Session, error) { + var user User - // Query row by username, scan password column - err := app.Db.QueryRow("SELECT \"Password\" FROM public.\"User\" WHERE \"Username\" = $1", username).Scan(&hashedPassword) + // Query row by username + err := app.Db.QueryRow("SELECT \"Id\", \"Username\", \"Password\", \"CreatedAt\", \"UpdatedAt\" FROM public.\"User\" WHERE \"Username\" = $1", username).Scan(&user.Id, &user.Username, &user.Password, &user.CreatedAt, &user.UpdatedAt) if err != nil { - log.Println("Unable to find row with username: " + username) + log.Println("Authentication error (user not found) for user:" + username) log.Println(err) - return "", err + return Session{}, err } + fmt.Println(user) // Validate password - err = bcrypt.CompareHashAndPassword(hashedPassword, []byte(password)) + err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) if err != nil { // Failed to validate password, doesn't match log.Println("Authentication error (incorrect password) for user:" + username) log.Println(err) - return "", err + return Session{}, err } else { - return createSessionCookie(app, w, username) + return CreateSession(app, w, user.Id) } } -// createSessionCookie creates a new session token and cookie and returns the token value -func createSessionCookie(app *app.App, w http.ResponseWriter, username string) (string, error) { - // Generate random 64 character string (alpha-numeric) - buff := make([]byte, int(math.Ceil(float64(64)/2))) - _, err := rand.Read(buff) - if err != nil { - log.Println("Error creating random buffer for session token value") - log.Println(err) - return "", err - } - str := hex.EncodeToString(buff) - token := str[:64] - - // If the auth_token column for any user matches the token, set existingAuthToken to true - var existingAuthToken bool - err = app.Db.QueryRow("SELECT EXISTS(SELECT 1 FROM public.\"User\" WHERE \"AuthToken\" = $1)", token).Scan(&existingAuthToken) - if err != nil { - log.Println("Error checking for existing auth token") - log.Println(err) - return "", err - } - - // If duplicate token found, recursively call function until unique token is generated - if existingAuthToken == true { - log.Println("Duplicate token found in sessions table") - return createSessionCookie(app, w, username) - } - - // Store token in auth_token column of the users table - _, err = app.Db.Exec("UPDATE public.\"User\" SET \"AuthToken\" = $1 WHERE \"Username\" = $2", token, username) - if err != nil { - log.Println("Error setting auth_token column in users table") - log.Println(err) - return "", err - } - - // Create session cookie, containing token - cookie := &http.Cookie{ - Name: "session", - Value: token, - Path: "/", - MaxAge: 86400, - HttpOnly: true, - Secure: true, - } - - http.SetCookie(w, cookie) - - return token, nil -} - -// ValidateSessionCookie validates the session cookie and returns the username of the user if valid -func ValidateSessionCookie(app *app.App, r *http.Request) (string, error) { - // Get cookie from request - cookie, err := r.Cookie("session") - if err != nil { - log.Println("Error getting cookie from request") - log.Println(err) - return "", err - } - - // Query row by token - var username string - err = app.Db.QueryRow("SELECT \"Username\" FROM public.\"User\" WHERE \"AuthToken\" = $1", cookie.Value).Scan(&username) - if err != nil { - log.Println("Error querying row by token") - log.Println(err) - return "", err - } - - return username, nil -} - // LogoutUser deletes the session cookie and token from the database func LogoutUser(app *app.App, w http.ResponseWriter, r *http.Request) { // Get cookie from request @@ -211,10 +141,9 @@ func LogoutUser(app *app.App, w http.ResponseWriter, r *http.Request) { } // Set token to empty string - sqlStatement := "UPDATE public.\"User\" SET \"AuthToken\" = $1 WHERE \"AuthToken\" = $2" - _, err = app.Db.Exec(sqlStatement, "", cookie.Value) + err = DeleteSessionByAuthToken(app, w, cookie.Value) if err != nil { - log.Println("Error setting auth_token column in users table") + log.Println("Error deleting session by auth token") log.Println(err) return }