webauthn.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
  2. // SPDX-License-Identifier: Apache-2.0
  3. package ui // import "miniflux.app/v2/internal/ui"
  4. import (
  5. "encoding/hex"
  6. "errors"
  7. "fmt"
  8. "log/slog"
  9. "net/http"
  10. "net/url"
  11. "github.com/go-webauthn/webauthn/protocol"
  12. "github.com/go-webauthn/webauthn/webauthn"
  13. "miniflux.app/v2/internal/config"
  14. "miniflux.app/v2/internal/crypto"
  15. "miniflux.app/v2/internal/http/request"
  16. "miniflux.app/v2/internal/http/response"
  17. "miniflux.app/v2/internal/model"
  18. "miniflux.app/v2/internal/ui/form"
  19. "miniflux.app/v2/internal/ui/view"
  20. )
  21. type WebAuthnUser struct {
  22. User *model.User
  23. AuthnID []byte
  24. Credentials []model.WebAuthnCredential
  25. }
  26. func (u WebAuthnUser) WebAuthnID() []byte {
  27. return u.AuthnID
  28. }
  29. func (u WebAuthnUser) WebAuthnName() string {
  30. return u.User.Username
  31. }
  32. func (u WebAuthnUser) WebAuthnDisplayName() string {
  33. return u.User.Username
  34. }
  35. func (u WebAuthnUser) WebAuthnIcon() string {
  36. return ""
  37. }
  38. func (u WebAuthnUser) WebAuthnCredentials() []webauthn.Credential {
  39. creds := make([]webauthn.Credential, len(u.Credentials))
  40. for i, cred := range u.Credentials {
  41. creds[i] = cred.Credential
  42. }
  43. return creds
  44. }
  45. func newWebAuthn() (*webauthn.WebAuthn, error) {
  46. baseURL, err := url.Parse(config.Opts.BaseURL())
  47. if err != nil {
  48. return nil, err
  49. }
  50. return webauthn.New(&webauthn.Config{
  51. RPDisplayName: "Miniflux",
  52. RPID: baseURL.Hostname(),
  53. RPOrigins: []string{config.Opts.RootURL()},
  54. })
  55. }
  56. func (h *handler) beginRegistration(w http.ResponseWriter, r *http.Request) {
  57. web, err := newWebAuthn()
  58. if err != nil {
  59. response.JSONServerError(w, r, err)
  60. return
  61. }
  62. user, err := h.store.UserByID(request.UserID(r))
  63. if err != nil {
  64. response.JSONServerError(w, r, err)
  65. return
  66. }
  67. credentials, err := h.store.WebAuthnCredentialsByUserID(user.ID)
  68. if err != nil {
  69. response.JSONServerError(w, r, err)
  70. return
  71. }
  72. credentialDescriptors := make([]protocol.CredentialDescriptor, len(credentials))
  73. for i, credential := range credentials {
  74. credentialDescriptors[i] = credential.Credential.Descriptor()
  75. }
  76. options, sessionData, err := web.BeginRegistration(
  77. WebAuthnUser{
  78. User: user,
  79. AuthnID: crypto.GenerateRandomBytes(32),
  80. },
  81. webauthn.WithExclusions(credentialDescriptors),
  82. webauthn.WithResidentKeyRequirement(protocol.ResidentKeyRequirementRequired),
  83. )
  84. if err != nil {
  85. response.JSONServerError(w, r, err)
  86. return
  87. }
  88. request.WebSession(r).SetWebAuthn(sessionData)
  89. response.JSON(w, r, options)
  90. }
  91. func (h *handler) finishRegistration(w http.ResponseWriter, r *http.Request) {
  92. web, err := newWebAuthn()
  93. if err != nil {
  94. response.JSONServerError(w, r, err)
  95. return
  96. }
  97. userID := request.UserID(r)
  98. user, err := h.store.UserByID(userID)
  99. if err != nil {
  100. response.JSONServerError(w, r, err)
  101. return
  102. }
  103. sessionData := request.WebSession(r).ConsumeWebAuthnSession()
  104. if sessionData == nil {
  105. response.JSONBadRequest(w, r, errors.New("missing webauthn session data"))
  106. return
  107. }
  108. webAuthnUser := WebAuthnUser{User: user, AuthnID: sessionData.UserID}
  109. credential, err := web.FinishRegistration(webAuthnUser, *sessionData, r)
  110. if err != nil {
  111. response.JSONServerError(w, r, err)
  112. return
  113. }
  114. err = h.store.AddWebAuthnCredential(userID, sessionData.UserID, credential)
  115. if err != nil {
  116. response.JSONServerError(w, r, err)
  117. return
  118. }
  119. handleEncoded := model.WebAuthnCredential{Handle: sessionData.UserID}.HandleEncoded()
  120. redirect := h.routePath("/webauthn/%s/rename", handleEncoded)
  121. response.JSON(w, r, map[string]string{"redirect": redirect})
  122. }
  123. func (h *handler) beginLogin(w http.ResponseWriter, r *http.Request) {
  124. web, err := newWebAuthn()
  125. if err != nil {
  126. response.JSONServerError(w, r, err)
  127. return
  128. }
  129. assertion, sessionData, err := web.BeginDiscoverableLogin()
  130. if err != nil {
  131. response.JSONServerError(w, r, err)
  132. return
  133. }
  134. request.WebSession(r).SetWebAuthn(sessionData)
  135. response.JSON(w, r, assertion)
  136. }
  137. func (h *handler) finishLogin(w http.ResponseWriter, r *http.Request) {
  138. web, err := newWebAuthn()
  139. if err != nil {
  140. response.JSONServerError(w, r, err)
  141. return
  142. }
  143. parsedResponse, err := protocol.ParseCredentialRequestResponseBody(r.Body)
  144. if err != nil {
  145. response.JSONServerError(w, r, err)
  146. return
  147. }
  148. slog.Debug("WebAuthn: parsed response flags",
  149. slog.Bool("user_present", parsedResponse.Response.AuthenticatorData.Flags.HasUserPresent()),
  150. slog.Bool("user_verified", parsedResponse.Response.AuthenticatorData.Flags.HasUserVerified()),
  151. slog.Bool("has_attested_credential_data", parsedResponse.Response.AuthenticatorData.Flags.HasAttestedCredentialData()),
  152. slog.Bool("has_backup_eligible", parsedResponse.Response.AuthenticatorData.Flags.HasBackupEligible()),
  153. slog.Bool("has_backup_state", parsedResponse.Response.AuthenticatorData.Flags.HasBackupState()),
  154. )
  155. sessionData := request.WebSession(r).ConsumeWebAuthnSession()
  156. if sessionData == nil {
  157. response.JSONBadRequest(w, r, errors.New("missing webauthn session data"))
  158. return
  159. }
  160. var resolvedUser *model.User
  161. var resolvedCredential *model.WebAuthnCredential
  162. userByHandle := func(rawID, userHandle []byte) (webauthn.User, error) {
  163. userID, credential, err := h.store.WebAuthnCredentialByHandle(userHandle)
  164. if err != nil {
  165. return nil, err
  166. }
  167. if userID == 0 || credential == nil {
  168. return nil, fmt.Errorf("no user found for handle %x", userHandle)
  169. }
  170. loadedUser, err := h.store.UserByID(userID)
  171. if err != nil {
  172. return nil, err
  173. }
  174. if loadedUser == nil {
  175. return nil, fmt.Errorf("no user found for handle %x", userHandle)
  176. }
  177. // One-shot backfill for credentials registered before the
  178. // backup_eligible column was added: trust the assertion's BE
  179. // once, then persist it after successful validation.
  180. if !credential.BackupEligibleKnown {
  181. credential.Credential.Flags.BackupEligible = parsedResponse.Response.AuthenticatorData.Flags.HasBackupEligible()
  182. }
  183. resolvedUser = loadedUser
  184. resolvedCredential = credential
  185. return WebAuthnUser{
  186. User: loadedUser,
  187. AuthnID: userHandle,
  188. Credentials: []model.WebAuthnCredential{*credential},
  189. }, nil
  190. }
  191. validatedCredential, err := web.ValidateDiscoverableLogin(userByHandle, *sessionData, parsedResponse)
  192. if err != nil {
  193. slog.Warn("WebAuthn: ValidateDiscoverableLogin failed",
  194. slog.String("client_ip", request.ClientIP(r)),
  195. slog.String("user_agent", r.UserAgent()),
  196. slog.Any("error", err),
  197. )
  198. response.JSONUnauthorized(w, r)
  199. return
  200. }
  201. user := resolvedUser
  202. matchingCredential := resolvedCredential
  203. if err := h.store.WebAuthnSaveLogin(matchingCredential.Handle, validatedCredential); err != nil {
  204. slog.Warn("WebAuthn: unable to persist credential state after login",
  205. slog.Int64("user_id", user.ID),
  206. slog.Any("error", err),
  207. )
  208. }
  209. slog.Info("User authenticated successfully with webauthn",
  210. slog.Bool("authentication_successful", true),
  211. slog.String("client_ip", request.ClientIP(r)),
  212. slog.String("user_agent", r.UserAgent()),
  213. slog.Int64("user_id", user.ID),
  214. slog.String("username", user.Username),
  215. )
  216. if err := h.store.SetLastLogin(user.ID); err != nil {
  217. slog.Warn("Unable to update last login date",
  218. slog.Int64("user_id", user.ID),
  219. slog.Any("error", err),
  220. )
  221. }
  222. if err := authenticateWebSession(w, r, h.store, user); err != nil {
  223. response.JSONServerError(w, r, err)
  224. return
  225. }
  226. response.NoContent(w, r)
  227. }
  228. func (h *handler) renameCredential(w http.ResponseWriter, r *http.Request) {
  229. view := view.New(h.tpl, r)
  230. user, err := h.store.UserByID(request.UserID(r))
  231. if err != nil {
  232. response.HTMLServerError(w, r, err)
  233. return
  234. }
  235. credentialHandleEncoded := request.RouteStringParam(r, "credentialHandle")
  236. credentialHandle, err := hex.DecodeString(credentialHandleEncoded)
  237. if err != nil {
  238. response.HTMLServerError(w, r, err)
  239. return
  240. }
  241. credUserID, credential, err := h.store.WebAuthnCredentialByHandle(credentialHandle)
  242. if err != nil {
  243. response.HTMLServerError(w, r, err)
  244. return
  245. }
  246. if credUserID != user.ID {
  247. response.HTMLForbidden(w, r)
  248. return
  249. }
  250. view.Set("form", form.WebauthnForm{Name: credential.Name})
  251. view.Set("cred", credential)
  252. view.Set("menu", "settings")
  253. view.Set("user", user)
  254. navMetadata, _ := h.store.GetNavMetadata(user.ID)
  255. view.Set("countUnread", navMetadata.CountUnread)
  256. view.Set("countErrorFeeds", navMetadata.CountErrorFeeds)
  257. response.HTML(w, r, view.Render("webauthn_rename"))
  258. }
  259. func (h *handler) saveCredential(w http.ResponseWriter, r *http.Request) {
  260. user, err := h.store.UserByID(request.UserID(r))
  261. if err != nil {
  262. response.HTMLServerError(w, r, err)
  263. return
  264. }
  265. credentialHandleEncoded := request.RouteStringParam(r, "credentialHandle")
  266. credentialHandle, err := hex.DecodeString(credentialHandleEncoded)
  267. if err != nil {
  268. response.HTMLServerError(w, r, err)
  269. return
  270. }
  271. credUserID, credential, err := h.store.WebAuthnCredentialByHandle(credentialHandle)
  272. if err != nil {
  273. response.HTMLServerError(w, r, err)
  274. return
  275. }
  276. if credUserID != user.ID {
  277. response.HTMLForbidden(w, r)
  278. return
  279. }
  280. webauthnForm := form.NewWebauthnForm(r)
  281. if validationErr := webauthnForm.Validate(); validationErr != nil {
  282. v := view.New(h.tpl, r)
  283. v.Set("form", webauthnForm)
  284. v.Set("cred", credential)
  285. v.Set("menu", "settings")
  286. v.Set("user", user)
  287. v.Set("errorMessage", validationErr.Translate(request.WebSession(r).Language()))
  288. navMetadata, _ := h.store.GetNavMetadata(user.ID)
  289. v.Set("countUnread", navMetadata.CountUnread)
  290. v.Set("countErrorFeeds", navMetadata.CountErrorFeeds)
  291. response.HTML(w, r, v.Render("webauthn_rename"))
  292. return
  293. }
  294. rowsAffected, err := h.store.WebAuthnUpdateName(user.ID, credentialHandle, webauthnForm.Name)
  295. if err != nil {
  296. response.HTMLServerError(w, r, err)
  297. return
  298. }
  299. if rowsAffected == 0 {
  300. response.HTMLNotFound(w, r)
  301. return
  302. }
  303. response.HTMLRedirect(w, r, h.routePath("/settings"))
  304. }
  305. func (h *handler) deleteCredential(w http.ResponseWriter, r *http.Request) {
  306. credentialHandleEncoded := request.RouteStringParam(r, "credentialHandle")
  307. credentialHandle, err := hex.DecodeString(credentialHandleEncoded)
  308. if err != nil {
  309. response.JSONServerError(w, r, err)
  310. return
  311. }
  312. err = h.store.DeleteCredentialByHandle(request.UserID(r), credentialHandle)
  313. if err != nil {
  314. response.JSONServerError(w, r, err)
  315. return
  316. }
  317. response.NoContent(w, r)
  318. }
  319. func (h *handler) deleteAllCredentials(w http.ResponseWriter, r *http.Request) {
  320. err := h.store.DeleteAllWebAuthnCredentialsByUserID(request.UserID(r))
  321. if err != nil {
  322. response.JSONServerError(w, r, err)
  323. return
  324. }
  325. response.NoContent(w, r)
  326. }