package handlers import ( "crypto/hmac" "crypto/rand" "crypto/sha256" "encoding/hex" "fmt" "net/http" "time" "github.com/gin-gonic/gin" "go.uber.org/zap" "github.com/kms/api-key-service/internal/auth" "github.com/kms/api-key-service/internal/config" "github.com/kms/api-key-service/internal/domain" "github.com/kms/api-key-service/internal/errors" "github.com/kms/api-key-service/internal/services" ) // AuthHandler handles authentication-related HTTP requests type AuthHandler struct { authService services.AuthenticationService tokenService services.TokenService headerValidator *auth.HeaderValidator config config.ConfigProvider errorHandler *errors.ErrorHandler logger *zap.Logger } // NewAuthHandler creates a new auth handler func NewAuthHandler( authService services.AuthenticationService, tokenService services.TokenService, config config.ConfigProvider, logger *zap.Logger, ) *AuthHandler { return &AuthHandler{ authService: authService, tokenService: tokenService, headerValidator: auth.NewHeaderValidator(config, logger), config: config, errorHandler: errors.NewErrorHandler(logger), logger: logger, } } // Login handles POST /login func (h *AuthHandler) Login(c *gin.Context) { var req domain.LoginRequest if err := c.ShouldBindJSON(&req); err != nil { h.errorHandler.HandleValidationError(c, "request_body", "Invalid login request format") return } // Validate authentication headers with HMAC signature userContext, err := h.headerValidator.ValidateAuthenticationHeaders(c.Request) if err != nil { h.errorHandler.HandleAuthenticationError(c, err) return } h.logger.Info("Processing login request", zap.String("user_id", userContext.UserID), zap.String("app_id", req.AppID)) // Generate user token token, err := h.tokenService.GenerateUserToken(c.Request.Context(), req.AppID, userContext.UserID, req.Permissions) if err != nil { h.errorHandler.HandleInternalError(c, err) return } if req.RedirectURI == "" { // If no redirect URI, return token directly via secure response body c.JSON(http.StatusOK, gin.H{ "token": token, "user_id": userContext.UserID, "app_id": req.AppID, "expires_in": 604800, // 7 days in seconds }) return } // For redirect flows, choose token delivery method // Default to cookie delivery for security tokenDelivery := req.TokenDelivery if tokenDelivery == "" { tokenDelivery = domain.TokenDeliveryCookie } h.logger.Debug("Token delivery mode", zap.String("mode", string(tokenDelivery))) // Generate a secure state parameter for CSRF protection state := h.generateSecureState(userContext.UserID, req.AppID) var redirectURL string switch tokenDelivery { case domain.TokenDeliveryQuery: // Deliver token via query parameter (for integrations like VS Code) redirectURL = req.RedirectURI + "?token=" + token + "&state=" + state case domain.TokenDeliveryCookie: // Deliver token via secure cookie (default, more secure) c.SetSameSite(http.SameSiteStrictMode) // In development mode, make cookie accessible to JavaScript for testing // In production, keep HTTP-only for security httpOnly := !h.config.IsDevelopment() secure := !h.config.IsDevelopment() // Only require HTTPS in production c.SetCookie( "auth_token", // name token, // value 604800, // maxAge (7 days) "/", // path "", // domain (empty for current domain) secure, // secure (HTTPS only in production) httpOnly, // httpOnly (no JavaScript access in production) ) // Redirect without token in URL for security redirectURL = req.RedirectURI + "?state=" + state default: // Invalid delivery mode, default to cookie redirectURL = req.RedirectURI + "?state=" + state } response := domain.LoginResponse{ RedirectURL: redirectURL, } c.JSON(http.StatusOK, response) } // generateSecureState generates a secure state parameter for OAuth flows func (h *AuthHandler) generateSecureState(userID, appID string) string { // Generate random bytes for state stateBytes := make([]byte, 16) if _, err := rand.Read(stateBytes); err != nil { h.logger.Error("Failed to generate random state", zap.Error(err)) // Fallback to less secure but functional state return fmt.Sprintf("state_%s_%s_%d", userID, appID, time.Now().UnixNano()) } // Create HMAC signature to prevent tampering stateData := fmt.Sprintf("%s:%s:%x", userID, appID, stateBytes) mac := hmac.New(sha256.New, []byte(h.config.GetString("AUTH_SIGNING_KEY"))) mac.Write([]byte(stateData)) signature := hex.EncodeToString(mac.Sum(nil)) // Return base64-encoded state with signature return hex.EncodeToString([]byte(fmt.Sprintf("%s.%s", stateData, signature))) } // Verify handles POST /verify func (h *AuthHandler) Verify(c *gin.Context) { var req domain.VerifyRequest if err := c.ShouldBindJSON(&req); err != nil { h.logger.Warn("Invalid verify request", zap.Error(err)) c.JSON(http.StatusBadRequest, gin.H{ "error": "Bad Request", "message": "Invalid request body: " + err.Error(), }) return } h.logger.Debug("Verifying token", zap.String("app_id", req.AppID)) response, err := h.tokenService.VerifyToken(c.Request.Context(), &req) if err != nil { h.logger.Error("Failed to verify token", zap.Error(err)) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Internal Server Error", "message": "Failed to verify token", }) return } c.JSON(http.StatusOK, response) } // Renew handles POST /renew func (h *AuthHandler) Renew(c *gin.Context) { var req domain.RenewRequest if err := c.ShouldBindJSON(&req); err != nil { h.logger.Warn("Invalid renew request", zap.Error(err)) c.JSON(http.StatusBadRequest, gin.H{ "error": "Bad Request", "message": "Invalid request body: " + err.Error(), }) return } h.logger.Info("Renewing token", zap.String("app_id", req.AppID), zap.String("user_id", req.UserID)) response, err := h.tokenService.RenewUserToken(c.Request.Context(), &req) if err != nil { h.logger.Error("Failed to renew token", zap.Error(err)) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Internal Server Error", "message": "Failed to renew token", }) return } c.JSON(http.StatusOK, response) }