diff options
Diffstat (limited to 'handlers')
| -rw-r--r-- | handlers/admin.go | 16 | ||||
| -rw-r--r-- | handlers/auth.go | 285 | ||||
| -rw-r--r-- | handlers/proxy.go | 4 | ||||
| -rw-r--r-- | handlers/user.go | 56 |
4 files changed, 290 insertions, 71 deletions
diff --git a/handlers/admin.go b/handlers/admin.go index d9fb310..44c36a5 100644 --- a/handlers/admin.go +++ b/handlers/admin.go @@ -24,7 +24,9 @@ func AdminPageHandler(w http.ResponseWriter, r *http.Request) { "Paths": config.Paths, "Users": users, } - templates.ExecuteTemplate(w, "admin.html", data) + if err := templates.ExecuteTemplate(w, "admin.html", data); err != nil { + logging.AppLog.Error("Failed to execute admin template", "error", err) + } } // GetUsersHandler returns a list of all users. @@ -38,14 +40,16 @@ func GetUsersHandler(w http.ResponseWriter, r *http.Request) { } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(users) + if err := json.NewEncoder(w).Encode(users); err != nil { + logging.AppLog.Error("Failed to encode users to JSON", "error", err, "ip", ip) + } } // UpdateUserRoleHandler updates a user's role. func UpdateUserRoleHandler(w http.ResponseWriter, r *http.Request) { ip := logging.GetClientIP(r) // Get admin user from context - adminUser, ok := r.Context().Value(types.SessionDataContextKey).(*models.User) + adminUser, ok := r.Context().Value(types.SessionDataContextKey).(*types.User) if !ok || adminUser == nil { logging.AppLog.Error("Failed to get user from context for admin action", "ip", ip) http.Error(w, "Unauthorized", http.StatusUnauthorized) @@ -90,7 +94,7 @@ func UpdateUserRoleHandler(w http.ResponseWriter, r *http.Request) { func DeleteUserHandler(w http.ResponseWriter, r *http.Request) { ip := logging.GetClientIP(r) // Get admin user from context - adminUser, ok := r.Context().Value(types.SessionDataContextKey).(*models.User) + adminUser, ok := r.Context().Value(types.SessionDataContextKey).(*types.User) if !ok || adminUser == nil { logging.AppLog.Error("Failed to get user from context for admin action", "ip", ip) http.Error(w, "Unauthorized", http.StatusUnauthorized) @@ -119,7 +123,7 @@ func DeleteUserHandler(w http.ResponseWriter, r *http.Request) { func SetUserActiveStatusHandler(w http.ResponseWriter, r *http.Request) { ip := logging.GetClientIP(r) // Get admin user from context - adminUser, ok := r.Context().Value(types.SessionDataContextKey).(*models.User) + adminUser, ok := r.Context().Value(types.SessionDataContextKey).(*types.User) if !ok || adminUser == nil { logging.AppLog.Error("Failed to get user from context for admin action", "ip", ip) http.Error(w, "Unauthorized", http.StatusUnauthorized) @@ -152,4 +156,4 @@ func SetUserActiveStatusHandler(w http.ResponseWriter, r *http.Request) { logging.SecurityLog.Info("ADMIN ACTION", "action", "set_active_status", "performed_by", adminUsername, "target_user_id", id, "new_status", req.IsActive, "ip", ip) w.WriteHeader(http.StatusOK) -}
\ No newline at end of file +} diff --git a/handlers/auth.go b/handlers/auth.go index 28c7fa7..2d5eb68 100644 --- a/handlers/auth.go +++ b/handlers/auth.go @@ -21,7 +21,9 @@ func renderLoginWithError(w http.ResponseWriter, message string) { "Error": message, } w.WriteHeader(http.StatusUnauthorized) // Set status to indicate failure - templates.ExecuteTemplate(w, "login.html", data) + if err := templates.ExecuteTemplate(w, "login.html", data); err != nil { + logging.AppLog.Error("Failed to execute login template with error", "error", err) + } } // LoginHandler handles displaying the login form and processing login requests. @@ -32,12 +34,18 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { } if r.Method == http.MethodGet { - templates.ExecuteTemplate(w, "login.html", data) + if err := templates.ExecuteTemplate(w, "login.html", data); err != nil { + logging.AppLog.Error("Failed to execute login template", "error", err, "ip", ip) + } return } if r.Method == http.MethodPost { - r.ParseForm() + if err := r.ParseForm(); err != nil { + logging.AppLog.Error("Failed to parse login form", "error", err, "ip", ip) + renderLoginWithError(w, "An internal error occurred. Please try again.") + return + } username := r.FormValue("username") password := r.FormValue("password") @@ -46,7 +54,9 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { logging.AppLog.Error("Database error during login", "error", err, "username", username, "ip", ip) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) - json.NewEncoder(w).Encode(map[string]string{"error": "An internal error occurred."}) + if err := json.NewEncoder(w).Encode(map[string]string{"error": "An internal error occurred."}); err != nil { + logging.AppLog.Error("Failed to encode json error response", "error", err, "ip", ip) + } return } @@ -62,8 +72,15 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { return } else { // If lockout expired, re-activate account and reset attempts - models.SetUserActiveStatus(user.ID, true) - models.RecordLoginSuccess(user.ID) // Resets failed_logins + if err := models.SetUserActiveStatus(user.ID, true); err != nil { + logging.AppLog.Error("Failed to reactivate user", "error", err, "user_id", user.ID, "ip", ip) + renderLoginWithError(w, "An internal error occurred. Please try again.") + return + } + if err := models.RecordLoginSuccess(user.ID); err != nil { + logging.AppLog.Error("Failed to reset failed login attempts", "error", err, "user_id", user.ID, "ip", ip) + // Don't block login, but log the failure to reset attempts + } } } } @@ -71,12 +88,16 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { // Validate user credentials if user == nil || !models.CheckPasswordHash(password, user.PasswordHash) { if user != nil { - models.RecordLoginFailure(user.ID) + if err := models.RecordLoginFailure(user.ID); err != nil { + logging.AppLog.Error("Failed to record login failure", "error", err, "user_id", user.ID, "ip", ip) + } // Re-fetch user to get updated failed_logins count updatedUser, _ := models.GetUserByID(user.ID) maxAttempts, _ := strconv.Atoi(os.Getenv("MAX_LOGIN_ATTEMPTS")) if updatedUser.FailedLogins >= maxAttempts { - models.SetUserActiveStatus(user.ID, false) + if err := models.SetUserActiveStatus(user.ID, false); err != nil { + logging.AppLog.Error("Failed to lock account", "error", err, "user_id", user.ID, "ip", ip) + } logging.SecurityLog.Warn("ACCOUNT LOCKED", "username", username, "ip", ip) } } @@ -86,7 +107,9 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { } // Login successful - models.RecordLoginSuccess(user.ID) + if err := models.RecordLoginSuccess(user.ID); err != nil { + logging.AppLog.Error("Failed to record login success", "error", err, "user_id", user.ID, "ip", ip) + } logging.SecurityLog.Info("LOGIN SUCCESS", "username", user.Username, "ip", ip) tokenDurationHours, err := strconv.Atoi(os.Getenv("TOKEN_DURATION_HOURS")) @@ -100,7 +123,9 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { logging.AppLog.Error("Failed to create auth token", "error", err, "username", user.Username, "ip", ip) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) - json.NewEncoder(w).Encode(map[string]string{"error": "Could not create session."}) + if err := json.NewEncoder(w).Encode(map[string]string{"error": "Could not create session."}); err != nil { + logging.AppLog.Error("Failed to encode json error response", "error", err, "username", user.Username, "ip", ip) + } return } @@ -121,6 +146,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusMethodNotAllowed) } + // LogoutHandler invalidates the user's token by expiring the cookie. func LogoutHandler(w http.ResponseWriter, r *http.Request) { ip := logging.GetClientIP(r) @@ -159,7 +185,9 @@ func renderRegisterWithError(w http.ResponseWriter, message string) { "Error": message, } w.WriteHeader(http.StatusBadRequest) // Set status to indicate failure - templates.ExecuteTemplate(w, "register.html", data) + if err := templates.ExecuteTemplate(w, "register.html", data); err != nil { + logging.AppLog.Error("Failed to execute register template with error", "error", err) + } } // RegisterHandler handles displaying and processing the user registration form. @@ -170,46 +198,52 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) { } if r.Method == http.MethodGet { - templates.ExecuteTemplate(w, "register.html", data) + if err := templates.ExecuteTemplate(w, "register.html", data); err != nil { + logging.AppLog.Error("Failed to execute register template", "error", err, "ip", ip) + } return } - if r.Method == http.MethodPost { - r.ParseForm() - username := r.FormValue("username") - password := r.FormValue("password") - confirmPassword := r.FormValue("confirm_password") - - // Validate password policy - if ok, message := utils.ValidatePassword(password); !ok { - renderRegisterWithError(w, message) - return - } - - if password != confirmPassword { - logging.AppLog.Warn("User registration failed", "reason", "passwords do not match", "ip", ip) - renderRegisterWithError(w, "Passwords do not match") - return - } - - existingUser, err := models.GetUserByUsername(username) - if err != nil { - logging.AppLog.Error("Error checking username during registration", "error", err, "ip", ip) - renderRegisterWithError(w, "An internal error occurred while checking the username.") - return - } - if existingUser != nil { - logging.AppLog.Warn("User registration failed", "reason", "username is already taken", "username", username, "ip", ip) - renderRegisterWithError(w, "Username is already taken") - return - } - - user, err := models.CreateUser(username, password, "user") - if err != nil { - logging.AppLog.Error("Failed to create user", "error", err, "username", username, "ip", ip) - renderRegisterWithError(w, "Failed to create user due to an internal error.") - return - } + if r.Method == http.MethodPost { + if err := r.ParseForm(); err != nil { + logging.AppLog.Error("Failed to parse registration form", "error", err, "ip", ip) + renderRegisterWithError(w, "An internal error occurred. Please try again.") + return + } + username := r.FormValue("username") + password := r.FormValue("password") + confirmPassword := r.FormValue("confirm_password") + + // Validate password policy + if ok, message := utils.ValidatePassword(password); !ok { + renderRegisterWithError(w, message) + return + } + + if password != confirmPassword { + logging.AppLog.Warn("User registration failed", "reason", "passwords do not match", "ip", ip) + renderRegisterWithError(w, "Passwords do not match") + return + } + + existingUser, err := models.GetUserByUsername(username) + if err != nil { + logging.AppLog.Error("Error checking username during registration", "error", err, "ip", ip) + renderRegisterWithError(w, "An internal error occurred while checking the username.") + return + } + if existingUser != nil { + logging.AppLog.Warn("User registration failed", "reason", "username is already taken", "username", username, "ip", ip) + renderRegisterWithError(w, "Username is already taken") + return + } + + user, err := models.CreateUser(username, password, "user") + if err != nil { + logging.AppLog.Error("Failed to create user", "error", err, "username", username, "ip", ip) + renderRegisterWithError(w, "Failed to create user due to an internal error.") + return + } logging.SecurityLog.Info("USER CREATED", "username", user.Username, "ip", ip) // Redirect to the login page so the user can sign in. @@ -219,3 +253,158 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusMethodNotAllowed) } + +// RefreshTokenHandler handles exchanging a valid refresh token for a new access token. +func RefreshTokenHandler(w http.ResponseWriter, r *http.Request) { + ip := logging.GetClientIP(r) + + // 1. Parse request body + var reqBody struct { + RefreshToken string `json:"refresh_token"` + } + if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + if reqBody.RefreshToken == "" { + http.Error(w, "Missing refresh_token", http.StatusBadRequest) + return + } + + // 2. Validate the refresh token in DB + refreshToken, err := models.GetRefreshTokenByToken(reqBody.RefreshToken) + if err != nil { + logging.AppLog.Error("Error getting refresh token from DB", "error", err, "ip", ip) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + if refreshToken == nil || refreshToken.IsRevoked || time.Now().After(refreshToken.ExpiresAt) { + logging.SecurityLog.Warn("REFRESH TOKEN FAIL", "reason", "token not found, revoked, or expired", "ip", ip) + http.Error(w, "Unauthorized: Invalid refresh token", http.StatusUnauthorized) + return + } + + // 3. Get user info + user, err := models.GetUserByID(refreshToken.UserID) + if err != nil || user == nil { + logging.AppLog.Error("Failed to get user from refresh token", "error", err, "user_id", refreshToken.UserID, "ip", ip) + http.Error(w, "Unauthorized: User not found", http.StatusUnauthorized) + return + } + + // 4. Issue a new access token + accessTokenDurationMinutes, _ := strconv.Atoi(os.Getenv("ACCESS_TOKEN_DURATION_MINUTES")) + if accessTokenDurationMinutes <= 0 { + accessTokenDurationMinutes = 15 + } + newAccessToken, err := utils.GenerateJWT(user, time.Duration(accessTokenDurationMinutes)*time.Minute) + if err != nil { + logging.AppLog.Error("Failed to generate new access token", "error", err, "username", user.Username, "ip", ip) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + // --- Optional: Token Rotation --- + // 5. Delete the old refresh token + if err := models.DeleteRefreshTokenByToken(refreshToken.Token); err != nil { + logging.AppLog.Error("Failed to delete old refresh token", "error", err, "ip", ip) + // Continue, but log the error + } + + // 6. Issue a new refresh token + refreshTokenDurationDays, _ := strconv.Atoi(os.Getenv("REFRESH_TOKEN_DURATION_DAYS")) + if refreshTokenDurationDays <= 0 { + refreshTokenDurationDays = 7 + } + newRefreshToken, err := models.CreateRefreshToken(user.ID, time.Duration(refreshTokenDurationDays)*24*time.Hour) + if err != nil { + logging.AppLog.Error("Failed to create new refresh token", "error", err, "username", user.Username, "ip", ip) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + // --- End Token Rotation --- + + // 7. Respond with new tokens + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(map[string]string{ + "access_token": newAccessToken, + "refresh_token": newRefreshToken.Token, + }); err != nil { + logging.AppLog.Error("Failed to encode token response", "error", err, "ip", ip) + } +} + +// TokenHandler handles issuing JWT tokens for API clients. +func TokenHandler(w http.ResponseWriter, r *http.Request) { + ip := logging.GetClientIP(r) + + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + var creds struct { + Username string `json:"username"` + Password string `json:"password"` + } + if err := json.NewDecoder(r.Body).Decode(&creds); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + // --- User Authentication Logic (similar to LoginHandler) --- + user, err := models.GetUserByUsername(creds.Username) + if err != nil { + logging.AppLog.Error("Database error during token issuance", "error", err, "username", creds.Username, "ip", ip) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + // NOTE: We are not performing lockout checks here for simplicity, + // but in a production system, you'd want to protect this endpoint + // against brute-force attacks as well. + if user == nil || !user.IsActive || !models.CheckPasswordHash(creds.Password, user.PasswordHash) { + logging.SecurityLog.Warn("TOKEN ISSUANCE FAIL", "username", creds.Username, "ip", ip, "reason", "invalid credentials or inactive user") + http.Error(w, "Unauthorized: Invalid credentials", http.StatusUnauthorized) + return + } + // --- End Authentication --- + + // --- Issue Tokens --- + // Access Token + accessTokenDurationMinutes, _ := strconv.Atoi(os.Getenv("ACCESS_TOKEN_DURATION_MINUTES")) + if accessTokenDurationMinutes <= 0 { + accessTokenDurationMinutes = 15 + } + accessToken, err := utils.GenerateJWT(user, time.Duration(accessTokenDurationMinutes)*time.Minute) + if err != nil { + logging.AppLog.Error("Failed to generate access token", "error", err, "username", user.Username, "ip", ip) + http.Error(w, "Failed to generate access token", http.StatusInternalServerError) + return + } + + // Refresh Token + refreshTokenDurationDays, _ := strconv.Atoi(os.Getenv("REFRESH_TOKEN_DURATION_DAYS")) + if refreshTokenDurationDays <= 0 { + refreshTokenDurationDays = 7 + } + refreshToken, err := models.CreateRefreshToken(user.ID, time.Duration(refreshTokenDurationDays)*24*time.Hour) + if err != nil { + logging.AppLog.Error("Failed to create refresh token", "error", err, "username", user.Username, "ip", ip) + http.Error(w, "Failed to create refresh token", http.StatusInternalServerError) + return + } + // --- End Issue Tokens --- + + logging.SecurityLog.Info("TOKEN ISSUANCE SUCCESS", "username", user.Username, "ip", ip) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(map[string]string{ + "access_token": accessToken, + "refresh_token": refreshToken.Token, + }); err != nil { + logging.AppLog.Error("Failed to encode token response", "error", err, "ip", ip) + } +} diff --git a/handlers/proxy.go b/handlers/proxy.go index bc31059..602fc78 100644 --- a/handlers/proxy.go +++ b/handlers/proxy.go @@ -19,7 +19,7 @@ func NewProxy(targetURL string) http.Handler { // The default director is mostly fine, but we want to log and ensure the Host header is correct. director := proxy.Director proxy.Director = func(req *http.Request) { - director(req) // Run the default director + director(req) // Run the default director req.Host = target.Host // Set the Host header to the target's host // Log the proxying action @@ -35,4 +35,4 @@ func NewProxy(targetURL string) http.Handler { } return proxy -}
\ No newline at end of file +} diff --git a/handlers/user.go b/handlers/user.go index 196405b..d55f93d 100644 --- a/handlers/user.go +++ b/handlers/user.go @@ -2,6 +2,7 @@ package handlers import ( "auth-proxy/config" + "auth-proxy/logging" "auth-proxy/models" "auth-proxy/types" "auth-proxy/utils" @@ -11,7 +12,7 @@ import ( // AccountPageHandler serves the account page, passing user data to the template. func AccountPageHandler(w http.ResponseWriter, r *http.Request) { - user, ok := r.Context().Value(types.SessionDataContextKey).(*models.User) + user, ok := r.Context().Value(types.SessionDataContextKey).(*types.User) if !ok || user == nil { // This should ideally not happen if AuthMiddleware is working correctly. http.Error(w, "User not authenticated", http.StatusUnauthorized) @@ -22,7 +23,9 @@ func AccountPageHandler(w http.ResponseWriter, r *http.Request) { "Paths": config.Paths, "User": user, } - templates.ExecuteTemplate(w, "account.html", data) + if err := templates.ExecuteTemplate(w, "account.html", data); err != nil { + logging.AppLog.Error("Failed to execute account template", "error", err, "user_id", user.ID) + } } // ChangePasswordHandler handles both displaying the page (GET) and processing the update (POST). @@ -32,69 +35,92 @@ func ChangePasswordHandler(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{ "Paths": config.Paths, } - templates.ExecuteTemplate(w, "account_password.html", data) + if err := templates.ExecuteTemplate(w, "account_password.html", data); err != nil { + logging.AppLog.Error("Failed to execute account_password template", "error", err) + } return } // POST request logic (API call) if r.Method == http.MethodPost { // This is a protected route, so AuthMiddleware should have put the user in the context. - user, ok := r.Context().Value(types.SessionDataContextKey).(*models.User) + user, ok := r.Context().Value(types.SessionDataContextKey).(*types.User) if !ok || user == nil { - http.Error(w, "User not authenticated", http.StatusUnauthorized) - return + http.Error(w, "User not authenticated", http.StatusUnauthorized) + return } w.Header().Set("Content-Type", "application/json") - r.ParseForm() + if err := r.ParseForm(); err != nil { + logging.AppLog.Error("Failed to parse change password form", "error", err) + w.WriteHeader(http.StatusBadRequest) + if err := json.NewEncoder(w).Encode(map[string]string{"error": "Could not read request."}); err != nil { + logging.AppLog.Error("Failed to encode json error response", "error", err) + } + return + } currentPassword := r.FormValue("current_password") newPassword := r.FormValue("new_password") confirmNewPassword := r.FormValue("confirm_new_password") if ok, message := utils.ValidatePassword(newPassword); !ok { w.WriteHeader(http.StatusBadRequest) - json.NewEncoder(w).Encode(map[string]string{"error": message}) + if err := json.NewEncoder(w).Encode(map[string]string{"error": message}); err != nil { + logging.AppLog.Error("Failed to encode json error response", "error", err) + } return } if newPassword != confirmNewPassword { w.WriteHeader(http.StatusBadRequest) - json.NewEncoder(w).Encode(map[string]string{"error": "New passwords do not match."}) + if err := json.NewEncoder(w).Encode(map[string]string{"error": "New passwords do not match."}); err != nil { + logging.AppLog.Error("Failed to encode json error response", "error", err) + } return } dbUser, err := models.GetUserByID(user.ID) if err != nil || dbUser == nil { w.WriteHeader(http.StatusInternalServerError) - json.NewEncoder(w).Encode(map[string]string{"error": "Could not retrieve user data."}) + if err := json.NewEncoder(w).Encode(map[string]string{"error": "Could not retrieve user data."}); err != nil { + logging.AppLog.Error("Failed to encode json error response", "error", err) + } return } if !models.CheckPasswordHash(currentPassword, dbUser.PasswordHash) { w.WriteHeader(http.StatusUnauthorized) - json.NewEncoder(w).Encode(map[string]string{"error": "Current password is incorrect."}) + if err := json.NewEncoder(w).Encode(map[string]string{"error": "Current password is incorrect."}); err != nil { + logging.AppLog.Error("Failed to encode json error response", "error", err) + } return } newPasswordHash, err := models.HashPassword(newPassword) if err != nil { w.WriteHeader(http.StatusInternalServerError) - json.NewEncoder(w).Encode(map[string]string{"error": "Failed to process new password."}) + if err := json.NewEncoder(w).Encode(map[string]string{"error": "Failed to process new password."}); err != nil { + logging.AppLog.Error("Failed to encode json error response", "error", err) + } return } if err := models.UpdateUserPassword(dbUser.ID, newPasswordHash); err != nil { w.WriteHeader(http.StatusInternalServerError) - json.NewEncoder(w).Encode(map[string]string{"error": "Failed to update password."}) + if err := json.NewEncoder(w).Encode(map[string]string{"error": "Failed to update password."}); err != nil { + logging.AppLog.Error("Failed to encode json error response", "error", err) + } return } w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(map[string]string{"message": "Password updated successfully."}) + if err := json.NewEncoder(w).Encode(map[string]string{"message": "Password updated successfully."}); err != nil { + logging.AppLog.Error("Failed to encode json error response", "error", err) + } return } // Handle other methods http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) -}
\ No newline at end of file +} |
