summaryrefslogtreecommitdiff
path: root/middleware/auth.go
blob: 3d6484fd2d327e94c0275797ad83dd4c6086775a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
package middleware

import (
	"auth-proxy/config"
	"auth-proxy/logging"
	"auth-proxy/models"
	"auth-proxy/types"
	"auth-proxy/utils"
	"context"
	"database/sql"
	"net/http"
	"strings"
)

// SessionAuth is a middleware that checks for a valid session cookie.
// If the session is valid, it proceeds. If not, it redirects to the login page.
// This is suitable for protecting frontend routes.
func SessionAuth(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 err != nil {
			if err == http.ErrNoCookie {
				logging.AppLog.Info("Redirecting to login (no session cookie)", "path", r.URL.Path, "ip", ip)
				http.Redirect(w, r, config.Paths.Login, http.StatusFound)
				return
			}
			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) // This still relies on the session token in the DB
		if err != nil && err != sql.ErrNoRows {
			logging.AppLog.Error("Error validating session token", "error", err, "path", r.URL.Path, "ip", ip)
			http.Error(w, "Internal Server Error", http.StatusInternalServerError)
			return
		}

		if user == nil {
			// Clear the invalid cookie
			http.SetCookie(w, &http.Cookie{
				Name:   "auth_token",
				Value:  "",
				Path:   "/",
				MaxAge: -1,
			})
			logging.SecurityLog.Warn("ACCESS DENIED", "reason", "invalid or expired session token", "path", r.URL.Path, "ip", ip)
			http.Redirect(w, r, config.Paths.Login, http.StatusFound)
			return
		}

		// Add user info to context and headers for downstream handlers
		ctx := context.WithValue(r.Context(), types.SessionDataContextKey, user)
		r.Header.Set("X-Username", user.Username)
		r.Header.Set("X-User-Role", user.Role)
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

// BearerAuth is a middleware that checks for a valid JWT Bearer token.
// If the token is valid, it adds the user info to the context and proceeds.
// If not, it returns a 401 Unauthorized error. Suitable for API endpoints.
func BearerAuth(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ip := logging.GetClientIP(r)
		authHeader := r.Header.Get("Authorization")

		// Prefer Authorization header if it exists
		if authHeader != "" {
			parts := strings.Split(authHeader, " ")
			if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
				logging.SecurityLog.Warn("API ACCESS DENIED", "reason", "invalid authorization header format", "path", r.URL.Path, "ip", ip)
				http.Error(w, "Unauthorized: Invalid authorization header format", http.StatusUnauthorized)
				return
			}

			tokenString := parts[1]
			payload, err := utils.ValidateJWT(tokenString)
			if err != nil {
				logging.SecurityLog.Warn("API ACCESS DENIED", "reason", "invalid JWT token", "error", err.Error(), "path", r.URL.Path, "ip", ip)
				http.Error(w, "Unauthorized: "+err.Error(), http.StatusUnauthorized)
				return
			}

			user, err := models.GetUserByID(payload.UserID)
			if err != nil || user == nil {
				if err == sql.ErrNoRows || user == nil {
					logging.SecurityLog.Error("API ACCESS DENIED", "reason", "user from token not found", "user_id", payload.UserID, "path", r.URL.Path, "ip", ip)
					http.Error(w, "Unauthorized: User not found", http.StatusUnauthorized)
				} else {
					logging.AppLog.Error("Error fetching user from DB", "error", err, "user_id", payload.UserID, "path", r.URL.Path, "ip", ip)
					http.Error(w, "Internal Server Error", http.StatusInternalServerError)
				}
				return
			}

			if !user.IsActive {
				logging.SecurityLog.Warn("API ACCESS DENIED", "reason", "user is inactive", "user_id", user.ID, "path", r.URL.Path, "ip", ip)
				http.Error(w, "Forbidden: User account is inactive", http.StatusForbidden)
				return
			}

			// Add user info to context and proceed
			ctx := context.WithValue(r.Context(), types.SessionDataContextKey, user)
			next.ServeHTTP(w, r.WithContext(ctx))
			return
		}

		// --- Fallback to Cookie-based session for browser clients ---
		cookie, err := r.Cookie("auth_token")
		if err != nil {
			// No Authorization header AND no cookie, deny access
			logging.SecurityLog.Warn("API ACCESS DENIED", "reason", "missing authentication token", "path", r.URL.Path, "ip", ip)
			http.Error(w, "Unauthorized: An authentication token is required", http.StatusUnauthorized)
			return
		}

		tokenStr := cookie.Value
		user, err := models.GetUserByToken(tokenStr)
		if err != nil || user == nil {
			logging.SecurityLog.Warn("API ACCESS DENIED", "reason", "invalid session cookie", "path", r.URL.Path, "ip", ip)
			http.Error(w, "Unauthorized: Invalid session", http.StatusUnauthorized)
			return
		}

		// User is authenticated via cookie, add to context and 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 either SessionAuth or BearerAuth.
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).(*types.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)
	})
}

// HandleOptions is a middleware that handles pre-flight OPTIONS requests.
// This should be placed before the XSRF middleware.
func HandleOptions(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method == http.MethodOptions {
			w.WriteHeader(http.StatusOK)
			return
		}
		next.ServeHTTP(w, r)
	})
}