mirror of
https://github.com/drakkan/sftpgo.git
synced 2025-12-08 07:10:56 +03:00
add builtin two-factor auth support
The builtin two-factor authentication is based on time-based one time passwords (RFC 6238) which works with Authy, Google Authenticator and other compatible apps.
This commit is contained in:
@@ -21,6 +21,8 @@ import (
|
||||
|
||||
ftpserver "github.com/fclairamb/ftpserverlib"
|
||||
"github.com/jlaffaye/ftp"
|
||||
"github.com/pquerna/otp"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -32,6 +34,7 @@ import (
|
||||
"github.com/drakkan/sftpgo/v2/httpdtest"
|
||||
"github.com/drakkan/sftpgo/v2/kms"
|
||||
"github.com/drakkan/sftpgo/v2/logger"
|
||||
"github.com/drakkan/sftpgo/v2/mfa"
|
||||
"github.com/drakkan/sftpgo/v2/sdk"
|
||||
"github.com/drakkan/sftpgo/v2/sftpd"
|
||||
"github.com/drakkan/sftpgo/v2/vfs"
|
||||
@@ -305,6 +308,12 @@ func TestMain(m *testing.M) {
|
||||
logger.ErrorToConsole("error initializing kms: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
mfaConfig := config.GetMFAConfig()
|
||||
err = mfaConfig.Initialize()
|
||||
if err != nil {
|
||||
logger.ErrorToConsole("error initializing MFA: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
httpdConf := config.GetHTTPDConfig()
|
||||
httpdConf.Bindings[0].Port = 8079
|
||||
@@ -587,6 +596,50 @@ func TestBasicFTPHandling(t *testing.T) {
|
||||
50*time.Millisecond)
|
||||
}
|
||||
|
||||
func TestMultiFactorAuth(t *testing.T) {
|
||||
u := getTestUser()
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
||||
configName, _, secret, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
|
||||
assert.NoError(t, err)
|
||||
user.Password = defaultPassword
|
||||
user.Filters.TOTPConfig = sdk.TOTPConfig{
|
||||
Enabled: true,
|
||||
ConfigName: configName,
|
||||
Secret: kms.NewPlainSecret(secret),
|
||||
Protocols: []string{common.ProtocolFTP},
|
||||
}
|
||||
err = dataprovider.UpdateUser(&user)
|
||||
assert.NoError(t, err)
|
||||
|
||||
user.Password = defaultPassword
|
||||
_, err = getFTPClient(user, true, nil)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), dataprovider.ErrInvalidCredentials.Error())
|
||||
}
|
||||
passcode, err := generateTOTPPasscode(secret, otp.AlgorithmSHA1)
|
||||
assert.NoError(t, err)
|
||||
user.Password = defaultPassword + passcode
|
||||
client, err := getFTPClient(user, true, nil)
|
||||
if assert.NoError(t, err) {
|
||||
err = checkBasicFTP(client)
|
||||
assert.NoError(t, err)
|
||||
err := client.Quit()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
// reusing the same passcode should not work
|
||||
_, err = getFTPClient(user, true, nil)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), dataprovider.ErrInvalidCredentials.Error())
|
||||
}
|
||||
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLoginInvalidCredentials(t *testing.T) {
|
||||
u := getTestUser()
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
@@ -3096,3 +3149,12 @@ func writeCerts(certPath, keyPath, caCrtPath, caCRLPath string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateTOTPPasscode(secret string, algo otp.Algorithm) (string, error) {
|
||||
return totp.GenerateCodeCustom(secret, time.Now(), totp.ValidateOpts{
|
||||
Period: 30,
|
||||
Skew: 1,
|
||||
Digits: otp.DigitsSix,
|
||||
Algorithm: algo,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user