webauthn.go 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
  2. // SPDX-License-Identifier: Apache-2.0
  3. package storage // import "miniflux.app/v2/internal/storage"
  4. import (
  5. "database/sql"
  6. "fmt"
  7. "log/slog"
  8. "github.com/go-webauthn/webauthn/webauthn"
  9. "miniflux.app/v2/internal/model"
  10. )
  11. // AddWebAuthnCredential handles storage of webauthn credentials.
  12. func (s *Storage) AddWebAuthnCredential(userID int64, handle []byte, credential *webauthn.Credential) error {
  13. query := `
  14. INSERT INTO webauthn_credentials
  15. (handle, cred_id, user_id, public_key, attestation_type, aaguid, sign_count, clone_warning, backup_eligible, backup_state)
  16. VALUES
  17. ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
  18. `
  19. _, err := s.db.Exec(
  20. query,
  21. handle,
  22. credential.ID,
  23. userID,
  24. credential.PublicKey,
  25. credential.AttestationType,
  26. credential.Authenticator.AAGUID,
  27. credential.Authenticator.SignCount,
  28. credential.Authenticator.CloneWarning,
  29. credential.Flags.BackupEligible,
  30. credential.Flags.BackupState,
  31. )
  32. return err
  33. }
  34. func (s *Storage) WebAuthnCredentialByHandle(handle []byte) (int64, *model.WebAuthnCredential, error) {
  35. var credential model.WebAuthnCredential
  36. var userID int64
  37. var backupEligible sql.NullBool
  38. query := `
  39. SELECT
  40. user_id,
  41. cred_id,
  42. public_key,
  43. attestation_type,
  44. aaguid,
  45. sign_count,
  46. clone_warning,
  47. added_on,
  48. last_seen_on,
  49. name,
  50. backup_eligible,
  51. backup_state
  52. FROM
  53. webauthn_credentials
  54. WHERE
  55. handle = $1
  56. `
  57. err := s.db.
  58. QueryRow(query, handle).
  59. Scan(
  60. &userID,
  61. &credential.Credential.ID,
  62. &credential.Credential.PublicKey,
  63. &credential.Credential.AttestationType,
  64. &credential.Credential.Authenticator.AAGUID,
  65. &credential.Credential.Authenticator.SignCount,
  66. &credential.Credential.Authenticator.CloneWarning,
  67. &credential.AddedOn,
  68. &credential.LastSeenOn,
  69. &credential.Name,
  70. &backupEligible,
  71. &credential.Credential.Flags.BackupState,
  72. )
  73. if err != nil {
  74. return 0, nil, err
  75. }
  76. if backupEligible.Valid {
  77. credential.Credential.Flags.BackupEligible = backupEligible.Bool
  78. credential.BackupEligibleKnown = true
  79. }
  80. credential.Handle = handle
  81. return userID, &credential, err
  82. }
  83. func (s *Storage) WebAuthnCredentialsByUserID(userID int64) ([]model.WebAuthnCredential, error) {
  84. query := `
  85. SELECT
  86. handle,
  87. cred_id,
  88. public_key,
  89. attestation_type,
  90. aaguid,
  91. sign_count,
  92. clone_warning,
  93. name,
  94. added_on,
  95. last_seen_on,
  96. backup_eligible,
  97. backup_state
  98. FROM
  99. webauthn_credentials
  100. WHERE
  101. user_id = $1
  102. `
  103. rows, err := s.db.Query(query, userID)
  104. if err != nil {
  105. return nil, err
  106. }
  107. defer rows.Close()
  108. var creds []model.WebAuthnCredential
  109. for rows.Next() {
  110. var cred model.WebAuthnCredential
  111. var backupEligible sql.NullBool
  112. err = rows.Scan(
  113. &cred.Handle,
  114. &cred.Credential.ID,
  115. &cred.Credential.PublicKey,
  116. &cred.Credential.AttestationType,
  117. &cred.Credential.Authenticator.AAGUID,
  118. &cred.Credential.Authenticator.SignCount,
  119. &cred.Credential.Authenticator.CloneWarning,
  120. &cred.Name,
  121. &cred.AddedOn,
  122. &cred.LastSeenOn,
  123. &backupEligible,
  124. &cred.Credential.Flags.BackupState,
  125. )
  126. if err != nil {
  127. return nil, err
  128. }
  129. if backupEligible.Valid {
  130. cred.Credential.Flags.BackupEligible = backupEligible.Bool
  131. cred.BackupEligibleKnown = true
  132. }
  133. creds = append(creds, cred)
  134. }
  135. return creds, nil
  136. }
  137. // WebAuthnSaveLogin writes back the per-assertion fields (sign count, clone warning, backup state, BE) the WebAuthn spec requires after every successful login.
  138. func (s *Storage) WebAuthnSaveLogin(handle []byte, credential *webauthn.Credential) error {
  139. query := `
  140. UPDATE webauthn_credentials
  141. SET last_seen_on = NOW(),
  142. sign_count = $1,
  143. clone_warning = $2,
  144. backup_eligible = $3,
  145. backup_state = $4
  146. WHERE handle = $5
  147. `
  148. _, err := s.db.Exec(
  149. query,
  150. credential.Authenticator.SignCount,
  151. credential.Authenticator.CloneWarning,
  152. credential.Flags.BackupEligible,
  153. credential.Flags.BackupState,
  154. handle,
  155. )
  156. if err != nil {
  157. return fmt.Errorf(`store: unable to update webauthn credential after login: %v`, err)
  158. }
  159. return nil
  160. }
  161. func (s *Storage) WebAuthnUpdateName(userID int64, handle []byte, name string) (int64, error) {
  162. query := "UPDATE webauthn_credentials SET name=$1 WHERE handle=$2 AND user_id=$3"
  163. result, err := s.db.Exec(query, name, handle, userID)
  164. if err != nil {
  165. return 0, fmt.Errorf(`store: unable to update name for webauthn credential: %v`, err)
  166. }
  167. rows, err := result.RowsAffected()
  168. if err != nil {
  169. return 0, fmt.Errorf(`store: unable to update name for webauthn credential: %v`, err)
  170. }
  171. return rows, nil
  172. }
  173. func (s *Storage) CountWebAuthnCredentialsByUserID(userID int64) int {
  174. var count int
  175. query := "SELECT COUNT(*) FROM webauthn_credentials WHERE user_id = $1"
  176. err := s.db.QueryRow(query, userID).Scan(&count)
  177. if err != nil {
  178. slog.Error("store: unable to count webauthn certs for user",
  179. slog.Int64("user_id", userID),
  180. slog.Any("error", err),
  181. )
  182. return 0
  183. }
  184. return count
  185. }
  186. func (s *Storage) DeleteCredentialByHandle(userID int64, handle []byte) error {
  187. query := "DELETE FROM webauthn_credentials WHERE user_id = $1 AND handle = $2"
  188. _, err := s.db.Exec(query, userID, handle)
  189. return err
  190. }
  191. func (s *Storage) DeleteAllWebAuthnCredentialsByUserID(userID int64) error {
  192. query := "DELETE FROM webauthn_credentials WHERE user_id = $1"
  193. _, err := s.db.Exec(query, userID)
  194. return err
  195. }