-
This commit is contained in:
@ -7,15 +7,21 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/kms/api-key-service/internal/authorization"
|
||||
"github.com/kms/api-key-service/internal/domain"
|
||||
"github.com/kms/api-key-service/internal/errors"
|
||||
"github.com/kms/api-key-service/internal/services"
|
||||
"github.com/kms/api-key-service/internal/validation"
|
||||
)
|
||||
|
||||
// ApplicationHandler handles application-related HTTP requests
|
||||
type ApplicationHandler struct {
|
||||
appService services.ApplicationService
|
||||
authService services.AuthenticationService
|
||||
logger *zap.Logger
|
||||
appService services.ApplicationService
|
||||
authService services.AuthenticationService
|
||||
authzService *authorization.AuthorizationService
|
||||
validator *validation.Validator
|
||||
errorHandler *errors.ErrorHandler
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewApplicationHandler creates a new application handler
|
||||
@ -25,9 +31,12 @@ func NewApplicationHandler(
|
||||
logger *zap.Logger,
|
||||
) *ApplicationHandler {
|
||||
return &ApplicationHandler{
|
||||
appService: appService,
|
||||
authService: authService,
|
||||
logger: logger,
|
||||
appService: appService,
|
||||
authService: authService,
|
||||
authzService: authorization.NewAuthorizationService(logger),
|
||||
validator: validation.NewValidator(logger),
|
||||
errorHandler: errors.NewErrorHandler(logger),
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,57 +44,99 @@ func NewApplicationHandler(
|
||||
func (h *ApplicationHandler) Create(c *gin.Context) {
|
||||
var req domain.CreateApplicationRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
h.logger.Warn("Invalid request body", zap.Error(err))
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Bad Request",
|
||||
"message": "Invalid request body: " + err.Error(),
|
||||
})
|
||||
h.errorHandler.HandleValidationError(c, "request_body", "Invalid application request format")
|
||||
return
|
||||
}
|
||||
|
||||
// Get user ID from context
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
h.logger.Error("User ID not found in context")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Internal Server Error",
|
||||
"message": "Authentication context not found",
|
||||
})
|
||||
// Get user ID from authenticated context
|
||||
userID := h.getUserIDFromContext(c)
|
||||
if userID == "" {
|
||||
h.errorHandler.HandleAuthenticationError(c, errors.NewUnauthorizedError("User authentication required"))
|
||||
return
|
||||
}
|
||||
|
||||
app, err := h.appService.Create(c.Request.Context(), &req, userID.(string))
|
||||
// Validate input
|
||||
validationErrors := h.validator.ValidateApplicationRequest(req.AppID, req.AppLink, req.CallbackURL, []string{})
|
||||
if len(validationErrors) > 0 {
|
||||
h.logger.Warn("Application validation failed",
|
||||
zap.String("user_id", userID),
|
||||
zap.Any("errors", validationErrors))
|
||||
h.errorHandler.HandleValidationError(c, "validation", "Invalid application data")
|
||||
return
|
||||
}
|
||||
|
||||
// Check authorization for creating applications
|
||||
authCtx := &authorization.AuthorizationContext{
|
||||
UserID: userID,
|
||||
ResourceType: authorization.ResourceTypeApplication,
|
||||
Action: authorization.ActionCreate,
|
||||
}
|
||||
|
||||
if err := h.authzService.AuthorizeResourceAccess(c.Request.Context(), authCtx); err != nil {
|
||||
h.errorHandler.HandleAuthorizationError(c, "application creation")
|
||||
return
|
||||
}
|
||||
|
||||
// Create the application
|
||||
app, err := h.appService.Create(c.Request.Context(), &req, userID)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to create application", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Internal Server Error",
|
||||
"message": "Failed to create application",
|
||||
})
|
||||
h.errorHandler.HandleInternalError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("Application created", zap.String("app_id", app.AppID))
|
||||
h.logger.Info("Application created successfully",
|
||||
zap.String("app_id", app.AppID),
|
||||
zap.String("user_id", userID))
|
||||
|
||||
c.JSON(http.StatusCreated, app)
|
||||
}
|
||||
|
||||
// getUserIDFromContext extracts user ID from Gin context
|
||||
func (h *ApplicationHandler) getUserIDFromContext(c *gin.Context) string {
|
||||
// Try to get from Gin context first (set by middleware)
|
||||
if userID, exists := c.Get("user_id"); exists {
|
||||
if id, ok := userID.(string); ok {
|
||||
return id
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to header (for compatibility)
|
||||
userEmail := c.GetHeader("X-User-Email")
|
||||
if userEmail != "" {
|
||||
return userEmail
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetByID handles GET /applications/:id
|
||||
func (h *ApplicationHandler) GetByID(c *gin.Context) {
|
||||
appID := c.Param("id")
|
||||
if appID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Bad Request",
|
||||
"message": "Application ID is required",
|
||||
})
|
||||
|
||||
// Get user ID from context
|
||||
userID := h.getUserIDFromContext(c)
|
||||
if userID == "" {
|
||||
h.errorHandler.HandleAuthenticationError(c, errors.NewUnauthorizedError("User authentication required"))
|
||||
return
|
||||
}
|
||||
|
||||
// Validate app ID
|
||||
if result := h.validator.ValidateAppID(appID); !result.Valid {
|
||||
h.errorHandler.HandleValidationError(c, "app_id", "Invalid application ID")
|
||||
return
|
||||
}
|
||||
|
||||
// Get the application first
|
||||
app, err := h.appService.GetByID(c.Request.Context(), appID)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get application", zap.Error(err), zap.String("app_id", appID))
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"error": "Not Found",
|
||||
"message": "Application not found",
|
||||
})
|
||||
h.errorHandler.HandleError(c, err, "Application not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Check authorization for reading this application
|
||||
if err := h.authzService.AuthorizeApplicationOwnership(userID, app); err != nil {
|
||||
h.errorHandler.HandleAuthorizationError(c, "application access")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@ -1,32 +1,48 @@
|
||||
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
|
||||
logger *zap.Logger
|
||||
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,
|
||||
logger: logger,
|
||||
authService: authService,
|
||||
tokenService: tokenService,
|
||||
headerValidator: auth.NewHeaderValidator(config, logger),
|
||||
config: config,
|
||||
errorHandler: errors.NewErrorHandler(logger),
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,58 +50,81 @@ func NewAuthHandler(
|
||||
func (h *AuthHandler) Login(c *gin.Context) {
|
||||
var req domain.LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
h.logger.Warn("Invalid login request", zap.Error(err))
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Bad Request",
|
||||
"message": "Invalid request body: " + err.Error(),
|
||||
})
|
||||
h.errorHandler.HandleValidationError(c, "request_body", "Invalid login request format")
|
||||
return
|
||||
}
|
||||
|
||||
// For now, we'll extract user ID from headers since we're using HeaderAuthenticationProvider
|
||||
userID := c.GetHeader("X-User-Email")
|
||||
if userID == "" {
|
||||
h.logger.Warn("User email not found in headers")
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "Unauthorized",
|
||||
"message": "User authentication required",
|
||||
})
|
||||
// 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", userID), zap.String("app_id", req.AppID))
|
||||
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, userID, req.Permissions)
|
||||
token, err := h.tokenService.GenerateUserToken(c.Request.Context(), req.AppID, userContext.UserID, req.Permissions)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to generate user token", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Internal Server Error",
|
||||
"message": "Failed to generate token",
|
||||
})
|
||||
h.errorHandler.HandleInternalError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// For now, we'll just return the token directly
|
||||
// In a real implementation, this would redirect to the callback URL
|
||||
response := domain.LoginResponse{
|
||||
RedirectURL: req.RedirectURI + "?token=" + token,
|
||||
}
|
||||
|
||||
if req.RedirectURI == "" {
|
||||
// If no redirect URI, return token directly
|
||||
// If no redirect URI, return token directly via secure response body
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"token": token,
|
||||
"user_id": userID,
|
||||
"user_id": userContext.UserID,
|
||||
"app_id": req.AppID,
|
||||
"expires_in": 604800, // 7 days in seconds
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// For redirect flows, use secure cookie-based token delivery
|
||||
// Set secure cookie with the token
|
||||
c.SetSameSite(http.SameSiteStrictMode)
|
||||
c.SetCookie(
|
||||
"auth_token", // name
|
||||
token, // value
|
||||
604800, // maxAge (7 days)
|
||||
"/", // path
|
||||
"", // domain (empty for current domain)
|
||||
true, // secure (HTTPS only)
|
||||
true, // httpOnly (no JavaScript access)
|
||||
)
|
||||
|
||||
// Generate a secure state parameter for CSRF protection
|
||||
state := h.generateSecureState(userContext.UserID, req.AppID)
|
||||
|
||||
// Redirect without token in URL
|
||||
response := domain.LoginResponse{
|
||||
RedirectURL: req.RedirectURI + "?state=" + state,
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user