two_factors_test.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. package database
  2. import (
  3. "context"
  4. "testing"
  5. "time"
  6. "github.com/stretchr/testify/assert"
  7. "github.com/stretchr/testify/require"
  8. "gorm.io/gorm"
  9. "gogs.io/gogs/internal/errutil"
  10. )
  11. func TestTwoFactor_BeforeCreate(t *testing.T) {
  12. now := time.Now()
  13. db := &gorm.DB{
  14. Config: &gorm.Config{
  15. SkipDefaultTransaction: true,
  16. NowFunc: func() time.Time {
  17. return now
  18. },
  19. },
  20. }
  21. t.Run("CreatedUnix has been set", func(t *testing.T) {
  22. tf := &TwoFactor{
  23. CreatedUnix: 1,
  24. }
  25. _ = tf.BeforeCreate(db)
  26. assert.Equal(t, int64(1), tf.CreatedUnix)
  27. })
  28. t.Run("CreatedUnix has not been set", func(t *testing.T) {
  29. tf := &TwoFactor{}
  30. _ = tf.BeforeCreate(db)
  31. assert.Equal(t, db.NowFunc().Unix(), tf.CreatedUnix)
  32. })
  33. }
  34. func TestTwoFactor_AfterFind(t *testing.T) {
  35. now := time.Now()
  36. db := &gorm.DB{
  37. Config: &gorm.Config{
  38. SkipDefaultTransaction: true,
  39. NowFunc: func() time.Time {
  40. return now
  41. },
  42. },
  43. }
  44. tf := &TwoFactor{
  45. CreatedUnix: now.Unix(),
  46. }
  47. _ = tf.AfterFind(db)
  48. assert.Equal(t, tf.CreatedUnix, tf.Created.Unix())
  49. }
  50. func TestTwoFactors(t *testing.T) {
  51. if testing.Short() {
  52. t.Skip()
  53. }
  54. t.Parallel()
  55. ctx := context.Background()
  56. s := &TwoFactorsStore{
  57. db: newTestDB(t, "TwoFactorsStore"),
  58. }
  59. for _, tc := range []struct {
  60. name string
  61. test func(t *testing.T, ctx context.Context, s *TwoFactorsStore)
  62. }{
  63. {"Create", twoFactorsCreate},
  64. {"GetByUserID", twoFactorsGetByUserID},
  65. {"IsEnabled", twoFactorsIsEnabled},
  66. {"UseRecoveryCode", twoFactorsUseRecoveryCode},
  67. } {
  68. t.Run(tc.name, func(t *testing.T) {
  69. t.Cleanup(func() {
  70. err := clearTables(t, s.db)
  71. require.NoError(t, err)
  72. })
  73. tc.test(t, ctx, s)
  74. })
  75. if t.Failed() {
  76. break
  77. }
  78. }
  79. }
  80. func twoFactorsCreate(t *testing.T, ctx context.Context, s *TwoFactorsStore) {
  81. // Create a 2FA token
  82. err := s.Create(ctx, 1, "secure-key", "secure-secret")
  83. require.NoError(t, err)
  84. // Get it back and check the Created field
  85. tf, err := s.GetByUserID(ctx, 1)
  86. require.NoError(t, err)
  87. assert.Equal(t, s.db.NowFunc().Format(time.RFC3339), tf.Created.UTC().Format(time.RFC3339))
  88. // Verify there are 10 recover codes generated
  89. var count int64
  90. err = s.db.Model(new(TwoFactorRecoveryCode)).Count(&count).Error
  91. require.NoError(t, err)
  92. assert.Equal(t, int64(10), count)
  93. }
  94. func twoFactorsGetByUserID(t *testing.T, ctx context.Context, s *TwoFactorsStore) {
  95. // Create a 2FA token for user 1
  96. err := s.Create(ctx, 1, "secure-key", "secure-secret")
  97. require.NoError(t, err)
  98. // We should be able to get it back
  99. _, err = s.GetByUserID(ctx, 1)
  100. require.NoError(t, err)
  101. // Try to get a non-existent 2FA token
  102. _, err = s.GetByUserID(ctx, 2)
  103. wantErr := ErrTwoFactorNotFound{args: errutil.Args{"userID": int64(2)}}
  104. assert.Equal(t, wantErr, err)
  105. }
  106. func twoFactorsIsEnabled(t *testing.T, ctx context.Context, s *TwoFactorsStore) {
  107. // Create a 2FA token for user 1
  108. err := s.Create(ctx, 1, "secure-key", "secure-secret")
  109. require.NoError(t, err)
  110. assert.True(t, s.IsEnabled(ctx, 1))
  111. assert.False(t, s.IsEnabled(ctx, 2))
  112. }
  113. func twoFactorsUseRecoveryCode(t *testing.T, ctx context.Context, s *TwoFactorsStore) {
  114. // Create 2FA tokens for two users
  115. err := s.Create(ctx, 1, "secure-key", "secure-secret")
  116. require.NoError(t, err)
  117. err = s.Create(ctx, 2, "secure-key", "secure-secret")
  118. require.NoError(t, err)
  119. // Get recovery codes for both users
  120. var user1Codes []TwoFactorRecoveryCode
  121. err = s.db.Where("user_id = ?", 1).Find(&user1Codes).Error
  122. require.NoError(t, err)
  123. require.NotEmpty(t, user1Codes)
  124. var user2Codes []TwoFactorRecoveryCode
  125. err = s.db.Where("user_id = ?", 2).Find(&user2Codes).Error
  126. require.NoError(t, err)
  127. require.NotEmpty(t, user2Codes)
  128. // User 1 should be able to use their own recovery code
  129. err = s.UseRecoveryCode(ctx, 1, user1Codes[0].Code)
  130. require.NoError(t, err)
  131. // Verify the code is now marked as used
  132. var usedCode TwoFactorRecoveryCode
  133. err = s.db.Where("id = ?", user1Codes[0].ID).First(&usedCode).Error
  134. require.NoError(t, err)
  135. assert.True(t, usedCode.IsUsed)
  136. // User 1 should NOT be able to use user 2's recovery code
  137. // This is the key security test - recovery codes must be scoped by user
  138. err = s.UseRecoveryCode(ctx, 1, user2Codes[0].Code)
  139. assert.True(t, IsTwoFactorRecoveryCodeNotFound(err), "expected recovery code not found error when using another user's code")
  140. // User 2's code should still be unused
  141. var user2Code TwoFactorRecoveryCode
  142. err = s.db.Where("id = ?", user2Codes[0].ID).First(&user2Code).Error
  143. require.NoError(t, err)
  144. assert.False(t, user2Code.IsUsed, "user 2's recovery code should not be marked as used")
  145. // User 2 should be able to use their own code
  146. err = s.UseRecoveryCode(ctx, 2, user2Codes[0].Code)
  147. require.NoError(t, err)
  148. // Using an already-used code should fail
  149. err = s.UseRecoveryCode(ctx, 1, user1Codes[0].Code)
  150. assert.True(t, IsTwoFactorRecoveryCodeNotFound(err), "expected error when reusing a recovery code")
  151. // Using a non-existent code should fail
  152. err = s.UseRecoveryCode(ctx, 1, "invalid-code")
  153. assert.True(t, IsTwoFactorRecoveryCodeNotFound(err), "expected error for invalid recovery code")
  154. }