mfa.go 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. // Copyright (C) 2019-2023 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 provides supports for Multi-Factor authentication modules
  15. package mfa
  16. import (
  17. "fmt"
  18. "time"
  19. )
  20. var (
  21. totpConfigs []*TOTPConfig
  22. serviceStatus ServiceStatus
  23. )
  24. // ServiceStatus defines the service status
  25. type ServiceStatus struct {
  26. IsActive bool `json:"is_active"`
  27. TOTPConfigs []TOTPConfig `json:"totp_configs"`
  28. }
  29. // GetStatus returns the service status
  30. func GetStatus() ServiceStatus {
  31. return serviceStatus
  32. }
  33. // Config defines configuration parameters for Multi-Factor authentication modules
  34. type Config struct {
  35. // Time-based one time passwords configurations
  36. TOTP []TOTPConfig `json:"totp" mapstructure:"totp"`
  37. }
  38. // Initialize configures the MFA support
  39. func (c *Config) Initialize() error {
  40. totpConfigs = nil
  41. serviceStatus.IsActive = false
  42. serviceStatus.TOTPConfigs = nil
  43. totp := make(map[string]bool)
  44. for _, totpConfig := range c.TOTP {
  45. totpConfig := totpConfig //pin
  46. if err := totpConfig.validate(); err != nil {
  47. totpConfigs = nil
  48. return fmt.Errorf("invalid TOTP config %+v: %v", totpConfig, err)
  49. }
  50. if _, ok := totp[totpConfig.Name]; ok {
  51. totpConfigs = nil
  52. return fmt.Errorf("totp: duplicate configuration name %q", totpConfig.Name)
  53. }
  54. totp[totpConfig.Name] = true
  55. totpConfigs = append(totpConfigs, &totpConfig)
  56. serviceStatus.IsActive = true
  57. serviceStatus.TOTPConfigs = append(serviceStatus.TOTPConfigs, totpConfig)
  58. }
  59. startCleanupTicker(2 * time.Minute)
  60. return nil
  61. }
  62. // GetAvailableTOTPConfigs returns the available TOTP configs
  63. func GetAvailableTOTPConfigs() []*TOTPConfig {
  64. return totpConfigs
  65. }
  66. // GetAvailableTOTPConfigNames returns the available TOTP config names
  67. func GetAvailableTOTPConfigNames() []string {
  68. var result []string
  69. for _, c := range totpConfigs {
  70. result = append(result, c.Name)
  71. }
  72. return result
  73. }
  74. // ValidateTOTPPasscode validates a TOTP passcode using the given secret and configName
  75. func ValidateTOTPPasscode(configName, passcode, secret string) (bool, error) {
  76. for _, config := range totpConfigs {
  77. if config.Name == configName {
  78. return config.validatePasscode(passcode, secret)
  79. }
  80. }
  81. return false, fmt.Errorf("totp: no configuration %q", configName)
  82. }
  83. // GenerateTOTPSecret generates a new TOTP secret and QR code for the given username
  84. // using the configuration with configName
  85. func GenerateTOTPSecret(configName, username string) (string, string, string, []byte, error) {
  86. for _, config := range totpConfigs {
  87. if config.Name == configName {
  88. issuer, secret, qrCode, err := config.generate(username, 200, 200)
  89. return configName, issuer, secret, qrCode, err
  90. }
  91. }
  92. return "", "", "", nil, fmt.Errorf("totp: no configuration %q", configName)
  93. }
  94. // the ticker cannot be started/stopped from multiple goroutines
  95. func startCleanupTicker(duration time.Duration) {
  96. stopCleanupTicker()
  97. cleanupTicker = time.NewTicker(duration)
  98. cleanupDone = make(chan bool)
  99. go func() {
  100. for {
  101. select {
  102. case <-cleanupDone:
  103. return
  104. case <-cleanupTicker.C:
  105. cleanupUsedPasscodes()
  106. }
  107. }
  108. }()
  109. }
  110. func stopCleanupTicker() {
  111. if cleanupTicker != nil {
  112. cleanupTicker.Stop()
  113. cleanupDone <- true
  114. cleanupTicker = nil
  115. }
  116. }