package middleware import ( "net/http" "strings" "github.com/creativenoz/aurganize-v62/backend/internal/services" "github.com/labstack/echo/v4" ) // AuthMiddleware provides authentication middleware for protecting routes. // This middleware intercepts HTTP requests to verify user authentication before // allowing access to protected resources. // // What is middleware? // Middleware is code that runs between receiving a request and executing the handler. // It's like a security checkpoint that requests must pass through. // // Request flow with middleware: // Client Request → CORS → AuthMiddleware → Route Handler → Response // // This middleware provides two authentication modes: // 1. Authenticate: REQUIRED authentication (blocks unauthenticated requests) // 2. OptionalAuth: OPTIONAL authentication (allows both authenticated and anonymous) // // Authentication sources (checked in order): // 1. HTTP-only cookie (primary for browser clients) // 2. Authorization header with Bearer token (for mobile/API clients) // // Why support both? // - Cookies: Secure for browsers (HttpOnly prevents XSS) // - Headers: Required for mobile apps and API clients // - Flexibility: Supports multiple client types // // What gets validated: // - JWT signature (ensures token wasn't tampered with) // - Token expiration (ensures token is still valid) // - Token type (ensures it's an access token, not refresh) // - Token format (ensures proper JWT structure) // // After successful authentication: // - User claims are stored in Echo context // - Downstream handlers can access user info via c.Get("user_id"), etc. // - No need to re-validate token in handlers type AuthMiddleware struct { authService *services.AuthService } // NewAuthMiddleware creates a new authentication middleware with injected dependencies. // This constructor follows the dependency injection pattern for: // - Testability: Can inject mock auth service for testing // - Flexibility: Can swap implementations without changing middleware // - Clear dependencies: Explicitly shows what middleware needs // // Parameters: // - authService: Service that handles token validation // // Returns: // - Fully initialized AuthMiddleware ready to protect routes // // Usage: // // authService := services.NewAuthService(...) // authMiddleware := middleware.NewAuthMiddleware(authService) // e.GET("/protected", handler, authMiddleware.Authenticate) func NewAuthMiddleware(authService *services.AuthService) *AuthMiddleware { return &AuthMiddleware{ authService: authService, } } // Authenticate is a REQUIRED authentication middleware. // Routes using this middleware will reject requests without valid authentication. // // When to use: // - Protected endpoints that require authentication // - User-specific operations (profile, settings, logout) // - Resource access control (only authenticated users) // - Any route that needs user identity // // Authentication flow: // 1. Extract token from cookie OR Authorization header // 2. Validate token (signature, expiration, type) // 3. If valid: Store claims in context, continue to handler // 4. If invalid: Return 401 Unauthorized, block request // // Token sources (priority order): // 1. Cookie: "access_token" (for browser clients) // 2. Header: "Authorization: Bearer " (for mobile/API clients) // // Why check cookie first? // - More secure for browsers (HttpOnly prevents XSS) // - Automatically sent by browsers // - Primary method for web applications // // Response codes: // - 200: Token valid, request proceeds to handler // - 401: Missing token, invalid token, or expired token // // What gets stored in context (accessible in handlers): // - user_id: UUID of authenticated user // - tenant_id: UUID of user's organization/tenant // - email: User's email address // - role: User's role (admin, user, etc.) // - claims: Full claims object (all token data) // // Handler access example: // // userID := c.Get("user_id").(uuid.UUID) // email := c.Get("email").(string) // role := c.Get("role").(string) // // Error handling: // - Missing token: "missing authentication token" // - Invalid format: "invalid authorization header format" // - Expired token: "token has expired" (client should refresh) // - Invalid token: "invalid token" (signature/tampering) // // Parameters: // - next: The next handler in the chain (the actual route handler) // // Returns: // - HandlerFunc that wraps the next handler with authentication // // Usage: // // // Protect single route // e.GET("/profile", profileHandler, authMiddleware.Authenticate) // // // Protect route group // protected := e.Group("/api", authMiddleware.Authenticate) // protected.GET("/users", listUsers) // protected.POST("/posts", createPost) func (m *AuthMiddleware) Authenticate(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { // Step 1: Try to get token from cookie first (browser clients) // This is the preferred method for web applications token, err := c.Cookie("access_token") var tokenString string if err == nil { // Cookie found - use its value // This path is taken by browser-based clients tokenString = token.Value } else { // Step 2: Cookie not found, try Authorization header (mobile/API clients) // Expected format: "Authorization: Bearer " authHeader := c.Request().Header.Get("Authorization") if authHeader == "" { // No cookie AND no header - user is not authenticated return echo.NewHTTPError(http.StatusUnauthorized, "missing authentication token") } // Step 3: Parse Authorization header // Expected format: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." // Split into ["Bearer", "token_string"] parts := strings.Split(authHeader, " ") // Validate header format if len(parts) != 2 || parts[0] != "Bearer" { // Invalid format examples: // - "Bearer" (no token) // - "Bearer token extra" (too many parts) // - "Basic base64string" (wrong auth type) // - "token" (missing "Bearer" prefix) return echo.NewHTTPError(http.StatusUnauthorized, "invalid authorization header format") } // Extract token (second part after "Bearer ") tokenString = parts[1] } // Step 4: Validate the access token // This checks: // - JWT signature (proves token wasn't tampered) // - Token expiration (ensures not expired) // - Token type (ensures it's "access" not "refresh") // - Token structure (valid JWT format) claims, err := m.authService.ValidateAccessToken(tokenString) if err != nil { // Handle specific error types if err == services.ErrExpiredToken { // Token is valid but expired // Client should use refresh token to get new access token // Return specific message so client knows to refresh return echo.NewHTTPError(http.StatusUnauthorized, "token has expired") } // Other errors: invalid signature, wrong type, malformed, etc. // Return generic error to avoid leaking information return echo.NewHTTPError(http.StatusUnauthorized, "invalid token") } // Step 5: Token is valid - store claims in context // Context values can be retrieved by downstream handlers // This avoids re-validating token in every handler // Store individual fields for easy access c.Set("user_id", claims.UserID) c.Set("tenant_id", claims.TenantID) c.Set("email", claims.Email) c.Set("role", claims.Role) // Store full claims object for advanced use cases c.Set("claims", claims) // Step 6: Continue to next handler (the actual route handler) // Request is now authenticated and handlers can access user info return next(c) } } // OptionalAuth is an OPTIONAL authentication middleware. // Routes using this middleware will work for both authenticated and anonymous users. // // When to use: // - Public endpoints that enhance experience for logged-in users // - Content that shows differently based on auth status // - APIs that return more data for authenticated users // - Features with both public and private modes // // Examples: // 1. Homepage: Shows personalized content if logged in, generic if not // 2. Blog post: Shows "Edit" button if author is logged in // 3. Search: Returns more results for authenticated users // 4. Comments: Shows "Reply" option if logged in // // Behavior: // - If valid token: Store claims in context, proceed (like Authenticate) // - If no token: Proceed anyway without claims // - If invalid token: Proceed anyway without claims (graceful degradation) // // Why not return error for invalid token? // - Allows graceful degradation (partial functionality) // - Doesn't block anonymous users // - Expired tokens don't break the page // - Better user experience // // How handlers detect authentication status: // // userID := c.Get("user_id") // if userID != nil { // // User is authenticated // authenticatedUserID := userID.(uuid.UUID) // // Show personalized content // } else { // // User is anonymous // // Show generic content // } // // Difference from Authenticate: // - Authenticate: BLOCKS unauthenticated requests (401 error) // - OptionalAuth: ALLOWS unauthenticated requests (no error) // // Token source: // - Only checks cookie (not Authorization header) // - Why? Browser-based clients naturally use cookies // - Mobile/API clients should use specific endpoints // // What gets stored (if authenticated): // - user_id: UUID of authenticated user // - tenant_id: UUID of user's organization // - email: User's email address // - role: User's role // - claims: Full claims object // // What gets stored (if not authenticated): // - Nothing - context values will be nil // // Parameters: // - next: The next handler in the chain (the actual route handler) // // Returns: // - HandlerFunc that wraps the next handler with optional authentication // // Usage: // // // Single route with optional auth // e.GET("/", homeHandler, authMiddleware.OptionalAuth) // // // Route group with optional auth // public := e.Group("/public", authMiddleware.OptionalAuth) // public.GET("/posts", listPosts) // Shows different content based on auth // public.GET("/post/:id", viewPost) // Shows edit button if authenticated // // Handler example: // // func homeHandler(c echo.Context) error { // userID := c.Get("user_id") // if userID != nil { // // Authenticated user // return c.Render(200, "home-authenticated", data) // } // // Anonymous user // return c.Render(200, "home-public", data) // } func (m *AuthMiddleware) OptionalAuth(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { // Step 1: Try to get token from cookie // We only check cookies for optional auth (not Authorization header) // This is intentional - optional auth is primarily for browser clients token, err := c.Cookie("access_token") if err != nil { // No cookie found - user is anonymous // This is OKAY for optional auth // Proceed to handler without setting context values // Handler will see nil values and know user is not authenticated return next(c) } // Step 2: Cookie found - validate the token // Even though auth is optional, we validate if token is present // This ensures we don't use invalid/expired tokens claims, err := m.authService.ValidateAccessToken(token.Value) if err != nil { // Token is invalid or expired // For optional auth, we don't return error // Just proceed without setting context values // User will be treated as anonymous // This provides graceful degradation return next(c) } // Step 3: Token is valid - store claims in context // Handler can now detect authenticated user via c.Get("user_id") // Same values as Authenticate middleware c.Set("user_id", claims.UserID) c.Set("tenant_id", claims.TenantID) c.Set("email", claims.Email) c.Set("role", claims.Role) c.Set("claims", claims) // Step 4: Continue to handler // Handler can check if user_id is nil to determine auth status return next(c) } }