You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

webauthn.go 4.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. // Copyright 2018 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package auth
  4. import (
  5. "errors"
  6. "net/http"
  7. "code.gitea.io/gitea/models/auth"
  8. user_model "code.gitea.io/gitea/models/user"
  9. wa "code.gitea.io/gitea/modules/auth/webauthn"
  10. "code.gitea.io/gitea/modules/base"
  11. "code.gitea.io/gitea/modules/context"
  12. "code.gitea.io/gitea/modules/log"
  13. "code.gitea.io/gitea/modules/setting"
  14. "code.gitea.io/gitea/services/externalaccount"
  15. "github.com/go-webauthn/webauthn/protocol"
  16. "github.com/go-webauthn/webauthn/webauthn"
  17. )
  18. var tplWebAuthn base.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. // Check auto-login.
  23. if checkAutoLogin(ctx) {
  24. return
  25. }
  26. // Ensure user is in a 2FA session.
  27. if ctx.Session.Get("twofaUid") == nil {
  28. ctx.ServerError("UserSignIn", errors.New("not in WebAuthn session"))
  29. return
  30. }
  31. hasTwoFactor, err := auth.HasTwoFactorByUID(ctx, ctx.Session.Get("twofaUid").(int64))
  32. if err != nil {
  33. ctx.ServerError("HasTwoFactorByUID", err)
  34. return
  35. }
  36. ctx.Data["HasTwoFactor"] = hasTwoFactor
  37. ctx.HTML(http.StatusOK, tplWebAuthn)
  38. }
  39. // WebAuthnLoginAssertion submits a WebAuthn challenge to the browser
  40. func WebAuthnLoginAssertion(ctx *context.Context) {
  41. // Ensure user is in a WebAuthn session.
  42. idSess, ok := ctx.Session.Get("twofaUid").(int64)
  43. if !ok || idSess == 0 {
  44. ctx.ServerError("UserSignIn", errors.New("not in WebAuthn session"))
  45. return
  46. }
  47. user, err := user_model.GetUserByID(ctx, idSess)
  48. if err != nil {
  49. ctx.ServerError("UserSignIn", err)
  50. return
  51. }
  52. exists, err := auth.ExistsWebAuthnCredentialsForUID(ctx, user.ID)
  53. if err != nil {
  54. ctx.ServerError("UserSignIn", err)
  55. return
  56. }
  57. if !exists {
  58. ctx.ServerError("UserSignIn", errors.New("no device registered"))
  59. return
  60. }
  61. assertion, sessionData, err := wa.WebAuthn.BeginLogin((*wa.User)(user))
  62. if err != nil {
  63. ctx.ServerError("webauthn.BeginLogin", err)
  64. return
  65. }
  66. if err := ctx.Session.Set("webauthnAssertion", sessionData); err != nil {
  67. ctx.ServerError("Session.Set", err)
  68. return
  69. }
  70. ctx.JSON(http.StatusOK, assertion)
  71. }
  72. // WebAuthnLoginAssertionPost validates the signature and logs the user in
  73. func WebAuthnLoginAssertionPost(ctx *context.Context) {
  74. idSess, ok := ctx.Session.Get("twofaUid").(int64)
  75. sessionData, okData := ctx.Session.Get("webauthnAssertion").(*webauthn.SessionData)
  76. if !ok || !okData || sessionData == nil || idSess == 0 {
  77. ctx.ServerError("UserSignIn", errors.New("not in WebAuthn session"))
  78. return
  79. }
  80. defer func() {
  81. _ = ctx.Session.Delete("webauthnAssertion")
  82. }()
  83. // Load the user from the db
  84. user, err := user_model.GetUserByID(ctx, idSess)
  85. if err != nil {
  86. ctx.ServerError("UserSignIn", err)
  87. return
  88. }
  89. log.Trace("Finishing webauthn authentication with user: %s", user.Name)
  90. // Now we do the equivalent of webauthn.FinishLogin using a combination of our session data
  91. // (from webauthnAssertion) and verify the provided request.0
  92. parsedResponse, err := protocol.ParseCredentialRequestResponse(ctx.Req)
  93. if err != nil {
  94. // Failed authentication attempt.
  95. log.Info("Failed authentication attempt for %s from %s: %v", user.Name, ctx.RemoteAddr(), err)
  96. ctx.Status(http.StatusForbidden)
  97. return
  98. }
  99. // Validate the parsed response.
  100. cred, err := wa.WebAuthn.ValidateLogin((*wa.User)(user), *sessionData, parsedResponse)
  101. if err != nil {
  102. // Failed authentication attempt.
  103. log.Info("Failed authentication attempt for %s from %s: %v", user.Name, ctx.RemoteAddr(), err)
  104. ctx.Status(http.StatusForbidden)
  105. return
  106. }
  107. // Ensure that the credential wasn't cloned by checking if CloneWarning is set.
  108. // (This is set if the sign counter is less than the one we have stored.)
  109. if cred.Authenticator.CloneWarning {
  110. log.Info("Failed authentication attempt for %s from %s: cloned credential", user.Name, ctx.RemoteAddr())
  111. ctx.Status(http.StatusForbidden)
  112. return
  113. }
  114. // Success! Get the credential and update the sign count with the new value we received.
  115. dbCred, err := auth.GetWebAuthnCredentialByCredID(ctx, user.ID, cred.ID)
  116. if err != nil {
  117. ctx.ServerError("GetWebAuthnCredentialByCredID", err)
  118. return
  119. }
  120. dbCred.SignCount = cred.Authenticator.SignCount
  121. if err := dbCred.UpdateSignCount(ctx); err != nil {
  122. ctx.ServerError("UpdateSignCount", err)
  123. return
  124. }
  125. // Now handle account linking if that's requested
  126. if ctx.Session.Get("linkAccount") != nil {
  127. if err := externalaccount.LinkAccountFromStore(ctx, ctx.Session, user); err != nil {
  128. ctx.ServerError("LinkAccountFromStore", err)
  129. return
  130. }
  131. }
  132. remember := ctx.Session.Get("twofaRemember").(bool)
  133. redirect := handleSignInFull(ctx, user, remember, false)
  134. if redirect == "" {
  135. redirect = setting.AppSubURL + "/"
  136. }
  137. _ = ctx.Session.Delete("twofaUid")
  138. ctx.JSONRedirect(redirect)
  139. }