Files
honeydany/app/dashboard/middleware.go
T
2025-09-28 21:28:39 +01:00

355 lines
11 KiB
Go

package dashboard
import (
"context"
"encoding/json"
"net/http"
"strings"
)
// ContextKey type for context keys
type ContextKey string
const (
UserContextKey ContextKey = "user"
)
// SecurityManager combines authentication and CSRF protection
type SecurityManager struct {
authManager *AuthManager
csrfManager *CSRFManager
validator *InputValidator
}
// NewSecurityManager creates a new security manager
func NewSecurityManager(authManager *AuthManager) *SecurityManager {
return &SecurityManager{
authManager: authManager,
csrfManager: NewCSRFManager(),
validator: NewInputValidator(),
}
}
// AuthMiddleware provides authentication for web pages
func (sm *SecurityManager) AuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Skip authentication for login page and static assets
if r.URL.Path == "/login" || r.URL.Path == "/api/auth/login" ||
strings.HasPrefix(r.URL.Path, "/static/") {
next(w, r)
return
}
// Check for session token in cookie
cookie, err := r.Cookie("session_token")
if err != nil {
sm.redirectToLogin(w, r)
return
}
user, err := sm.authManager.ValidateSession(cookie.Value)
if err != nil {
sm.redirectToLogin(w, r)
return
}
// Add user to request context
ctx := context.WithValue(r.Context(), UserContextKey, user)
next(w, r.WithContext(ctx))
}
}
// APIAuthMiddleware provides authentication for API endpoints
func (sm *SecurityManager) APIAuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var user *User
var err error
// Check for API key in header
apiKey := r.Header.Get("X-API-Key")
if apiKey != "" {
user, err = sm.authManager.ValidateAPIKey(apiKey)
if err != nil {
sm.sendJSONError(w, "Invalid API key", http.StatusUnauthorized)
return
}
} else {
// Check for session token in cookie (for web-based API calls)
cookie, err := r.Cookie("session_token")
if err != nil {
sm.sendJSONError(w, "Authentication required", http.StatusUnauthorized)
return
}
user, err = sm.authManager.ValidateSession(cookie.Value)
if err != nil {
sm.sendJSONError(w, "Invalid session", http.StatusUnauthorized)
return
}
}
// Add user to request context
ctx := context.WithValue(r.Context(), UserContextKey, user)
next(w, r.WithContext(ctx))
}
}
// RoleMiddleware checks if user has required role
func (sm *SecurityManager) RoleMiddleware(requiredRole string) func(http.HandlerFunc) http.HandlerFunc {
return func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := sm.GetUserFromContext(r.Context())
if user == nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
if !sm.hasPermission(user.Role, requiredRole) {
http.Error(w, "Insufficient permissions", http.StatusForbidden)
return
}
next(w, r)
}
}
}
// CSRFMiddleware provides CSRF protection
func (sm *SecurityManager) CSRFMiddleware(next http.HandlerFunc) http.HandlerFunc {
return sm.csrfManager.CSRFMiddleware(next)
}
// GetUserFromContext extracts user from request context
func (sm *SecurityManager) GetUserFromContext(ctx context.Context) *User {
user, ok := ctx.Value(UserContextKey).(*User)
if !ok {
return nil
}
return user
}
// hasPermission checks if a role has permission for an action
func (sm *SecurityManager) hasPermission(userRole, requiredRole string) bool {
roleHierarchy := map[string]int{
"readonly": 1,
"user": 2,
"admin": 3,
}
userLevel, exists := roleHierarchy[userRole]
if !exists {
return false
}
requiredLevel, exists := roleHierarchy[requiredRole]
if !exists {
return false
}
return userLevel >= requiredLevel
}
// redirectToLogin redirects to login page
func (sm *SecurityManager) redirectToLogin(w http.ResponseWriter, r *http.Request) {
// If it's an AJAX request, return JSON error
if r.Header.Get("X-Requested-With") == "XMLHttpRequest" ||
strings.Contains(r.Header.Get("Accept"), "application/json") {
sm.sendJSONError(w, "Authentication required", http.StatusUnauthorized)
return
}
// Otherwise redirect to login page
http.Redirect(w, r, "/login?redirect="+r.URL.Path, http.StatusSeeOther)
}
// sendJSONError sends a JSON error response
func (sm *SecurityManager) sendJSONError(w http.ResponseWriter, message string, statusCode int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(map[string]string{"error": message})
}
// AddCSRFToken adds CSRF token to response data
func (sm *SecurityManager) AddCSRFToken(w http.ResponseWriter, data map[string]interface{}) error {
return sm.csrfManager.AddCSRFTokenToResponse(w, data)
}
// ValidateInput validates input using the input validator
func (sm *SecurityManager) ValidateInput() *InputValidator {
return sm.validator
}
// GenerateCSRFToken generates a new CSRF token
func (sm *SecurityManager) GenerateCSRFToken() (string, error) {
return sm.csrfManager.GenerateToken()
}
// LoginHandler handles user login
func (sm *SecurityManager) LoginHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
// Show login form
sm.showLoginForm(w, r)
return
}
if r.Method == "POST" {
sm.handleLogin(w, r)
return
}
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
// LogoutHandler handles user logout
func (sm *SecurityManager) LogoutHandler(w http.ResponseWriter, r *http.Request) {
// Get session token from cookie
cookie, err := r.Cookie("session_token")
if err == nil {
// Delete session from database
sm.authManager.DeleteSession(cookie.Value)
}
// Clear session cookie
http.SetCookie(w, &http.Cookie{
Name: "session_token",
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
Secure: r.TLS != nil,
SameSite: http.SameSiteStrictMode,
})
// Redirect to login page
http.Redirect(w, r, "/login", http.StatusSeeOther)
}
// showLoginForm displays the login form
func (sm *SecurityManager) showLoginForm(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{
"Title": "Login",
"Redirect": r.URL.Query().Get("redirect"),
"Error": r.URL.Query().Get("error"),
}
// Add CSRF token
sm.AddCSRFToken(w, data)
// For now, return a simple HTML form
// This will be replaced with proper template rendering
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(`
<!DOCTYPE html>
<html class="dark h-full">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Login - Honeypot Dashboard</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
primary: {
50: '#eff6ff', 100: '#dbeafe', 200: '#bfdbfe', 300: '#93c5fd', 400: '#60a5fa',
500: '#3b82f6', 600: '#2563eb', 700: '#1d4ed8', 800: '#1e40af', 900: '#1e3a8a'
}
}
}
}
}
</script>
</head>
<body class="h-full bg-gray-900">
<div class="min-h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div class="max-w-md w-full space-y-8">
<div>
<h2 class="mt-6 text-center text-3xl font-extrabold text-white">
Sign in to Honeypot Dashboard
</h2>
</div>
<form class="mt-8 space-y-6" method="POST">
<input type="hidden" name="csrf_token" value="` + data["CSRFToken"].(string) + `">
<input type="hidden" name="redirect" value="` + data["Redirect"].(string) + `">
<div class="rounded-md shadow-sm -space-y-px">
<div>
<label for="username" class="sr-only">Username</label>
<input id="username" name="username" type="text" required
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-700 bg-gray-800 text-gray-100 placeholder-gray-400 rounded-t-md focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
placeholder="Username">
</div>
<div>
<label for="password" class="sr-only">Password</label>
<input id="password" name="password" type="password" required
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-700 bg-gray-800 text-gray-100 placeholder-gray-400 rounded-b-md focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
placeholder="Password">
</div>
</div>
<div>
<button type="submit"
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">
Sign in
</button>
</div>
</form>
</div>
</div>
</body>
</html>
`))
}
// handleLogin processes login form submission
func (sm *SecurityManager) handleLogin(w http.ResponseWriter, r *http.Request) {
// Parse form data
err := r.ParseForm()
if err != nil {
http.Redirect(w, r, "/login?error=Invalid+form+data", http.StatusSeeOther)
return
}
username := r.FormValue("username")
password := r.FormValue("password")
redirect := r.FormValue("redirect")
// Validate input
if err := sm.validator.ValidateUsername(username); err != nil {
http.Redirect(w, r, "/login?error="+err.Error(), http.StatusSeeOther)
return
}
// Authenticate user
user, err := sm.authManager.AuthenticateUser(username, password)
if err != nil {
http.Redirect(w, r, "/login?error=Invalid+credentials", http.StatusSeeOther)
return
}
// Create session
session, err := sm.authManager.CreateSession(user.ID, r.RemoteAddr, r.UserAgent())
if err != nil {
http.Redirect(w, r, "/login?error=Failed+to+create+session", http.StatusSeeOther)
return
}
// Set session cookie
http.SetCookie(w, &http.Cookie{
Name: "session_token",
Value: session.Token,
Path: "/",
Expires: session.ExpiresAt,
HttpOnly: true,
Secure: r.TLS != nil,
SameSite: http.SameSiteStrictMode,
})
// Redirect to intended page or dashboard
if redirect != "" && redirect != "/login" {
http.Redirect(w, r, redirect, http.StatusSeeOther)
} else {
http.Redirect(w, r, "/", http.StatusSeeOther)
}
}