summaryrefslogtreecommitdiff
path: root/middleware
diff options
context:
space:
mode:
authorhaturatu <taro@eyes4you.org>2025-10-12 15:01:34 +0900
committerhaturatu <taro@eyes4you.org>2025-10-12 15:01:34 +0900
commit10c90d8654b94916bf0a4ffb8263e512a8e6be04 (patch)
treedd109fb357abb40f943a7818fedc659e69ee5d11 /middleware
first commit
Diffstat (limited to 'middleware')
-rw-r--r--middleware/auth.go93
-rw-r--r--middleware/ratelimit.go74
2 files changed, 167 insertions, 0 deletions
diff --git a/middleware/auth.go b/middleware/auth.go
new file mode 100644
index 0000000..08ad088
--- /dev/null
+++ b/middleware/auth.go
@@ -0,0 +1,93 @@
+package middleware
+
+import (
+ "auth-proxy/config"
+ "auth-proxy/logging"
+ "auth-proxy/models"
+ "auth-proxy/types"
+ "context"
+ "database/sql"
+ "net/http"
+ "strings"
+)
+
+// AuthMiddleware checks for a valid Bearer token, suitable for API endpoints.
+// If the token is valid, it adds the user info to the context and proceeds.
+// If not, it returns a 401 Unauthorized error.
+func AuthMiddleware(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ ip := logging.GetClientIP(r)
+ cookie, err := r.Cookie("auth_token")
+
+ // If the cookie is not found
+ if err != nil {
+ if err == http.ErrNoCookie {
+ // If it's an API request, return 401.
+ if strings.HasPrefix(r.URL.Path, "/api/") {
+ logging.AppLog.Info("API access denied (no token)", "path", r.URL.Path, "ip", ip)
+ http.Error(w, "Unauthorized: A valid token is required", http.StatusUnauthorized)
+ return
+ }
+ // Otherwise, it's a page navigation, so redirect to login.
+ logging.AppLog.Info("Redirecting to login (no token)", "path", r.URL.Path, "ip", ip)
+ http.Redirect(w, r, config.Paths.Login, http.StatusFound)
+ return
+ }
+ // For any other error retrieving the cookie
+ logging.AppLog.Error("Error reading auth cookie", "error", err, "path", r.URL.Path, "ip", ip)
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ tokenStr := cookie.Value
+ user, err := models.GetUserByToken(tokenStr)
+ // Check for database errors first
+ if err != nil && err != sql.ErrNoRows {
+ logging.AppLog.Error("Error validating token", "error", err, "path", r.URL.Path, "ip", ip)
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ if user == nil {
+ // Token was provided but is invalid or expired. Clear the bad cookie.
+ http.SetCookie(w, &http.Cookie{
+ Name: "auth_token",
+ Value: "",
+ Path: "/",
+ MaxAge: -1,
+ })
+ logging.SecurityLog.Warn("ACCESS DENIED", "reason", "invalid or expired token", "path", r.URL.Path, "ip", ip)
+ // Redirect to login, as the user needs to re-authenticate.
+ http.Redirect(w, r, config.Paths.Login, http.StatusFound)
+ return
+ }
+
+ // User is authenticated, proceed
+ ctx := context.WithValue(r.Context(), types.SessionDataContextKey, user)
+ next.ServeHTTP(w, r.WithContext(ctx))
+ })
+}
+
+// AdminMiddleware checks if the user has the 'admin' role.
+// This middleware should be used after AuthMiddleware.
+func AdminMiddleware(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ ip := logging.GetClientIP(r)
+ // Get user from context
+ user, ok := r.Context().Value(types.SessionDataContextKey).(*models.User)
+ if !ok || user == nil {
+ logging.SecurityLog.Warn("ADMIN ACCESS DENIED", "reason", "no user data in context", "path", r.URL.Path, "ip", ip)
+ http.Error(w, "Forbidden", http.StatusForbidden)
+ return
+ }
+
+ if user.Role != "admin" {
+ logging.SecurityLog.Warn("ADMIN ACCESS DENIED", "reason", "insufficient permissions", "user", user.Username, "role", user.Role, "path", r.URL.Path, "ip", ip)
+ http.Error(w, "Forbidden", http.StatusForbidden)
+ return
+ }
+
+ logging.SecurityLog.Info("ADMIN ACCESS GRANTED", "user", user.Username, "path", r.URL.Path, "ip", ip)
+ next.ServeHTTP(w, r)
+ })
+}
diff --git a/middleware/ratelimit.go b/middleware/ratelimit.go
new file mode 100644
index 0000000..065a0a2
--- /dev/null
+++ b/middleware/ratelimit.go
@@ -0,0 +1,74 @@
+package middleware
+
+import (
+ "net"
+ "net/http"
+ "os"
+ "strconv"
+ "sync"
+ "time"
+)
+
+type RateLimiter struct {
+ requests map[string][]int64
+ mutex sync.Mutex
+ max int
+ window time.Duration
+}
+
+func NewRateLimiter() *RateLimiter {
+ maxRequests, err := strconv.Atoi(os.Getenv("USER_CREATION_RATE_LIMIT_MAX_REQUESTS"))
+ if err != nil || maxRequests <= 0 {
+ maxRequests = 5 // Default value
+ }
+
+ windowSeconds, err := strconv.Atoi(os.Getenv("USER_CREATION_RATE_LIMIT_WINDOW_SECONDS"))
+ if err != nil || windowSeconds <= 0 {
+ windowSeconds = 3600 // Default value (1 hour)
+ }
+
+ return &RateLimiter{
+ requests: make(map[string][]int64),
+ max: maxRequests,
+ window: time.Duration(windowSeconds) * time.Second,
+ }
+}
+
+func (l *RateLimiter) RateLimitMiddleware(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ l.mutex.Lock()
+ defer l.mutex.Unlock()
+
+ ip, _, err := net.SplitHostPort(r.RemoteAddr)
+ if err != nil {
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ now := time.Now().Unix()
+ if _, ok := l.requests[ip]; !ok {
+ l.requests[ip] = []int64{}
+ }
+
+ // Filter out old timestamps
+ var recentRequests []int64
+ for _, ts := range l.requests[ip] {
+ if now-ts < int64(l.window.Seconds()) {
+ recentRequests = append(recentRequests, ts)
+ }
+ }
+ l.requests[ip] = recentRequests
+
+ if len(l.requests[ip]) >= l.max {
+ http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
+ return
+ }
+
+ // Only add a timestamp for POST requests (actual registration attempts)
+ if r.Method == http.MethodPost {
+ l.requests[ip] = append(l.requests[ip], now)
+ }
+
+ next.ServeHTTP(w, r)
+ })
+}