handler_auth.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. package auth
  2. import (
  3. "context"
  4. "errors"
  5. "net/http"
  6. "github.com/gin-gonic/gin"
  7. )
  8. // 认证接口的 handler 实现,对照 docs/api.md 第二节。
  9. // bind 解析 JSON 请求体。
  10. func bind(c *gin.Context, dst any) bool {
  11. if err := c.ShouldBindJSON(dst); err != nil {
  12. BadRequest(c, err)
  13. return false
  14. }
  15. return true
  16. }
  17. // Login 实现 POST /api/auth/login(api.md 2.1)。
  18. func (h *Handler) Login(c *gin.Context) {
  19. var req loginRequest
  20. if !bind(c, &req) {
  21. return
  22. }
  23. deviceCookie, _ := c.Cookie(CookieTrustedDevice)
  24. res, err := h.svc.Login(c.Request.Context(), req.Username, req.Password,
  25. req.TrustDevice, deviceCookie, c.Request.UserAgent())
  26. if err != nil {
  27. switch {
  28. case errors.Is(err, ErrInvalidCredentials):
  29. // 失败审计(audit.md 第三节)
  30. h.failLogin(c, req.Username, ReasonBadPassword)
  31. Unauthorized(c, ErrInvalidCredentials.Error())
  32. default:
  33. InternalError(c, err)
  34. }
  35. return
  36. }
  37. if res.TotpRequired {
  38. c.JSON(http.StatusOK, loginResponse{
  39. User: userBriefResponse{ID: res.User.ID, Username: res.User.Username},
  40. TotpRequired: true,
  41. TempToken: res.TempToken,
  42. })
  43. return
  44. }
  45. h.succeedLogin(c, res)
  46. }
  47. // LoginTotp 实现 POST /api/auth/login/totp(api.md 2.2)。
  48. func (h *Handler) LoginTotp(c *gin.Context) {
  49. var req loginTotpRequest
  50. if !bind(c, &req) {
  51. return
  52. }
  53. res, err := h.svc.LoginTotp(c.Request.Context(), req.TempToken, req.Code, c.Request.UserAgent())
  54. if err != nil {
  55. switch {
  56. case errors.Is(err, ErrPendingExpired):
  57. Unauthorized(c, ErrPendingExpired.Error())
  58. case errors.Is(err, ErrBadTotp):
  59. h.failLogin(c, h.usernameFromPending(c.Request.Context(), req.TempToken), ReasonBadTotp)
  60. Unauthorized(c, ErrBadTotp.Error())
  61. default:
  62. InternalError(c, err)
  63. }
  64. return
  65. }
  66. h.succeedLogin(c, res)
  67. }
  68. // succeedLogin 下发 cookie 并写 login_success 审计。
  69. func (h *Handler) succeedLogin(c *gin.Context, res LoginResult) {
  70. setSessionCookie(c, h.CookieDomain, h.CookieSecure, res.SessionToken)
  71. if res.DeviceToken != "" {
  72. setTrustedDeviceCookie(c, h.CookieDomain, h.CookieSecure, res.DeviceToken)
  73. }
  74. if h.OnLoginSuccess != nil {
  75. h.OnLoginSuccess(c, LoginAudit{UserID: res.User.ID, Username: res.User.Username, Via: res.AuditVia})
  76. }
  77. if res.DeviceToken != "" && h.OnTrustedDeviceAdd != nil {
  78. h.OnTrustedDeviceAdd(c, AuditContext{UserID: res.User.ID, Username: res.User.Username,
  79. IP: c.ClientIP(), UA: c.Request.UserAgent()}, res.DeviceName)
  80. }
  81. c.JSON(http.StatusOK, loginResponse{
  82. User: userBriefResponse{ID: res.User.ID, Username: res.User.Username},
  83. TotpRequired: false,
  84. })
  85. }
  86. func (h *Handler) failLogin(c *gin.Context, username, reason string) {
  87. if h.OnLoginFailure != nil {
  88. h.OnLoginFailure(c, username, reason)
  89. }
  90. }
  91. // Logout 实现 POST /api/auth/logout(api.md 2.3)。
  92. func (h *Handler) Logout(c *gin.Context) {
  93. ac := h.auditContext(c)
  94. if err := h.svc.Logout(c.Request.Context(), SessionID(c)); err != nil {
  95. InternalError(c, err)
  96. return
  97. }
  98. clearSessionCookie(c, h.CookieDomain, h.CookieSecure)
  99. // 授信设备 cookie 保留(api.md 2.3)
  100. if SessionID(c) != "" && h.OnLogout != nil {
  101. h.OnLogout(c, ac)
  102. }
  103. c.Status(http.StatusNoContent)
  104. }
  105. // SetupTotp 实现 POST /api/auth/totp/setup(api.md 2.4)。
  106. func (h *Handler) SetupTotp(c *gin.Context) {
  107. userID := UserID(c)
  108. secret, uri, err := h.svc.SetupTotp(userID, h.issuer)
  109. if err != nil {
  110. switch {
  111. case errors.Is(err, ErrTotpAlreadyEnabled):
  112. Conflict(c, ErrTotpAlreadyEnabled.Error())
  113. default:
  114. InternalError(c, err)
  115. }
  116. return
  117. }
  118. c.JSON(http.StatusOK, gin.H{"secret": secret, "otpauth_uri": uri})
  119. }
  120. // VerifyTotp 实现 POST /api/auth/totp/verify(api.md 2.5)。
  121. func (h *Handler) VerifyTotp(c *gin.Context) {
  122. var req totpVerifyRequest
  123. if !bind(c, &req) {
  124. return
  125. }
  126. err := h.svc.VerifyTotp(c.Request.Context(), UserID(c), req.Secret, req.Code)
  127. if err != nil {
  128. switch {
  129. case errors.Is(err, ErrBadTotp):
  130. ValidationFailed(c, map[string]string{"code": ErrBadTotp.Error()})
  131. case errors.Is(err, ErrTotpAlreadyEnabled):
  132. Conflict(c, ErrTotpAlreadyEnabled.Error())
  133. default:
  134. InternalError(c, err)
  135. }
  136. return
  137. }
  138. if h.OnTotpBound != nil {
  139. h.OnTotpBound(c, h.auditContext(c))
  140. }
  141. c.JSON(http.StatusOK, gin.H{"totp_enabled": true})
  142. }
  143. // DisableTotp 实现 POST /api/auth/totp/disable(api.md 2.6)。
  144. func (h *Handler) DisableTotp(c *gin.Context) {
  145. var req totpDisableRequest
  146. if !bind(c, &req) {
  147. return
  148. }
  149. err := h.svc.DisableTotp(c.Request.Context(), UserID(c), req.Code)
  150. if err != nil {
  151. switch {
  152. case errors.Is(err, ErrBadTotp):
  153. Unauthorized(c, ErrBadTotp.Error())
  154. case errors.Is(err, ErrTotpNotEnabled):
  155. ValidationFailed(c, map[string]string{"code": ErrTotpNotEnabled.Error()})
  156. default:
  157. InternalError(c, err)
  158. }
  159. return
  160. }
  161. if h.OnTotpDisabled != nil {
  162. h.OnTotpDisabled(c, h.auditContext(c))
  163. }
  164. c.JSON(http.StatusOK, gin.H{"totp_enabled": false})
  165. }
  166. // ChangePassword 实现 POST /api/auth/password(api.md 2.7)。
  167. func (h *Handler) ChangePassword(c *gin.Context) {
  168. var req passwordChangeRequest
  169. if !bind(c, &req) {
  170. return
  171. }
  172. ac := h.auditContext(c)
  173. err := h.svc.ChangePassword(c.Request.Context(), UserID(c), req.OldPassword, req.NewPassword, req.TotpCode)
  174. if err != nil {
  175. switch {
  176. case errors.Is(err, ErrInvalidCredentials):
  177. Unauthorized(c, ErrInvalidCredentials.Error())
  178. case errors.Is(err, ErrBadTotp):
  179. Unauthorized(c, ErrBadTotp.Error())
  180. case IsPasswordPolicyError(err):
  181. ValidationFailed(c, map[string]string{"new_password": err.Error()})
  182. default:
  183. InternalError(c, err)
  184. }
  185. return
  186. }
  187. if h.OnPasswordChanged != nil {
  188. h.OnPasswordChanged(c, ac)
  189. }
  190. // 当前会话也被删除,客户端需要重新登录(api.md 2.7 备注)
  191. clearSessionCookie(c, h.CookieDomain, h.CookieSecure)
  192. c.Status(http.StatusNoContent)
  193. }
  194. // Me 实现 GET /api/me(api.md 11.1)。
  195. func (h *Handler) Me(c *gin.Context) {
  196. id := CurrentIdentity(c)
  197. if !id.IsLoggedIn() {
  198. c.JSON(http.StatusOK, meResponse{Anonymous: true})
  199. return
  200. }
  201. user, err := h.store.GetUser(c.Request.Context(), id.UserID)
  202. if err != nil {
  203. InternalError(c, err)
  204. return
  205. }
  206. c.JSON(http.StatusOK, meResponse{
  207. Anonymous: false,
  208. AuthType: id.String(),
  209. Scope: id.Scope,
  210. User: &userBriefResponse{ID: user.ID, Username: user.Username},
  211. })
  212. }
  213. // usernameFromPending 用临时 token 反查用户名,供 login_failure 审计使用。
  214. // 查不到时返回空串(audit_events.actor_username 允许空串)。
  215. func (h *Handler) usernameFromPending(ctx context.Context, tempToken string) string {
  216. base, _ := splitPendingToken(tempToken)
  217. username, err := h.svc.pendingUsername(ctx, HashToken(base))
  218. if err != nil {
  219. return ""
  220. }
  221. return username
  222. }
苏ICP备2026082406号
苏公网安备 待审核