diff options
| author | haturatu <taro@eyes4you.org> | 2025-10-12 15:01:34 +0900 |
|---|---|---|
| committer | haturatu <taro@eyes4you.org> | 2025-10-12 15:01:34 +0900 |
| commit | 10c90d8654b94916bf0a4ffb8263e512a8e6be04 (patch) | |
| tree | dd109fb357abb40f943a7818fedc659e69ee5d11 /middleware | |
first commit
Diffstat (limited to 'middleware')
| -rw-r--r-- | middleware/auth.go | 93 | ||||
| -rw-r--r-- | middleware/ratelimit.go | 74 |
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) + }) +} |
