mfa.go 4.1 KB

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