summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd/server/main.go43
-rw-r--r--go.mod2
-rw-r--r--go.sum4
-rw-r--r--handlers/admin.go8
-rw-r--r--handlers/auth.go36
-rw-r--r--handlers/php_proxy.go4
-rw-r--r--handlers/user.go10
-rw-r--r--templates/account_password.html1
-rw-r--r--templates/admin.html1
-rw-r--r--templates/js/admin.js3
-rw-r--r--templates/login.html1
-rw-r--r--templates/php/account_password.php1
-rw-r--r--templates/php/login.php1
-rw-r--r--templates/php/register.php1
-rw-r--r--templates/register.html1
15 files changed, 85 insertions, 32 deletions
diff --git a/cmd/server/main.go b/cmd/server/main.go
index 196164e..b60c1e0 100644
--- a/cmd/server/main.go
+++ b/cmd/server/main.go
@@ -15,6 +15,7 @@ import (
"time"
"github.com/joho/godotenv"
+ "github.com/gorilla/csrf"
)
func main() {
@@ -113,6 +114,7 @@ func main() {
// --- Protected API Endpoints (for Admin UI, protected by SessionAuth) ---
adminCreateUserAPI := http.HandlerFunc(handlers.CreateUserHandler)
+ getUsersHandler := http.HandlerFunc(handlers.GetUsersHandler)
adminUsersAPI := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
adminCreateUserAPI.ServeHTTP(w, r)
@@ -175,24 +177,41 @@ func main() {
}
listenAddr := ":" + port
- // --- Start Server ---
- logging.AppLog.Info("Server starting on " + listenAddr)
-
- // Start periodic background tasks
-
- go startBackgroundTasks(pool, rateLimiter)
-
-
+ // --- CSRF Protection ---
+ csrfSecret := os.Getenv("CSRF_SECRET_KEY")
+ if csrfSecret == "" {
+ logging.AppLog.Error("CSRF_SECRET_KEY environment variable not set")
+ os.Exit(1)
+ }
+ // In production, you should set csrf.Secure(true).
+ // You can use an environment variable to control this setting.
+ csrfOptions := []csrf.Option{
+ csrf.Secure(true), // Set to true in production
+ csrf.Path("/"),
+ }
- if err := http.ListenAndServe(listenAddr, finalHandler); err != nil {
+ trustedOrigins := os.Getenv("CSRF_TRUSTED_ORIGINS")
+ if trustedOrigins != "" {
+ origins := strings.Split(trustedOrigins, ",")
+ csrfOptions = append(csrfOptions, csrf.TrustedOrigins(origins))
+ }
- logging.AppLog.Error("Server failed to start", "error", err)
+ csrfMiddleware := csrf.Protect(
+ []byte(csrfSecret),
+ csrfOptions...,
+ )
- os.Exit(1)
+ // --- Start Server ---
+ logging.AppLog.Info("Server starting on " + listenAddr)
- }
+ // Start periodic background tasks
+ go startBackgroundTasks(pool, rateLimiter)
+ if err := http.ListenAndServe(listenAddr, csrfMiddleware(finalHandler)); err != nil {
+ logging.AppLog.Error("Server failed to start", "error", err)
+ os.Exit(1)
}
+}
diff --git a/go.mod b/go.mod
index 2ebcdf1..6375907 100644
--- a/go.mod
+++ b/go.mod
@@ -12,5 +12,7 @@ require (
require (
filippo.io/edwards25519 v1.1.0 // indirect
+ github.com/gorilla/csrf v1.7.3 // indirect
+ github.com/gorilla/securecookie v1.1.2 // indirect
github.com/tomasen/fcgi_client v0.0.0-20180423082037-2bb3d819fd19 // indirect
)
diff --git a/go.sum b/go.sum
index d255927..aabfbb8 100644
--- a/go.sum
+++ b/go.sum
@@ -2,6 +2,10 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
+github.com/gorilla/csrf v1.7.3 h1:BHWt6FTLZAb2HtWT5KDBf6qgpZzvtbp9QWDRKZMXJC0=
+github.com/gorilla/csrf v1.7.3/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
+github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
+github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
diff --git a/handlers/admin.go b/handlers/admin.go
index 8f302c0..5614e0e 100644
--- a/handlers/admin.go
+++ b/handlers/admin.go
@@ -6,12 +6,15 @@ import (
"auth-proxy/models"
"auth-proxy/types"
"auth-proxy/worker"
+ "auth-proxy/utils"
"encoding/json"
"fmt"
"net/http"
"strconv"
"sync"
"time"
+
+ "github.com/gorilla/csrf"
)
var workerPool *types.WorkerPool
@@ -32,8 +35,9 @@ func AdminPageHandler(w http.ResponseWriter, r *http.Request) {
}
data := map[string]interface{}{
- "Paths": config.Paths,
- "Users": users,
+ "Paths": config.Paths,
+ "Users": users,
+ "CSRFToken": csrf.Token(r),
}
if err := templates.ExecuteTemplate(w, "admin.html", data); err != nil {
logging.AppLog.Error("Failed to execute admin template", "error", err)
diff --git a/handlers/auth.go b/handlers/auth.go
index fbd1349..0eb4851 100644
--- a/handlers/auth.go
+++ b/handlers/auth.go
@@ -11,15 +11,18 @@ import (
"os"
"strconv"
"time"
+
+ "github.com/gorilla/csrf"
)
var templates = template.Must(template.ParseGlob("templates/*.html"))
-func renderLoginWithError(w http.ResponseWriter, message string) {
+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 {
@@ -35,6 +38,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
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)
@@ -45,7 +49,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
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, "An internal error occurred. Please try again.")
+ renderLoginWithError(w, r, "An internal error occurred. Please try again.")
return
}
username := r.FormValue("username")
@@ -70,13 +74,13 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
lockoutDuration := time.Duration(lockoutMinutes) * time.Minute
if time.Since(user.UpdatedAt) < lockoutDuration {
logging.SecurityLog.Warn("LOGIN FAIL", "username", username, "ip", ip, "reason", "account locked")
- renderLoginWithError(w, "Account is temporarily locked.")
+ renderLoginWithError(w, r, "Account is temporarily locked.")
return
} else {
// If lockout expired, re-activate account and reset attempts
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.")
+ renderLoginWithError(w, r, "An internal error occurred. Please try again.")
return
}
if err := models.RecordLoginSuccess(user.ID); err != nil {
@@ -104,7 +108,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
}
}
logging.SecurityLog.Warn("LOGIN FAIL", "username", username, "ip", ip, "reason", "invalid credentials")
- renderLoginWithError(w, "Invalid username or password.")
+ renderLoginWithError(w, r, "Invalid username or password.")
return
}
@@ -181,10 +185,11 @@ func LogoutHandler(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, config.Paths.Login, http.StatusFound)
}
-func renderRegisterWithError(w http.ResponseWriter, message string) {
+func renderRegisterWithError(w http.ResponseWriter, r *http.Request, message string) {
data := map[string]interface{}{
- "Paths": config.Paths,
- "Error": message,
+ "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 {
@@ -200,7 +205,8 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) {
}
ip := logging.GetClientIP(r)
data := map[string]interface{}{
- "Paths": config.Paths,
+ "Paths": config.Paths,
+ "CSRFToken": csrf.Token(r),
}
if r.Method == http.MethodGet {
@@ -213,7 +219,7 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) {
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.")
+ renderRegisterWithError(w, r, "An internal error occurred. Please try again.")
return
}
username := r.FormValue("username")
@@ -222,32 +228,32 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) {
// Validate password policy
if ok, message := utils.ValidatePassword(password); !ok {
- renderRegisterWithError(w, message)
+ renderRegisterWithError(w, r, message)
return
}
if password != confirmPassword {
logging.AppLog.Warn("User registration failed", "reason", "passwords do not match", "ip", ip)
- renderRegisterWithError(w, "Passwords do not match")
+ 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, "An internal error occurred while checking the username.")
+ 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, "Username is already taken")
+ 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, "Failed to create user due to an internal error.")
+ renderRegisterWithError(w, r, "Failed to create user due to an internal error.")
return
}
logging.SecurityLog.Info("USER CREATED", "username", user.Username, "ip", ip)
diff --git a/handlers/php_proxy.go b/handlers/php_proxy.go
index d30f363..24a38dc 100644
--- a/handlers/php_proxy.go
+++ b/handlers/php_proxy.go
@@ -13,6 +13,7 @@ import (
"strconv"
"strings"
+ "github.com/gorilla/csrf"
fcgiclient "github.com/tomasen/fcgi_client"
)
@@ -78,6 +79,9 @@ func NewPhpProxyHandler(scriptName string) http.Handler {
env["AUTH_PATH_ADMIN_USERS_API"] = config.Paths.AdminUsersAPI
env["AUTH_ASSETS_PATH"] = config.Paths.Assets
+ // Pass CSRF token to PHP
+ env["HTTP_X_CSRF_TOKEN"] = csrf.Token(r)
+
// Asynchronously log the proxying action to avoid blocking the request.
worker.Submit(workerPool, func() {
diff --git a/handlers/user.go b/handlers/user.go
index d55f93d..7999755 100644
--- a/handlers/user.go
+++ b/handlers/user.go
@@ -8,6 +8,8 @@ import (
"auth-proxy/utils"
"encoding/json"
"net/http"
+
+ "github.com/gorilla/csrf"
)
// AccountPageHandler serves the account page, passing user data to the template.
@@ -20,8 +22,9 @@ func AccountPageHandler(w http.ResponseWriter, r *http.Request) {
}
data := map[string]interface{}{
- "Paths": config.Paths,
- "User": user,
+ "Paths": config.Paths,
+ "User": user,
+ "CSRFToken": csrf.Token(r),
}
if err := templates.ExecuteTemplate(w, "account.html", data); err != nil {
logging.AppLog.Error("Failed to execute account template", "error", err, "user_id", user.ID)
@@ -33,7 +36,8 @@ func ChangePasswordHandler(w http.ResponseWriter, r *http.Request) {
// For GET request, just serve the page.
if r.Method == http.MethodGet {
data := map[string]interface{}{
- "Paths": config.Paths,
+ "Paths": config.Paths,
+ "CSRFToken": csrf.Token(r),
}
if err := templates.ExecuteTemplate(w, "account_password.html", data); err != nil {
logging.AppLog.Error("Failed to execute account_password template", "error", err)
diff --git a/templates/account_password.html b/templates/account_password.html
index ba40761..68fb95f 100644
--- a/templates/account_password.html
+++ b/templates/account_password.html
@@ -11,6 +11,7 @@
<div class="container content-container">
<h2>Change Password</h2>
<form id="change-password-form" action="{{.Paths.AccountPassword}}" method="post">
+ <input type="hidden" name="gorilla.csrf.Token" value="{{.CSRFToken}}">
<div class="form-group">
<label for="current_password">Current Password</label>
<input type="password" id="current_password" name="current_password" required>
diff --git a/templates/admin.html b/templates/admin.html
index ac4ba94..c2f9488 100644
--- a/templates/admin.html
+++ b/templates/admin.html
@@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta name="csrf-token" content="{{.CSRFToken}}">
<title>Admin Dashboard</title>
<link rel="stylesheet" href="{{.Paths.Assets}}/css/style.css">
<link rel="stylesheet" href="{{.Paths.Assets}}/css/admin.css">
diff --git a/templates/js/admin.js b/templates/js/admin.js
index 4dff572..0b26e2f 100644
--- a/templates/js/admin.js
+++ b/templates/js/admin.js
@@ -7,8 +7,11 @@
* @returns {Promise<Response>}
*/
async function fetchApi(url, options = {}) {
+ const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
+
options.headers = {
...options.headers,
+ 'X-CSRF-Token': csrfToken
};
// Set content type for methods that have a body.
diff --git a/templates/login.html b/templates/login.html
index 12f27eb..b2f847c 100644
--- a/templates/login.html
+++ b/templates/login.html
@@ -10,6 +10,7 @@
<div class="container form-container">
<h2>Login Required</h2>
<form id="login-form" action="{{.Paths.Login}}" method="post">
+ <input type="hidden" name="gorilla.csrf.Token" value="{{.CSRFToken}}">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required>
diff --git a/templates/php/account_password.php b/templates/php/account_password.php
index b287c90..8a1871d 100644
--- a/templates/php/account_password.php
+++ b/templates/php/account_password.php
@@ -18,6 +18,7 @@
<div class="container content-container">
<h2>Change Password</h2>
<form id="change-password-form" action="<?php echo htmlspecialchars($paths['AccountPassword']); ?>" method="post">
+ <input type="hidden" name="gorilla.csrf.Token" value="<?php echo htmlspecialchars($_SERVER['HTTP_X_CSRF_TOKEN']); ?>">
<div class="form-group">
<label for="current_password">Current Password</label>
<input type="password" id="current_password" name="current_password" required>
diff --git a/templates/php/login.php b/templates/php/login.php
index f3e5113..c4644ef 100644
--- a/templates/php/login.php
+++ b/templates/php/login.php
@@ -17,6 +17,7 @@
<div class="container form-container">
<h2>Login Required</h2>
<form id="login-form" action="<?php echo htmlspecialchars($paths['Login']); ?>" method="post">
+ <input type="hidden" name="gorilla.csrf.Token" value="<?php echo htmlspecialchars($_SERVER['HTTP_X_CSRF_TOKEN']); ?>">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required>
diff --git a/templates/php/register.php b/templates/php/register.php
index 956467a..31edf25 100644
--- a/templates/php/register.php
+++ b/templates/php/register.php
@@ -17,6 +17,7 @@
<div class="container form-container">
<h2>Create Account</h2>
<form id="register-form" action="<?php echo htmlspecialchars($paths['Register']); ?>" method="post">
+ <input type="hidden" name="gorilla.csrf.Token" value="<?php echo htmlspecialchars($_SERVER['HTTP_X_CSRF_TOKEN']); ?>">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required>
diff --git a/templates/register.html b/templates/register.html
index d98ef4d..7db7972 100644
--- a/templates/register.html
+++ b/templates/register.html
@@ -10,6 +10,7 @@
<div class="container form-container">
<h2>Create Account</h2>
<form id="register-form" action="{{.Paths.Register}}" method="post">
+ <input type="hidden" name="gorilla.csrf.Token" value="{{.CSRFToken}}">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required>