summaryrefslogtreecommitdiff
path: root/handlers/auth.go
diff options
context:
space:
mode:
Diffstat (limited to 'handlers/auth.go')
-rw-r--r--handlers/auth.go407
1 files changed, 0 insertions, 407 deletions
diff --git a/handlers/auth.go b/handlers/auth.go
deleted file mode 100644
index 4b75e0b..0000000
--- a/handlers/auth.go
+++ /dev/null
@@ -1,407 +0,0 @@
-package handlers
-
-import (
- "auth-proxy/config"
- "auth-proxy/logging"
- "auth-proxy/models"
- "auth-proxy/types"
- "auth-proxy/utils"
- "encoding/json"
- "errors"
- "html/template"
- "net/http"
- "time"
-
- "github.com/gorilla/csrf"
-)
-
-var templates = template.Must(template.ParseGlob("templates/*.html"))
-
-// authenticateUser handles the core logic for authenticating a user, including brute-force protection.
-// It returns the authenticated user or an error if authentication fails.
-func authenticateUser(username, password, ip string) (*types.User, error) {
- user, err := models.GetUserByUsername(username)
- if err != nil {
- logging.AppLog.Error("Database error during authentication", "error", err, "username", username, "ip", ip)
- return nil, errors.New("internal server error")
- }
-
- if user == nil {
- logging.SecurityLog.Warn("AUTH FAIL", "username", username, "ip", ip, "reason", "user not found")
- return nil, errors.New("invalid username or password")
- }
-
- // Step 1: Check if the account is locked.
- if !user.IsActive {
- lockoutDuration := time.Duration(config.Paths.LockoutDurationMinutes) * time.Minute
-
- // Check if the lockout period has expired.
- if time.Since(user.UpdatedAt) < lockoutDuration {
- logging.SecurityLog.Warn("AUTH FAIL", "username", username, "ip", ip, "reason", "account locked")
- return nil, errors.New("account is temporarily locked")
- } else {
- // Lockout has expired. Reactivate the account but do not log the user in on this attempt.
- if err := models.SetUserActiveStatus(user.ID, true); err != nil {
- logging.AppLog.Error("Failed to reactivate user", "error", err, "user_id", user.ID, "ip", ip)
- return nil, errors.New("internal server error")
- }
- if err := models.RecordLoginSuccess(user.ID); err != nil {
- logging.AppLog.Error("Failed to reset failed login attempts on reactivation", "error", err, "user_id", user.ID, "ip", ip)
- }
- logging.SecurityLog.Info("ACCOUNT UNLOCKED", "username", username, "ip", ip)
- return nil, errors.New("account has been unlocked, please try again")
- }
- }
-
- // Step 2: If the account is active, validate credentials.
- if !models.CheckPasswordHash(password, user.PasswordHash) {
- // Record the failure
- 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 := config.Paths.MaxLoginAttempts
-
- // Lock the account if max attempts are exceeded
- if maxAttempts > 0 && updatedUser.FailedLogins >= maxAttempts {
- 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)
- }
-
- logging.SecurityLog.Warn("AUTH FAIL", "username", username, "ip", ip, "reason", "invalid credentials")
- return nil, errors.New("invalid username or password")
- }
-
- // Step 3: Login successful.
- 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("AUTH SUCCESS", "username", user.Username, "ip", ip)
-
- return user, nil
-}
-
-func renderLoginWithError(w http.ResponseWriter, r *http.Request, message string) {
- data := map[string]interface{}{
- "Paths": config.Paths,
- "Error": message,
- "RegisterEnabled": config.Paths.RegisterEnabled,
- "CSRFToken": csrf.Token(r),
- }
- w.WriteHeader(http.StatusUnauthorized) // Set status to indicate failure
- 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.
-func LoginHandler(w http.ResponseWriter, r *http.Request) {
- ip := logging.GetClientIP(r)
-
- if r.Method == http.MethodGet {
- data := map[string]interface{}{
- "Paths": config.Paths,
- "RegisterEnabled": config.Paths.RegisterEnabled,
- "CSRFToken": csrf.Token(r),
- }
- 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 {
- if err := r.ParseForm(); err != nil {
- logging.AppLog.Error("Failed to parse login form", "error", err, "ip", ip)
- renderLoginWithError(w, r, "An internal error occurred. Please try again.")
- return
- }
- username := r.FormValue("username")
- password := r.FormValue("password")
-
- user, err := authenticateUser(username, password, ip)
- if err != nil {
- // Map authentication error to user-facing message
- errMsg := "Invalid username or password."
- if err.Error() == "account is temporarily locked" {
- errMsg = "Account is temporarily locked."
- } else if err.Error() == "internal server error" {
- errMsg = "An internal error occurred. Please try again."
- } else if err.Error() == "account has been unlocked, please try again" {
- errMsg = "Account has been unlocked. Please try logging in again."
- }
- renderLoginWithError(w, r, errMsg)
- return
- }
-
- // --- Create Session Cookie ---
- duration := time.Duration(config.Paths.TokenDurationHours) * time.Hour
-
- token, err := models.CreateAuthToken(user.ID, duration)
- if err != nil {
- logging.AppLog.Error("Failed to create auth token", "error", err, "username", user.Username, "ip", ip)
- renderLoginWithError(w, r, "Could not create session.")
- return
- }
-
- http.SetCookie(w, &http.Cookie{
- Name: "auth_token",
- Value: token.Token,
- Expires: token.ExpiresAt,
- Path: "/",
- HttpOnly: true,
- Secure: r.TLS != nil, // Use Secure flag if on HTTPS
- SameSite: http.SameSiteLaxMode,
- })
-
- // Redirect to the root path, which is the proxied application.
- http.Redirect(w, r, "/", http.StatusFound)
- return
- }
-
- 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)
- cookie, err := r.Cookie("auth_token")
- if err != nil {
- // No cookie, nothing to do.
- http.Redirect(w, r, config.Paths.Login, http.StatusFound)
- return
- }
-
- err = models.DeleteAuthToken(cookie.Value)
- if err != nil {
- // Log the error but still proceed with logging the user out client-side.
- logging.AppLog.Error("Failed to delete token on logout", "error", err, "ip", ip)
- }
-
- // Expire the cookie by setting its max age to a negative number.
- http.SetCookie(w, &http.Cookie{
- Name: "auth_token",
- Value: "",
- Path: "/",
- Expires: time.Unix(0, 0),
- MaxAge: -1,
- HttpOnly: true,
- Secure: r.TLS != nil,
- SameSite: http.SameSiteLaxMode,
- })
-
- logging.SecurityLog.Info("LOGOUT SUCCESS", "ip", ip)
- http.Redirect(w, r, config.Paths.Login, http.StatusFound)
-}
-
-func renderRegisterWithError(w http.ResponseWriter, r *http.Request, message string) {
- data := map[string]interface{}{
- "Paths": config.Paths,
- "Error": message,
- "CSRFToken": csrf.Token(r),
- }
- w.WriteHeader(http.StatusBadRequest) // Set status to indicate failure
- 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.
-func RegisterHandler(w http.ResponseWriter, r *http.Request) {
- if !config.Paths.RegisterEnabled {
- http.Error(w, "User registration is disabled.", http.StatusForbidden)
- return
- }
- ip := logging.GetClientIP(r)
- data := map[string]interface{}{
- "Paths": config.Paths,
- "CSRFToken": csrf.Token(r),
- }
-
- if r.Method == http.MethodGet {
- 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 {
- if err := r.ParseForm(); err != nil {
- logging.AppLog.Error("Failed to parse registration form", "error", err, "ip", ip)
- renderRegisterWithError(w, r, "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, r, message)
- return
- }
-
- if password != confirmPassword {
- logging.AppLog.Warn("User registration failed", "reason", "passwords do not match", "ip", ip)
- renderRegisterWithError(w, r, "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, r, "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, r, "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, r, "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.
- http.Redirect(w, r, config.Paths.Login, http.StatusFound)
- return
- }
-
- 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
- newAccessToken, err := utils.GenerateJWT(user, time.Duration(config.Paths.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
- newRefreshToken, err := models.CreateRefreshToken(user.ID, time.Duration(config.Paths.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
- }
-
- // Use the shared authentication logic
- user, err := authenticateUser(creds.Username, creds.Password, ip)
- if err != nil {
- // Return a generic error to the API client
- http.Error(w, "Unauthorized: "+err.Error(), http.StatusUnauthorized)
- return
- }
-
- // --- Issue Tokens ---
- // Access Token
- accessToken, err := utils.GenerateJWT(user, time.Duration(config.Paths.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
- refreshToken, err := models.CreateRefreshToken(user.ID, time.Duration(config.Paths.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 ---
-
- 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)
- }
-}