/auth/nonce/nonce.go

https://github.com/target/goalert · Go · 105 lines · 78 code · 14 blank · 13 comment · 14 complexity · f10efd90601458287babc44181ee8b2b MD5 · raw file

  1. package nonce
  2. import (
  3. "context"
  4. "database/sql"
  5. "github.com/target/goalert/util"
  6. "github.com/target/goalert/util/log"
  7. "time"
  8. "github.com/pkg/errors"
  9. uuid "github.com/satori/go.uuid"
  10. )
  11. // Store allows generating and consuming nonce values.
  12. type Store interface {
  13. New() [16]byte
  14. Consume(context.Context, [16]byte) (bool, error)
  15. Shutdown(context.Context) error
  16. }
  17. // DB implements the Store interface using postgres as it's backend.
  18. type DB struct {
  19. db *sql.DB
  20. shutdown chan context.Context
  21. consume *sql.Stmt
  22. cleanup *sql.Stmt
  23. }
  24. // NewDB prepares a new DB instance for the given sql.DB.
  25. func NewDB(ctx context.Context, db *sql.DB) (*DB, error) {
  26. p := &util.Prepare{DB: db, Ctx: ctx}
  27. d := &DB{
  28. db: db,
  29. shutdown: make(chan context.Context),
  30. consume: p.P(`
  31. insert into auth_nonce (id)
  32. values ($1)
  33. on conflict do nothing
  34. `),
  35. cleanup: p.P(`
  36. delete from auth_nonce
  37. where created_at < now() - '1 week'::interval
  38. `),
  39. }
  40. if p.Err != nil {
  41. return nil, p.Err
  42. }
  43. go d.loop()
  44. return d, nil
  45. }
  46. func (db *DB) loop() {
  47. defer close(db.shutdown)
  48. t := time.NewTicker(time.Hour * 24)
  49. defer t.Stop()
  50. for {
  51. select {
  52. case <-t.C:
  53. _, err := db.cleanup.ExecContext(context.Background())
  54. if err != nil {
  55. log.Log(context.Background(), errors.Wrap(err, "cleanup old nonce values"))
  56. }
  57. case <-db.shutdown:
  58. return
  59. }
  60. }
  61. }
  62. // Shutdown allows gracefully shutting down the nonce store.
  63. func (db *DB) Shutdown(ctx context.Context) error {
  64. if db == nil {
  65. return nil
  66. }
  67. db.shutdown <- ctx
  68. // wait for it to complete
  69. <-db.shutdown
  70. return nil
  71. }
  72. // New will generate a new cryptographically random nonce value.
  73. func (db *DB) New() (id [16]byte) {
  74. copy(id[:], uuid.NewV4().Bytes())
  75. return id
  76. }
  77. // Consume will record the use of a nonce value.
  78. //
  79. // An error is returned if it is not possible to validate the nonce value.
  80. // Otherwise true/false is returned to indicate if the id is valid.
  81. //
  82. // The first call to Consume for a given ID will return true, subsequent calls
  83. // for the same ID will return false.
  84. func (db *DB) Consume(ctx context.Context, id [16]byte) (bool, error) {
  85. res, err := db.consume.ExecContext(ctx, uuid.UUID(id).String())
  86. if err != nil {
  87. return false, err
  88. }
  89. n, _ := res.RowsAffected()
  90. return n == 1, nil
  91. }