passkey.go 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. package user
  2. import (
  3. "encoding/base64"
  4. "fmt"
  5. "github.com/0xJacky/Nginx-UI/api"
  6. "github.com/0xJacky/Nginx-UI/internal/cache"
  7. "github.com/0xJacky/Nginx-UI/internal/passkey"
  8. "github.com/0xJacky/Nginx-UI/internal/user"
  9. "github.com/0xJacky/Nginx-UI/model"
  10. "github.com/0xJacky/Nginx-UI/query"
  11. "github.com/gin-gonic/gin"
  12. "github.com/go-webauthn/webauthn/webauthn"
  13. "github.com/google/uuid"
  14. "github.com/spf13/cast"
  15. "github.com/uozi-tech/cosy"
  16. "github.com/uozi-tech/cosy/logger"
  17. "gorm.io/gorm"
  18. "net/http"
  19. "strings"
  20. "time"
  21. )
  22. const passkeyTimeout = 30 * time.Second
  23. func buildCachePasskeyRegKey(id uint64) string {
  24. return fmt.Sprintf("passkey-reg-%d", id)
  25. }
  26. func GetPasskeyConfigStatus(c *gin.Context) {
  27. c.JSON(http.StatusOK, gin.H{
  28. "status": passkey.Enabled(),
  29. })
  30. }
  31. func BeginPasskeyRegistration(c *gin.Context) {
  32. u := api.CurrentUser(c)
  33. webauthnInstance := passkey.GetInstance()
  34. options, sessionData, err := webauthnInstance.BeginRegistration(u)
  35. if err != nil {
  36. cosy.ErrHandler(c, err)
  37. return
  38. }
  39. cache.Set(buildCachePasskeyRegKey(u.ID), sessionData, passkeyTimeout)
  40. c.JSON(http.StatusOK, options.Response)
  41. }
  42. func FinishPasskeyRegistration(c *gin.Context) {
  43. cUser := api.CurrentUser(c)
  44. webauthnInstance := passkey.GetInstance()
  45. sessionDataBytes, ok := cache.Get(buildCachePasskeyRegKey(cUser.ID))
  46. if !ok {
  47. cosy.ErrHandler(c, user.ErrSessionNotFound)
  48. return
  49. }
  50. sessionData := sessionDataBytes.(*webauthn.SessionData)
  51. credential, err := webauthnInstance.FinishRegistration(cUser, *sessionData, c.Request)
  52. if err != nil {
  53. cosy.ErrHandler(c, err)
  54. return
  55. }
  56. cache.Del(buildCachePasskeyRegKey(cUser.ID))
  57. rawId := strings.TrimRight(base64.StdEncoding.EncodeToString(credential.ID), "=")
  58. passkeyName := c.Query("name")
  59. p := query.Passkey
  60. err = p.Create(&model.Passkey{
  61. UserID: cUser.ID,
  62. Name: passkeyName,
  63. RawID: rawId,
  64. Credential: credential,
  65. LastUsedAt: time.Now().Unix(),
  66. })
  67. if err != nil {
  68. cosy.ErrHandler(c, err)
  69. return
  70. }
  71. c.JSON(http.StatusOK, gin.H{
  72. "message": "ok",
  73. })
  74. }
  75. func BeginPasskeyLogin(c *gin.Context) {
  76. if !passkey.Enabled() {
  77. cosy.ErrHandler(c, user.ErrWebAuthnNotConfigured)
  78. return
  79. }
  80. webauthnInstance := passkey.GetInstance()
  81. options, sessionData, err := webauthnInstance.BeginDiscoverableLogin()
  82. if err != nil {
  83. cosy.ErrHandler(c, err)
  84. return
  85. }
  86. sessionID := uuid.NewString()
  87. cache.Set(sessionID, sessionData, passkeyTimeout)
  88. c.JSON(http.StatusOK, gin.H{
  89. "session_id": sessionID,
  90. "options": options,
  91. })
  92. }
  93. func FinishPasskeyLogin(c *gin.Context) {
  94. if !passkey.Enabled() {
  95. cosy.ErrHandler(c, user.ErrWebAuthnNotConfigured)
  96. return
  97. }
  98. sessionId := c.GetHeader("X-Passkey-Session-ID")
  99. sessionDataBytes, ok := cache.Get(sessionId)
  100. if !ok {
  101. cosy.ErrHandler(c, user.ErrSessionNotFound)
  102. return
  103. }
  104. webauthnInstance := passkey.GetInstance()
  105. sessionData := sessionDataBytes.(*webauthn.SessionData)
  106. var outUser *model.User
  107. _, err := webauthnInstance.FinishDiscoverableLogin(
  108. func(rawID, userHandle []byte) (user webauthn.User, err error) {
  109. encodeRawID := strings.TrimRight(base64.StdEncoding.EncodeToString(rawID), "=")
  110. u := query.User
  111. logger.Debug("[WebAuthn] Discoverable Login", cast.ToInt(string(userHandle)))
  112. p := query.Passkey
  113. _, _ = p.Where(p.RawID.Eq(encodeRawID)).Updates(&model.Passkey{
  114. LastUsedAt: time.Now().Unix(),
  115. })
  116. outUser, err = u.FirstByID(cast.ToUint64(string(userHandle)))
  117. return outUser, err
  118. }, *sessionData, c.Request)
  119. if err != nil {
  120. cosy.ErrHandler(c, err)
  121. return
  122. }
  123. b := query.BanIP
  124. clientIP := c.ClientIP()
  125. // login success, clear banned record
  126. _, _ = b.Where(b.IP.Eq(clientIP)).Delete()
  127. logger.Info("[User Login]", outUser.Name)
  128. token, err := user.GenerateJWT(outUser)
  129. if err != nil {
  130. c.JSON(http.StatusInternalServerError, LoginResponse{
  131. Message: err.Error(),
  132. })
  133. return
  134. }
  135. secureSessionID := user.SetSecureSessionID(outUser.ID)
  136. c.JSON(http.StatusOK, LoginResponse{
  137. Code: LoginSuccess,
  138. Message: "ok",
  139. Token: token,
  140. SecureSessionID: secureSessionID,
  141. })
  142. }
  143. func GetPasskeyList(c *gin.Context) {
  144. u := api.CurrentUser(c)
  145. p := query.Passkey
  146. passkeys, err := p.Where(p.UserID.Eq(u.ID)).Find()
  147. if err != nil {
  148. cosy.ErrHandler(c, err)
  149. return
  150. }
  151. if len(passkeys) == 0 {
  152. passkeys = make([]*model.Passkey, 0)
  153. }
  154. c.JSON(http.StatusOK, passkeys)
  155. }
  156. func UpdatePasskey(c *gin.Context) {
  157. u := api.CurrentUser(c)
  158. cosy.Core[model.Passkey](c).
  159. SetValidRules(gin.H{
  160. "name": "required",
  161. }).GormScope(func(tx *gorm.DB) *gorm.DB {
  162. return tx.Where("user_id", u.ID)
  163. }).Modify()
  164. }
  165. func DeletePasskey(c *gin.Context) {
  166. u := api.CurrentUser(c)
  167. cosy.Core[model.Passkey](c).
  168. GormScope(func(tx *gorm.DB) *gorm.DB {
  169. return tx.Where("user_id", u.ID)
  170. }).PermanentlyDelete()
  171. }