summaryrefslogtreecommitdiff
path: root/handlers/auth.go
diff options
context:
space:
mode:
Diffstat (limited to 'handlers/auth.go')
-rw-r--r--handlers/auth.go285
1 files changed, 237 insertions, 48 deletions
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)
+ }
+}