package handlers import ( "errors" "net/http" "github.com/creativenoz/aurganize-v62/backend/internal/config" "github.com/creativenoz/aurganize-v62/backend/internal/services" "github.com/labstack/echo/v4" "github.com/rs/zerolog/log" ) // AuthHandler handles all authentication-related HTTP requests. // This handler is responsible for: // 1. User login (validating credentials, generating tokens) // 2. Token refresh (obtaining new access tokens using refresh tokens) // 3. User logout (revoking refresh tokens, clearing cookies) // // Architecture pattern used: Handler -> Service -> Repository // - Handler: Handles HTTP concerns (request parsing, response formatting, cookies) // - Service: Implements business logic (token generation, validation) // - Repository: Handles data persistence (database operations) // // This separation ensures: // - Clean code organization // - Testability (can mock dependencies) // - Reusability (services can be used by other handlers) type AuthHandler struct { config *config.Config // Application configuration (JWT settings, cookie config, etc.) authService *services.AuthService // Service for token generation and validation logic userService *services.UserService // Service for user-related operations (authentication, fetching user data) } // NewAuthHandler creates a new instance of AuthHandler with injected dependencies. // This constructor follows the dependency injection pattern: // - Dependencies are passed in rather than created internally // - Makes testing easier (can pass mock implementations) // - Makes dependencies explicit and visible // - Follows SOLID principles (Dependency Inversion) // // Parameters: // - cfg: Configuration containing JWT secrets, cookie settings, etc. // - authServ: Service for handling authentication logic // - userServ: Service for handling user operations // // Returns: // - Fully initialized AuthHandler ready to handle requests func NewAuthHandler( cfg *config.Config, authServ *services.AuthService, userServ *services.UserService, ) *AuthHandler { log.Info(). Str("handler", "auth"). Str("cookie_domain", cfg.Cookie.CookieDomain). Bool("cookie_secure", cfg.Cookie.CookieSecure). Dur("access_expiry", cfg.JWT.AccessExpiry). Dur("refresh_expiry", cfg.JWT.RefreshExpiry). Msg("auth handler initialized") return &AuthHandler{ config: cfg, authService: authServ, userService: userServ, } } // LoginRequest represents the expected JSON structure for login requests. // JSON tags specify how struct fields map to JSON keys. // Validate tags specify validation rules applied by Echo's validator. // // Why use struct tags? // - json: Controls JSON serialization/deserialization // - validate: Enables automatic validation (email format, required fields, etc.) // // This approach provides: // - Type safety (compile-time checking) // - Automatic validation (don't need manual validation code) // - Clear API contract (documents expected request format) type LoginRequest struct { Email string `json:"email" validate:"required,email"` // Email must be present and valid format Password string `json:"password" validate:"required"` // Password must be present (no format validation for flexibility) } // LoginResponse represents the JSON structure returned after successful login. // Contains everything the client needs to maintain an authenticated session: // - User data for display/personalization // - Access token for API requests // - Refresh token for obtaining new access tokens // - Expiration time for token lifetime management // // Why include both tokens in response AND cookies? // - Cookies: Used for browser-based requests (more secure with HttpOnly flag) // - JSON body: Used by mobile apps or clients that prefer token management in localStorage // - This dual approach supports multiple client types type LoginResponse struct { User interface{} `json:"user"` // User object (actual type depends on sanitization) AccessToken string `json:"access_token"` // JWT access token for API authentication RefreshToken string `json:"refresh_token"` // JWT refresh token for obtaining new access tokens ExpiresIn int `json:"expires_in"` // Access token lifetime in seconds } // TokenRefreshRequest represents the request body for token refresh with rotation. // This struct is used when the client provides the refresh token in the request body // instead of (or in addition to) cookies. // // Use cases: // - Mobile apps that manage tokens in secure storage // - SPAs that prefer localStorage/sessionStorage over cookies // - Cross-origin scenarios where cookies may not work // - Testing and development // // The refresh token can come from either: // 1. Request body (this struct) - for programmatic clients // 2. HTTP-only cookie - for browser clients // The handler will check both sources type TokenRefreshRequest struct { RefreshToken string `json:"refresh_token" validate:"required"` // JWT refresh token to rotate } // TokenRefreshResponse represents the response after successful token refresh with rotation. // Contains both new access and refresh tokens, requiring client to update stored tokens. // // Why both tokens are returned: // - AccessToken: New short-lived token for immediate API use // - RefreshToken: New refresh token (old one is now invalid) // - ExpiresIn: Tells client when to request next refresh // // IMPORTANT: Client MUST store the new refresh token, as the old one is invalidated. // Attempting to reuse the old refresh token will fail and may trigger security alerts. type TokenRefreshResponse struct { AccessToken string `json:"access_token"` // New JWT access token for API authentication RefreshToken string `json:"refresh_token"` // New JWT refresh token (MUST replace old one) ExpiresIn int `json:"expires_in"` // Access token lifetime in seconds } // Login handles user authentication and token generation. // This endpoint processes login requests through several steps: // // Flow: // 1. Parse and validate request body (email, password) // 2. Authenticate user credentials against database // 3. Check if user account is active // 4. Generate access token (short-lived, for API requests) // 5. Generate refresh token (long-lived, for obtaining new access tokens) // 6. Store tokens in HTTP-only cookies (XSS protection) // 7. Update user's last login timestamp and IP // 8. Return user data and tokens in response // // Security measures: // - Passwords never returned (sanitized in response) // - Generic error messages (prevents email enumeration) // - HttpOnly cookies (prevents JavaScript access) // - Account status check (prevents access to deactivated accounts) // - IP and user agent tracking (audit trail, session management) // // Error handling: // - 400: Invalid request format or validation errors // - 401: Invalid credentials (wrong email or password) // - 403: Account exists but not active (suspended, pending verification, etc.) // - 500: Server errors (token generation failure, database errors) func (h *AuthHandler) Login(c echo.Context) error { log.Info(). Str("handler", "auth"). Str("action", "login_attempt"). Str("ip", c.RealIP()). Str("user_agent", c.Request().UserAgent()). Msg("login attempt started") // Step 1: Parse request body into LoginRequest struct var req LoginRequest // Bind() extracts JSON from request body and populates the struct // It handles: // - JSON parsing // - Type conversion // - Field mapping based on json tags if err := c.Bind(&req); err != nil { log.Warn(). Str("handler", "auth"). Str("action", "login_bind_failed"). Str("ip", c.RealIP()). Err(err). Msg("failed to bind login request") // Return 400 Bad Request if JSON is malformed or doesn't match struct return echo.NewHTTPError(http.StatusBadRequest, "invalid request") } // Step 2: Validate request using struct validation tags // Echo's validator checks: // - required: Email and password must be present // - email: Email must be valid format (contains @, proper structure) if err := c.Validate(&req); err != nil { log.Warn(). Str("handler", "auth"). Str("action", "login_validation_failed"). Str("email", req.Email). Str("validation_error", err.Error()). Str("ip", c.RealIP()). Msg("login request validation failed") // Return 400 Bad Request with specific validation error // err.Error() contains details like "Email is required" or "Email is invalid" return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } // Get request context for passing to service layer // Context allows: // - Request cancellation propagation // - Timeout enforcement // - Value passing (trace IDs, user info, etc.) ctx := c.Request().Context() log.Info(). Str("handler", "auth"). Str("action", "authenticate_attempt"). Str("email", req.Email). Str("ip", c.RealIP()). Msg("attempting to authenticate user") // Step 3: Authenticate user by email and password // This calls the user service which: // 1. Looks up user by email // 2. Verifies password using bcrypt // 3. Returns user object if valid user, err := h.userService.AuthenticateUserByEmail(ctx, req.Email, req.Password) if err != nil { log.Warn(). Str("handler", "auth"). Str("action", "authentication_failed"). Str("email", req.Email). Str("ip", c.RealIP()). Err(err). Msg("user authentication failed") // Return 401 Unauthorized with generic message // We use a generic message to prevent email enumeration attacks // (attacker can't tell if email exists but password is wrong) return echo.NewHTTPError(http.StatusUnauthorized, "invalid credentials") } // Step 4: Check if user account is active // Status could be: "active", "suspended", "pending_verification", "deleted", etc. // Only "active" users can log in if user.Status != "active" { log.Warn(). Str("handler", "auth"). Str("action", "inactive_account_login"). Str("user_id", user.ID.String()). Str("email", user.Email). Str("status", user.Status). Str("ip", c.RealIP()). Msg("login attempt on inactive account") // Return 403 Forbidden (authenticated but not authorized) // Different from 401 because we know who they are, but they can't access return echo.NewHTTPError(http.StatusForbidden, "account is not active") } log.Info(). Str("handler", "auth"). Str("action", "authentication_success"). Str("user_id", user.ID.String()). Str("email", user.Email). Str("tenant_id", user.TenantID.String()). Str("role", user.Role). Msg("user authenticated successfully, generating tokens") // Step 5: Generate access token // Access token is short-lived (typically 15 minutes) and contains: // - User ID, tenant ID, email, role // - Expiration time // - Issuer information // Used for authenticating API requests accessToken, err := h.authService.GenerateAccessToken(user) if err != nil { log.Error(). Str("handler", "auth"). Str("action", "access_token_generation_failed"). Str("user_id", user.ID.String()). Str("email", user.Email). Err(err). Msg("failed to generate access token") // Return 500 Internal Server Error // Token generation should rarely fail unless there's a configuration issue return echo.NewHTTPError(http.StatusInternalServerError, "failed to generate access token") } // Extract client information for session tracking // User agent identifies the client (browser type, OS, etc.) userAgent := c.Request().UserAgent() // Real IP handles proxies and load balancers to get actual client IP ipAddress := c.RealIP() log.Info(). Str("handler", "auth"). Str("action", "generating_refresh_token"). Str("user_id", user.ID.String()). Str("ip", ipAddress). Str("user_agent", userAgent). Msg("generating refresh token and creating session") // Step 6: Generate refresh token // Refresh token is long-lived (typically 7 days) and: // - Creates a session record in database // - Stores hashed token for validation // - Tracks device information (user agent, IP) // - Enables session management (list active sessions, revoke specific sessions) refreshToken, _, err := h.authService.GenerateRefreshToken(ctx, user, &userAgent, &ipAddress) if err != nil { log.Error(). Str("handler", "auth"). Str("action", "refresh_token_generation_failed"). Str("user_id", user.ID.String()). Str("email", user.Email). Err(err). Msg("failed to generate refresh token") // Return 500 Internal Server Error // This could fail due to database issues return echo.NewHTTPError(http.StatusInternalServerError, "failed to generate refresh token") } // Step 7: Set tokens as HTTP-only cookies // This provides security benefits: // - HttpOnly flag prevents JavaScript access (XSS protection) // - Secure flag (in production) ensures HTTPS-only transmission // - SameSite flag provides CSRF protection // Cookies are automatically sent with requests, no client-side token management needed h.setAccessTokenCookie(c, accessToken) h.setRefreshTokenCookie(c, refreshToken) log.Debug(). Str("handler", "auth"). Str("action", "updating_last_login"). Str("user_id", user.ID.String()). Str("ip", ipAddress). Msg("updating user last login timestamp") // Step 8: Update user's last login information // Track when and from where user logged in for: // - Security audit trail // - User awareness (show "last login" in UI) // - Suspicious activity detection // We ignore errors here (don't fail login if this update fails) if err = h.userService.UpdateLastLogin(ctx, user.ID, &ipAddress); err != nil { log.Warn(). Str("handler", "auth"). Str("action", "last_login_update_failed"). Str("user_id", user.ID.String()). Err(err). Msg("failed to update last login (non-critical)") } log.Info(). Str("handler", "auth"). Str("action", "login_success"). Str("user_id", user.ID.String()). Str("email", user.Email). Str("tenant_id", user.TenantID.String()). Str("role", user.Role). Str("ip", ipAddress). Str("user_agent", userAgent). Msg("user logged in successfully") // Step 9: Return successful response with user data and tokens // Response includes: // - Sanitized user object (passwords removed) // - Both tokens (for non-cookie clients like mobile apps) // - Token expiration time return c.JSON(http.StatusOK, LoginResponse{ User: h.sanitizeUser(user), AccessToken: accessToken, RefreshToken: refreshToken, ExpiresIn: int(h.config.JWT.AccessExpiry.Seconds()), // Convert duration to seconds }) } // Refresh handles access token renewal using a refresh token. // This endpoint allows clients to obtain a new access token without re-entering credentials. // // Why separate access and refresh tokens? // - Security: Access tokens are short-lived (15 min) limiting exposure if stolen // - UX: Refresh tokens are long-lived (7 days) so users don't constantly re-login // - Control: Can revoke refresh tokens (logout all devices) without affecting active requests // // Flow: // 1. Extract refresh token from HTTP-only cookie // 2. Validate refresh token (signature, expiration, revocation status) // 3. Fetch user from database (ensure user still exists and is active) // 4. Generate new access token // 5. Set new access token cookie // 6. Return new access token in response // // Security measures: // - Refresh token stored in database (can be revoked) // - Validates token hasn't been revoked // - Checks token expiration // - Updates session last-used timestamp // // Error handling: // - 401: Missing refresh token, invalid token, expired token, user not found // - 500: Token generation failure func (h *AuthHandler) Refresh(c echo.Context) error { log.Info(). Str("handler", "auth"). Str("action", "refresh_attempt"). Str("ip", c.RealIP()). Msg("token refresh attempt started") // Step 1: Extract refresh token from cookie // Cookie name must match what was set during login ("refresh_token") // Cookies are automatically parsed by Echo from Cookie header cookie, err := c.Cookie("refresh_token") if err != nil { log.Warn(). Str("handler", "auth"). Str("action", "refresh_missing_token"). Str("ip", c.RealIP()). Msg("refresh token cookie not found") // Return 401 if cookie is missing // This means user is not authenticated or cookie expired/was deleted return echo.NewHTTPError(http.StatusUnauthorized, "missing refresh token") } // Get request context ctx := c.Request().Context() log.Debug(). Str("handler", "auth"). Str("action", "validating_refresh_token"). Str("ip", c.RealIP()). Msg("validating refresh token") // Step 2: Validate the refresh token // This process: // 1. Verifies JWT signature using refresh secret // 2. Checks token expiration time // 3. Looks up session in database using SessionID from claims // 4. Verifies session hasn't been revoked // 5. Updates session's last_used_at timestamp // Returns claims (user ID, session ID, etc.) and session object claims, _, err := h.authService.ValidateRefreshToken(ctx, cookie.Value) if err != nil { log.Warn(). Str("handler", "auth"). Str("action", "refresh_validation_failed"). Str("ip", c.RealIP()). Err(err). Msg("refresh token validation failed") // Return 401 with error details // Could be: "invalid token", "token expired", "token revoked" return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) } log.Debug(). Str("handler", "auth"). Str("action", "refresh_fetching_user"). Str("user_id", claims.UserID.String()). Str("session_id", claims.SessionID.String()). Msg("refresh token validated, fetching user data") // Step 3: Fetch current user from database // We re-fetch the user to ensure: // - User still exists (not deleted) // - User data is current (role might have changed) // - User is still active (account not suspended) user, err := h.userService.GetByID(ctx, claims.UserID) if err != nil { log.Warn(). Str("handler", "auth"). Str("action", "refresh_user_not_found"). Str("user_id", claims.UserID.String()). Str("session_id", claims.SessionID.String()). Err(err). Msg("user not found during token refresh") // Return 401 if user not found // This could mean user was deleted since token was issued return echo.NewHTTPError(http.StatusUnauthorized, "user not found") } // Step 4: Generate new access token // New token contains current user data (including any role changes) accessToken, err := h.authService.GenerateAccessToken(user) // Note: typo "Tokken" if err != nil { log.Error(). Str("handler", "auth"). Str("action", "refresh_token_generation_failed"). Str("user_id", user.ID.String()). Err(err). Msg("failed to generate new access token during refresh") // Return 500 if token generation fails return echo.NewHTTPError(http.StatusInternalServerError, "failed to generate access token") } // Step 5: Set new access token cookie // Only the access token is refreshed, refresh token remains the same // This is more secure - refresh token changes only on login/explicit refresh h.setAccessTokenCookie(c, accessToken) log.Info(). Str("handler", "auth"). Str("action", "refresh_success"). Str("user_id", user.ID.String()). Str("session_id", claims.SessionID.String()). Str("ip", c.RealIP()). Msg("access token refreshed successfully") // Step 6: Return new access token // Response contains: // - New access token (for non-cookie clients) // - Expiration time (so client knows when to refresh again) return c.JSON(http.StatusOK, map[string]interface{}{ "access_token": accessToken, "expires_in": int(h.config.JWT.AccessExpiry.Seconds()), }) } // RefreshTokenWithRotation handles the token refresh endpoint with rotation enabled. // This endpoint implements refresh token rotation for enhanced security: // // What is token rotation? // - Every time a refresh token is used, a NEW refresh token is issued // - The old refresh token is immediately invalidated (revoked in database) // - Client receives both new access token AND new refresh token // - Client must store the new refresh token for next refresh // // Security benefits over non-rotating tokens: // 1. Limited exposure window: Stolen tokens become useless after legitimate user refreshes // 2. Theft detection: Reusing old tokens after rotation indicates potential compromise // 3. Reduced attack surface: Each token is single-use after rotation // 4. Fresh cryptographic material: New random token generated each time // // Token sources (checked in order): // 1. Request body (req.RefreshToken) - for mobile/SPA clients // 2. HTTP-only cookie - for browser-based clients // This dual approach supports multiple client types // // Response (Error - 401 Unauthorized): // - Missing refresh token (not in body or cookie) // - Invalid token signature // - Expired refresh token // - Revoked session (token already used after rotation) // - User not found or account inactive // // Response (Error - 500 Internal Server Error): // - Token generation failure // - Database errors // // Security considerations: // - Old refresh token is immediately invalidated after successful rotation // - Attempting to reuse old token may trigger security alerts (theft detection) // - Both tokens should be transmitted over HTTPS only in production // - Cookies use HttpOnly flag to prevent JavaScript access (XSS protection) // - Session tracks device/IP for security monitoring // // Client implementation requirements: // - MUST store the new refresh token from response // - MUST discard the old refresh token immediately // - MUST NOT retry with old token if refresh fails // - SHOULD implement secure token storage (keychain, secure storage, etc.) func (h *AuthHandler) RefreshTokenWithRotation(c echo.Context) error { log.Info(). Str("handler", "auth"). Str("action", "token_rotation_attempt"). Str("ip", c.RealIP()). Msg("token rotation with refresh attempt started") // Step 1: Parse request body (optional - might use cookie instead) var req TokenRefreshRequest // Attempt to bind JSON from request body // This is optional - we'll also check cookies // Bind error is not fatal here if err := c.Bind(&req); err != nil { // Binding failed - no body or malformed JSON // Not an error yet, we'll check cookie next req.RefreshToken = "" // Ensure empty if bind failed } // Step 2: Determine refresh token source // Check request body first, then fall back to cookie // This supports both browser and non-browser clients var refreshToken string var tokenSource string if req.RefreshToken != "" { // Token provided in request body (mobile/SPA clients) refreshToken = req.RefreshToken tokenSource = "request_body" } else { // Try to get token from HTTP-only cookie (browser clients) cookie, err := c.Cookie("refresh_token") if err != nil { log.Warn(). Str("handler", "auth"). Str("action", "rotation_missing_token"). Str("ip", c.RealIP()). Msg("refresh token not found in body or cookie") // No token in body or cookie return echo.NewHTTPError(http.StatusUnauthorized, "missing refresh token") } refreshToken = cookie.Value tokenSource = "cookie" } log.Debug(). Str("handler", "auth"). Str("action", "rotation_token_source"). Str("token_source", tokenSource). Str("ip", c.RealIP()). Msg("refresh token source identified") // Step 3: Validate that we have a token if refreshToken == "" { return echo.NewHTTPError(http.StatusUnauthorized, "missing refresh token") } // Get request context for cancellation and timeout propagation ctx := c.Request().Context() // Step 4: Extract client metadata for new session // This information is stored with the new session for: // - Security monitoring (detect unusual locations/devices) // - Session display (show user their active sessions) // - Audit trail (track when/where tokens were used) userAgent := c.Request().UserAgent() ipAddress := c.RealIP() log.Info(). Str("handler", "auth"). Str("action", "rotating_refresh_token"). Str("ip", ipAddress). Str("user_agent", userAgent). Str("token_source", tokenSource). Msg("rotating refresh token and creating new session") // Step 5: Rotate the refresh token // This process: // 1. Validates old token (JWT + session check) // 2. Generates new access token // 3. Generates new refresh token (creates new session) // 4. Revokes old session (invalidates old token) newAccessToken, newRefreshToken, _, err := h.authService.RotateRefreshToken( ctx, refreshToken, &userAgent, &ipAddress, ) if err != nil { // Handle specific error types with appropriate HTTP status and messages // Using errors.Is() for proper error comparison if errors.Is(err, services.ErrExpiredToken) { log.Warn(). Str("handler", "auth"). Str("action", "rotation_expired_token"). Str("ip", ipAddress). Str("token_source", tokenSource). Msg("refresh token expired during rotation") // Refresh token has expired (needs re-login) return echo.NewHTTPError(http.StatusUnauthorized, "refresh token expired") } if errors.Is(err, services.ErrRevokedToken) { log.Warn(). Str("handler", "auth"). Str("action", "rotation_revoked_token"). Str("ip", ipAddress). Str("token_source", tokenSource). Msg("revoked refresh token used in rotation attempt") // Session was revoked (logout, password change, etc.) return echo.NewHTTPError(http.StatusUnauthorized, "refresh token revoked") } if errors.Is(err, services.ErrInvalidToken) { log.Warn(). Str("handler", "auth"). Str("action", "rotation_invalid_token"). Str("ip", ipAddress). Str("token_source", tokenSource). Msg("invalid refresh token in rotation attempt") // Token signature invalid or malformed return echo.NewHTTPError(http.StatusUnauthorized, "invalid refresh token") } log.Error(). Str("handler", "auth"). Str("action", "rotation_failed"). Str("ip", ipAddress). Str("token_source", tokenSource). Err(err). Msg("token rotation failed with unexpected error") // Generic error for unexpected cases return echo.NewHTTPError(http.StatusUnauthorized, "invalid refresh token") } // Step 6: Set new tokens in HTTP-only cookies // This benefits browser clients: // - Cookies automatically sent with requests // - HttpOnly prevents JavaScript access (XSS protection) // - Browser handles storage securely // Non-browser clients will use tokens from response body h.setAccessTokenCookie(c, newAccessToken) h.setRefreshTokenCookie(c, newRefreshToken) log.Info(). Str("handler", "auth"). Str("action", "rotation_success"). Str("ip", ipAddress). Str("user_agent", userAgent). Str("token_source", tokenSource). Msg("refresh token rotated successfully, new tokens issued") // Step 7: Return new tokens in response body // Both browser and non-browser clients receive tokens // Non-browser clients MUST store the new refresh token // Browser clients benefit from having tokens available in JavaScript if needed return c.JSON(http.StatusOK, TokenRefreshResponse{ AccessToken: newAccessToken, RefreshToken: newRefreshToken, ExpiresIn: int(h.config.JWT.AccessExpiry.Seconds()), }) } // Logout handles user logout by revoking the refresh token and clearing cookies. // This endpoint invalidates the current session and removes authentication cookies. // // Why logout is important: // - Security: Revokes refresh token so it can't be used to get new access tokens // - Privacy: Removes tokens from browser // - Session management: Marks session as ended in database // // Flow: // 1. Extract refresh token from cookie // 2. Revoke the refresh token in database // 3. Clear both access and refresh token cookies // 4. Return success (204 No Content) // // Graceful handling: // - If no refresh token cookie: Still clears cookies and returns success // - If token revocation fails: Still clears cookies (client-side cleanup) // - Always returns success to avoid information leakage // // Error handling: // - No errors returned to client (always succeeds) // - Errors are silently handled to prevent information disclosure func (h *AuthHandler) Logout(c echo.Context) error { log.Info(). Str("handler", "auth"). Str("action", "logout_attempt"). Str("ip", c.RealIP()). Msg("user logout attempt") // Step 1: Attempt to get refresh token from cookie cookie, err := c.Cookie("refresh_token") if err != nil { log.Debug(). Str("handler", "auth"). Str("action", "logout_no_token"). Str("ip", c.RealIP()). Msg("logout attempt without refresh token cookie") // No cookie found - user might have already logged out or session expired // Still clear cookies (might be stale access token) and return success h.clearAuthCookies(c) return c.NoContent(http.StatusOK) } // Get request context ctx := c.Request().Context() log.Info(). Str("handler", "auth"). Str("action", "revoking_refresh_token"). Str("ip", c.RealIP()). Msg("revoking refresh token for logout") // Step 2: Revoke the refresh token in database // This marks the session as revoked with reason "user_logout" // Updates: is_revoked=true, revoked_at=NOW(), revoked_reason='user_logout' // We ignore errors here - even if revocation fails, we clear client cookies if err = h.authService.RevokeRefreshToken(ctx, cookie.Value); err != nil { log.Warn(). Str("handler", "auth"). Str("action", "revocation_failed"). Str("ip", c.RealIP()). Err(err). Msg("failed to revoke refresh token during logout (continuing anyway)") } // Step 3: Clear authentication cookies from browser // Sets MaxAge=-1 which tells browser to immediately delete cookies // Clears both access_token and refresh_token cookies h.clearAuthCookies(c) log.Info(). Str("handler", "auth"). Str("action", "logout_success"). Str("ip", c.RealIP()). Msg("user logged out successfully") // Step 4: Return success with no content // 204 No Content is appropriate for successful logout // No response body needed return c.NoContent(http.StatusOK) } // setAccessTokenCookie creates and sets the access token cookie with security flags. // This cookie stores the JWT access token for subsequent API requests. // // Cookie configuration explained: // - Name: "access_token" - identifies this cookie // - Value: The JWT token string // - Path: "/" - cookie sent for all paths on domain // - Domain: From config (e.g., "localhost", ".example.com") // - MaxAge: Token lifetime in seconds (how long browser keeps cookie) // - Secure: Only sent over HTTPS (true in production) // - HttpOnly: Cannot be accessed by JavaScript (XSS protection) // - SameSite: CSRF protection (controls when cookie is sent) // // Why these settings? // - HttpOnly: Prevents XSS attacks from stealing tokens // - Secure: Prevents tokens from being sent over unencrypted connections // - SameSite: Prevents CSRF attacks by controlling cross-site cookie sending // - Path=/: Makes cookie available to all API endpoints func (h *AuthHandler) setAccessTokenCookie(c echo.Context, token string) { log.Debug(). Str("handler", "auth"). Str("action", "set_access_cookie"). Str("domain", h.config.Cookie.CookieDomain). Bool("secure", h.config.Cookie.CookieSecure). Int("max_age", int(h.config.JWT.AccessExpiry.Seconds())). Msg("setting access token cookie") cookie := &http.Cookie{ Name: "access_token", Value: token, Path: "/", // Available to all paths Domain: h.config.Cookie.CookieDomain, MaxAge: int(h.config.JWT.AccessExpiry.Seconds()), // Browser deletes after this time Secure: h.config.Cookie.CookieSecure, // HTTPS only in production HttpOnly: true, // JavaScript cannot access (XSS protection) SameSite: h.parseSameSite(h.config.Cookie.CookieSameSite), // CSRF protection } c.SetCookie(cookie) } // setRefreshTokenCookie creates and sets the refresh token cookie with security flags. // This cookie stores the JWT refresh token for obtaining new access tokens. // // Similar to access token cookie but with longer lifetime (7 days vs 15 minutes). // Uses same security flags (HttpOnly, Secure, SameSite) for protection. // // Why separate cookies? // - Different lifetimes (access=short, refresh=long) // - Different purposes (access=API requests, refresh=token renewal) // - Can revoke one without affecting the other // - Follows OAuth 2.0 best practices func (h *AuthHandler) setRefreshTokenCookie(c echo.Context, token string) { log.Debug(). Str("handler", "auth"). Str("action", "set_refresh_cookie"). Str("domain", h.config.Cookie.CookieDomain). Bool("secure", h.config.Cookie.CookieSecure). Int("max_age", int(h.config.JWT.RefreshExpiry.Seconds())). Msg("setting refresh token cookie") cookie := &http.Cookie{ Name: "refresh_token", Value: token, Path: "/", // Available to all paths Domain: h.config.Cookie.CookieDomain, MaxAge: int(h.config.JWT.RefreshExpiry.Seconds()), // Much longer than access token Secure: h.config.Cookie.CookieSecure, // HTTPS only in production HttpOnly: true, // JavaScript cannot access (XSS protection) SameSite: h.parseSameSite(h.config.Cookie.CookieSameSite), // CSRF protection } c.SetCookie(cookie) } // clearAuthCookies removes both access and refresh token cookies from the browser. // This is called during logout to clean up authentication state. // // How cookie deletion works: // - Set MaxAge=-1 which tells browser to immediately delete the cookie // - Set empty Value to clear any existing value // - Keep same Name, Path, and Domain so browser knows which cookie to delete // // Why we still set Secure and HttpOnly: // - Browser needs these to match original cookie attributes for deletion // - Ensures cookie is properly identified and removed func (h *AuthHandler) clearAuthCookies(c echo.Context) { log.Debug(). Str("handler", "auth"). Str("action", "clear_auth_cookies"). Msg("clearing access and refresh token cookies") // Create cookie with MaxAge=-1 to delete access token accessCookie := &http.Cookie{ Name: "access_token", Value: "", // Empty value Path: "/", Domain: h.config.Cookie.CookieDomain, MaxAge: -1, // Negative MaxAge means delete immediately Secure: h.config.Cookie.CookieSecure, HttpOnly: true, } // Create cookie with MaxAge=-1 to delete refresh token refreshCookie := &http.Cookie{ Name: "refresh_token", Value: "", // Empty value Path: "/", Domain: h.config.Cookie.CookieDomain, MaxAge: -1, // Negative MaxAge means delete immediately Secure: h.config.Cookie.CookieSecure, HttpOnly: true, } // Set both cookies (browser will delete them) c.SetCookie(accessCookie) c.SetCookie(refreshCookie) } // parseSameSite converts a string SameSite policy to http.SameSite type. // SameSite is a cookie attribute that controls when cookies are sent in cross-site requests. // // Values explained: // - "strict": Cookie never sent in cross-site requests (most secure, may break some flows) // Example: User clicks link from email to your site - no cookie sent // - "lax": Cookie sent on top-level navigation (GET) but not on embedded requests (balanced) // Example: User clicks link - cookie sent; Embedded image - cookie not sent // - "none": Cookie always sent (requires Secure=true, needed for some third-party integrations) // Example: Your API called from different domain - cookie sent // - default: Browser decides (usually similar to "lax") // // Why this matters for security: // - Prevents CSRF attacks by limiting when cookies are sent // - "lax" is recommended for most authentication cookies (good security + usability) // - "strict" can break legitimate flows (like OAuth redirects) // - "none" should only be used when necessary (requires HTTPS) func (h *AuthHandler) parseSameSite(s string) http.SameSite { switch s { case "strict": return http.SameSiteStrictMode // Never send cookie cross-site case "lax": return http.SameSiteLaxMode // Send on top-level navigation only case "none": return http.SameSiteNoneMode // Always send (requires Secure=true) default: return http.SameSiteDefaultMode // Let browser decide } } // sanitizeUser removes sensitive information from user object before sending to client. // Currently just returns the user as-is, but should remove: // - password_hash: Never send password hashes to client // - internal IDs: Remove any internal tracking IDs // - audit fields: Consider removing internal timestamps // // TODO: Implement actual sanitization: // - Remove PasswordHash field // - Consider using a separate UserResponse struct // - Transform to DTO (Data Transfer Object) pattern // // Why sanitization is critical: // - Security: Prevents exposing sensitive data // - Privacy: User data should be minimal // - API contract: Clearly defines what clients receive func (h *AuthHandler) sanitizeUser(user interface{}) interface{} { // TODO: Actually sanitize the user object // Current implementation just passes through - should remove sensitive fields return user }