feat(auth): implement strict and secure user registration

This commit is contained in:
2026-04-08 15:37:32 +02:00
parent fd9aca9ffc
commit 91e10560a6
8 changed files with 304 additions and 11 deletions

View File

@@ -61,8 +61,8 @@ func RequireAuth(next http.Handler) http.Handler {
// RequireGlobalAuth ensures that a valid user is in the context for all routes except login and static
func RequireGlobalAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Allow unauthenticated access to login, search, and static files
if r.URL.Path == "/login" || strings.HasPrefix(r.URL.Path, "/static/") ||
// Allow unauthenticated access to login, register, search, and static files
if r.URL.Path == "/login" || r.URL.Path == "/register" || strings.HasPrefix(r.URL.Path, "/static/") ||
r.URL.Path == "/search" || r.URL.Path == "/api/search" || r.URL.Path == "/api/search-quick" ||
r.URL.Path == "/" {
next.ServeHTTP(w, r)

View File

@@ -0,0 +1,48 @@
package middleware
import (
"net/http"
"net/url"
)
// VerifyOrigin prevents simple CSRF by ensuring the Origin or Referer header matches the Host header
// for state-changing endpoints (POST/PUT/DELETE).
func VerifyOrigin(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet || r.Method == http.MethodHead || r.Method == http.MethodOptions {
next.ServeHTTP(w, r)
return
}
origin := r.Header.Get("Origin")
if origin == "" {
referer := r.Header.Get("Referer")
if referer == "" {
// If neither is present, and it's a POST/PUT/DELETE, reject it (strict policy)
http.Error(w, "Missing Origin or Referer header", http.StatusForbidden)
return
}
refURL, err := url.Parse(referer)
if err != nil {
http.Error(w, "Invalid Referer header", http.StatusForbidden)
return
}
origin = refURL.Scheme + "://" + refURL.Host
}
host := r.Host
// Optional: strip port if you only care about domain
// If origin doesn't match host (accounting for potential schema prefixes)
expectedHTTP := "http://" + host
expectedHTTPS := "https://" + host
if origin != expectedHTTP && origin != expectedHTTPS {
http.Error(w, "Cross-Site Request Forgery (CSRF) origin mismatch", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}

View File

@@ -0,0 +1,81 @@
package middleware
import (
"net/http"
"strings"
"sync"
"time"
)
type visitor struct {
attempts int
lastSeen time.Time
}
var (
visitors = make(map[string]*visitor)
mu sync.Mutex
)
func init() {
go cleanupVisitors()
}
func cleanupVisitors() {
for {
time.Sleep(time.Minute)
mu.Lock()
for ip, v := range visitors {
if time.Since(v.lastSeen) > 3*time.Minute {
delete(visitors, ip)
}
}
mu.Unlock()
}
}
// getIP attempts to get the real IP, falling back to RemoteAddr
func getIP(r *http.Request) string {
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
ips := strings.Split(xff, ",")
return strings.TrimSpace(ips[0])
}
if realIP := r.Header.Get("X-Real-IP"); realIP != "" {
return realIP
}
ip := r.RemoteAddr
if colonIdx := strings.LastIndex(ip, ":"); colonIdx != -1 {
ip = ip[:colonIdx]
}
return ip
}
// RateLimitAuth limits login/register attempts to prevent brute force
func RateLimitAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip := getIP(r)
mu.Lock()
v, exists := visitors[ip]
if !exists {
visitors[ip] = &visitor{1, time.Now()}
} else {
// Reset attempts if it's been more than a minute
if time.Since(v.lastSeen) > time.Minute {
v.attempts = 0
}
v.attempts++
v.lastSeen = time.Now()
}
// If more than 5 attempts within a minute, block
if exists && v.attempts > 5 {
mu.Unlock()
http.Error(w, "Too many requests. Please try again later.", http.StatusTooManyRequests)
return
}
mu.Unlock()
next.ServeHTTP(w, r)
})
}