package middleware import ( "baron-sso-backend/internal/domain" "log/slog" "strings" "time" "github.com/gofiber/fiber/v2" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" ) type ApiKeyAuthConfig struct { DB *gorm.DB } func ApiKeyAuth(config ApiKeyAuthConfig) fiber.Handler { return func(c *fiber.Ctx) error { // 1. 헤더에서 ID와 Secret 추출 clientID := c.Get("X-Baron-Key-ID") plainSecret := c.Get("X-Baron-Key-Secret") // 헤더가 둘 다 없으면 API Key 인증 시도가 아닌 것으로 간주하고 다음으로 넘김 (UI 세션 등을 위해) if clientID == "" && plainSecret == "" { return c.Next() } if clientID == "" || plainSecret == "" { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ "error": "API Key ID or Secret is missing", }) } // 2. DB에서 ClientID로 키 정보 조회 var apiKey domain.ApiKey if err := config.DB.Where("client_id = ? AND status = ?", clientID, "active").First(&apiKey).Error; err != nil { if err == gorm.ErrRecordNotFound { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ "error": "Invalid or inactive API Key", }) } return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": "Database error during authentication", }) } // 3. Secret 해시 검증 if err := bcrypt.CompareHashAndPassword([]byte(apiKey.ClientSecretHash), []byte(plainSecret)); err != nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ "error": "Invalid API Secret", }) } // 4. (비동기) 마지막 사용 시간 업데이트 go func(id string) { now := time.Now() config.DB.Model(&domain.ApiKey{}).Where("id = ?", id).Update("last_used_at", &now) }(apiKey.ID) // 5. 컨텍스트에 권한 정보 저장 c.Locals("apiKeyName", apiKey.Name) c.Locals("apiScopes", apiKey.Scopes) // 6. Scope 기반 권한 검증 (RBAC) if !validateScope(c.Method(), c.Path(), apiKey.Scopes) { slog.Warn("API Key scope insufficient", "name", apiKey.Name, "method", c.Method(), "path", c.Path(), "has_scopes", apiKey.Scopes) return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ "error": "Insufficient permissions (Scope mismatch)", }) } slog.Debug("API Key authenticated and authorized", "name", apiKey.Name, "path", c.Path()) return c.Next() } } // validateScope - 요청된 메서드와 경로가 허용된 Scopes에 포함되는지 검사합니다. func validateScope(method, path string, rawScopes string) bool { scopes := strings.Fields(rawScopes) scopeMap := make(map[string]bool) for _, s := range scopes { scopeMap[s] = true } if strings.Contains(path, "/integrations/org-context") { return method == fiber.MethodGet && scopeMap["org-context:read"] } // 1. 감사 로그 관련 (audit:*) if strings.Contains(path, "/admin/audit") || strings.Contains(path, "/v1/audit") { if method == fiber.MethodGet { return scopeMap["audit:read"] } return scopeMap["audit:write"] } // 2. 사용자 관리 관련 (user:*) if strings.Contains(path, "/admin/users") { if method == fiber.MethodGet { return scopeMap["user:read"] } return scopeMap["user:write"] } // 3. 테넌트 관리 관련 (tenant:*) if strings.Contains(path, "/admin/tenants") || strings.Contains(path, "/admin/relying-parties") { if method == fiber.MethodGet { return scopeMap["tenant:read"] } return scopeMap["tenant:write"] } // 4. API 체크 등 공통 (기본 허용) if strings.HasSuffix(path, "/admin/check") { return true } return false }