package handlers import ( "crypto/hmac" "crypto/rand" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "html/template" "net/http" "path/filepath" "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 loginTemplate *template.Template } // LoginPageData represents data passed to the login HTML template type LoginPageData struct { Token string TokenJSON template.JS RedirectURLJSON template.JS ExpiresAt string AppID string UserID string } // NewAuthHandler creates a new auth handler func NewAuthHandler( authService services.AuthenticationService, tokenService services.TokenService, config config.ConfigProvider, logger *zap.Logger, ) *AuthHandler { // Load login template templatePath := filepath.Join("templates", "login.html") loginTemplate, err := template.ParseFiles(templatePath) if err != nil { logger.Error("Failed to load login template", zap.Error(err), zap.String("path", templatePath)) // Template loading failure is not fatal, we'll fall back to JSON } return &AuthHandler{ authService: authService, tokenService: tokenService, headerValidator: auth.NewHeaderValidator(config, logger), config: config, errorHandler: errors.NewErrorHandler(logger), logger: logger, loginTemplate: loginTemplate, } } // Login handles login requests (both GET for HTML and POST for JSON) func (h *AuthHandler) Login(c *gin.Context) { // Handle GET requests or requests that prefer HTML acceptHeader := c.GetHeader("Accept") contentType := c.GetHeader("Content-Type") isJSONRequest := (c.Request.Method == "POST" && (contentType == "application/json" || (acceptHeader != "" && (acceptHeader == "application/json" || (acceptHeader != "text/html" && acceptHeader != "*/*"))))) var req domain.LoginRequest if isJSONRequest { // Handle JSON POST request (existing API behavior) if err := c.ShouldBindJSON(&req); err != nil { h.errorHandler.HandleValidationError(c, "request_body", "Invalid login request format") return } } else { // Handle HTML request (GET or POST with form data) req.AppID = c.Query("app_id") req.RedirectURI = c.Query("redirect_uri") // Parse permissions from query parameter (comma-separated) if perms := c.Query("permissions"); perms != "" { // Simple parsing for comma-separated permissions req.Permissions = []string{perms} // Simplified for this example } // If no app_id provided, show error if req.AppID == "" { h.renderLoginError(c, "Missing required parameter: app_id", isJSONRequest) return } } // Validate authentication headers with HMAC signature userContext, err := h.headerValidator.ValidateAuthenticationHeaders(c.Request) if err != nil { if isJSONRequest { h.errorHandler.HandleAuthenticationError(c, err) } else { h.renderLoginError(c, "Authentication failed: "+err.Error(), isJSONRequest) } 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 { if isJSONRequest { h.errorHandler.HandleInternalError(c, err) } else { h.renderLoginError(c, "Failed to generate token: "+err.Error(), isJSONRequest) } return } // For JSON requests without redirect URI, return token directly if isJSONRequest && req.RedirectURI == "" { c.JSON(http.StatusOK, gin.H{ "token": token, "user_id": userContext.UserID, "app_id": req.AppID, "expires_in": 604800, // 7 days in seconds }) return } // Handle redirect flows - always deliver token via query parameter var redirectURL string if req.RedirectURI != "" { // Generate a secure state parameter for CSRF protection state := h.generateSecureState(userContext.UserID, req.AppID) redirectURL = req.RedirectURI + "?token=" + token + "&state=" + state } // Return appropriate response format if isJSONRequest { response := domain.LoginResponse{ RedirectURL: redirectURL, } c.JSON(http.StatusOK, response) } else { // Render HTML page h.renderLoginPage(c, token, redirectURL, userContext.UserID, req.AppID) } } // renderLoginPage renders the HTML login page with token information func (h *AuthHandler) renderLoginPage(c *gin.Context, token, redirectURL, userID, appID string) { if h.loginTemplate == nil { // Fallback to JSON if template not available c.JSON(http.StatusOK, gin.H{ "token": token, "redirect_url": redirectURL, "user_id": userID, "app_id": appID, "message": "Login successful - HTML template not available", }) return } // Prepare template data tokenJSON, _ := json.Marshal(token) redirectURLJSON, _ := json.Marshal(redirectURL) data := LoginPageData{ Token: token, TokenJSON: template.JS(tokenJSON), RedirectURLJSON: template.JS(redirectURLJSON), ExpiresAt: time.Now().Add(7 * 24 * time.Hour).Format("Jan 2, 2006 at 3:04 PM MST"), AppID: appID, UserID: userID, } c.Header("Content-Type", "text/html; charset=utf-8") // Override CSP for login page to allow inline styles and scripts c.Header("Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'") if err := h.loginTemplate.Execute(c.Writer, data); err != nil { h.logger.Error("Failed to render login template", zap.Error(err)) // Fallback to JSON response c.JSON(http.StatusOK, gin.H{ "token": token, "redirect_url": redirectURL, "user_id": userID, "app_id": appID, "message": "Login successful - template render failed", }) } } // renderLoginError renders an error page or JSON error response func (h *AuthHandler) renderLoginError(c *gin.Context, message string, isJSON bool) { if isJSON { c.JSON(http.StatusBadRequest, gin.H{ "error": "Bad Request", "message": message, }) return } // Simple HTML error page c.Header("Content-Type", "text/html; charset=utf-8") // Override CSP for error page to allow inline styles c.Header("Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'") c.String(http.StatusBadRequest, `