package auth import ( "crypto/subtle" "encoding/json" "fmt" "net/http" "strings" "time" "freeleaps.com/gitea-webhook-ambassador/internal/logger" "github.com/golang-jwt/jwt/v5" ) type Middleware struct { secretKey string } func NewMiddleware(secretKey string) *Middleware { logger.Debug("Creating auth middleware with secret key length: %d", len(secretKey)) return &Middleware{ secretKey: secretKey, } } // VerifyToken verifies a JWT token and returns an error if invalid func (m *Middleware) VerifyToken(r *http.Request) error { // Get token from Authorization header authHeader := r.Header.Get("Authorization") if authHeader == "" { return fmt.Errorf("no authorization header") } // Remove 'Bearer ' prefix tokenString := strings.TrimPrefix(authHeader, "Bearer ") // Parse and validate token token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return []byte(m.secretKey), nil }) if err != nil { return fmt.Errorf("invalid token: %w", err) } if !token.Valid { return fmt.Errorf("token is not valid") } return nil } // LoginRequest represents the login request body type LoginRequest struct { SecretKey string `json:"secret_key"` } // LoginResponse represents the login response type LoginResponse struct { Token string `json:"token"` } // HandleLogin handles the login API request func (m *Middleware) HandleLogin(w http.ResponseWriter, r *http.Request) { // Only accept POST requests if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // Parse JSON request var req LoginRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } // Validate secret key if subtle.ConstantTimeCompare([]byte(req.SecretKey), []byte(m.secretKey)) != 1 { w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]string{ "error": "Invalid secret key", }) return } // Generate JWT token token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "exp": time.Now().Add(24 * time.Hour).Unix(), "iat": time.Now().Unix(), }) // Sign the token tokenString, err := token.SignedString([]byte(m.secretKey)) if err != nil { logger.Error("Failed to generate token: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } // Return the token w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(LoginResponse{ Token: tokenString, }) } // Authenticate middleware for protecting routes func (m *Middleware) Authenticate(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Skip authentication for login page and static assets if r.URL.Path == "/login" || strings.HasPrefix(r.URL.Path, "/css/") || strings.HasPrefix(r.URL.Path, "/js/") || strings.HasPrefix(r.URL.Path, "/img/") { next.ServeHTTP(w, r) return } if err := m.VerifyToken(r); err != nil { logger.Debug("Token verification failed: %v", err) if r.Header.Get("X-Requested-With") == "XMLHttpRequest" { w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]string{ "error": "Invalid or expired token", }) } else { http.Redirect(w, r, "/login", http.StatusSeeOther) } return } // Token is valid, proceed next.ServeHTTP(w, r) }) }