org
This commit is contained in:
283
kms/internal/handlers/application.go
Normal file
283
kms/internal/handlers/application.go
Normal file
@ -0,0 +1,283 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"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
|
||||
authzService *authorization.AuthorizationService
|
||||
validator *validation.Validator
|
||||
errorHandler *errors.ErrorHandler
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewApplicationHandler creates a new application handler
|
||||
func NewApplicationHandler(
|
||||
appService services.ApplicationService,
|
||||
authService services.AuthenticationService,
|
||||
logger *zap.Logger,
|
||||
) *ApplicationHandler {
|
||||
return &ApplicationHandler{
|
||||
appService: appService,
|
||||
authService: authService,
|
||||
authzService: authorization.NewAuthorizationService(logger),
|
||||
validator: validation.NewValidator(logger),
|
||||
errorHandler: errors.NewErrorHandler(logger),
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Create handles POST /applications
|
||||
func (h *ApplicationHandler) Create(c *gin.Context) {
|
||||
var req domain.CreateApplicationRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
h.errorHandler.HandleValidationError(c, "request_body", "Invalid application request format")
|
||||
return
|
||||
}
|
||||
|
||||
// Get user ID from authenticated context
|
||||
userID := h.getUserIDFromContext(c)
|
||||
if userID == "" {
|
||||
h.errorHandler.HandleAuthenticationError(c, errors.NewUnauthorizedError("User authentication required"))
|
||||
return
|
||||
}
|
||||
|
||||
// Validate input (skip permissions validation for application creation)
|
||||
var validationErrors []validation.ValidationError
|
||||
|
||||
// Validate app ID
|
||||
if result := h.validator.ValidateAppID(req.AppID); !result.Valid {
|
||||
validationErrors = append(validationErrors, result.Errors...)
|
||||
}
|
||||
|
||||
// Validate app link URL
|
||||
if result := h.validator.ValidateURL(req.AppLink, "app_link"); !result.Valid {
|
||||
validationErrors = append(validationErrors, result.Errors...)
|
||||
}
|
||||
|
||||
// Validate callback URL
|
||||
if result := h.validator.ValidateURL(req.CallbackURL, "callback_url"); !result.Valid {
|
||||
validationErrors = append(validationErrors, result.Errors...)
|
||||
}
|
||||
|
||||
// Validate token prefix if provided
|
||||
if result := h.validator.ValidateTokenPrefix(req.TokenPrefix); !result.Valid {
|
||||
validationErrors = append(validationErrors, result.Errors...)
|
||||
}
|
||||
|
||||
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.errorHandler.HandleInternalError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
// 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))
|
||||
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
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, app)
|
||||
}
|
||||
|
||||
// List handles GET /applications
|
||||
func (h *ApplicationHandler) List(c *gin.Context) {
|
||||
// Parse pagination parameters
|
||||
limit := 50
|
||||
offset := 0
|
||||
|
||||
if l := c.Query("limit"); l != "" {
|
||||
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
|
||||
limit = parsed
|
||||
}
|
||||
}
|
||||
|
||||
if o := c.Query("offset"); o != "" {
|
||||
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
|
||||
offset = parsed
|
||||
}
|
||||
}
|
||||
|
||||
apps, err := h.appService.List(c.Request.Context(), limit, offset)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to list applications", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Internal Server Error",
|
||||
"message": "Failed to list applications",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"data": apps,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
"count": len(apps),
|
||||
})
|
||||
}
|
||||
|
||||
// Update handles PUT /applications/:id
|
||||
func (h *ApplicationHandler) Update(c *gin.Context) {
|
||||
appID := c.Param("id")
|
||||
if appID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Bad Request",
|
||||
"message": "Application ID is required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var req domain.UpdateApplicationRequest
|
||||
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(),
|
||||
})
|
||||
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",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
app, err := h.appService.Update(c.Request.Context(), appID, &req, userID.(string))
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to update application", zap.Error(err), zap.String("app_id", appID))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Internal Server Error",
|
||||
"message": "Failed to update application",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("Application updated", zap.String("app_id", appID))
|
||||
c.JSON(http.StatusOK, app)
|
||||
}
|
||||
|
||||
// Delete handles DELETE /applications/:id
|
||||
func (h *ApplicationHandler) Delete(c *gin.Context) {
|
||||
appID := c.Param("id")
|
||||
if appID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Bad Request",
|
||||
"message": "Application ID is required",
|
||||
})
|
||||
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",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
err := h.appService.Delete(c.Request.Context(), appID, userID.(string))
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to delete application", zap.Error(err), zap.String("app_id", appID))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Internal Server Error",
|
||||
"message": "Failed to delete application",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("Application deleted", zap.String("app_id", appID))
|
||||
c.JSON(http.StatusNoContent, nil)
|
||||
}
|
||||
282
kms/internal/handlers/audit.go
Normal file
282
kms/internal/handlers/audit.go
Normal file
@ -0,0 +1,282 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/kms/api-key-service/internal/audit"
|
||||
"github.com/kms/api-key-service/internal/errors"
|
||||
"github.com/kms/api-key-service/internal/services"
|
||||
"github.com/kms/api-key-service/internal/validation"
|
||||
)
|
||||
|
||||
// AuditHandler handles audit-related HTTP requests
|
||||
type AuditHandler struct {
|
||||
auditLogger audit.AuditLogger
|
||||
authService services.AuthenticationService
|
||||
validator *validation.Validator
|
||||
errorHandler *errors.ErrorHandler
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewAuditHandler creates a new audit handler
|
||||
func NewAuditHandler(
|
||||
auditLogger audit.AuditLogger,
|
||||
authService services.AuthenticationService,
|
||||
logger *zap.Logger,
|
||||
) *AuditHandler {
|
||||
return &AuditHandler{
|
||||
auditLogger: auditLogger,
|
||||
authService: authService,
|
||||
validator: validation.NewValidator(logger),
|
||||
errorHandler: errors.NewErrorHandler(logger),
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// AuditQueryRequest represents the request for querying audit events
|
||||
type AuditQueryRequest struct {
|
||||
EventTypes []string `json:"event_types,omitempty" form:"event_types"`
|
||||
Statuses []string `json:"statuses,omitempty" form:"statuses"`
|
||||
ActorID string `json:"actor_id,omitempty" form:"actor_id"`
|
||||
ResourceID string `json:"resource_id,omitempty" form:"resource_id"`
|
||||
ResourceType string `json:"resource_type,omitempty" form:"resource_type"`
|
||||
StartTime *string `json:"start_time,omitempty" form:"start_time"`
|
||||
EndTime *string `json:"end_time,omitempty" form:"end_time"`
|
||||
Limit int `json:"limit,omitempty" form:"limit"`
|
||||
Offset int `json:"offset,omitempty" form:"offset"`
|
||||
OrderBy string `json:"order_by,omitempty" form:"order_by"`
|
||||
OrderDesc *bool `json:"order_desc,omitempty" form:"order_desc"`
|
||||
}
|
||||
|
||||
// AuditStatsRequest represents the request for audit statistics
|
||||
type AuditStatsRequest struct {
|
||||
EventTypes []string `json:"event_types,omitempty" form:"event_types"`
|
||||
StartTime *string `json:"start_time,omitempty" form:"start_time"`
|
||||
EndTime *string `json:"end_time,omitempty" form:"end_time"`
|
||||
GroupBy string `json:"group_by,omitempty" form:"group_by"`
|
||||
}
|
||||
|
||||
// AuditResponse represents the response structure for audit queries
|
||||
type AuditResponse struct {
|
||||
Events []AuditEventResponse `json:"events"`
|
||||
Total int `json:"total"`
|
||||
Limit int `json:"limit"`
|
||||
Offset int `json:"offset"`
|
||||
}
|
||||
|
||||
// AuditEventResponse represents a single audit event in API responses
|
||||
type AuditEventResponse struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
ActorID string `json:"actor_id,omitempty"`
|
||||
ActorIP string `json:"actor_ip,omitempty"`
|
||||
UserAgent string `json:"user_agent,omitempty"`
|
||||
ResourceID string `json:"resource_id,omitempty"`
|
||||
ResourceType string `json:"resource_type,omitempty"`
|
||||
Action string `json:"action"`
|
||||
Description string `json:"description"`
|
||||
Details map[string]interface{} `json:"details,omitempty"`
|
||||
RequestID string `json:"request_id,omitempty"`
|
||||
SessionID string `json:"session_id,omitempty"`
|
||||
}
|
||||
|
||||
// ListEvents handles GET /audit/events
|
||||
func (h *AuditHandler) ListEvents(c *gin.Context) {
|
||||
// Parse query parameters
|
||||
var req AuditQueryRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
h.errorHandler.HandleValidationError(c, "query_params", "Invalid query parameters")
|
||||
return
|
||||
}
|
||||
|
||||
// Set defaults
|
||||
if req.Limit <= 0 || req.Limit > 1000 {
|
||||
req.Limit = 100
|
||||
}
|
||||
if req.Offset < 0 {
|
||||
req.Offset = 0
|
||||
}
|
||||
if req.OrderBy == "" {
|
||||
req.OrderBy = "timestamp"
|
||||
}
|
||||
if req.OrderDesc == nil {
|
||||
orderDesc := true
|
||||
req.OrderDesc = &orderDesc
|
||||
}
|
||||
|
||||
// Convert request to audit filter
|
||||
filter := &audit.AuditFilter{
|
||||
ActorID: req.ActorID,
|
||||
ResourceID: req.ResourceID,
|
||||
ResourceType: req.ResourceType,
|
||||
Limit: req.Limit,
|
||||
Offset: req.Offset,
|
||||
OrderBy: req.OrderBy,
|
||||
OrderDesc: *req.OrderDesc,
|
||||
}
|
||||
|
||||
// Convert event types
|
||||
for _, et := range req.EventTypes {
|
||||
filter.EventTypes = append(filter.EventTypes, audit.EventType(et))
|
||||
}
|
||||
|
||||
// Convert statuses
|
||||
for _, st := range req.Statuses {
|
||||
filter.Statuses = append(filter.Statuses, audit.EventStatus(st))
|
||||
}
|
||||
|
||||
// Parse time filters
|
||||
if req.StartTime != nil && *req.StartTime != "" {
|
||||
if startTime, err := time.Parse(time.RFC3339, *req.StartTime); err == nil {
|
||||
filter.StartTime = &startTime
|
||||
} else {
|
||||
h.errorHandler.HandleValidationError(c, "start_time", "Invalid start_time format, use RFC3339")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if req.EndTime != nil && *req.EndTime != "" {
|
||||
if endTime, err := time.Parse(time.RFC3339, *req.EndTime); err == nil {
|
||||
filter.EndTime = &endTime
|
||||
} else {
|
||||
h.errorHandler.HandleValidationError(c, "end_time", "Invalid end_time format, use RFC3339")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Query audit events
|
||||
events, err := h.auditLogger.QueryEvents(c.Request.Context(), filter)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to query audit events", zap.Error(err))
|
||||
h.errorHandler.HandleInternalError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Convert to response format
|
||||
response := &AuditResponse{
|
||||
Events: make([]AuditEventResponse, len(events)),
|
||||
Total: len(events), // Note: This is just the count of returned events, not total matching
|
||||
Limit: req.Limit,
|
||||
Offset: req.Offset,
|
||||
}
|
||||
|
||||
for i, event := range events {
|
||||
response.Events[i] = AuditEventResponse{
|
||||
ID: event.ID.String(),
|
||||
Type: string(event.Type),
|
||||
Status: string(event.Status),
|
||||
Timestamp: event.Timestamp.Format(time.RFC3339),
|
||||
ActorID: event.ActorID,
|
||||
ActorIP: event.ActorIP,
|
||||
UserAgent: event.UserAgent,
|
||||
ResourceID: event.ResourceID,
|
||||
ResourceType: event.ResourceType,
|
||||
Action: event.Action,
|
||||
Description: event.Description,
|
||||
Details: event.Details,
|
||||
RequestID: event.RequestID,
|
||||
SessionID: event.SessionID,
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetEvent handles GET /audit/events/:id
|
||||
func (h *AuditHandler) GetEvent(c *gin.Context) {
|
||||
eventIDStr := c.Param("id")
|
||||
eventID, err := uuid.Parse(eventIDStr)
|
||||
if err != nil {
|
||||
h.errorHandler.HandleValidationError(c, "id", "Invalid event ID format")
|
||||
return
|
||||
}
|
||||
|
||||
// Get the specific audit event
|
||||
event, err := h.auditLogger.GetEventByID(c.Request.Context(), eventID)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get audit event", zap.Error(err), zap.String("event_id", eventID.String()))
|
||||
// Check if it's a not found error
|
||||
if err.Error() == "audit event with ID '"+eventID.String()+"' not found" {
|
||||
h.errorHandler.HandleNotFoundError(c, "audit_event", "Audit event not found")
|
||||
} else {
|
||||
h.errorHandler.HandleInternalError(c, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Convert to response format
|
||||
response := AuditEventResponse{
|
||||
ID: event.ID.String(),
|
||||
Type: string(event.Type),
|
||||
Status: string(event.Status),
|
||||
Timestamp: event.Timestamp.Format(time.RFC3339),
|
||||
ActorID: event.ActorID,
|
||||
ActorIP: event.ActorIP,
|
||||
UserAgent: event.UserAgent,
|
||||
ResourceID: event.ResourceID,
|
||||
ResourceType: event.ResourceType,
|
||||
Action: event.Action,
|
||||
Description: event.Description,
|
||||
Details: event.Details,
|
||||
RequestID: event.RequestID,
|
||||
SessionID: event.SessionID,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetStats handles GET /audit/stats
|
||||
func (h *AuditHandler) GetStats(c *gin.Context) {
|
||||
// Parse query parameters
|
||||
var req AuditStatsRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
h.errorHandler.HandleValidationError(c, "query_params", "Invalid query parameters")
|
||||
return
|
||||
}
|
||||
|
||||
// Convert request to audit stats filter
|
||||
filter := &audit.AuditStatsFilter{
|
||||
GroupBy: req.GroupBy,
|
||||
}
|
||||
|
||||
// Convert event types
|
||||
for _, et := range req.EventTypes {
|
||||
filter.EventTypes = append(filter.EventTypes, audit.EventType(et))
|
||||
}
|
||||
|
||||
// Parse time filters
|
||||
if req.StartTime != nil && *req.StartTime != "" {
|
||||
if startTime, err := time.Parse(time.RFC3339, *req.StartTime); err == nil {
|
||||
filter.StartTime = &startTime
|
||||
} else {
|
||||
h.errorHandler.HandleValidationError(c, "start_time", "Invalid start_time format, use RFC3339")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if req.EndTime != nil && *req.EndTime != "" {
|
||||
if endTime, err := time.Parse(time.RFC3339, *req.EndTime); err == nil {
|
||||
filter.EndTime = &endTime
|
||||
} else {
|
||||
h.errorHandler.HandleValidationError(c, "end_time", "Invalid end_time format, use RFC3339")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Get audit statistics
|
||||
stats, err := h.auditLogger.GetEventStats(c.Request.Context(), filter)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get audit statistics", zap.Error(err))
|
||||
h.errorHandler.HandleInternalError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, stats)
|
||||
}
|
||||
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)
|
||||
}
|
||||
72
kms/internal/handlers/health.go
Normal file
72
kms/internal/handlers/health.go
Normal file
@ -0,0 +1,72 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/kms/api-key-service/internal/repository"
|
||||
)
|
||||
|
||||
// HealthHandler handles health check endpoints
|
||||
type HealthHandler struct {
|
||||
db repository.DatabaseProvider
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewHealthHandler creates a new health handler
|
||||
func NewHealthHandler(db repository.DatabaseProvider, logger *zap.Logger) *HealthHandler {
|
||||
return &HealthHandler{
|
||||
db: db,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// HealthResponse represents the health check response
|
||||
type HealthResponse struct {
|
||||
Status string `json:"status"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Checks map[string]string `json:"checks,omitempty"`
|
||||
}
|
||||
|
||||
// Health handles basic health check - lightweight endpoint for load balancers
|
||||
func (h *HealthHandler) Health(c *gin.Context) {
|
||||
response := HealthResponse{
|
||||
Status: "healthy",
|
||||
Timestamp: time.Now().UTC().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// Ready handles readiness check - checks if service is ready to accept traffic
|
||||
func (h *HealthHandler) Ready(c *gin.Context) {
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
checks := make(map[string]string)
|
||||
status := "ready"
|
||||
statusCode := http.StatusOK
|
||||
|
||||
// Check database connectivity
|
||||
if err := h.db.Ping(ctx); err != nil {
|
||||
h.logger.Error("Database health check failed", zap.Error(err))
|
||||
checks["database"] = "unhealthy: " + err.Error()
|
||||
status = "not ready"
|
||||
statusCode = http.StatusServiceUnavailable
|
||||
} else {
|
||||
checks["database"] = "healthy"
|
||||
}
|
||||
|
||||
response := HealthResponse{
|
||||
Status: status,
|
||||
Timestamp: time.Now().UTC().Format(time.RFC3339),
|
||||
Checks: checks,
|
||||
}
|
||||
|
||||
c.JSON(statusCode, response)
|
||||
}
|
||||
394
kms/internal/handlers/oauth2.go
Normal file
394
kms/internal/handlers/oauth2.go
Normal file
@ -0,0 +1,394 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"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"
|
||||
)
|
||||
|
||||
// OAuth2Handler handles OAuth2/OIDC authentication flows
|
||||
type OAuth2Handler struct {
|
||||
config config.ConfigProvider
|
||||
logger *zap.Logger
|
||||
oauth2Provider *auth.OAuth2Provider
|
||||
authService services.AuthenticationService
|
||||
}
|
||||
|
||||
// NewOAuth2Handler creates a new OAuth2 handler
|
||||
func NewOAuth2Handler(
|
||||
config config.ConfigProvider,
|
||||
logger *zap.Logger,
|
||||
authService services.AuthenticationService,
|
||||
) *OAuth2Handler {
|
||||
oauth2Provider := auth.NewOAuth2Provider(config, logger)
|
||||
|
||||
return &OAuth2Handler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
oauth2Provider: oauth2Provider,
|
||||
authService: authService,
|
||||
}
|
||||
}
|
||||
|
||||
// AuthorizeRequest represents the OAuth2 authorization request
|
||||
type AuthorizeRequest struct {
|
||||
RedirectURI string `json:"redirect_uri" validate:"required,url"`
|
||||
State string `json:"state,omitempty"`
|
||||
}
|
||||
|
||||
// AuthorizeResponse represents the OAuth2 authorization response
|
||||
type AuthorizeResponse struct {
|
||||
AuthURL string `json:"auth_url"`
|
||||
State string `json:"state"`
|
||||
CodeVerifier string `json:"code_verifier"` // In production, this should be stored securely
|
||||
}
|
||||
|
||||
// CallbackRequest represents the OAuth2 callback request
|
||||
type CallbackRequest struct {
|
||||
Code string `json:"code" validate:"required"`
|
||||
State string `json:"state,omitempty"`
|
||||
RedirectURI string `json:"redirect_uri" validate:"required,url"`
|
||||
CodeVerifier string `json:"code_verifier" validate:"required"`
|
||||
}
|
||||
|
||||
// CallbackResponse represents the OAuth2 callback response
|
||||
type CallbackResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
RefreshToken string `json:"refresh_token,omitempty"`
|
||||
UserInfo *auth.UserInfo `json:"user_info"`
|
||||
JWTToken string `json:"jwt_token"`
|
||||
}
|
||||
|
||||
// RefreshRequest represents the token refresh request
|
||||
type RefreshRequest struct {
|
||||
RefreshToken string `json:"refresh_token" validate:"required"`
|
||||
}
|
||||
|
||||
// RefreshResponse represents the token refresh response
|
||||
type RefreshResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
RefreshToken string `json:"refresh_token,omitempty"`
|
||||
JWTToken string `json:"jwt_token"`
|
||||
}
|
||||
|
||||
// RegisterRoutes registers OAuth2 routes
|
||||
func (h *OAuth2Handler) RegisterRoutes(router *mux.Router) {
|
||||
oauth2Router := router.PathPrefix("/oauth2").Subrouter()
|
||||
|
||||
oauth2Router.HandleFunc("/authorize", h.Authorize).Methods("POST")
|
||||
oauth2Router.HandleFunc("/callback", h.Callback).Methods("POST")
|
||||
oauth2Router.HandleFunc("/refresh", h.Refresh).Methods("POST")
|
||||
oauth2Router.HandleFunc("/userinfo", h.GetUserInfo).Methods("GET")
|
||||
}
|
||||
|
||||
// Authorize initiates the OAuth2 authorization flow
|
||||
func (h *OAuth2Handler) Authorize(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
h.logger.Debug("Processing OAuth2 authorization request")
|
||||
|
||||
var req AuthorizeRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.logger.Warn("Invalid authorization request", zap.Error(err))
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate state if not provided
|
||||
if req.State == "" {
|
||||
state, err := h.generateState()
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to generate state", zap.Error(err))
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
req.State = state
|
||||
}
|
||||
|
||||
// Generate authorization URL
|
||||
authURL, err := h.oauth2Provider.GenerateAuthURL(ctx, req.State, req.RedirectURI)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to generate authorization URL", zap.Error(err))
|
||||
|
||||
if appErr, ok := err.(*errors.AppError); ok {
|
||||
http.Error(w, appErr.Message, appErr.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, "Failed to generate authorization URL", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// In production, store the code verifier securely (e.g., in session or cache)
|
||||
// For now, we'll return it in the response
|
||||
codeVerifier, err := h.generateCodeVerifier()
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to generate code verifier", zap.Error(err))
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := AuthorizeResponse{
|
||||
AuthURL: authURL,
|
||||
State: req.State,
|
||||
CodeVerifier: codeVerifier,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
h.logger.Error("Failed to encode authorization response", zap.Error(err))
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Debug("Authorization URL generated successfully",
|
||||
zap.String("state", req.State),
|
||||
zap.String("redirect_uri", req.RedirectURI))
|
||||
}
|
||||
|
||||
// Callback handles the OAuth2 callback and exchanges code for tokens
|
||||
func (h *OAuth2Handler) Callback(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
h.logger.Debug("Processing OAuth2 callback")
|
||||
|
||||
var req CallbackRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.logger.Warn("Invalid callback request", zap.Error(err))
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Exchange authorization code for tokens
|
||||
tokenResp, err := h.oauth2Provider.ExchangeCodeForToken(ctx, req.Code, req.RedirectURI, req.CodeVerifier)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to exchange code for token", zap.Error(err))
|
||||
|
||||
if appErr, ok := err.(*errors.AppError); ok {
|
||||
http.Error(w, appErr.Message, appErr.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, "Failed to exchange authorization code", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Get user information
|
||||
userInfo, err := h.oauth2Provider.GetUserInfo(ctx, tokenResp.AccessToken)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get user info", zap.Error(err))
|
||||
|
||||
if appErr, ok := err.(*errors.AppError); ok {
|
||||
http.Error(w, appErr.Message, appErr.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, "Failed to get user information", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate internal JWT token for the user
|
||||
jwtToken, err := h.generateInternalJWTToken(ctx, userInfo)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to generate internal JWT token", zap.Error(err))
|
||||
http.Error(w, "Failed to generate authentication token", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := CallbackResponse{
|
||||
AccessToken: tokenResp.AccessToken,
|
||||
TokenType: tokenResp.TokenType,
|
||||
ExpiresIn: tokenResp.ExpiresIn,
|
||||
RefreshToken: tokenResp.RefreshToken,
|
||||
UserInfo: userInfo,
|
||||
JWTToken: jwtToken,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
h.logger.Error("Failed to encode callback response", zap.Error(err))
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("OAuth2 callback processed successfully",
|
||||
zap.String("user_id", userInfo.Sub),
|
||||
zap.String("email", userInfo.Email))
|
||||
}
|
||||
|
||||
// Refresh refreshes an access token using refresh token
|
||||
func (h *OAuth2Handler) Refresh(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
h.logger.Debug("Processing token refresh request")
|
||||
|
||||
var req RefreshRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.logger.Warn("Invalid refresh request", zap.Error(err))
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh the access token
|
||||
tokenResp, err := h.oauth2Provider.RefreshAccessToken(ctx, req.RefreshToken)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to refresh access token", zap.Error(err))
|
||||
|
||||
if appErr, ok := err.(*errors.AppError); ok {
|
||||
http.Error(w, appErr.Message, appErr.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, "Failed to refresh access token", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Get updated user information
|
||||
userInfo, err := h.oauth2Provider.GetUserInfo(ctx, tokenResp.AccessToken)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get user info during refresh", zap.Error(err))
|
||||
|
||||
if appErr, ok := err.(*errors.AppError); ok {
|
||||
http.Error(w, appErr.Message, appErr.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, "Failed to get user information", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate new internal JWT token
|
||||
jwtToken, err := h.generateInternalJWTToken(ctx, userInfo)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to generate internal JWT token during refresh", zap.Error(err))
|
||||
http.Error(w, "Failed to generate authentication token", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := RefreshResponse{
|
||||
AccessToken: tokenResp.AccessToken,
|
||||
TokenType: tokenResp.TokenType,
|
||||
ExpiresIn: tokenResp.ExpiresIn,
|
||||
RefreshToken: tokenResp.RefreshToken,
|
||||
JWTToken: jwtToken,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
h.logger.Error("Failed to encode refresh response", zap.Error(err))
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Debug("Token refresh completed successfully",
|
||||
zap.String("user_id", userInfo.Sub))
|
||||
}
|
||||
|
||||
// GetUserInfo retrieves user information from the current session
|
||||
func (h *OAuth2Handler) GetUserInfo(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
h.logger.Debug("Processing user info request")
|
||||
|
||||
// Extract JWT token from Authorization header
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
http.Error(w, "Authorization header required", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Remove "Bearer " prefix
|
||||
tokenString := authHeader
|
||||
if len(authHeader) > 7 && authHeader[:7] == "Bearer " {
|
||||
tokenString = authHeader[7:]
|
||||
}
|
||||
|
||||
// Validate JWT token
|
||||
authContext, err := h.authService.ValidateJWTToken(ctx, tokenString)
|
||||
if err != nil {
|
||||
h.logger.Warn("Invalid JWT token in user info request", zap.Error(err))
|
||||
http.Error(w, "Invalid or expired token", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Return user information from JWT claims
|
||||
userInfo := map[string]interface{}{
|
||||
"sub": authContext.UserID,
|
||||
"email": authContext.Claims["email"],
|
||||
"name": authContext.Claims["name"],
|
||||
"permissions": authContext.Permissions,
|
||||
"app_id": authContext.AppID,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(userInfo); err != nil {
|
||||
h.logger.Error("Failed to encode user info response", zap.Error(err))
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Debug("User info request completed successfully",
|
||||
zap.String("user_id", authContext.UserID))
|
||||
}
|
||||
|
||||
// generateState generates a random state parameter for OAuth2
|
||||
func (h *OAuth2Handler) generateState() (string, error) {
|
||||
bytes := make([]byte, 32)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.URLEncoding.EncodeToString(bytes), nil
|
||||
}
|
||||
|
||||
// generateCodeVerifier generates a PKCE code verifier
|
||||
func (h *OAuth2Handler) generateCodeVerifier() (string, error) {
|
||||
bytes := make([]byte, 32)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(bytes), nil
|
||||
}
|
||||
|
||||
// generateInternalJWTToken generates an internal JWT token for authenticated users
|
||||
func (h *OAuth2Handler) generateInternalJWTToken(ctx context.Context, userInfo *auth.UserInfo) (string, error) {
|
||||
// Create user token with information from OAuth2 provider
|
||||
userToken := &domain.UserToken{
|
||||
AppID: h.config.GetString("INTERNAL_APP_ID"),
|
||||
UserID: userInfo.Sub,
|
||||
Permissions: []string{"read", "write"}, // Default permissions, should be based on user roles
|
||||
IssuedAt: time.Now(),
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour), // 24 hour expiration
|
||||
MaxValidAt: time.Now().Add(7 * 24 * time.Hour), // 7 days max validity
|
||||
TokenType: domain.TokenTypeUser,
|
||||
Claims: map[string]string{
|
||||
"sub": userInfo.Sub,
|
||||
"email": userInfo.Email,
|
||||
"name": userInfo.Name,
|
||||
"email_verified": func() string {
|
||||
if userInfo.EmailVerified {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}(),
|
||||
},
|
||||
}
|
||||
|
||||
// Generate JWT token using authentication service
|
||||
return h.authService.GenerateJWTToken(ctx, userToken)
|
||||
}
|
||||
352
kms/internal/handlers/saml.go
Normal file
352
kms/internal/handlers/saml.go
Normal file
@ -0,0 +1,352 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"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"
|
||||
)
|
||||
|
||||
// SAMLHandler handles SAML authentication endpoints
|
||||
type SAMLHandler struct {
|
||||
samlProvider *auth.SAMLProvider
|
||||
sessionService services.SessionService
|
||||
authService services.AuthenticationService
|
||||
tokenService services.TokenService
|
||||
config config.ConfigProvider
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewSAMLHandler creates a new SAML handler
|
||||
func NewSAMLHandler(
|
||||
config config.ConfigProvider,
|
||||
sessionService services.SessionService,
|
||||
authService services.AuthenticationService,
|
||||
tokenService services.TokenService,
|
||||
logger *zap.Logger,
|
||||
) (*SAMLHandler, error) {
|
||||
samlProvider, err := auth.NewSAMLProvider(config, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &SAMLHandler{
|
||||
samlProvider: samlProvider,
|
||||
sessionService: sessionService,
|
||||
authService: authService,
|
||||
config: config,
|
||||
logger: logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RegisterRoutes registers SAML routes
|
||||
func (h *SAMLHandler) RegisterRoutes(router *mux.Router) {
|
||||
// SAML endpoints
|
||||
router.HandleFunc("/auth/saml/login", h.InitiateSAMLLogin).Methods("GET")
|
||||
router.HandleFunc("/auth/saml/acs", h.HandleSAMLResponse).Methods("POST")
|
||||
router.HandleFunc("/auth/saml/metadata", h.GetServiceProviderMetadata).Methods("GET")
|
||||
router.HandleFunc("/auth/saml/slo", h.HandleSingleLogout).Methods("GET", "POST")
|
||||
}
|
||||
|
||||
// InitiateSAMLLogin initiates SAML authentication
|
||||
func (h *SAMLHandler) InitiateSAMLLogin(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.config.GetBool("SAML_ENABLED") {
|
||||
h.writeErrorResponse(w, errors.NewConfigurationError("SAML authentication is not enabled"))
|
||||
return
|
||||
}
|
||||
|
||||
// Get query parameters
|
||||
appID := r.URL.Query().Get("app_id")
|
||||
redirectURL := r.URL.Query().Get("redirect_url")
|
||||
|
||||
if appID == "" {
|
||||
h.writeErrorResponse(w, errors.NewValidationError("app_id parameter is required"))
|
||||
return
|
||||
}
|
||||
|
||||
// Generate relay state with app_id and redirect_url
|
||||
relayState := appID
|
||||
if redirectURL != "" {
|
||||
relayState += "|" + redirectURL
|
||||
}
|
||||
|
||||
h.logger.Debug("Initiating SAML login",
|
||||
zap.String("app_id", appID),
|
||||
zap.String("redirect_url", redirectURL))
|
||||
|
||||
// Generate SAML authentication request
|
||||
authURL, requestID, err := h.samlProvider.GenerateAuthRequest(r.Context(), relayState)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to generate SAML auth request", zap.Error(err))
|
||||
h.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Store request ID in session/cache for validation
|
||||
// In production, you should store this securely
|
||||
h.logger.Debug("Generated SAML auth request",
|
||||
zap.String("request_id", requestID),
|
||||
zap.String("auth_url", authURL))
|
||||
|
||||
// Redirect to IdP
|
||||
http.Redirect(w, r, authURL, http.StatusFound)
|
||||
}
|
||||
|
||||
// HandleSAMLResponse handles SAML assertion consumer service (ACS)
|
||||
func (h *SAMLHandler) HandleSAMLResponse(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.config.GetBool("SAML_ENABLED") {
|
||||
h.writeErrorResponse(w, errors.NewConfigurationError("SAML authentication is not enabled"))
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Debug("Handling SAML response")
|
||||
|
||||
// Parse form data
|
||||
if err := r.ParseForm(); err != nil {
|
||||
h.writeErrorResponse(w, errors.NewValidationError("Failed to parse form data").WithInternal(err))
|
||||
return
|
||||
}
|
||||
|
||||
samlResponse := r.FormValue("SAMLResponse")
|
||||
relayState := r.FormValue("RelayState")
|
||||
|
||||
if samlResponse == "" {
|
||||
h.writeErrorResponse(w, errors.NewValidationError("SAMLResponse is required"))
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Debug("Processing SAML response", zap.String("relay_state", relayState))
|
||||
|
||||
// Process SAML response
|
||||
// In production, you should retrieve and validate the original request ID
|
||||
authContext, err := h.samlProvider.ProcessSAMLResponse(r.Context(), samlResponse, "")
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to process SAML response", zap.Error(err))
|
||||
h.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse relay state to get app_id and redirect_url
|
||||
appID, redirectURL := h.parseRelayState(relayState)
|
||||
if appID == "" {
|
||||
h.writeErrorResponse(w, errors.NewValidationError("Invalid relay state: missing app_id"))
|
||||
return
|
||||
}
|
||||
|
||||
// Create user session
|
||||
sessionReq := &domain.CreateSessionRequest{
|
||||
UserID: authContext.UserID,
|
||||
AppID: appID,
|
||||
SessionType: domain.SessionTypeWeb,
|
||||
IPAddress: h.getClientIP(r),
|
||||
UserAgent: r.UserAgent(),
|
||||
ExpiresAt: time.Now().Add(8 * time.Hour), // 8 hour session
|
||||
Permissions: authContext.Permissions,
|
||||
Claims: authContext.Claims,
|
||||
}
|
||||
|
||||
session, err := h.sessionService.CreateSession(r.Context(), sessionReq)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to create session", zap.Error(err))
|
||||
h.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate JWT token for the session using the existing token service
|
||||
userToken := &domain.UserToken{
|
||||
AppID: appID,
|
||||
UserID: authContext.UserID,
|
||||
Permissions: authContext.Permissions,
|
||||
IssuedAt: time.Now(),
|
||||
ExpiresAt: session.ExpiresAt,
|
||||
MaxValidAt: session.ExpiresAt,
|
||||
TokenType: domain.TokenTypeUser,
|
||||
Claims: authContext.Claims,
|
||||
}
|
||||
|
||||
tokenString, err := h.authService.GenerateJWTToken(r.Context(), userToken)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to create JWT token", zap.Error(err))
|
||||
h.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Debug("SAML authentication successful",
|
||||
zap.String("user_id", authContext.UserID),
|
||||
zap.String("session_id", session.ID.String()))
|
||||
|
||||
// If redirect URL is provided, redirect with token
|
||||
if redirectURL != "" {
|
||||
// Add token as query parameter or fragment
|
||||
redirectURL += "?token=" + tokenString
|
||||
http.Redirect(w, r, redirectURL, http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, return JSON response
|
||||
response := map[string]interface{}{
|
||||
"success": true,
|
||||
"token": tokenString,
|
||||
"user": map[string]interface{}{
|
||||
"id": authContext.UserID,
|
||||
"email": authContext.Claims["email"],
|
||||
"name": authContext.Claims["name"],
|
||||
},
|
||||
"session_id": session.ID.String(),
|
||||
"expires_at": session.ExpiresAt,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// GetServiceProviderMetadata returns SP metadata XML
|
||||
func (h *SAMLHandler) GetServiceProviderMetadata(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.config.GetBool("SAML_ENABLED") {
|
||||
h.writeErrorResponse(w, errors.NewConfigurationError("SAML authentication is not enabled"))
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Debug("Generating SP metadata")
|
||||
|
||||
metadata, err := h.samlProvider.GenerateServiceProviderMetadata()
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to generate SP metadata", zap.Error(err))
|
||||
h.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/xml")
|
||||
w.Write([]byte(metadata))
|
||||
}
|
||||
|
||||
// HandleSingleLogout handles SAML single logout
|
||||
func (h *SAMLHandler) HandleSingleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.config.GetBool("SAML_ENABLED") {
|
||||
h.writeErrorResponse(w, errors.NewConfigurationError("SAML authentication is not enabled"))
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Debug("Handling SAML single logout")
|
||||
|
||||
// Get session ID from query parameter or form
|
||||
sessionID := r.URL.Query().Get("session_id")
|
||||
if sessionID == "" && r.Method == "POST" {
|
||||
r.ParseForm()
|
||||
sessionID = r.FormValue("session_id")
|
||||
}
|
||||
|
||||
if sessionID != "" {
|
||||
// Revoke specific session
|
||||
h.logger.Debug("Revoking session", zap.String("session_id", sessionID))
|
||||
// Implementation would depend on how you store session IDs
|
||||
// For now, we'll just log it
|
||||
}
|
||||
|
||||
// In a full implementation, you would:
|
||||
// 1. Parse the SAML LogoutRequest
|
||||
// 2. Validate the request
|
||||
// 3. Revoke the user's sessions
|
||||
// 4. Generate a LogoutResponse
|
||||
// 5. Redirect back to the IdP
|
||||
|
||||
// For now, return a simple success response
|
||||
response := map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "Logout successful",
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// parseRelayState parses the relay state to extract app_id and redirect_url
|
||||
func (h *SAMLHandler) parseRelayState(relayState string) (appID, redirectURL string) {
|
||||
if relayState == "" {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// RelayState format: "app_id|redirect_url" or just "app_id"
|
||||
parts := []string{relayState}
|
||||
if len(relayState) > 0 && relayState[0] != '|' {
|
||||
// Split on first pipe character
|
||||
for i, char := range relayState {
|
||||
if char == '|' {
|
||||
parts = []string{relayState[:i], relayState[i+1:]}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
appID = parts[0]
|
||||
if len(parts) > 1 {
|
||||
redirectURL = parts[1]
|
||||
}
|
||||
|
||||
return appID, redirectURL
|
||||
}
|
||||
|
||||
// getClientIP extracts the client IP address from the request
|
||||
func (h *SAMLHandler) getClientIP(r *http.Request) string {
|
||||
// Check X-Forwarded-For header first
|
||||
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
||||
// Take the first IP if multiple are present
|
||||
if idx := len(xff); idx > 0 {
|
||||
for i, char := range xff {
|
||||
if char == ',' {
|
||||
return xff[:i]
|
||||
}
|
||||
}
|
||||
return xff
|
||||
}
|
||||
}
|
||||
|
||||
// Check X-Real-IP header
|
||||
if xri := r.Header.Get("X-Real-IP"); xri != "" {
|
||||
return xri
|
||||
}
|
||||
|
||||
// Fall back to RemoteAddr
|
||||
return r.RemoteAddr
|
||||
}
|
||||
|
||||
// writeErrorResponse writes an error response
|
||||
func (h *SAMLHandler) writeErrorResponse(w http.ResponseWriter, err error) {
|
||||
var statusCode int
|
||||
var errorCode string
|
||||
|
||||
switch {
|
||||
case errors.IsValidationError(err):
|
||||
statusCode = http.StatusBadRequest
|
||||
errorCode = "VALIDATION_ERROR"
|
||||
case errors.IsAuthenticationError(err):
|
||||
statusCode = http.StatusUnauthorized
|
||||
errorCode = "AUTHENTICATION_ERROR"
|
||||
case errors.IsConfigurationError(err):
|
||||
statusCode = http.StatusServiceUnavailable
|
||||
errorCode = "CONFIGURATION_ERROR"
|
||||
default:
|
||||
statusCode = http.StatusInternalServerError
|
||||
errorCode = "INTERNAL_ERROR"
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"success": false,
|
||||
"error": map[string]interface{}{
|
||||
"code": errorCode,
|
||||
"message": err.Error(),
|
||||
},
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(statusCode)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
231
kms/internal/handlers/token.go
Normal file
231
kms/internal/handlers/token.go
Normal file
@ -0,0 +1,231 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// TokenHandler handles token-related HTTP requests
|
||||
type TokenHandler struct {
|
||||
tokenService services.TokenService
|
||||
authService services.AuthenticationService
|
||||
validator *validation.Validator
|
||||
errorHandler *errors.ErrorHandler
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewTokenHandler creates a new token handler
|
||||
func NewTokenHandler(
|
||||
tokenService services.TokenService,
|
||||
authService services.AuthenticationService,
|
||||
logger *zap.Logger,
|
||||
) *TokenHandler {
|
||||
return &TokenHandler{
|
||||
tokenService: tokenService,
|
||||
authService: authService,
|
||||
validator: validation.NewValidator(logger),
|
||||
errorHandler: errors.NewErrorHandler(logger),
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Create handles POST /applications/:id/tokens
|
||||
func (h *TokenHandler) Create(c *gin.Context) {
|
||||
// Validate application ID parameter
|
||||
appID := c.Param("id")
|
||||
if appID == "" {
|
||||
h.errorHandler.HandleValidationError(c, "id", "Application ID is required")
|
||||
return
|
||||
}
|
||||
|
||||
// Bind and validate JSON request
|
||||
var req domain.CreateStaticTokenRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
h.logger.Warn("Invalid request body", zap.Error(err))
|
||||
h.errorHandler.HandleValidationError(c, "request_body", "Invalid request body format")
|
||||
return
|
||||
}
|
||||
|
||||
// Set app ID from URL parameter
|
||||
req.AppID = appID
|
||||
|
||||
// Basic validation - the service layer will do more comprehensive validation
|
||||
if req.AppID == "" {
|
||||
h.errorHandler.HandleValidationError(c, "app_id", "Application ID is required")
|
||||
return
|
||||
}
|
||||
|
||||
// Get user ID from context
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
h.logger.Error("User ID not found in context")
|
||||
h.errorHandler.HandleAuthenticationError(c, errors.NewAuthenticationError("Authentication context not found"))
|
||||
return
|
||||
}
|
||||
|
||||
userIDStr, ok := userID.(string)
|
||||
if !ok {
|
||||
h.logger.Error("Invalid user ID type in context", zap.Any("user_id", userID))
|
||||
h.errorHandler.HandleInternalError(c, errors.NewInternalError("Invalid authentication context"))
|
||||
return
|
||||
}
|
||||
|
||||
// Create the token
|
||||
token, err := h.tokenService.CreateStaticToken(c.Request.Context(), &req, userIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to create token",
|
||||
zap.Error(err),
|
||||
zap.String("app_id", appID),
|
||||
zap.String("user_id", userIDStr))
|
||||
|
||||
// Handle different types of errors appropriately
|
||||
if errors.IsNotFound(err) {
|
||||
h.errorHandler.HandleError(c, err, "Application not found")
|
||||
} else if errors.IsValidationError(err) {
|
||||
h.errorHandler.HandleValidationError(c, "token", "Token creation validation failed")
|
||||
} else if errors.IsAuthorizationError(err) {
|
||||
h.errorHandler.HandleAuthorizationError(c, "token_creation")
|
||||
} else {
|
||||
h.errorHandler.HandleInternalError(c, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("Token created successfully",
|
||||
zap.String("token_id", token.ID.String()),
|
||||
zap.String("app_id", appID),
|
||||
zap.String("user_id", userIDStr))
|
||||
|
||||
c.JSON(http.StatusCreated, token)
|
||||
}
|
||||
|
||||
// ListByApp handles GET /applications/:id/tokens
|
||||
func (h *TokenHandler) ListByApp(c *gin.Context) {
|
||||
// Validate application ID parameter
|
||||
appID := c.Param("id")
|
||||
if appID == "" {
|
||||
h.errorHandler.HandleValidationError(c, "id", "Application ID is required")
|
||||
return
|
||||
}
|
||||
|
||||
// Parse and validate pagination parameters
|
||||
limit := 50
|
||||
offset := 0
|
||||
|
||||
if l := c.Query("limit"); l != "" {
|
||||
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 && parsed <= 1000 {
|
||||
limit = parsed
|
||||
} else if parsed <= 0 || parsed > 1000 {
|
||||
h.errorHandler.HandleValidationError(c, "limit", "Limit must be between 1 and 1000")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if o := c.Query("offset"); o != "" {
|
||||
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
|
||||
offset = parsed
|
||||
} else if parsed < 0 {
|
||||
h.errorHandler.HandleValidationError(c, "offset", "Offset must be non-negative")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// List tokens
|
||||
tokens, err := h.tokenService.ListByApp(c.Request.Context(), appID, limit, offset)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to list tokens",
|
||||
zap.Error(err),
|
||||
zap.String("app_id", appID),
|
||||
zap.Int("limit", limit),
|
||||
zap.Int("offset", offset))
|
||||
|
||||
// Handle different types of errors appropriately
|
||||
if errors.IsNotFound(err) {
|
||||
h.errorHandler.HandleNotFoundError(c, "application", "Application not found")
|
||||
} else if errors.IsAuthorizationError(err) {
|
||||
h.errorHandler.HandleAuthorizationError(c, "token_list")
|
||||
} else {
|
||||
h.errorHandler.HandleInternalError(c, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Debug("Tokens listed successfully",
|
||||
zap.String("app_id", appID),
|
||||
zap.Int("token_count", len(tokens)),
|
||||
zap.Int("limit", limit),
|
||||
zap.Int("offset", offset))
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"data": tokens,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
"count": len(tokens),
|
||||
})
|
||||
}
|
||||
|
||||
// Delete handles DELETE /tokens/:id
|
||||
func (h *TokenHandler) Delete(c *gin.Context) {
|
||||
// Validate token ID parameter
|
||||
tokenIDStr := c.Param("id")
|
||||
if tokenIDStr == "" {
|
||||
h.errorHandler.HandleValidationError(c, "id", "Token ID is required")
|
||||
return
|
||||
}
|
||||
|
||||
tokenID, err := uuid.Parse(tokenIDStr)
|
||||
if err != nil {
|
||||
h.logger.Warn("Invalid token ID format", zap.String("token_id", tokenIDStr), zap.Error(err))
|
||||
h.errorHandler.HandleValidationError(c, "id", "Invalid token ID format")
|
||||
return
|
||||
}
|
||||
|
||||
// Get user ID from context
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
h.logger.Error("User ID not found in context")
|
||||
h.errorHandler.HandleAuthenticationError(c, errors.NewAuthenticationError("Authentication context not found"))
|
||||
return
|
||||
}
|
||||
|
||||
userIDStr, ok := userID.(string)
|
||||
if !ok {
|
||||
h.logger.Error("Invalid user ID type in context", zap.Any("user_id", userID))
|
||||
h.errorHandler.HandleInternalError(c, errors.NewInternalError("Invalid authentication context"))
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the token
|
||||
err = h.tokenService.Delete(c.Request.Context(), tokenID, userIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to delete token",
|
||||
zap.Error(err),
|
||||
zap.String("token_id", tokenID.String()),
|
||||
zap.String("user_id", userIDStr))
|
||||
|
||||
// Handle different types of errors appropriately
|
||||
if errors.IsNotFound(err) {
|
||||
h.errorHandler.HandleNotFoundError(c, "token", "Token not found")
|
||||
} else if errors.IsAuthorizationError(err) {
|
||||
h.errorHandler.HandleAuthorizationError(c, "token_deletion")
|
||||
} else {
|
||||
h.errorHandler.HandleInternalError(c, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("Token deleted successfully",
|
||||
zap.String("token_id", tokenID.String()),
|
||||
zap.String("user_id", userIDStr))
|
||||
|
||||
c.JSON(http.StatusNoContent, nil)
|
||||
}
|
||||
Reference in New Issue
Block a user