step_up.go 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. package middleware
  2. import (
  3. "context"
  4. "fmt"
  5. "github.com/Wei-Shaw/sub2api/internal/service"
  6. "github.com/gin-gonic/gin"
  7. )
  8. // StepUpAuthMiddleware 敏感操作 step-up 2FA 门控中间件类型。
  9. type StepUpAuthMiddleware gin.HandlerFunc
  10. // stepUpGrantChecker 抽象 TOTP step-up 授权检查能力(由 TotpService 实现)。
  11. type stepUpGrantChecker interface {
  12. HasStepUpGrant(ctx context.Context, userID int64, sessionKey string) (bool, error)
  13. }
  14. // stepUpUserReader 抽象用户读取能力(检查 TOTP 是否启用)。
  15. type stepUpUserReader interface {
  16. GetByID(ctx context.Context, id int64) (*service.User, error)
  17. }
  18. // stepUpSettingReader 抽象 step-up 功能开关读取能力(由 SettingService 实现)。
  19. type stepUpSettingReader interface {
  20. IsStepUpEnabled(ctx context.Context) bool
  21. }
  22. // StepUpSessionKey 计算 step-up 授权的会话键:
  23. // 优先绑定当前会话(refresh token family),无会话 ID 的旧 token 退化为用户级键。
  24. func StepUpSessionKey(c *gin.Context, userID int64) string {
  25. if sid := c.GetString(ContextKeySessionID); sid != "" {
  26. return sid
  27. }
  28. return fmt.Sprintf("u%d", userID)
  29. }
  30. // NewStepUpAuthMiddleware 创建敏感操作 step-up 2FA 门控中间件。
  31. //
  32. // 功能开关 step_up_enabled(默认关闭)关闭时中间件直接放行,行为与门控引入前一致。
  33. // 开启时的通过条件(全部满足):
  34. // 1. 必须是 JWT 认证的真人会话——admin API key(机器凭证)一律拒绝
  35. // 2. 当前用户已启用 TOTP(未启用则拒绝并提示先启用 2FA)
  36. // 3. 当前会话在有效期内完成过 TOTP step-up 验证(POST /api/v1/user/totp/step-up)
  37. //
  38. // 失败响应使用可区分的错误码,前端据此弹出 TOTP 验证对话框后重试。
  39. func NewStepUpAuthMiddleware(
  40. totpService *service.TotpService,
  41. userService *service.UserService,
  42. settingService *service.SettingService,
  43. ) StepUpAuthMiddleware {
  44. return StepUpAuthMiddleware(stepUpAuth(totpService, userService, stepUpSettingsOrNil(settingService)))
  45. }
  46. // stepUpSettingsOrNil 将可能为 nil 的具体指针归一化为接口,
  47. // 避免 typed-nil 装箱后绕过 enforceStepUp 内的 nil 判断。
  48. func stepUpSettingsOrNil(settingService *service.SettingService) stepUpSettingReader {
  49. if settingService == nil {
  50. return nil
  51. }
  52. return settingService
  53. }
  54. func stepUpAuth(grantChecker stepUpGrantChecker, userReader stepUpUserReader, settings stepUpSettingReader) gin.HandlerFunc {
  55. return func(c *gin.Context) {
  56. if !enforceStepUp(c, grantChecker, userReader, settings) {
  57. return
  58. }
  59. c.Next()
  60. }
  61. }
  62. // EnforceStepUp 对当前请求执行与 StepUpAuthMiddleware 相同语义的 step-up 门控,
  63. // 供 handler 在需要按请求内容条件触发时调用(如仅当把用户角色提升为管理员时)。
  64. // 校验失败时写入错误响应并中止请求,返回 false;通过返回 true。
  65. func EnforceStepUp(
  66. c *gin.Context,
  67. totpService *service.TotpService,
  68. userService *service.UserService,
  69. settingService *service.SettingService,
  70. ) bool {
  71. return enforceStepUp(c, totpService, userService, stepUpSettingsOrNil(settingService))
  72. }
  73. // EnforceStepUpAlways 与 EnforceStepUp 语义相同但不读取功能开关,无条件执行门控。
  74. // 供调用方已确知门控必须生效的场景使用(如"关闭 step-up 开关"本身:调用方刚从
  75. // 持久化设置读到开关为开启状态,不应依赖二次读取——读取失败会导致门控被跳过)。
  76. func EnforceStepUpAlways(
  77. c *gin.Context,
  78. totpService *service.TotpService,
  79. userService *service.UserService,
  80. ) bool {
  81. return enforceStepUp(c, totpService, userService, nil)
  82. }
  83. func enforceStepUp(c *gin.Context, grantChecker stepUpGrantChecker, userReader stepUpUserReader, settings stepUpSettingReader) bool {
  84. // 功能开关关闭时直接放行(含 admin API key),恢复门控引入前的行为。
  85. // settings 为 nil 时保持门控(fail-closed):正常装配不会出现 nil。
  86. if settings != nil && !settings.IsStepUpEnabled(c.Request.Context()) {
  87. return true
  88. }
  89. if c.GetString("auth_method") == service.AuditAuthMethodAdminAPIKey {
  90. AbortWithError(c, 403, "STEP_UP_ADMIN_API_KEY_FORBIDDEN",
  91. "Admin API key cannot access this endpoint; a two-factor verified admin session is required")
  92. return false
  93. }
  94. subject, ok := GetAuthSubjectFromContext(c)
  95. if !ok || subject.UserID <= 0 {
  96. AbortWithError(c, 401, "UNAUTHORIZED", "Authorization required")
  97. return false
  98. }
  99. user, err := userReader.GetByID(c.Request.Context(), subject.UserID)
  100. if err != nil {
  101. AbortWithError(c, 500, "INTERNAL_ERROR", "Failed to load user")
  102. return false
  103. }
  104. if !user.TotpEnabled {
  105. AbortWithError(c, 403, "STEP_UP_TOTP_NOT_ENABLED",
  106. "This operation requires two-factor authentication; please enable TOTP first")
  107. return false
  108. }
  109. sessionKey := StepUpSessionKey(c, subject.UserID)
  110. granted, err := grantChecker.HasStepUpGrant(c.Request.Context(), subject.UserID, sessionKey)
  111. if err != nil {
  112. // 安全门控故障时选择 fail-closed。
  113. AbortWithError(c, 503, "STEP_UP_UNAVAILABLE", "Step-up verification service unavailable")
  114. return false
  115. }
  116. if !granted {
  117. AbortWithError(c, 403, "STEP_UP_REQUIRED",
  118. "This operation requires recent two-factor verification")
  119. return false
  120. }
  121. return true
  122. }