org
This commit is contained in:
311
kms/internal/handlers/auth.go
Normal file
311
kms/internal/handlers/auth.go
Normal file
@ -0,0 +1,311 @@
|
||||
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, `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Login Error</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; }
|
||||
.error { background: #f8d7da; color: #721c24; padding: 15px; border-radius: 5px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Login Error</h1>
|
||||
<div class="error">%s</div>
|
||||
<p><a href="javascript:history.back()">Go back</a></p>
|
||||
</body>
|
||||
</html>`, message)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user