mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-25 00:50:31 +00:00
WebClient: remove data schema usage from mfa page
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
59bdd4bc4e
commit
ac309cf9a3
15 changed files with 168 additions and 87 deletions
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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++ {
|
||||
|
|
|
@ -635,6 +635,8 @@ paths:
|
|||
type: string
|
||||
secret:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
qr_code:
|
||||
type: string
|
||||
format: byte
|
||||
|
|
|
@ -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){
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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');
|
||||
|
|
Loading…
Reference in a new issue