mfa_test.go 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. // Copyright (C) 2019-2022 Nicola Murino
  2. //
  3. // This program is free software: you can redistribute it and/or modify
  4. // it under the terms of the GNU Affero General Public License as published
  5. // by the Free Software Foundation, version 3.
  6. //
  7. // This program is distributed in the hope that it will be useful,
  8. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. // GNU Affero General Public License for more details.
  11. //
  12. // You should have received a copy of the GNU Affero General Public License
  13. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  14. package mfa
  15. import (
  16. "testing"
  17. "time"
  18. "github.com/pquerna/otp"
  19. "github.com/pquerna/otp/totp"
  20. "github.com/stretchr/testify/assert"
  21. )
  22. func TestMFAConfig(t *testing.T) {
  23. config := Config{
  24. TOTP: []TOTPConfig{
  25. {},
  26. },
  27. }
  28. configName1 := "config1"
  29. configName2 := "config2"
  30. configName3 := "config3"
  31. err := config.Initialize()
  32. assert.Error(t, err)
  33. config.TOTP[0].Name = configName1
  34. err = config.Initialize()
  35. assert.Error(t, err)
  36. config.TOTP[0].Issuer = "issuer"
  37. err = config.Initialize()
  38. assert.Error(t, err)
  39. config.TOTP[0].Algo = TOTPAlgoSHA1
  40. err = config.Initialize()
  41. assert.NoError(t, err)
  42. config.TOTP = append(config.TOTP, TOTPConfig{
  43. Name: configName1,
  44. Issuer: "SFTPGo",
  45. Algo: TOTPAlgoSHA512,
  46. })
  47. err = config.Initialize()
  48. assert.Error(t, err)
  49. config.TOTP[1].Name = configName2
  50. err = config.Initialize()
  51. assert.NoError(t, err)
  52. assert.Len(t, GetAvailableTOTPConfigs(), 2)
  53. assert.Len(t, GetAvailableTOTPConfigNames(), 2)
  54. config.TOTP = append(config.TOTP, TOTPConfig{
  55. Name: configName3,
  56. Issuer: "SFTPGo",
  57. Algo: TOTPAlgoSHA256,
  58. })
  59. err = config.Initialize()
  60. assert.NoError(t, err)
  61. assert.Len(t, GetAvailableTOTPConfigs(), 3)
  62. if assert.Len(t, GetAvailableTOTPConfigNames(), 3) {
  63. assert.Contains(t, GetAvailableTOTPConfigNames(), configName1)
  64. assert.Contains(t, GetAvailableTOTPConfigNames(), configName2)
  65. assert.Contains(t, GetAvailableTOTPConfigNames(), configName3)
  66. }
  67. status := GetStatus()
  68. assert.True(t, status.IsActive)
  69. if assert.Len(t, status.TOTPConfigs, 3) {
  70. assert.Equal(t, configName1, status.TOTPConfigs[0].Name)
  71. assert.Equal(t, configName2, status.TOTPConfigs[1].Name)
  72. assert.Equal(t, configName3, status.TOTPConfigs[2].Name)
  73. }
  74. // now generate some secrets and validate some passcodes
  75. _, _, _, _, err = GenerateTOTPSecret("", "") //nolint:dogsled
  76. assert.Error(t, err)
  77. match, err := ValidateTOTPPasscode("", "", "")
  78. assert.Error(t, err)
  79. assert.False(t, match)
  80. cfgName, _, secret, _, err := GenerateTOTPSecret(configName1, "user1")
  81. assert.NoError(t, err)
  82. assert.NotEmpty(t, secret)
  83. assert.Equal(t, configName1, cfgName)
  84. passcode, err := generatePasscode(secret, otp.AlgorithmSHA1)
  85. assert.NoError(t, err)
  86. match, err = ValidateTOTPPasscode(configName1, passcode, secret)
  87. assert.NoError(t, err)
  88. assert.True(t, match)
  89. match, err = ValidateTOTPPasscode(configName1, passcode, secret)
  90. assert.ErrorIs(t, err, errPasscodeUsed)
  91. assert.False(t, match)
  92. passcode, err = generatePasscode(secret, otp.AlgorithmSHA256)
  93. assert.NoError(t, err)
  94. // config1 uses sha1 algo
  95. match, err = ValidateTOTPPasscode(configName1, passcode, secret)
  96. assert.NoError(t, err)
  97. assert.False(t, match)
  98. // config3 use the expected algo
  99. match, err = ValidateTOTPPasscode(configName3, passcode, secret)
  100. assert.NoError(t, err)
  101. assert.True(t, match)
  102. stopCleanupTicker()
  103. }
  104. func TestCleanupPasscodes(t *testing.T) {
  105. usedPasscodes.Store("key", time.Now().Add(-24*time.Hour).UTC())
  106. startCleanupTicker(30 * time.Millisecond)
  107. assert.Eventually(t, func() bool {
  108. _, ok := usedPasscodes.Load("key")
  109. return !ok
  110. }, 1000*time.Millisecond, 100*time.Millisecond)
  111. stopCleanupTicker()
  112. }
  113. func TestTOTPGenerateErrors(t *testing.T) {
  114. config := TOTPConfig{
  115. Name: "name",
  116. Issuer: "",
  117. algo: otp.AlgorithmSHA1,
  118. }
  119. // issuer cannot be empty
  120. _, _, _, err := config.generate("username", 200, 200) //nolint:dogsled
  121. assert.Error(t, err)
  122. config.Issuer = "issuer"
  123. // we cannot encode an image smaller than 45x45
  124. _, _, _, err = config.generate("username", 30, 30) //nolint:dogsled
  125. assert.Error(t, err)
  126. }
  127. func generatePasscode(secret string, algo otp.Algorithm) (string, error) {
  128. return totp.GenerateCodeCustom(secret, time.Now(), totp.ValidateOpts{
  129. Period: 30,
  130. Skew: 1,
  131. Digits: otp.DigitsSix,
  132. Algorithm: algo,
  133. })
  134. }