455 lines
16 KiB
Go
455 lines
16 KiB
Go
package middleware
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/creativenoz/aurganize-v62/backend/internal/services"
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
// 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 {
|
|
log.Info().
|
|
Str("middleware", "auth").
|
|
Str("component", "middleware_init").
|
|
Bool("has_auth_service", authService != nil).
|
|
Msg("authentication middleware initialized")
|
|
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 <token>" (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 {
|
|
log.Debug().
|
|
Str("middleware", "auth").
|
|
Str("action", "authenticate_check_started").
|
|
Str("path", c.Request().URL.Path).
|
|
Str("method", c.Request().Method).
|
|
Str("ip", c.RealIP()).
|
|
Str("user_agent", c.Request().UserAgent()).
|
|
Msg("checking authentication for protected route")
|
|
// 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
|
|
var tokenSource string
|
|
if err == nil {
|
|
tokenSource = "cookie"
|
|
// Cookie found - use its value
|
|
// This path is taken by browser-based clients
|
|
tokenString = token.Value
|
|
log.Debug().
|
|
Str("middleware", "auth").
|
|
Str("action", "token_found_in_cookie").
|
|
Str("path", c.Request().URL.Path).
|
|
Msg("access token found in cookie")
|
|
} else {
|
|
log.Debug().
|
|
Str("middleware", "auth").
|
|
Str("action", "no_cookie_checking_header").
|
|
Str("path", c.Request().URL.Path).
|
|
Msg("no cookie found, checking authorization header")
|
|
// Step 2: Cookie not found, try Authorization header (mobile/API clients)
|
|
// Expected format: "Authorization: Bearer <token>"
|
|
authHeader := c.Request().Header.Get("Authorization")
|
|
if authHeader == "" {
|
|
log.Warn().
|
|
Str("middleware", "auth").
|
|
Str("action", "missing_authentication").
|
|
Str("path", c.Request().URL.Path).
|
|
Str("method", c.Request().Method).
|
|
Str("ip", c.RealIP()).
|
|
Str("user_agent", c.Request().UserAgent()).
|
|
Msg("authentication required but no token provided")
|
|
|
|
// 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" {
|
|
log.Warn().
|
|
Str("middleware", "auth").
|
|
Str("action", "invalid_auth_header_format").
|
|
Str("invalid_header", authHeader).
|
|
Int("header_parts_count", len(parts)).
|
|
Str("path", c.Request().URL.Path).
|
|
Str("ip", c.RealIP()).
|
|
Msg("authorization header present but format is invalid")
|
|
// 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]
|
|
tokenSource = "header"
|
|
|
|
log.Debug().
|
|
Str("middleware", "auth").
|
|
Str("action", "token_found_in_header").
|
|
Str("path", c.Request().URL.Path).
|
|
Msg("access token found in authorization header")
|
|
}
|
|
|
|
log.Debug().
|
|
Str("middleware", "auth").
|
|
Str("action", "validating_access_token").
|
|
Str("token_source", tokenSource).
|
|
Str("path", c.Request().URL.Path).
|
|
Msg("validating access token")
|
|
// 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 {
|
|
log.Warn().
|
|
Str("middleware", "auth").
|
|
Str("action", "expired_token").
|
|
Str("token_source", tokenSource).
|
|
Str("path", c.Request().URL.Path).
|
|
Str("ip", c.RealIP()).
|
|
Str("user_agent", c.Request().UserAgent()).
|
|
Msg("access token has expired")
|
|
|
|
// 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")
|
|
}
|
|
log.Warn().
|
|
Str("middleware", "auth").
|
|
Str("action", "invalid_token").
|
|
Err(err).
|
|
Str("token_source", tokenSource).
|
|
Str("path", c.Request().URL.Path).
|
|
Str("ip", c.RealIP()).
|
|
Str("user_agent", c.Request().UserAgent()).
|
|
Msg("token validation failed - invalid or tampered token")
|
|
// Other errors: invalid signature, wrong type, malformed, etc.
|
|
// Return generic error to avoid leaking information
|
|
return echo.NewHTTPError(http.StatusUnauthorized, "invalid token")
|
|
}
|
|
|
|
log.Info().
|
|
Str("middleware", "auth").
|
|
Str("action", "authentication_success").
|
|
Str("user_id", claims.UserID.String()).
|
|
Str("tenant_id", claims.TenantID.String()).
|
|
Str("email", claims.Email).
|
|
Str("role", claims.Role).
|
|
Str("token_source", tokenSource).
|
|
Str("path", c.Request().URL.Path).
|
|
Str("method", c.Request().Method).
|
|
Str("ip", c.RealIP()).
|
|
Msg("user authenticated successfully")
|
|
// 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 {
|
|
log.Debug().
|
|
Str("middleware", "auth").
|
|
Str("action", "optional_auth_check_started").
|
|
Str("path", c.Request().URL.Path).
|
|
Str("method", c.Request().Method).
|
|
Str("ip", c.RealIP()).
|
|
Msg("checking optional authentication")
|
|
// 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 {
|
|
log.Debug().
|
|
Str("middleware", "auth").
|
|
Str("action", "optional_auth_anonymous").
|
|
Str("path", c.Request().URL.Path).
|
|
Str("ip", c.RealIP()).
|
|
Str("user_agent", c.Request().UserAgent()).
|
|
Msg("no authentication cookie - proceeding as anonymous user")
|
|
// 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)
|
|
}
|
|
log.Debug().
|
|
Str("middleware", "auth").
|
|
Str("action", "optional_auth_validating_token").
|
|
Str("path", c.Request().URL.Path).
|
|
Msg("cookie found in optional auth, validating token")
|
|
// 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 {
|
|
log.Debug().
|
|
Str("middleware", "auth").
|
|
Str("action", "optional_auth_token_invalid").
|
|
Err(err).
|
|
Str("path", c.Request().URL.Path).
|
|
Str("ip", c.RealIP()).
|
|
Msg("token validation failed in optional auth - proceeding as anonymous")
|
|
|
|
// 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)
|
|
}
|
|
log.Info().
|
|
Str("middleware", "auth").
|
|
Str("action", "optional_auth_authenticated").
|
|
Str("user_id", claims.UserID.String()).
|
|
Str("tenant_id", claims.TenantID.String()).
|
|
Str("email", claims.Email).
|
|
Str("role", claims.Role).
|
|
Str("path", c.Request().URL.Path).
|
|
Str("ip", c.RealIP()).
|
|
Msg("authenticated user accessing optionally-protected route")
|
|
// 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)
|
|
}
|
|
}
|