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) assert.NoError(t, err)
} }
// add multi-factor authentication // 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) assert.NoError(t, err)
user.Password = defaultPassword user.Password = defaultPassword
user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{ user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{
Enabled: true, Enabled: true,
ConfigName: configName, ConfigName: configName,
Secret: kms.NewPlainSecret(secret), Secret: kms.NewPlainSecret(key.Secret()),
Protocols: []string{common.ProtocolSSH}, Protocols: []string{common.ProtocolSSH},
} }
err = dataprovider.UpdateUser(&user, "", "", "") err = dataprovider.UpdateUser(&user, "", "", "")
assert.NoError(t, err) assert.NoError(t, err)
passcode, err := generateTOTPPasscode(secret, otp.AlgorithmSHA1) passcode, err := generateTOTPPasscode(key.Secret(), otp.AlgorithmSHA1)
assert.NoError(t, err) assert.NoError(t, err)
passwordAsked := false passwordAsked := false
passcodeAsked := false passcodeAsked := false

View file

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

View file

@ -1064,13 +1064,13 @@ func TestMultiFactorAuth(t *testing.T) {
user, _, err := httpdtest.AddUser(u, http.StatusCreated) user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err) 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) assert.NoError(t, err)
user.Password = defaultPassword user.Password = defaultPassword
user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{ user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{
Enabled: true, Enabled: true,
ConfigName: configName, ConfigName: configName,
Secret: kms.NewPlainSecret(secret), Secret: kms.NewPlainSecret(key.Secret()),
Protocols: []string{common.ProtocolFTP}, Protocols: []string{common.ProtocolFTP},
} }
err = dataprovider.UpdateUser(&user, "", "", "") err = dataprovider.UpdateUser(&user, "", "", "")
@ -1081,7 +1081,7 @@ func TestMultiFactorAuth(t *testing.T) {
if assert.Error(t, err) { if assert.Error(t, err) {
assert.Contains(t, err.Error(), dataprovider.ErrInvalidCredentials.Error()) 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) assert.NoError(t, err)
user.Password = defaultPassword + passcode user.Password = defaultPassword + passcode
client, err := getFTPClient(user, true, nil) 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") 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) assert.NoError(t, err)
user.Password = defaultPassword user.Password = defaultPassword
user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{ user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{
Enabled: true, Enabled: true,
ConfigName: configName, ConfigName: configName,
Secret: kms.NewPlainSecret(secret), Secret: kms.NewPlainSecret(key.Secret()),
Protocols: []string{common.ProtocolFTP}, Protocols: []string{common.ProtocolFTP},
} }
err = dataprovider.UpdateUser(&user, "", "", "") err = dataprovider.UpdateUser(&user, "", "", "")
assert.NoError(t, err) assert.NoError(t, err)
passcode, err := generateTOTPPasscode(secret, otp.AlgorithmSHA1) passcode, err := generateTOTPPasscode(key.Secret(), otp.AlgorithmSHA1)
assert.NoError(t, err) assert.NoError(t, err)
user.Password = defaultPassword + passcode user.Password = defaultPassword + passcode
client, err := getFTPClient(user, true, nil) client, err := getFTPClient(user, true, nil)

View file

@ -1016,7 +1016,7 @@ func TestCiphers(t *testing.T) {
TLSCipherSuites: []string{}, TLSCipherSuites: []string{},
} }
b.setCiphers() 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.TLSCipherSuites = []string{"TLS_AES_128_GCM_SHA256", "TLS_AES_256_GCM_SHA384"}
b.setCiphers() b.setCiphers()
require.Len(t, b.ciphers, 2) require.Len(t, b.ciphers, 2)

View file

@ -15,9 +15,12 @@
package httpd package httpd
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"io"
"net/http" "net/http"
"strconv"
"strings" "strings"
"github.com/go-chi/render" "github.com/go-chi/render"
@ -40,6 +43,7 @@ type generateTOTPResponse struct {
ConfigName string `json:"config_name"` ConfigName string `json:"config_name"`
Issuer string `json:"issuer"` Issuer string `json:"issuer"`
Secret string `json:"secret"` Secret string `json:"secret"`
URL string `json:"url"`
QRCode []byte `json:"qr_code"` QRCode []byte `json:"qr_code"`
} }
@ -79,19 +83,33 @@ func generateTOTPSecret(w http.ResponseWriter, r *http.Request) {
sendAPIResponse(w, r, err, "", http.StatusBadRequest) sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return return
} }
configName, issuer, secret, qrCode, err := mfa.GenerateTOTPSecret(req.ConfigName, accountName) configName, key, qrCode, err := mfa.GenerateTOTPSecret(req.ConfigName, accountName)
if err != nil { if err != nil {
sendAPIResponse(w, r, err, "", http.StatusBadRequest) sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return return
} }
render.JSON(w, r, generateTOTPResponse{ render.JSON(w, r, generateTOTPResponse{
ConfigName: configName, ConfigName: configName,
Issuer: issuer, Issuer: key.Issuer(),
Secret: secret, Secret: key.Secret(),
URL: key.URL(),
QRCode: qrCode, 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) { func saveTOTPConfig(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
claims, err := getTokenClaims(r) claims, err := getTokenClaims(r)

View file

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

View file

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

View file

@ -16,8 +16,12 @@
package mfa package mfa
import ( import (
"bytes"
"fmt" "fmt"
"image/png"
"time" "time"
"github.com/pquerna/otp"
) )
var ( 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 // GenerateTOTPSecret generates a new TOTP secret and QR code for the given username
// using the configuration with configName // 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 { for _, config := range totpConfigs {
if config.Name == configName { if config.Name == configName {
issuer, secret, qrCode, err := config.generate(username, 200, 200) key, qrCode, err := config.generate(username, 200, 200)
return configName, issuer, secret, qrCode, err 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 // the ticker cannot be started/stopped from multiple goroutines

View file

@ -21,6 +21,7 @@ import (
"github.com/pquerna/otp" "github.com/pquerna/otp"
"github.com/pquerna/otp/totp" "github.com/pquerna/otp/totp"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestMFAConfig(t *testing.T) { func TestMFAConfig(t *testing.T) {
@ -76,38 +77,56 @@ func TestMFAConfig(t *testing.T) {
assert.Equal(t, configName3, status.TOTPConfigs[2].Name) assert.Equal(t, configName3, status.TOTPConfigs[2].Name)
} }
// now generate some secrets and validate some passcodes // now generate some secrets and validate some passcodes
_, _, _, _, err = GenerateTOTPSecret("", "") //nolint:dogsled _, _, _, err = GenerateTOTPSecret("", "") //nolint:dogsled
assert.Error(t, err) assert.Error(t, err)
match, err := ValidateTOTPPasscode("", "", "") match, err := ValidateTOTPPasscode("", "", "")
assert.Error(t, err) assert.Error(t, err)
assert.False(t, match) assert.False(t, match)
cfgName, _, secret, _, err := GenerateTOTPSecret(configName1, "user1") cfgName, key, _, err := GenerateTOTPSecret(configName1, "user1")
assert.NoError(t, err) assert.NoError(t, err)
assert.NotEmpty(t, secret) assert.NotEmpty(t, key.Secret())
assert.Equal(t, configName1, cfgName) assert.Equal(t, configName1, cfgName)
passcode, err := generatePasscode(secret, otp.AlgorithmSHA1) passcode, err := generatePasscode(key.Secret(), otp.AlgorithmSHA1)
assert.NoError(t, err) assert.NoError(t, err)
match, err = ValidateTOTPPasscode(configName1, passcode, secret) match, err = ValidateTOTPPasscode(configName1, passcode, key.Secret())
assert.NoError(t, err) assert.NoError(t, err)
assert.True(t, match) assert.True(t, match)
match, err = ValidateTOTPPasscode(configName1, passcode, secret) match, err = ValidateTOTPPasscode(configName1, passcode, key.Secret())
assert.ErrorIs(t, err, errPasscodeUsed) assert.ErrorIs(t, err, errPasscodeUsed)
assert.False(t, match) assert.False(t, match)
passcode, err = generatePasscode(secret, otp.AlgorithmSHA256) passcode, err = generatePasscode(key.Secret(), otp.AlgorithmSHA256)
assert.NoError(t, err) assert.NoError(t, err)
// config1 uses sha1 algo // config1 uses sha1 algo
match, err = ValidateTOTPPasscode(configName1, passcode, secret) match, err = ValidateTOTPPasscode(configName1, passcode, key.Secret())
assert.NoError(t, err) assert.NoError(t, err)
assert.False(t, match) assert.False(t, match)
// config3 use the expected algo // config3 use the expected algo
match, err = ValidateTOTPPasscode(configName3, passcode, secret) match, err = ValidateTOTPPasscode(configName3, passcode, key.Secret())
assert.NoError(t, err) assert.NoError(t, err)
assert.True(t, match) assert.True(t, match)
stopCleanupTicker() 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) { func TestCleanupPasscodes(t *testing.T) {
usedPasscodes.Store("key", time.Now().Add(-24*time.Hour).UTC()) usedPasscodes.Store("key", time.Now().Add(-24*time.Hour).UTC())
startCleanupTicker(30 * time.Millisecond) startCleanupTicker(30 * time.Millisecond)
@ -125,11 +144,11 @@ func TestTOTPGenerateErrors(t *testing.T) {
algo: otp.AlgorithmSHA1, algo: otp.AlgorithmSHA1,
} }
// issuer cannot be empty // issuer cannot be empty
_, _, _, err := config.generate("username", 200, 200) //nolint:dogsled _, _, err := config.generate("username", 200, 200) //nolint:dogsled
assert.Error(t, err) assert.Error(t, err)
config.Issuer = "issuer" config.Issuer = "issuer"
// we cannot encode an image smaller than 45x45 // 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) 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 // 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{ key, err := totp.Generate(totp.GenerateOpts{
Issuer: c.Issuer, Issuer: c.Issuer,
AccountName: username, AccountName: username,
@ -98,15 +98,15 @@ func (c *TOTPConfig) generate(username string, qrCodeWidth, qrCodeHeight int) (s
Algorithm: c.algo, Algorithm: c.algo,
}) })
if err != nil { if err != nil {
return "", "", nil, err return nil, nil, err
} }
var buf bytes.Buffer var buf bytes.Buffer
img, err := key.Image(qrCodeWidth, qrCodeHeight) img, err := key.Image(qrCodeWidth, qrCodeHeight)
if err != nil { if err != nil {
return "", "", nil, err return nil, nil, err
} }
err = png.Encode(&buf, img) err = png.Encode(&buf, img)
return key.Issuer(), key.Secret(), buf.Bytes(), err return key, buf.Bytes(), err
} }
func cleanupUsedPasscodes() { func cleanupUsedPasscodes() {

View file

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

View file

@ -635,6 +635,8 @@ paths:
type: string type: string
secret: secret:
type: string type: string
url:
type: string
qr_code: qr_code:
type: string type: string
format: byte 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 () { KTUtil.onDOMContentLoaded(function () {
var dismissErrorBtn = $('#id_dismiss_error_msg'); var dismissErrorBtn = $('#id_dismiss_error_msg');
if (dismissErrorBtn){ if (dismissErrorBtn){

View file

@ -285,9 +285,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
dt.on('draw', function () { dt.on('draw', function () {
let dirBrowserNav = document.getElementById("dirs_browser_nav"); let dirBrowserNav = document.getElementById("dirs_browser_nav");
while (dirBrowserNav.firstChild) { clearChilds(dirBrowserNav);
dirBrowserNav.removeChild(dirBrowserNav.firstChild);
}
let mainNavIcon = document.createElement("i"); let mainNavIcon = document.createElement("i");
mainNavIcon.classList.add("ki-duotone", "ki-home", "fs-1", "text-primary", "me-3"); mainNavIcon.classList.add("ki-duotone", "ki-home", "fs-1", "text-primary", "me-3");
let mainNavLink = document.createElement("a"); 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"> <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. 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>
<div class="pt-5 text-center"> <div id="id_qr_code_container" class="pt-5 text-center">
<img id="id_qr_code" src="data:image/gif;base64, R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="QR code" class="mw-150px"/>
</div> </div>
<div class="notice d-flex bg-light-warning rounded border-warning border border-dashed my-10 p-6"> <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"> <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; return;
} }
$('#id_secret').text(response.data.secret); $('#id_secret').text(response.data.secret);
$('#id_qr_code').attr('src', 'data:image/png;base64, '+ response.data.qr_code);
$('#errorModalMsg').addClass("d-none"); $('#errorModalMsg').addClass("d-none");
$('#id_passcode').val(""); $('#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(); qrModal.show();
}).catch(function (error){ }).catch(function (error){
el.removeAttribute('data-kt-indicator'); el.removeAttribute('data-kt-indicator');