WebClient: remove data schema usage from mfa page

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2023-11-18 20:06:31 +01:00
parent 59bdd4bc4e
commit ac309cf9a3
No known key found for this signature in database
GPG key ID: 935D2952DEC4EECF
15 changed files with 168 additions and 87 deletions

View file

@ -7767,18 +7767,18 @@ func TestBuiltinKeyboardInteractiveAuthentication(t *testing.T) {
assert.NoError(t, err)
}
// add multi-factor authentication
configName, _, secret, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
configName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
assert.NoError(t, err)
user.Password = defaultPassword
user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{
Enabled: true,
ConfigName: configName,
Secret: kms.NewPlainSecret(secret),
Secret: kms.NewPlainSecret(key.Secret()),
Protocols: []string{common.ProtocolSSH},
}
err = dataprovider.UpdateUser(&user, "", "", "")
assert.NoError(t, err)
passcode, err := generateTOTPPasscode(secret, otp.AlgorithmSHA1)
passcode, err := generateTOTPPasscode(key.Secret(), otp.AlgorithmSHA1)
assert.NoError(t, err)
passwordAsked := false
passcodeAsked := false

View file

@ -116,9 +116,6 @@ type Binding struct {
func (b *Binding) setCiphers() {
b.ciphers = util.GetTLSCiphersFromNames(b.TLSCipherSuites)
if len(b.ciphers) == 0 {
b.ciphers = nil
}
}
func (b *Binding) isMutualTLSEnabled() bool {

View file

@ -1064,13 +1064,13 @@ func TestMultiFactorAuth(t *testing.T) {
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
configName, _, secret, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
configName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
assert.NoError(t, err)
user.Password = defaultPassword
user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{
Enabled: true,
ConfigName: configName,
Secret: kms.NewPlainSecret(secret),
Secret: kms.NewPlainSecret(key.Secret()),
Protocols: []string{common.ProtocolFTP},
}
err = dataprovider.UpdateUser(&user, "", "", "")
@ -1081,7 +1081,7 @@ func TestMultiFactorAuth(t *testing.T) {
if assert.Error(t, err) {
assert.Contains(t, err.Error(), dataprovider.ErrInvalidCredentials.Error())
}
passcode, err := generateTOTPPasscode(secret, otp.AlgorithmSHA1)
passcode, err := generateTOTPPasscode(key.Secret(), otp.AlgorithmSHA1)
assert.NoError(t, err)
user.Password = defaultPassword + passcode
client, err := getFTPClient(user, true, nil)
@ -1138,18 +1138,18 @@ func TestSecondFactorRequirement(t *testing.T) {
assert.Contains(t, err.Error(), "second factor authentication is not set")
}
configName, _, secret, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
configName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
assert.NoError(t, err)
user.Password = defaultPassword
user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{
Enabled: true,
ConfigName: configName,
Secret: kms.NewPlainSecret(secret),
Secret: kms.NewPlainSecret(key.Secret()),
Protocols: []string{common.ProtocolFTP},
}
err = dataprovider.UpdateUser(&user, "", "", "")
assert.NoError(t, err)
passcode, err := generateTOTPPasscode(secret, otp.AlgorithmSHA1)
passcode, err := generateTOTPPasscode(key.Secret(), otp.AlgorithmSHA1)
assert.NoError(t, err)
user.Password = defaultPassword + passcode
client, err := getFTPClient(user, true, nil)

View file

@ -1016,7 +1016,7 @@ func TestCiphers(t *testing.T) {
TLSCipherSuites: []string{},
}
b.setCiphers()
require.Nil(t, b.ciphers)
require.Equal(t, util.GetTLSCiphersFromNames(nil), b.ciphers)
b.TLSCipherSuites = []string{"TLS_AES_128_GCM_SHA256", "TLS_AES_256_GCM_SHA384"}
b.setCiphers()
require.Len(t, b.ciphers, 2)

View file

@ -15,9 +15,12 @@
package httpd
import (
"bytes"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"github.com/go-chi/render"
@ -40,6 +43,7 @@ type generateTOTPResponse struct {
ConfigName string `json:"config_name"`
Issuer string `json:"issuer"`
Secret string `json:"secret"`
URL string `json:"url"`
QRCode []byte `json:"qr_code"`
}
@ -79,19 +83,33 @@ func generateTOTPSecret(w http.ResponseWriter, r *http.Request) {
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
configName, issuer, secret, qrCode, err := mfa.GenerateTOTPSecret(req.ConfigName, accountName)
configName, key, qrCode, err := mfa.GenerateTOTPSecret(req.ConfigName, accountName)
if err != nil {
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
render.JSON(w, r, generateTOTPResponse{
ConfigName: configName,
Issuer: issuer,
Secret: secret,
Issuer: key.Issuer(),
Secret: key.Secret(),
URL: key.URL(),
QRCode: qrCode,
})
}
func getQRCode(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
img, err := mfa.GenerateQRCodeFromURL(r.URL.Query().Get("url"), 400, 400)
if err != nil {
sendAPIResponse(w, r, nil, "unable to generate qr code", http.StatusInternalServerError)
return
}
imgSize := int64(len(img))
w.Header().Set("Content-Length", strconv.FormatInt(imgSize, 10))
w.Header().Set("Content-Type", "image/png")
io.CopyN(w, bytes.NewBuffer(img), imgSize) //nolint:errcheck
}
func saveTOTPConfig(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
claims, err := getTokenClaims(r)

View file

@ -3016,14 +3016,14 @@ func TestPermMFADisabled(t *testing.T) {
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
configName, _, secret, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
configName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
assert.NoError(t, err)
token, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)
assert.NoError(t, err)
userTOTPConfig := dataprovider.UserTOTPConfig{
Enabled: true,
ConfigName: configName,
Secret: kms.NewPlainSecret(secret),
Secret: kms.NewPlainSecret(key.Secret()),
Protocols: []string{common.ProtocolSSH},
}
asJSON, err := json.Marshal(userTOTPConfig)
@ -3309,12 +3309,12 @@ func TestTwoFactorRequirements(t *testing.T) {
checkResponseCode(t, http.StatusForbidden, rr)
assert.Contains(t, rr.Body.String(), "Two-factor authentication requirements not met, please configure two-factor authentication for the following protocols")
configName, _, secret, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
configName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
assert.NoError(t, err)
userTOTPConfig := dataprovider.UserTOTPConfig{
Enabled: true,
ConfigName: configName,
Secret: kms.NewPlainSecret(secret),
Secret: kms.NewPlainSecret(key.Secret()),
Protocols: []string{common.ProtocolHTTP},
}
asJSON, err := json.Marshal(userTOTPConfig)
@ -3335,7 +3335,7 @@ func TestTwoFactorRequirements(t *testing.T) {
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
// now get new tokens and check that the two factor requirements are now met
passcode, err := generateTOTPPasscode(secret)
passcode, err := generateTOTPPasscode(key.Secret())
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%v%v", httpBaseURL, userTokenPath), nil)
assert.NoError(t, err)
@ -3371,14 +3371,14 @@ func TestLoginUserAPITOTP(t *testing.T) {
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
assert.NoError(t, err)
configName, _, secret, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
configName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
assert.NoError(t, err)
token, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)
assert.NoError(t, err)
userTOTPConfig := dataprovider.UserTOTPConfig{
Enabled: true,
ConfigName: configName,
Secret: kms.NewPlainSecret(secret),
Secret: kms.NewPlainSecret(key.Secret()),
Protocols: []string{common.ProtocolHTTP},
}
asJSON, err := json.Marshal(userTOTPConfig)
@ -3431,7 +3431,7 @@ func TestLoginUserAPITOTP(t *testing.T) {
err = resp.Body.Close()
assert.NoError(t, err)
passcode, err := generateTOTPPasscode(secret)
passcode, err := generateTOTPPasscode(key.Secret())
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%v%v", httpBaseURL, userTokenPath), nil)
assert.NoError(t, err)
@ -3471,14 +3471,14 @@ func TestLoginAdminAPITOTP(t *testing.T) {
admin, _, err := httpdtest.AddAdmin(admin, http.StatusCreated)
assert.NoError(t, err)
configName, _, secret, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], admin.Username)
configName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], admin.Username)
assert.NoError(t, err)
altToken, err := getJWTAPITokenFromTestServer(altAdminUsername, altAdminPassword)
assert.NoError(t, err)
adminTOTPConfig := dataprovider.AdminTOTPConfig{
Enabled: true,
ConfigName: configName,
Secret: kms.NewPlainSecret(secret),
Secret: kms.NewPlainSecret(key.Secret()),
}
asJSON, err := json.Marshal(adminTOTPConfig)
assert.NoError(t, err)
@ -3512,7 +3512,7 @@ func TestLoginAdminAPITOTP(t *testing.T) {
err = resp.Body.Close()
assert.NoError(t, err)
passcode, err := generateTOTPPasscode(secret)
passcode, err := generateTOTPPasscode(key.Secret())
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%v%v", httpBaseURL, tokenPath), nil)
assert.NoError(t, err)
@ -6266,12 +6266,12 @@ func TestAdminGenerateRecoveryCodesSaveError(t *testing.T) {
a.Username = "adMiN@example.com "
admin, _, err := httpdtest.AddAdmin(a, http.StatusCreated)
assert.NoError(t, err)
configName, _, secret, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], admin.Username)
configName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], admin.Username)
assert.NoError(t, err)
admin.Filters.TOTPConfig = dataprovider.AdminTOTPConfig{
Enabled: true,
ConfigName: configName,
Secret: kms.NewPlainSecret(secret),
Secret: kms.NewPlainSecret(key.Secret()),
}
admin.Password = defaultTokenAuthPass
err = dataprovider.UpdateAdmin(&admin, "", "", "")
@ -6280,7 +6280,7 @@ func TestAdminGenerateRecoveryCodesSaveError(t *testing.T) {
assert.NoError(t, err)
assert.True(t, admin.Filters.TOTPConfig.Enabled)
passcode, err := generateTOTPPasscode(secret)
passcode, err := generateTOTPPasscode(key.Secret())
assert.NoError(t, err)
adminAPIToken, err := getJWTAPITokenFromTestServerWithPasscode(a.Username, defaultTokenAuthPass, passcode)
assert.NoError(t, err)
@ -6332,12 +6332,12 @@ func TestNamingRules(t *testing.T) {
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
assert.Equal(t, "user@user.me", user.Username)
configName, _, secret, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
configName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
assert.NoError(t, err)
user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{
Enabled: true,
ConfigName: configName,
Secret: kms.NewPlainSecret(secret),
Secret: kms.NewPlainSecret(key.Secret()),
Protocols: []string{common.ProtocolSSH},
}
user.Password = u.Password
@ -6626,13 +6626,13 @@ func TestSaveErrors(t *testing.T) {
assert.NoError(t, err)
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
configName, _, secret, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
configName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
assert.NoError(t, err)
user.Password = u.Password
user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{
Enabled: true,
ConfigName: configName,
Secret: kms.NewPlainSecret(secret),
Secret: kms.NewPlainSecret(key.Secret()),
Protocols: []string{common.ProtocolSSH, common.ProtocolHTTP},
}
user.Filters.RecoveryCodes = recoveryCodes
@ -6654,7 +6654,7 @@ func TestSaveErrors(t *testing.T) {
admin.Filters.TOTPConfig = dataprovider.AdminTOTPConfig{
Enabled: true,
ConfigName: configName,
Secret: kms.NewPlainSecret(secret),
Secret: kms.NewPlainSecret(key.Secret()),
}
admin.Filters.RecoveryCodes = recoveryCodes
err = dataprovider.UpdateAdmin(&admin, "", "", "")
@ -8764,14 +8764,14 @@ func TestAdminTwoFactorLogin(t *testing.T) {
admin, _, err := httpdtest.AddAdmin(admin, http.StatusCreated)
assert.NoError(t, err)
// enable two factor authentication
configName, _, secret, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], admin.Username)
configName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], admin.Username)
assert.NoError(t, err)
altToken, err := getJWTAPITokenFromTestServer(altAdminUsername, altAdminPassword)
assert.NoError(t, err)
adminTOTPConfig := dataprovider.AdminTOTPConfig{
Enabled: true,
ConfigName: configName,
Secret: kms.NewPlainSecret(secret),
Secret: kms.NewPlainSecret(key.Secret()),
}
asJSON, err := json.Marshal(adminTOTPConfig)
assert.NoError(t, err)
@ -8876,7 +8876,7 @@ func TestAdminTwoFactorLogin(t *testing.T) {
checkResponseCode(t, http.StatusFound, rr)
assert.Equal(t, webClientLoginPath, rr.Header().Get("Location"))
passcode, err := generateTOTPPasscode(secret)
passcode, err := generateTOTPPasscode(key.Secret())
assert.NoError(t, err)
form = make(url.Values)
form.Set("passcode", passcode)
@ -9481,7 +9481,7 @@ func TestWebUserTwoFactorLogin(t *testing.T) {
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
// enable two factor authentication
configName, _, secret, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
configName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
assert.NoError(t, err)
token, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)
assert.NoError(t, err)
@ -9493,7 +9493,7 @@ func TestWebUserTwoFactorLogin(t *testing.T) {
userTOTPConfig := dataprovider.UserTOTPConfig{
Enabled: true,
ConfigName: configName,
Secret: kms.NewPlainSecret(secret),
Secret: kms.NewPlainSecret(key.Secret()),
Protocols: []string{common.ProtocolHTTP},
}
asJSON, err := json.Marshal(userTOTPConfig)
@ -9593,7 +9593,7 @@ func TestWebUserTwoFactorLogin(t *testing.T) {
checkResponseCode(t, http.StatusFound, rr)
assert.Equal(t, webLoginPath, rr.Header().Get("Location"))
passcode, err := generateTOTPPasscode(secret)
passcode, err := generateTOTPPasscode(key.Secret())
assert.NoError(t, err)
form = make(url.Values)
form.Set("passcode", passcode)
@ -9698,7 +9698,21 @@ func TestWebUserTwoFactorLogin(t *testing.T) {
setJWTCookieForReq(req, authenticatedCookie)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
// get MFA qrcode
req, err = http.NewRequest(http.MethodGet, path.Join(webClientMFAPath, "qrcode?url="+url.QueryEscape(key.URL())), nil)
assert.NoError(t, err)
req.RemoteAddr = defaultRemoteAddr
setJWTCookieForReq(req, authenticatedCookie)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
assert.Equal(t, "image/png", rr.Header().Get("Content-Type"))
// invalid MFA url
req, err = http.NewRequest(http.MethodGet, path.Join(webClientMFAPath, "qrcode?url="+url.QueryEscape("http://foo\x7f.eu")), nil)
assert.NoError(t, err)
req.RemoteAddr = defaultRemoteAddr
setJWTCookieForReq(req, authenticatedCookie)
rr = executeRequest(req)
checkResponseCode(t, http.StatusInternalServerError, rr)
// check that the recovery code was marked as used
req, err = http.NewRequest(http.MethodGet, user2FARecoveryCodesPath, nil)
assert.NoError(t, err)
@ -9827,7 +9841,7 @@ func TestWebUserTwoFactorLogin(t *testing.T) {
func TestWebUserTwoFactoryLoginRedirect(t *testing.T) {
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
assert.NoError(t, err)
configName, _, secret, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
configName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
assert.NoError(t, err)
token, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)
@ -9835,7 +9849,7 @@ func TestWebUserTwoFactoryLoginRedirect(t *testing.T) {
userTOTPConfig := dataprovider.UserTOTPConfig{
Enabled: true,
ConfigName: configName,
Secret: kms.NewPlainSecret(secret),
Secret: kms.NewPlainSecret(key.Secret()),
Protocols: []string{common.ProtocolHTTP},
}
asJSON, err := json.Marshal(userTOTPConfig)
@ -9890,7 +9904,7 @@ func TestWebUserTwoFactoryLoginRedirect(t *testing.T) {
checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), fmt.Sprintf("action=%q", expectedURI))
// login with the passcode
passcode, err := generateTOTPPasscode(secret)
passcode, err := generateTOTPPasscode(key.Secret())
assert.NoError(t, err)
form = make(url.Values)
form.Set("passcode", passcode)
@ -15432,7 +15446,7 @@ func TestWebEditFile(t *testing.T) {
testFile1 := "testfile1.txt"
testFile2 := "testfile2"
file1Size := int64(65536)
file2Size := int64(1048576 * 2)
file2Size := int64(1048576 * 5)
err = createTestFile(filepath.Join(user.GetHomeDir(), testFile1), file1Size)
assert.NoError(t, err)
err = createTestFile(filepath.Join(user.GetHomeDir(), testFile2), file2Size)
@ -19110,14 +19124,14 @@ func TestWebAdminBasicMock(t *testing.T) {
checkResponseCode(t, http.StatusSeeOther, rr)
// add TOTP config
configName, _, secret, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], altAdminUsername)
configName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], altAdminUsername)
assert.NoError(t, err)
altToken, err := getJWTWebTokenFromTestServer(altAdminUsername, altAdminPassword)
assert.NoError(t, err)
adminTOTPConfig := dataprovider.AdminTOTPConfig{
Enabled: true,
ConfigName: configName,
Secret: kms.NewPlainSecret(secret),
Secret: kms.NewPlainSecret(key.Secret()),
}
asJSON, err := json.Marshal(adminTOTPConfig)
assert.NoError(t, err)
@ -20093,14 +20107,14 @@ func TestWebUserUpdateMock(t *testing.T) {
lastPwdChange := user.LastPasswordChange
assert.Greater(t, lastPwdChange, int64(0))
// add TOTP config
configName, _, secret, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
configName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
assert.NoError(t, err)
userToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)
assert.NoError(t, err)
userTOTPConfig := dataprovider.UserTOTPConfig{
Enabled: true,
ConfigName: configName,
Secret: kms.NewPlainSecret(secret),
Secret: kms.NewPlainSecret(key.Secret()),
Protocols: []string{common.ProtocolSSH, common.ProtocolFTP},
}
asJSON, err := json.Marshal(userTOTPConfig)

View file

@ -1571,6 +1571,8 @@ func (s *httpdServer) setupWebClientRoutes() {
Post(webChangeClientPwdPath, s.handleWebClientChangePwdPost)
router.With(s.checkHTTPUserPerm(sdk.WebClientMFADisabled), s.refreshCookie).
Get(webClientMFAPath, s.handleWebClientMFA)
router.With(s.checkHTTPUserPerm(sdk.WebClientMFADisabled), s.refreshCookie).
Get(webClientMFAPath+"/qrcode", getQRCode)
router.With(s.checkHTTPUserPerm(sdk.WebClientMFADisabled), verifyCSRFHeader).
Post(webClientTOTPGeneratePath, generateTOTPSecret)
router.With(s.checkHTTPUserPerm(sdk.WebClientMFADisabled), verifyCSRFHeader).
@ -1644,6 +1646,7 @@ func (s *httpdServer) setupWebAdminRoutes() {
router.With(s.requireBuiltinLogin).Post(webChangeAdminPwdPath, s.handleWebAdminChangePwdPost)
router.With(s.refreshCookie, s.requireBuiltinLogin).Get(webAdminMFAPath, s.handleWebAdminMFA)
router.With(s.refreshCookie, s.requireBuiltinLogin).Get(webAdminMFAPath+"/qrcode", getQRCode)
router.With(verifyCSRFHeader, s.requireBuiltinLogin).Post(webAdminTOTPGeneratePath, generateTOTPSecret)
router.With(verifyCSRFHeader, s.requireBuiltinLogin).Post(webAdminTOTPValidatePath, validateTOTPPasscode)
router.With(verifyCSRFHeader, s.requireBuiltinLogin).Post(webAdminTOTPSavePath, saveTOTPConfig)

View file

@ -16,8 +16,12 @@
package mfa
import (
"bytes"
"fmt"
"image/png"
"time"
"github.com/pquerna/otp"
)
var (
@ -94,15 +98,30 @@ func ValidateTOTPPasscode(configName, passcode, secret string) (bool, error) {
// GenerateTOTPSecret generates a new TOTP secret and QR code for the given username
// using the configuration with configName
func GenerateTOTPSecret(configName, username string) (string, string, string, []byte, error) {
func GenerateTOTPSecret(configName, username string) (string, *otp.Key, []byte, error) {
for _, config := range totpConfigs {
if config.Name == configName {
issuer, secret, qrCode, err := config.generate(username, 200, 200)
return configName, issuer, secret, qrCode, err
key, qrCode, err := config.generate(username, 200, 200)
return configName, key, qrCode, err
}
}
return "", "", "", nil, fmt.Errorf("totp: no configuration %q", configName)
return "", nil, nil, fmt.Errorf("totp: no configuration %q", configName)
}
// GenerateQRCodeFromURL generates a QR code from a TOTP URL
func GenerateQRCodeFromURL(url string, width, height int) ([]byte, error) {
key, err := otp.NewKeyFromURL(url)
if err != nil {
return nil, err
}
var buf bytes.Buffer
img, err := key.Image(width, height)
if err != nil {
return nil, err
}
err = png.Encode(&buf, img)
return buf.Bytes(), err
}
// the ticker cannot be started/stopped from multiple goroutines

View file

@ -21,6 +21,7 @@ import (
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMFAConfig(t *testing.T) {
@ -76,38 +77,56 @@ func TestMFAConfig(t *testing.T) {
assert.Equal(t, configName3, status.TOTPConfigs[2].Name)
}
// now generate some secrets and validate some passcodes
_, _, _, _, err = GenerateTOTPSecret("", "") //nolint:dogsled
_, _, _, err = GenerateTOTPSecret("", "") //nolint:dogsled
assert.Error(t, err)
match, err := ValidateTOTPPasscode("", "", "")
assert.Error(t, err)
assert.False(t, match)
cfgName, _, secret, _, err := GenerateTOTPSecret(configName1, "user1")
cfgName, key, _, err := GenerateTOTPSecret(configName1, "user1")
assert.NoError(t, err)
assert.NotEmpty(t, secret)
assert.NotEmpty(t, key.Secret())
assert.Equal(t, configName1, cfgName)
passcode, err := generatePasscode(secret, otp.AlgorithmSHA1)
passcode, err := generatePasscode(key.Secret(), otp.AlgorithmSHA1)
assert.NoError(t, err)
match, err = ValidateTOTPPasscode(configName1, passcode, secret)
match, err = ValidateTOTPPasscode(configName1, passcode, key.Secret())
assert.NoError(t, err)
assert.True(t, match)
match, err = ValidateTOTPPasscode(configName1, passcode, secret)
match, err = ValidateTOTPPasscode(configName1, passcode, key.Secret())
assert.ErrorIs(t, err, errPasscodeUsed)
assert.False(t, match)
passcode, err = generatePasscode(secret, otp.AlgorithmSHA256)
passcode, err = generatePasscode(key.Secret(), otp.AlgorithmSHA256)
assert.NoError(t, err)
// config1 uses sha1 algo
match, err = ValidateTOTPPasscode(configName1, passcode, secret)
match, err = ValidateTOTPPasscode(configName1, passcode, key.Secret())
assert.NoError(t, err)
assert.False(t, match)
// config3 use the expected algo
match, err = ValidateTOTPPasscode(configName3, passcode, secret)
match, err = ValidateTOTPPasscode(configName3, passcode, key.Secret())
assert.NoError(t, err)
assert.True(t, match)
stopCleanupTicker()
}
func TestGenerateQRCodeFromURL(t *testing.T) {
_, err := GenerateQRCodeFromURL("http://foo\x7f.cloud", 200, 200)
assert.Error(t, err)
config := TOTPConfig{
Name: "config name",
Issuer: "SFTPGo",
Algo: TOTPAlgoSHA256,
}
key, qrCode, err := config.generate("a", 150, 150)
require.NoError(t, err)
qrCode1, err := GenerateQRCodeFromURL(key.URL(), 150, 150)
require.NoError(t, err)
assert.Equal(t, qrCode, qrCode1)
_, err = GenerateQRCodeFromURL(key.URL(), 10, 10)
assert.Error(t, err)
}
func TestCleanupPasscodes(t *testing.T) {
usedPasscodes.Store("key", time.Now().Add(-24*time.Hour).UTC())
startCleanupTicker(30 * time.Millisecond)
@ -125,11 +144,11 @@ func TestTOTPGenerateErrors(t *testing.T) {
algo: otp.AlgorithmSHA1,
}
// issuer cannot be empty
_, _, _, err := config.generate("username", 200, 200) //nolint:dogsled
_, _, err := config.generate("username", 200, 200) //nolint:dogsled
assert.Error(t, err)
config.Issuer = "issuer"
// we cannot encode an image smaller than 45x45
_, _, _, err = config.generate("username", 30, 30) //nolint:dogsled
_, _, err = config.generate("username", 30, 30) //nolint:dogsled
assert.Error(t, err)
}

View file

@ -90,7 +90,7 @@ func (c *TOTPConfig) validatePasscode(passcode, secret string) (bool, error) {
}
// generate generates a new TOTP secret and QR code for the given username
func (c *TOTPConfig) generate(username string, qrCodeWidth, qrCodeHeight int) (string, string, []byte, error) {
func (c *TOTPConfig) generate(username string, qrCodeWidth, qrCodeHeight int) (*otp.Key, []byte, error) {
key, err := totp.Generate(totp.GenerateOpts{
Issuer: c.Issuer,
AccountName: username,
@ -98,15 +98,15 @@ func (c *TOTPConfig) generate(username string, qrCodeWidth, qrCodeHeight int) (s
Algorithm: c.algo,
})
if err != nil {
return "", "", nil, err
return nil, nil, err
}
var buf bytes.Buffer
img, err := key.Image(qrCodeWidth, qrCodeHeight)
if err != nil {
return "", "", nil, err
return nil, nil, err
}
err = png.Encode(&buf, img)
return key.Issuer(), key.Secret(), buf.Bytes(), err
return key, buf.Bytes(), err
}
func cleanupUsedPasscodes() {

View file

@ -2811,19 +2811,19 @@ func TestInteractiveLoginWithPasscode(t *testing.T) {
_, _, err = getKeyboardInteractiveSftpClient(user, []string{"wrong_password"})
assert.Error(t, err)
// add multi-factor authentication
configName, _, secret, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
configName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
assert.NoError(t, err)
user.Password = defaultPassword
user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{
Enabled: true,
ConfigName: configName,
Secret: kms.NewPlainSecret(secret),
Secret: kms.NewPlainSecret(key.Secret()),
Protocols: []string{common.ProtocolSSH},
}
err = dataprovider.UpdateUser(&user, "", "", "")
assert.NoError(t, err)
passcode, err := totp.GenerateCodeCustom(secret, time.Now(), totp.ValidateOpts{
passcode, err := totp.GenerateCodeCustom(key.Secret(), time.Now(), totp.ValidateOpts{
Period: 30,
Skew: 1,
Digits: otp.DigitsSix,
@ -2860,18 +2860,18 @@ func TestInteractiveLoginWithPasscode(t *testing.T) {
_, _, err = getCustomAuthSftpClient(user, authMethods, "")
assert.Error(t, err)
// correct passcode but the script returns an error
configName, _, secret, _, err = mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
configName, key, _, err = mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
assert.NoError(t, err)
user.Password = defaultPassword
user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{
Enabled: true,
ConfigName: configName,
Secret: kms.NewPlainSecret(secret),
Secret: kms.NewPlainSecret(key.Secret()),
Protocols: []string{common.ProtocolSSH},
}
err = dataprovider.UpdateUser(&user, "", "", "")
assert.NoError(t, err)
passcode, err = totp.GenerateCodeCustom(secret, time.Now(), totp.ValidateOpts{
passcode, err = totp.GenerateCodeCustom(key.Secret(), time.Now(), totp.ValidateOpts{
Period: 30,
Skew: 1,
Digits: otp.DigitsSix,
@ -2950,13 +2950,13 @@ func TestSecondFactorRequirement(t *testing.T) {
_, _, err = getSftpClient(user, usePubKey)
assert.Error(t, err)
configName, _, secret, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
configName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
assert.NoError(t, err)
user.Password = defaultPassword
user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{
Enabled: true,
ConfigName: configName,
Secret: kms.NewPlainSecret(secret),
Secret: kms.NewPlainSecret(key.Secret()),
Protocols: []string{common.ProtocolSSH},
}
err = dataprovider.UpdateUser(&user, "", "", "")
@ -3159,13 +3159,13 @@ func TestPreLoginHookPreserveMFAConfig(t *testing.T) {
assert.NoError(t, err)
assert.Len(t, user.Filters.RecoveryCodes, 0)
assert.False(t, user.Filters.TOTPConfig.Enabled)
configName, _, secret, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
configName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
assert.NoError(t, err)
user.Password = defaultPassword
user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{
Enabled: true,
ConfigName: configName,
Secret: kms.NewPlainSecret(secret),
Secret: kms.NewPlainSecret(key.Secret()),
Protocols: []string{common.ProtocolSSH},
}
for i := 0; i < 12; i++ {
@ -4235,13 +4235,13 @@ func TestExternalAuthPreserveMFAConfig(t *testing.T) {
assert.NoError(t, err)
assert.Len(t, user.Filters.RecoveryCodes, 0)
assert.False(t, user.Filters.TOTPConfig.Enabled)
configName, _, secret, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
configName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
assert.NoError(t, err)
user.Password = defaultPassword
user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{
Enabled: true,
ConfigName: configName,
Secret: kms.NewPlainSecret(secret),
Secret: kms.NewPlainSecret(key.Secret()),
Protocols: []string{common.ProtocolSSH},
}
for i := 0; i < 12; i++ {

View file

@ -635,6 +635,8 @@ paths:
type: string
secret:
type: string
url:
type: string
qr_code:
type: string
format: byte

View file

@ -151,6 +151,12 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
});
}
function clearChilds(el) {
while (el.firstChild) {
el.removeChild(el.firstChild);
}
}
KTUtil.onDOMContentLoaded(function () {
var dismissErrorBtn = $('#id_dismiss_error_msg');
if (dismissErrorBtn){

View file

@ -285,9 +285,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
dt.on('draw', function () {
let dirBrowserNav = document.getElementById("dirs_browser_nav");
while (dirBrowserNav.firstChild) {
dirBrowserNav.removeChild(dirBrowserNav.firstChild);
}
clearChilds(dirBrowserNav);
let mainNavIcon = document.createElement("i");
mainNavIcon.classList.add("ki-duotone", "ki-home", "fs-1", "text-primary", "me-3");
let mainNavLink = document.createElement("a");

View file

@ -217,8 +217,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<div class="text-gray-700 fw-semibold fs-6 mb-10">
Use your preferred Authenticator App (e.g. Microsoft Authenticator, Google Authenticator, Authy, 1Password etc. ) to scan the QR code. It will generate an authentication code for you to enter below.
</div>
<div class="pt-5 text-center">
<img id="id_qr_code" src="data:image/gif;base64, R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="QR code" class="mw-150px"/>
<div id="id_qr_code_container" class="pt-5 text-center">
</div>
<div class="notice d-flex bg-light-warning rounded border-warning border border-dashed my-10 p-6">
<i class="ki-duotone ki-information fs-2tx text-warning me-4">
@ -520,9 +519,15 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
return;
}
$('#id_secret').text(response.data.secret);
$('#id_qr_code').attr('src', 'data:image/png;base64, '+ response.data.qr_code);
$('#errorModalMsg').addClass("d-none");
$('#id_passcode').val("");
let qrCodeContainer = document.getElementById("id_qr_code_container");
clearChilds(qrCodeContainer);
let qrCodeImg = document.createElement("img");
qrCodeImg.classList.add("mw-150px");
qrCodeImg.src = "{{.MFAURL}}/qrcode?url="+encodeURIComponent(response.data.url);
qrCodeImg.alt = "QR code";
qrCodeContainer.appendChild(qrCodeImg);
qrModal.show();
}).catch(function (error){
el.removeAttribute('data-kt-indicator');