mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-21 23:20:24 +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)
|
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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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++ {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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){
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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');
|
||||||
|
|
Loading…
Reference in a new issue