summaryrefslogtreecommitdiff
path: root/handlers
diff options
context:
space:
mode:
authorhaturatu <taro@eyes4you.org>2025-10-13 02:25:46 +0900
committerhaturatu <taro@eyes4you.org>2025-10-13 02:25:46 +0900
commitcb4a103e0b59519933b1ccbd0d8897d3c7238273 (patch)
tree47cf53a76265a0203eb59e59e06ce0d7b14df038 /handlers
parent6f19c4dcb7702f002d2a1b579f2418e1bc10b18f (diff)
ref: refactor
Diffstat (limited to 'handlers')
-rw-r--r--handlers/auth.go407
-rw-r--r--handlers/auth_login.go165
-rw-r--r--handlers/auth_logout.go41
-rw-r--r--handlers/auth_register.go92
-rw-r--r--handlers/auth_token.go138
5 files changed, 436 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)
- }
-}
diff --git a/handlers/auth_login.go b/handlers/auth_login.go
new file mode 100644
index 0000000..855fa8a
--- /dev/null
+++ b/handlers/auth_login.go
@@ -0,0 +1,165 @@
+package handlers
+
+import (
+ "auth-proxy/config"
+ "auth-proxy/logging"
+ "auth-proxy/models"
+ "auth-proxy/types"
+ "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)
+}
diff --git a/handlers/auth_logout.go b/handlers/auth_logout.go
new file mode 100644
index 0000000..d008a81
--- /dev/null
+++ b/handlers/auth_logout.go
@@ -0,0 +1,41 @@
+package handlers
+
+import (
+ "auth-proxy/config"
+ "auth-proxy/logging"
+ "auth-proxy/models"
+ "net/http"
+ "time"
+)
+
+// 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)
+}
diff --git a/handlers/auth_register.go b/handlers/auth_register.go
new file mode 100644
index 0000000..9b93494
--- /dev/null
+++ b/handlers/auth_register.go
@@ -0,0 +1,92 @@
+package handlers
+
+import (
+ "auth-proxy/config"
+ "auth-proxy/logging"
+ "auth-proxy/models"
+ "auth-proxy/utils"
+ "net/http"
+
+ "github.com/gorilla/csrf"
+)
+
+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)
+}
diff --git a/handlers/auth_token.go b/handlers/auth_token.go
new file mode 100644
index 0000000..7ea7ef6
--- /dev/null
+++ b/handlers/auth_token.go
@@ -0,0 +1,138 @@
+package handlers
+
+import (
+ "auth-proxy/config"
+ "auth-proxy/logging"
+ "auth-proxy/models"
+ "auth-proxy/utils"
+ "encoding/json"
+ "net/http"
+ "time"
+)
+
+// 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)
+ }
+}