webauthn.go 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  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. "bytes"
  6. "encoding/hex"
  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/cookie"
  16. "miniflux.app/v2/internal/http/request"
  17. "miniflux.app/v2/internal/http/response/html"
  18. "miniflux.app/v2/internal/http/response/json"
  19. "miniflux.app/v2/internal/http/route"
  20. "miniflux.app/v2/internal/model"
  21. "miniflux.app/v2/internal/ui/form"
  22. "miniflux.app/v2/internal/ui/session"
  23. "miniflux.app/v2/internal/ui/view"
  24. )
  25. type WebAuthnUser struct {
  26. User *model.User
  27. AuthnID []byte
  28. Credentials []model.WebAuthnCredential
  29. }
  30. func (u WebAuthnUser) WebAuthnID() []byte {
  31. return u.AuthnID
  32. }
  33. func (u WebAuthnUser) WebAuthnName() string {
  34. return u.User.Username
  35. }
  36. func (u WebAuthnUser) WebAuthnDisplayName() string {
  37. return u.User.Username
  38. }
  39. func (u WebAuthnUser) WebAuthnIcon() string {
  40. return ""
  41. }
  42. func (u WebAuthnUser) WebAuthnCredentials() []webauthn.Credential {
  43. creds := make([]webauthn.Credential, len(u.Credentials))
  44. for i, cred := range u.Credentials {
  45. creds[i] = cred.Credential
  46. }
  47. return creds
  48. }
  49. func newWebAuthn(h *handler) (*webauthn.WebAuthn, error) {
  50. url, err := url.Parse(config.Opts.BaseURL())
  51. if err != nil {
  52. return nil, err
  53. }
  54. return webauthn.New(&webauthn.Config{
  55. RPDisplayName: "Miniflux",
  56. RPID: url.Hostname(),
  57. RPOrigin: config.Opts.RootURL(),
  58. })
  59. }
  60. func (h *handler) beginRegistration(w http.ResponseWriter, r *http.Request) {
  61. web, err := newWebAuthn(h)
  62. if err != nil {
  63. json.ServerError(w, r, err)
  64. return
  65. }
  66. uid := request.UserID(r)
  67. if uid == 0 {
  68. json.Unauthorized(w, r)
  69. return
  70. }
  71. user, err := h.store.UserByID(uid)
  72. if err != nil {
  73. json.ServerError(w, r, err)
  74. return
  75. }
  76. var creds []model.WebAuthnCredential
  77. creds, err = h.store.WebAuthnCredentialsByUserID(user.ID)
  78. if err != nil {
  79. json.ServerError(w, r, err)
  80. return
  81. }
  82. credsDescriptors := make([]protocol.CredentialDescriptor, len(creds))
  83. for i, cred := range creds {
  84. credsDescriptors[i] = cred.Credential.Descriptor()
  85. }
  86. options, sessionData, err := web.BeginRegistration(
  87. WebAuthnUser{
  88. user,
  89. crypto.GenerateRandomBytes(32),
  90. nil,
  91. },
  92. webauthn.WithExclusions(credsDescriptors),
  93. )
  94. if err != nil {
  95. json.ServerError(w, r, err)
  96. return
  97. }
  98. s := session.New(h.store, request.SessionID(r))
  99. s.SetWebAuthnSessionData(&model.WebAuthnSession{SessionData: sessionData})
  100. json.OK(w, r, options)
  101. }
  102. func (h *handler) finishRegistration(w http.ResponseWriter, r *http.Request) {
  103. web, err := newWebAuthn(h)
  104. if err != nil {
  105. json.ServerError(w, r, err)
  106. return
  107. }
  108. uid := request.UserID(r)
  109. if uid == 0 {
  110. json.Unauthorized(w, r)
  111. return
  112. }
  113. user, err := h.store.UserByID(uid)
  114. if err != nil {
  115. json.ServerError(w, r, err)
  116. return
  117. }
  118. sessionData := request.WebAuthnSessionData(r)
  119. webAuthnUser := WebAuthnUser{user, sessionData.UserID, nil}
  120. cred, err := web.FinishRegistration(webAuthnUser, *sessionData.SessionData, r)
  121. if err != nil {
  122. json.ServerError(w, r, err)
  123. return
  124. }
  125. err = h.store.AddWebAuthnCredential(uid, sessionData.UserID, cred)
  126. if err != nil {
  127. json.ServerError(w, r, err)
  128. return
  129. }
  130. handleEncoded := model.WebAuthnCredential{Handle: sessionData.UserID}.HandleEncoded()
  131. redirect := route.Path(h.router, "webauthnRename", "credentialHandle", handleEncoded)
  132. json.OK(w, r, map[string]string{"redirect": redirect})
  133. }
  134. func (h *handler) beginLogin(w http.ResponseWriter, r *http.Request) {
  135. web, err := newWebAuthn(h)
  136. if err != nil {
  137. json.ServerError(w, r, err)
  138. return
  139. }
  140. var user *model.User
  141. username := request.QueryStringParam(r, "username", "")
  142. if username != "" {
  143. user, err = h.store.UserByUsername(username)
  144. if err != nil {
  145. json.Unauthorized(w, r)
  146. return
  147. }
  148. }
  149. var assertion *protocol.CredentialAssertion
  150. var sessionData *webauthn.SessionData
  151. if user != nil {
  152. creds, err := h.store.WebAuthnCredentialsByUserID(user.ID)
  153. if err != nil {
  154. json.ServerError(w, r, err)
  155. return
  156. }
  157. assertion, sessionData, err = web.BeginLogin(WebAuthnUser{user, nil, creds})
  158. if err != nil {
  159. json.ServerError(w, r, err)
  160. return
  161. }
  162. } else {
  163. assertion, sessionData, err = web.BeginDiscoverableLogin()
  164. if err != nil {
  165. json.ServerError(w, r, err)
  166. return
  167. }
  168. }
  169. s := session.New(h.store, request.SessionID(r))
  170. s.SetWebAuthnSessionData(&model.WebAuthnSession{SessionData: sessionData})
  171. json.OK(w, r, assertion)
  172. }
  173. func (h *handler) finishLogin(w http.ResponseWriter, r *http.Request) {
  174. web, err := newWebAuthn(h)
  175. if err != nil {
  176. json.ServerError(w, r, err)
  177. return
  178. }
  179. parsedResponse, err := protocol.ParseCredentialRequestResponseBody(r.Body)
  180. if err != nil {
  181. json.ServerError(w, r, err)
  182. return
  183. }
  184. sessionData := request.WebAuthnSessionData(r)
  185. var user *model.User
  186. username := request.QueryStringParam(r, "username", "")
  187. if username != "" {
  188. user, err = h.store.UserByUsername(username)
  189. if err != nil {
  190. json.Unauthorized(w, r)
  191. return
  192. }
  193. }
  194. var cred *model.WebAuthnCredential
  195. if user != nil {
  196. creds, err := h.store.WebAuthnCredentialsByUserID(user.ID)
  197. if err != nil {
  198. json.ServerError(w, r, err)
  199. return
  200. }
  201. sessionData.SessionData.UserID = parsedResponse.Response.UserHandle
  202. credCredential, err := web.ValidateLogin(WebAuthnUser{user, parsedResponse.Response.UserHandle, creds}, *sessionData.SessionData, parsedResponse)
  203. if err != nil {
  204. json.Unauthorized(w, r)
  205. return
  206. }
  207. for _, credTest := range creds {
  208. if bytes.Equal(credCredential.ID, credTest.Credential.ID) {
  209. cred = &credTest
  210. }
  211. }
  212. if cred == nil {
  213. json.ServerError(w, r, fmt.Errorf("no matching credential for %v", credCredential))
  214. return
  215. }
  216. } else {
  217. userByHandle := func(rawID, userHandle []byte) (webauthn.User, error) {
  218. var uid int64
  219. uid, cred, err = h.store.WebAuthnCredentialByHandle(userHandle)
  220. if err != nil {
  221. return nil, err
  222. }
  223. if uid == 0 {
  224. return nil, fmt.Errorf("no user found for handle %x", userHandle)
  225. }
  226. user, err = h.store.UserByID(uid)
  227. if err != nil {
  228. return nil, err
  229. }
  230. if user == nil {
  231. return nil, fmt.Errorf("no user found for handle %x", userHandle)
  232. }
  233. return WebAuthnUser{user, userHandle, []model.WebAuthnCredential{*cred}}, nil
  234. }
  235. _, err = web.ValidateDiscoverableLogin(userByHandle, *sessionData.SessionData, parsedResponse)
  236. if err != nil {
  237. json.Unauthorized(w, r)
  238. return
  239. }
  240. }
  241. sessionToken, _, err := h.store.CreateUserSessionFromUsername(user.Username, r.UserAgent(), request.ClientIP(r))
  242. if err != nil {
  243. json.ServerError(w, r, err)
  244. return
  245. }
  246. h.store.WebAuthnSaveLogin(cred.Handle)
  247. slog.Info("User authenticated successfully with webauthn",
  248. slog.Bool("authentication_successful", true),
  249. slog.String("client_ip", request.ClientIP(r)),
  250. slog.String("user_agent", r.UserAgent()),
  251. slog.Int64("user_id", user.ID),
  252. slog.String("username", user.Username),
  253. )
  254. h.store.SetLastLogin(user.ID)
  255. sess := session.New(h.store, request.SessionID(r))
  256. sess.SetLanguage(user.Language)
  257. sess.SetTheme(user.Theme)
  258. http.SetCookie(w, cookie.New(
  259. cookie.CookieUserSessionID,
  260. sessionToken,
  261. config.Opts.HTTPS,
  262. config.Opts.BasePath(),
  263. ))
  264. json.NoContent(w, r)
  265. }
  266. func (h *handler) renameCredential(w http.ResponseWriter, r *http.Request) {
  267. sess := session.New(h.store, request.SessionID(r))
  268. view := view.New(h.tpl, r, sess)
  269. user, err := h.store.UserByID(request.UserID(r))
  270. if err != nil {
  271. html.ServerError(w, r, err)
  272. return
  273. }
  274. credentialHandleEncoded := request.RouteStringParam(r, "credentialHandle")
  275. credentialHandle, err := hex.DecodeString(credentialHandleEncoded)
  276. if err != nil {
  277. html.ServerError(w, r, err)
  278. return
  279. }
  280. cred_uid, cred, err := h.store.WebAuthnCredentialByHandle(credentialHandle)
  281. if err != nil {
  282. html.ServerError(w, r, err)
  283. return
  284. }
  285. if cred_uid != user.ID {
  286. html.Forbidden(w, r)
  287. return
  288. }
  289. webauthnForm := form.WebauthnForm{Name: cred.Name}
  290. view.Set("form", webauthnForm)
  291. view.Set("cred", cred)
  292. view.Set("menu", "settings")
  293. view.Set("user", user)
  294. view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
  295. view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
  296. html.OK(w, r, view.Render("webauthn_rename"))
  297. }
  298. func (h *handler) saveCredential(w http.ResponseWriter, r *http.Request) {
  299. _, err := h.store.UserByID(request.UserID(r))
  300. if err != nil {
  301. html.ServerError(w, r, err)
  302. return
  303. }
  304. credentialHandleEncoded := request.RouteStringParam(r, "credentialHandle")
  305. credentialHandle, err := hex.DecodeString(credentialHandleEncoded)
  306. if err != nil {
  307. html.ServerError(w, r, err)
  308. return
  309. }
  310. newName := r.FormValue("name")
  311. err = h.store.WebAuthnUpdateName(credentialHandle, newName)
  312. if err != nil {
  313. html.ServerError(w, r, err)
  314. return
  315. }
  316. html.Redirect(w, r, route.Path(h.router, "settings"))
  317. }
  318. func (h *handler) deleteCredential(w http.ResponseWriter, r *http.Request) {
  319. uid := request.UserID(r)
  320. if uid == 0 {
  321. json.Unauthorized(w, r)
  322. return
  323. }
  324. credentialHandleEncoded := request.RouteStringParam(r, "credentialHandle")
  325. credentialHandle, err := hex.DecodeString(credentialHandleEncoded)
  326. if err != nil {
  327. json.ServerError(w, r, err)
  328. return
  329. }
  330. err = h.store.DeleteCredentialByHandle(uid, []byte(credentialHandle))
  331. if err != nil {
  332. json.ServerError(w, r, err)
  333. return
  334. }
  335. json.NoContent(w, r)
  336. }
  337. func (h *handler) deleteAllCredentials(w http.ResponseWriter, r *http.Request) {
  338. err := h.store.DeleteAllWebAuthnCredentialsByUserID(request.UserID(r))
  339. if err != nil {
  340. json.ServerError(w, r, err)
  341. return
  342. }
  343. json.NoContent(w, r)
  344. }