passkey.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576
  1. package controller
  2. import (
  3. "errors"
  4. "fmt"
  5. "net/http"
  6. "strconv"
  7. "time"
  8. "github.com/QuantumNous/new-api/common"
  9. "github.com/QuantumNous/new-api/model"
  10. passkeysvc "github.com/QuantumNous/new-api/service/passkey"
  11. "github.com/QuantumNous/new-api/setting/system_setting"
  12. "github.com/gin-contrib/sessions"
  13. "github.com/gin-gonic/gin"
  14. "github.com/go-webauthn/webauthn/protocol"
  15. webauthnlib "github.com/go-webauthn/webauthn/webauthn"
  16. )
  17. func PasskeyRegisterBegin(c *gin.Context) {
  18. if !system_setting.GetPasskeySettings().Enabled {
  19. c.JSON(http.StatusOK, gin.H{
  20. "success": false,
  21. "message": "管理员未启用 Passkey 登录",
  22. })
  23. return
  24. }
  25. user, err := getSessionUser(c)
  26. if err != nil {
  27. c.JSON(http.StatusUnauthorized, gin.H{
  28. "success": false,
  29. "message": err.Error(),
  30. })
  31. return
  32. }
  33. if !requirePasskeyRegistrationVerification(c, user.Id) {
  34. return
  35. }
  36. credential, err := model.GetPasskeyByUserID(user.Id)
  37. if err != nil && !errors.Is(err, model.ErrPasskeyNotFound) {
  38. common.ApiError(c, err)
  39. return
  40. }
  41. if errors.Is(err, model.ErrPasskeyNotFound) {
  42. credential = nil
  43. }
  44. wa, err := passkeysvc.BuildWebAuthn(c.Request)
  45. if err != nil {
  46. common.ApiError(c, err)
  47. return
  48. }
  49. waUser := passkeysvc.NewWebAuthnUser(user, credential)
  50. var options []webauthnlib.RegistrationOption
  51. if credential != nil {
  52. descriptor := credential.ToWebAuthnCredential().Descriptor()
  53. options = append(options, webauthnlib.WithExclusions([]protocol.CredentialDescriptor{descriptor}))
  54. }
  55. creation, sessionData, err := wa.BeginRegistration(waUser, options...)
  56. if err != nil {
  57. common.ApiError(c, err)
  58. return
  59. }
  60. if err := passkeysvc.SaveSessionData(c, passkeysvc.RegistrationSessionKey, sessionData); err != nil {
  61. common.ApiError(c, err)
  62. return
  63. }
  64. c.JSON(http.StatusOK, gin.H{
  65. "success": true,
  66. "message": "",
  67. "data": gin.H{
  68. "options": creation,
  69. },
  70. })
  71. }
  72. func PasskeyRegisterFinish(c *gin.Context) {
  73. if !system_setting.GetPasskeySettings().Enabled {
  74. c.JSON(http.StatusOK, gin.H{
  75. "success": false,
  76. "message": "管理员未启用 Passkey 登录",
  77. })
  78. return
  79. }
  80. user, err := getSessionUser(c)
  81. if err != nil {
  82. c.JSON(http.StatusUnauthorized, gin.H{
  83. "success": false,
  84. "message": err.Error(),
  85. })
  86. return
  87. }
  88. if !requirePasskeyRegistrationVerification(c, user.Id) {
  89. return
  90. }
  91. wa, err := passkeysvc.BuildWebAuthn(c.Request)
  92. if err != nil {
  93. common.ApiError(c, err)
  94. return
  95. }
  96. credentialRecord, err := model.GetPasskeyByUserID(user.Id)
  97. if err != nil && !errors.Is(err, model.ErrPasskeyNotFound) {
  98. common.ApiError(c, err)
  99. return
  100. }
  101. if errors.Is(err, model.ErrPasskeyNotFound) {
  102. credentialRecord = nil
  103. }
  104. sessionData, err := passkeysvc.PopSessionData(c, passkeysvc.RegistrationSessionKey)
  105. if err != nil {
  106. common.ApiError(c, err)
  107. return
  108. }
  109. waUser := passkeysvc.NewWebAuthnUser(user, credentialRecord)
  110. credential, err := wa.FinishRegistration(waUser, *sessionData, c.Request)
  111. if err != nil {
  112. common.ApiError(c, err)
  113. return
  114. }
  115. passkeyCredential := model.NewPasskeyCredentialFromWebAuthn(user.Id, credential)
  116. if passkeyCredential == nil {
  117. common.ApiErrorMsg(c, "无法创建 Passkey 凭证")
  118. return
  119. }
  120. if err := model.UpsertPasskeyCredential(passkeyCredential); err != nil {
  121. common.ApiError(c, err)
  122. return
  123. }
  124. c.JSON(http.StatusOK, gin.H{
  125. "success": true,
  126. "message": "Passkey 注册成功",
  127. })
  128. }
  129. func PasskeyDelete(c *gin.Context) {
  130. user, err := getSessionUser(c)
  131. if err != nil {
  132. c.JSON(http.StatusUnauthorized, gin.H{
  133. "success": false,
  134. "message": err.Error(),
  135. })
  136. return
  137. }
  138. if !requirePasskeyDeleteVerification(c, user.Id) {
  139. return
  140. }
  141. if err := model.DeletePasskeyByUserID(user.Id); err != nil {
  142. common.ApiError(c, err)
  143. return
  144. }
  145. c.JSON(http.StatusOK, gin.H{
  146. "success": true,
  147. "message": "Passkey 已解绑",
  148. })
  149. }
  150. func PasskeyStatus(c *gin.Context) {
  151. user, err := getSessionUser(c)
  152. if err != nil {
  153. c.JSON(http.StatusUnauthorized, gin.H{
  154. "success": false,
  155. "message": err.Error(),
  156. })
  157. return
  158. }
  159. credential, err := model.GetPasskeyByUserID(user.Id)
  160. if errors.Is(err, model.ErrPasskeyNotFound) {
  161. c.JSON(http.StatusOK, gin.H{
  162. "success": true,
  163. "message": "",
  164. "data": gin.H{
  165. "enabled": false,
  166. },
  167. })
  168. return
  169. }
  170. if err != nil {
  171. common.ApiError(c, err)
  172. return
  173. }
  174. data := gin.H{
  175. "enabled": true,
  176. "last_used_at": credential.LastUsedAt,
  177. }
  178. c.JSON(http.StatusOK, gin.H{
  179. "success": true,
  180. "message": "",
  181. "data": data,
  182. })
  183. }
  184. func PasskeyLoginBegin(c *gin.Context) {
  185. if !system_setting.GetPasskeySettings().Enabled {
  186. c.JSON(http.StatusOK, gin.H{
  187. "success": false,
  188. "message": "管理员未启用 Passkey 登录",
  189. })
  190. return
  191. }
  192. wa, err := passkeysvc.BuildWebAuthn(c.Request)
  193. if err != nil {
  194. common.ApiError(c, err)
  195. return
  196. }
  197. assertion, sessionData, err := wa.BeginDiscoverableLogin()
  198. if err != nil {
  199. common.ApiError(c, err)
  200. return
  201. }
  202. if err := passkeysvc.SaveSessionData(c, passkeysvc.LoginSessionKey, sessionData); err != nil {
  203. common.ApiError(c, err)
  204. return
  205. }
  206. c.JSON(http.StatusOK, gin.H{
  207. "success": true,
  208. "message": "",
  209. "data": gin.H{
  210. "options": assertion,
  211. },
  212. })
  213. }
  214. func PasskeyLoginFinish(c *gin.Context) {
  215. if !system_setting.GetPasskeySettings().Enabled {
  216. c.JSON(http.StatusOK, gin.H{
  217. "success": false,
  218. "message": "管理员未启用 Passkey 登录",
  219. })
  220. return
  221. }
  222. wa, err := passkeysvc.BuildWebAuthn(c.Request)
  223. if err != nil {
  224. common.ApiError(c, err)
  225. return
  226. }
  227. sessionData, err := passkeysvc.PopSessionData(c, passkeysvc.LoginSessionKey)
  228. if err != nil {
  229. common.ApiError(c, err)
  230. return
  231. }
  232. handler := func(rawID, userHandle []byte) (webauthnlib.User, error) {
  233. // 首先通过凭证ID查找用户
  234. credential, err := model.GetPasskeyByCredentialID(rawID)
  235. if err != nil {
  236. return nil, fmt.Errorf("未找到 Passkey 凭证: %w", err)
  237. }
  238. // 通过凭证获取用户
  239. user := &model.User{Id: credential.UserID}
  240. if err := user.FillUserById(); err != nil {
  241. return nil, fmt.Errorf("用户信息获取失败: %w", err)
  242. }
  243. if user.Status != common.UserStatusEnabled {
  244. return nil, errors.New("该用户已被禁用")
  245. }
  246. if len(userHandle) > 0 {
  247. userID, parseErr := strconv.Atoi(string(userHandle))
  248. if parseErr != nil {
  249. // 记录异常但继续验证,因为某些客户端可能使用非数字格式
  250. common.SysLog(fmt.Sprintf("PasskeyLogin: userHandle parse error for credential, length: %d", len(userHandle)))
  251. } else if userID != user.Id {
  252. return nil, errors.New("用户句柄与凭证不匹配")
  253. }
  254. }
  255. return passkeysvc.NewWebAuthnUser(user, credential), nil
  256. }
  257. waUser, credential, err := wa.FinishPasskeyLogin(handler, *sessionData, c.Request)
  258. if err != nil {
  259. common.ApiError(c, err)
  260. return
  261. }
  262. userWrapper, ok := waUser.(*passkeysvc.WebAuthnUser)
  263. if !ok {
  264. common.ApiErrorMsg(c, "Passkey 登录状态异常")
  265. return
  266. }
  267. modelUser := userWrapper.ModelUser()
  268. if modelUser == nil {
  269. common.ApiErrorMsg(c, "Passkey 登录状态异常")
  270. return
  271. }
  272. if modelUser.Status != common.UserStatusEnabled {
  273. common.ApiErrorMsg(c, "该用户已被禁用")
  274. return
  275. }
  276. // 更新凭证信息
  277. updatedCredential := model.NewPasskeyCredentialFromWebAuthn(modelUser.Id, credential)
  278. if updatedCredential == nil {
  279. common.ApiErrorMsg(c, "Passkey 凭证更新失败")
  280. return
  281. }
  282. now := time.Now()
  283. updatedCredential.LastUsedAt = &now
  284. if err := model.UpsertPasskeyCredential(updatedCredential); err != nil {
  285. common.ApiError(c, err)
  286. return
  287. }
  288. setupLogin(modelUser, c)
  289. return
  290. }
  291. func AdminResetPasskey(c *gin.Context) {
  292. id, err := strconv.Atoi(c.Param("id"))
  293. if err != nil {
  294. common.ApiErrorMsg(c, "无效的用户 ID")
  295. return
  296. }
  297. user := &model.User{Id: id}
  298. if err := user.FillUserById(); err != nil {
  299. common.ApiError(c, err)
  300. return
  301. }
  302. if _, err := model.GetPasskeyByUserID(user.Id); err != nil {
  303. if errors.Is(err, model.ErrPasskeyNotFound) {
  304. c.JSON(http.StatusOK, gin.H{
  305. "success": false,
  306. "message": "该用户尚未绑定 Passkey",
  307. })
  308. return
  309. }
  310. common.ApiError(c, err)
  311. return
  312. }
  313. if err := model.DeletePasskeyByUserID(user.Id); err != nil {
  314. common.ApiError(c, err)
  315. return
  316. }
  317. c.JSON(http.StatusOK, gin.H{
  318. "success": true,
  319. "message": "Passkey 已重置",
  320. })
  321. }
  322. func PasskeyVerifyBegin(c *gin.Context) {
  323. if !system_setting.GetPasskeySettings().Enabled {
  324. c.JSON(http.StatusOK, gin.H{
  325. "success": false,
  326. "message": "管理员未启用 Passkey 登录",
  327. })
  328. return
  329. }
  330. user, err := getSessionUser(c)
  331. if err != nil {
  332. c.JSON(http.StatusUnauthorized, gin.H{
  333. "success": false,
  334. "message": err.Error(),
  335. })
  336. return
  337. }
  338. credential, err := model.GetPasskeyByUserID(user.Id)
  339. if err != nil {
  340. c.JSON(http.StatusOK, gin.H{
  341. "success": false,
  342. "message": "该用户尚未绑定 Passkey",
  343. })
  344. return
  345. }
  346. wa, err := passkeysvc.BuildWebAuthn(c.Request)
  347. if err != nil {
  348. common.ApiError(c, err)
  349. return
  350. }
  351. waUser := passkeysvc.NewWebAuthnUser(user, credential)
  352. assertion, sessionData, err := wa.BeginLogin(waUser)
  353. if err != nil {
  354. common.ApiError(c, err)
  355. return
  356. }
  357. if err := passkeysvc.SaveSessionData(c, passkeysvc.VerifySessionKey, sessionData); err != nil {
  358. common.ApiError(c, err)
  359. return
  360. }
  361. c.JSON(http.StatusOK, gin.H{
  362. "success": true,
  363. "message": "",
  364. "data": gin.H{
  365. "options": assertion,
  366. },
  367. })
  368. }
  369. func PasskeyVerifyFinish(c *gin.Context) {
  370. if !system_setting.GetPasskeySettings().Enabled {
  371. c.JSON(http.StatusOK, gin.H{
  372. "success": false,
  373. "message": "管理员未启用 Passkey 登录",
  374. })
  375. return
  376. }
  377. user, err := getSessionUser(c)
  378. if err != nil {
  379. c.JSON(http.StatusUnauthorized, gin.H{
  380. "success": false,
  381. "message": err.Error(),
  382. })
  383. return
  384. }
  385. wa, err := passkeysvc.BuildWebAuthn(c.Request)
  386. if err != nil {
  387. common.ApiError(c, err)
  388. return
  389. }
  390. credential, err := model.GetPasskeyByUserID(user.Id)
  391. if err != nil {
  392. c.JSON(http.StatusOK, gin.H{
  393. "success": false,
  394. "message": "该用户尚未绑定 Passkey",
  395. })
  396. return
  397. }
  398. sessionData, err := passkeysvc.PopSessionData(c, passkeysvc.VerifySessionKey)
  399. if err != nil {
  400. common.ApiError(c, err)
  401. return
  402. }
  403. waUser := passkeysvc.NewWebAuthnUser(user, credential)
  404. _, err = wa.FinishLogin(waUser, *sessionData, c.Request)
  405. if err != nil {
  406. common.ApiError(c, err)
  407. return
  408. }
  409. // 更新凭证的最后使用时间
  410. now := time.Now()
  411. credential.LastUsedAt = &now
  412. if err := model.UpsertPasskeyCredential(credential); err != nil {
  413. common.ApiError(c, err)
  414. return
  415. }
  416. session := sessions.Default(c)
  417. // Mark passkey as ready; /api/verify will convert this into the final secure verification session.
  418. session.Set(PasskeyReadySessionKey, time.Now().Unix())
  419. session.Delete(SecureVerificationSessionKey)
  420. session.Delete(secureVerificationMethodSessionKey)
  421. if err := session.Save(); err != nil {
  422. common.ApiError(c, fmt.Errorf("保存验证状态失败: %v", err))
  423. return
  424. }
  425. c.JSON(http.StatusOK, gin.H{
  426. "success": true,
  427. "message": "Passkey 验证成功",
  428. })
  429. }
  430. func getSessionUser(c *gin.Context) (*model.User, error) {
  431. session := sessions.Default(c)
  432. idRaw := session.Get("id")
  433. if idRaw == nil {
  434. return nil, errors.New("未登录")
  435. }
  436. id, ok := idRaw.(int)
  437. if !ok {
  438. return nil, errors.New("无效的会话信息")
  439. }
  440. user := &model.User{Id: id}
  441. if err := user.FillUserById(); err != nil {
  442. return nil, err
  443. }
  444. if user.Status != common.UserStatusEnabled {
  445. return nil, errors.New("该用户已被禁用")
  446. }
  447. return user, nil
  448. }
  449. func requirePasskeyRegistrationVerification(c *gin.Context, userID int) bool {
  450. twoFA, err := model.GetTwoFAByUserId(userID)
  451. if err != nil {
  452. common.ApiError(c, err)
  453. return false
  454. }
  455. if twoFA == nil || !twoFA.IsEnabled {
  456. return true
  457. }
  458. return requireSecureVerificationMethod(c, secureVerificationMethod2FA)
  459. }
  460. func requirePasskeyDeleteVerification(c *gin.Context, userID int) bool {
  461. twoFA, err := model.GetTwoFAByUserId(userID)
  462. if err != nil {
  463. common.ApiError(c, err)
  464. return false
  465. }
  466. if twoFA != nil && twoFA.IsEnabled {
  467. return requireSecureVerificationMethod(c, secureVerificationMethod2FA)
  468. }
  469. _, err = model.GetPasskeyByUserID(userID)
  470. if err != nil {
  471. if errors.Is(err, model.ErrPasskeyNotFound) {
  472. c.JSON(http.StatusOK, gin.H{
  473. "success": false,
  474. "message": "该用户尚未绑定 Passkey",
  475. })
  476. return false
  477. }
  478. common.ApiError(c, err)
  479. return false
  480. }
  481. return requireSecureVerificationMethod(c, secureVerificationMethodPasskey)
  482. }
  483. func requireSecureVerificationMethod(c *gin.Context, method string) bool {
  484. session := sessions.Default(c)
  485. verifiedAt, ok := session.Get(SecureVerificationSessionKey).(int64)
  486. if !ok || time.Now().Unix()-verifiedAt >= SecureVerificationTimeout {
  487. session.Delete(SecureVerificationSessionKey)
  488. session.Delete(secureVerificationMethodSessionKey)
  489. _ = session.Save()
  490. common.ApiErrorMsg(c, "请先完成安全验证")
  491. return false
  492. }
  493. if verifiedMethod, ok := session.Get(secureVerificationMethodSessionKey).(string); !ok || verifiedMethod != method {
  494. common.ApiErrorMsg(c, "请先完成对应的安全验证")
  495. return false
  496. }
  497. return true
  498. }