gitea源码

webauthn.go 8.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. // Copyright 2018 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package auth
  4. import (
  5. "encoding/binary"
  6. "errors"
  7. "net/http"
  8. "code.gitea.io/gitea/models/auth"
  9. user_model "code.gitea.io/gitea/models/user"
  10. wa "code.gitea.io/gitea/modules/auth/webauthn"
  11. "code.gitea.io/gitea/modules/log"
  12. "code.gitea.io/gitea/modules/setting"
  13. "code.gitea.io/gitea/modules/templates"
  14. "code.gitea.io/gitea/services/context"
  15. "github.com/go-webauthn/webauthn/protocol"
  16. "github.com/go-webauthn/webauthn/webauthn"
  17. )
  18. var tplWebAuthn templates.TplName = "user/auth/webauthn"
  19. // WebAuthn shows the WebAuthn login page
  20. func WebAuthn(ctx *context.Context) {
  21. ctx.Data["Title"] = ctx.Tr("twofa")
  22. if CheckAutoLogin(ctx) {
  23. return
  24. }
  25. // Ensure user is in a 2FA session.
  26. if ctx.Session.Get("twofaUid") == nil {
  27. ctx.ServerError("UserSignIn", errors.New("not in WebAuthn session"))
  28. return
  29. }
  30. hasTwoFactor, err := auth.HasTwoFactorByUID(ctx, ctx.Session.Get("twofaUid").(int64))
  31. if err != nil {
  32. ctx.ServerError("HasTwoFactorByUID", err)
  33. return
  34. }
  35. ctx.Data["HasTwoFactor"] = hasTwoFactor
  36. ctx.HTML(http.StatusOK, tplWebAuthn)
  37. }
  38. // WebAuthnPasskeyAssertion submits a WebAuthn challenge for the passkey login to the browser
  39. func WebAuthnPasskeyAssertion(ctx *context.Context) {
  40. if !setting.Service.EnablePasskeyAuth {
  41. ctx.HTTPError(http.StatusForbidden)
  42. return
  43. }
  44. assertion, sessionData, err := wa.WebAuthn.BeginDiscoverableLogin()
  45. if err != nil {
  46. ctx.ServerError("webauthn.BeginDiscoverableLogin", err)
  47. return
  48. }
  49. if err := ctx.Session.Set("webauthnPasskeyAssertion", sessionData); err != nil {
  50. ctx.ServerError("Session.Set", err)
  51. return
  52. }
  53. ctx.JSON(http.StatusOK, assertion)
  54. }
  55. // WebAuthnPasskeyLogin handles the WebAuthn login process using a Passkey
  56. func WebAuthnPasskeyLogin(ctx *context.Context) {
  57. if !setting.Service.EnablePasskeyAuth {
  58. ctx.HTTPError(http.StatusForbidden)
  59. return
  60. }
  61. sessionData, okData := ctx.Session.Get("webauthnPasskeyAssertion").(*webauthn.SessionData)
  62. if !okData || sessionData == nil {
  63. ctx.ServerError("ctx.Session.Get", errors.New("not in WebAuthn session"))
  64. return
  65. }
  66. defer func() {
  67. _ = ctx.Session.Delete("webauthnPasskeyAssertion")
  68. }()
  69. // Validate the parsed response.
  70. // ParseCredentialRequestResponse+ValidateDiscoverableLogin equals to FinishDiscoverableLogin, but we need to ParseCredentialRequestResponse first to get flags
  71. var user *user_model.User
  72. parsedResponse, err := protocol.ParseCredentialRequestResponse(ctx.Req)
  73. if err != nil {
  74. // Failed authentication attempt.
  75. log.Info("Failed authentication attempt for %s from %s: %v", user.Name, ctx.RemoteAddr(), err)
  76. ctx.Status(http.StatusForbidden)
  77. return
  78. }
  79. cred, err := wa.WebAuthn.ValidateDiscoverableLogin(func(rawID, userHandle []byte) (webauthn.User, error) {
  80. userID, n := binary.Varint(userHandle)
  81. if n <= 0 {
  82. return nil, errors.New("invalid rawID")
  83. }
  84. var err error
  85. user, err = user_model.GetUserByID(ctx, userID)
  86. if err != nil {
  87. return nil, err
  88. }
  89. return wa.NewWebAuthnUser(ctx, user, parsedResponse.Response.AuthenticatorData.Flags), nil
  90. }, *sessionData, parsedResponse)
  91. if err != nil {
  92. // Failed authentication attempt.
  93. log.Info("Failed authentication attempt for passkey from %s: %v", ctx.RemoteAddr(), err)
  94. ctx.Status(http.StatusForbidden)
  95. return
  96. }
  97. if !cred.Flags.UserPresent {
  98. ctx.Status(http.StatusBadRequest)
  99. return
  100. }
  101. if user == nil {
  102. ctx.Status(http.StatusBadRequest)
  103. return
  104. }
  105. // Ensure that the credential wasn't cloned by checking if CloneWarning is set.
  106. // (This is set if the sign counter is less than the one we have stored.)
  107. if cred.Authenticator.CloneWarning {
  108. log.Info("Failed authentication attempt for %s from %s: cloned credential", user.Name, ctx.RemoteAddr())
  109. ctx.Status(http.StatusForbidden)
  110. return
  111. }
  112. // Success! Get the credential and update the sign count with the new value we received.
  113. dbCred, err := auth.GetWebAuthnCredentialByCredID(ctx, user.ID, cred.ID)
  114. if err != nil {
  115. ctx.ServerError("GetWebAuthnCredentialByCredID", err)
  116. return
  117. }
  118. dbCred.SignCount = cred.Authenticator.SignCount
  119. if err := dbCred.UpdateSignCount(ctx); err != nil {
  120. ctx.ServerError("UpdateSignCount", err)
  121. return
  122. }
  123. // Now handle account linking if that's requested
  124. if ctx.Session.Get("linkAccount") != nil {
  125. if err := linkAccountFromContext(ctx, user); err != nil {
  126. ctx.ServerError("LinkAccountFromStore", err)
  127. return
  128. }
  129. }
  130. remember := false // TODO: implement remember me
  131. redirect := handleSignInFull(ctx, user, remember, false)
  132. if redirect == "" {
  133. redirect = setting.AppSubURL + "/"
  134. }
  135. ctx.JSONRedirect(redirect)
  136. }
  137. // WebAuthnLoginAssertion submits a WebAuthn challenge to the browser
  138. func WebAuthnLoginAssertion(ctx *context.Context) {
  139. // Ensure user is in a WebAuthn session.
  140. idSess, ok := ctx.Session.Get("twofaUid").(int64)
  141. if !ok || idSess == 0 {
  142. ctx.ServerError("UserSignIn", errors.New("not in WebAuthn session"))
  143. return
  144. }
  145. user, err := user_model.GetUserByID(ctx, idSess)
  146. if err != nil {
  147. ctx.ServerError("UserSignIn", err)
  148. return
  149. }
  150. exists, err := auth.ExistsWebAuthnCredentialsForUID(ctx, user.ID)
  151. if err != nil {
  152. ctx.ServerError("UserSignIn", err)
  153. return
  154. }
  155. if !exists {
  156. ctx.ServerError("UserSignIn", errors.New("no device registered"))
  157. return
  158. }
  159. webAuthnUser := wa.NewWebAuthnUser(ctx, user)
  160. assertion, sessionData, err := wa.WebAuthn.BeginLogin(webAuthnUser)
  161. if err != nil {
  162. ctx.ServerError("webauthn.BeginLogin", err)
  163. return
  164. }
  165. if err := ctx.Session.Set("webauthnAssertion", sessionData); err != nil {
  166. ctx.ServerError("Session.Set", err)
  167. return
  168. }
  169. ctx.JSON(http.StatusOK, assertion)
  170. }
  171. // WebAuthnLoginAssertionPost validates the signature and logs the user in
  172. func WebAuthnLoginAssertionPost(ctx *context.Context) {
  173. idSess, ok := ctx.Session.Get("twofaUid").(int64)
  174. sessionData, okData := ctx.Session.Get("webauthnAssertion").(*webauthn.SessionData)
  175. if !ok || !okData || sessionData == nil || idSess == 0 {
  176. ctx.ServerError("UserSignIn", errors.New("not in WebAuthn session"))
  177. return
  178. }
  179. defer func() {
  180. _ = ctx.Session.Delete("webauthnAssertion")
  181. }()
  182. // Load the user from the db
  183. user, err := user_model.GetUserByID(ctx, idSess)
  184. if err != nil {
  185. ctx.ServerError("UserSignIn", err)
  186. return
  187. }
  188. log.Trace("Finishing webauthn authentication with user: %s", user.Name)
  189. // Now we do the equivalent of webauthn.FinishLogin using a combination of our session data
  190. // (from webauthnAssertion) and verify the provided request.0
  191. parsedResponse, err := protocol.ParseCredentialRequestResponse(ctx.Req)
  192. if err != nil {
  193. // Failed authentication attempt.
  194. log.Info("Failed authentication attempt for %s from %s: %v", user.Name, ctx.RemoteAddr(), err)
  195. ctx.Status(http.StatusForbidden)
  196. return
  197. }
  198. // Validate the parsed response.
  199. webAuthnUser := wa.NewWebAuthnUser(ctx, user, parsedResponse.Response.AuthenticatorData.Flags)
  200. cred, err := wa.WebAuthn.ValidateLogin(webAuthnUser, *sessionData, parsedResponse)
  201. if err != nil {
  202. // Failed authentication attempt.
  203. log.Info("Failed authentication attempt for %s from %s: %v", user.Name, ctx.RemoteAddr(), err)
  204. ctx.Status(http.StatusForbidden)
  205. return
  206. }
  207. // Ensure that the credential wasn't cloned by checking if CloneWarning is set.
  208. // (This is set if the sign counter is less than the one we have stored.)
  209. if cred.Authenticator.CloneWarning {
  210. log.Info("Failed authentication attempt for %s from %s: cloned credential", user.Name, ctx.RemoteAddr())
  211. ctx.Status(http.StatusForbidden)
  212. return
  213. }
  214. // Success! Get the credential and update the sign count with the new value we received.
  215. dbCred, err := auth.GetWebAuthnCredentialByCredID(ctx, user.ID, cred.ID)
  216. if err != nil {
  217. ctx.ServerError("GetWebAuthnCredentialByCredID", err)
  218. return
  219. }
  220. dbCred.SignCount = cred.Authenticator.SignCount
  221. if err := dbCred.UpdateSignCount(ctx); err != nil {
  222. ctx.ServerError("UpdateSignCount", err)
  223. return
  224. }
  225. // Now handle account linking if that's requested
  226. if ctx.Session.Get("linkAccount") != nil {
  227. if err := linkAccountFromContext(ctx, user); err != nil {
  228. ctx.ServerError("LinkAccountFromStore", err)
  229. return
  230. }
  231. }
  232. remember := ctx.Session.Get("twofaRemember").(bool)
  233. redirect := handleSignInFull(ctx, user, remember, false)
  234. if redirect == "" {
  235. redirect = setting.AppSubURL + "/"
  236. }
  237. _ = ctx.Session.Delete("twofaUid")
  238. ctx.JSONRedirect(redirect)
  239. }