From 51306e92c4d9fe4ed8a07acf92a5b9b70c34a311 Mon Sep 17 00:00:00 2001 From: Roland Osborne Date: Tue, 21 May 2024 00:56:25 -0700 Subject: [PATCH] adding mfa to admin login --- doc/api.oa3 | 151 ++++++++++++++++-- net/server/internal/api_addAdminMFAuth.go | 73 +++++++++ net/server/internal/api_addNodeAccount.go | 2 +- .../internal/api_addNodeAccountAccess.go | 2 +- net/server/internal/api_getAdminMFAuth.go | 20 +++ .../internal/api_getNodeAccountImage.go | 2 +- net/server/internal/api_getNodeAccounts.go | 2 +- net/server/internal/api_getNodeConfig.go | 2 +- net/server/internal/api_removeAdminMFAuth.go | 40 +++++ net/server/internal/api_removeNodeAccount.go | 2 +- net/server/internal/api_setAdminAccess.go | 45 ++++++ net/server/internal/api_setAdminMFAuth.go | 92 +++++++++++ .../internal/api_setNodeAccountStatus.go | 2 +- net/server/internal/api_setNodeConfig.go | 2 +- net/server/internal/authUtil.go | 24 +++ net/server/internal/configUtil.go | 17 ++ net/server/internal/routers.go | 35 ++++ net/web/src/access/admin/useAdmin.hook.js | 6 +- net/web/src/api/getAdminMFAuth.js | 8 + net/web/src/api/setNodeAccess.js | 8 + net/web/src/constants/Strings.js | 42 +++++ net/web/src/dashboard/Dashboard.jsx | 48 +++++- net/web/src/dashboard/useDashboard.hook.js | 18 ++- 23 files changed, 621 insertions(+), 22 deletions(-) create mode 100644 net/server/internal/api_addAdminMFAuth.go create mode 100644 net/server/internal/api_getAdminMFAuth.go create mode 100644 net/server/internal/api_removeAdminMFAuth.go create mode 100644 net/server/internal/api_setAdminAccess.go create mode 100644 net/server/internal/api_setAdminMFAuth.go create mode 100644 net/web/src/api/getAdminMFAuth.js create mode 100644 net/web/src/api/setNodeAccess.js diff --git a/doc/api.oa3 b/doc/api.oa3 index e0f31af4..537e624c 100644 --- a/doc/api.oa3 +++ b/doc/api.oa3 @@ -86,7 +86,140 @@ paths: description: permission denied '500': description: internal server error - + + /admin/access: + put: + tags: + - admin + description: Acquire new session token for admin endpoints + operationId: set-admin-session + parameters: + - name: token + in: query + description: access token + required: true + schema: + type: string + - name: code + in: query + description: totp code + required: false + schema: + type: string + responses: + '201': + description: generated + content: + application/json: + schema: + type: string + '401': + description: invalid token + '405': + description: totp code required but not set + '429': + description: temporarily locked due to too many failures + '500': + description: internal server error + + /admin/mfauth: + get: + tags: + - admin + description: check if multi-factor authentication enabled + operationId: get-admin-mfa + parameters: + - name: token + in: query + description: session token + required: true + schema: + type: string + responses: + '200': + description: success + content: + application/json: + schema: + type: boolean + '401': + description: permission denied + '500': + description: internal server error + post: + tags: + - admin + description: Enable multi-factor authentication + operationId: add-admin-mfa + parameters: + - name: token + in: query + description: session token + required: true + schema: + type: string + responses: + '201': + description: success + content: + application/json: + schema: + type: string + '401': + description: permission denied + '500': + description: internal server error + put: + tags: + - admin + description: Confirm multi-factor authentication + operationId: confirm-admin-mfa + parameters: + - name: token + in: query + description: session token + required: false + schema: + type: string + - name: code + in: query + description: totp code generated from secret + required: true + schema: + type: string + responses: + '200': + description: success + '401': + description: permission denied + '403': + description: totp code not correct + '405': + description: totp code required but not set + '429': + description: temporarily locked due to too many failures + '500': + description: internal server error + delete: + tags: + - admin + description: Disable multi-factor authentication + operationId: remove-admin-mfa + parameters: + - name: token + in: query + description: session token + required: false + schema: + type: string + responses: + '200': + description: success + '401': + description: permission denied + '500': + description: internal server error + /admin/config: get: tags: @@ -96,7 +229,7 @@ paths: parameters: - name: token in: query - description: token for admin access + description: session token for admin access required: true schema: type: string @@ -119,7 +252,7 @@ paths: parameters: - name: token in: query - description: token for admin access + description: session token for admin access required: true schema: type: string @@ -146,7 +279,7 @@ paths: parameters: - name: token in: query - description: token for admin access + description: session token for admin access required: true schema: type: string @@ -173,7 +306,7 @@ paths: parameters: - name: token in: query - description: token for admin access + description: session token for admin access required: true schema: type: string @@ -204,7 +337,7 @@ paths: type: string - name: token in: query - description: token for admin access + description: session token for admin access required: true schema: type: string @@ -238,7 +371,7 @@ paths: type: string - name: token in: query - description: token for admin access + description: session token for admin access required: true schema: type: string @@ -261,7 +394,7 @@ paths: parameters: - name: token in: query - description: token for admin access + description: session token for admin access required: true schema: type: string @@ -298,7 +431,7 @@ paths: type: string - name: token in: query - description: token for admin access + description: session token for admin access required: true schema: type: string diff --git a/net/server/internal/api_addAdminMFAuth.go b/net/server/internal/api_addAdminMFAuth.go new file mode 100644 index 00000000..d7898e72 --- /dev/null +++ b/net/server/internal/api_addAdminMFAuth.go @@ -0,0 +1,73 @@ +package databag + +import ( + "bytes" + "net/http" + "image/png" + "github.com/pquerna/otp" + "github.com/pquerna/otp/totp" + "databag/internal/store" + "encoding/base64" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +//AddAdminMFAuth enables multi-factor auth on the given account +func AddAdminMFAuth(w http.ResponseWriter, r *http.Request) { + + // validate login + if code, err := ParamSessionToken(r); err != nil { + ErrResponse(w, code, err) + return + } + + key, err := totp.Generate(totp.GenerateOpts{ + Issuer: APPMFAIssuer, + AccountName: "admin", + Digits: otp.DigitsSix, + Algorithm: otp.AlgorithmSHA256, + }) + + err = store.DB.Transaction(func(tx *gorm.DB) error { + + // upsert mfa enabled + if res := tx.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "config_id"}}, + DoUpdates: clause.AssignmentColumns([]string{"bool_value"}), + }).Create(&store.Config{ConfigID: CNFMFAEnabled, BoolValue: false}).Error; res != nil { + return res + } + + // upsert mfa confirmed + if res := tx.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "config_id"}}, + DoUpdates: clause.AssignmentColumns([]string{"bool_value"}), + }).Create(&store.Config{ConfigID: CNFMFAConfirmed, BoolValue: true}).Error; res != nil { + return res + } + + // upsert mfa secret + if res := tx.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "config_id"}}, + DoUpdates: clause.AssignmentColumns([]string{"str_value"}), + }).Create(&store.Config{ConfigID: CNFMFASecret, StrValue: key.Secret()}).Error; res != nil { + return res + } + + return nil + }) + if err != nil { + ErrResponse(w, http.StatusInternalServerError, err) + return + } + + var buf bytes.Buffer + img, err := key.Image(200, 200) + if err != nil { + panic(err) + } + png.Encode(&buf, img) + enc := base64.StdEncoding.EncodeToString(buf.Bytes()) + + WriteResponse(w, MFASecret{ Image: "data:image/png;base64," + enc, Text: key.Secret() }) +} diff --git a/net/server/internal/api_addNodeAccount.go b/net/server/internal/api_addNodeAccount.go index 85132c3b..7d01809f 100644 --- a/net/server/internal/api_addNodeAccount.go +++ b/net/server/internal/api_addNodeAccount.go @@ -11,7 +11,7 @@ import ( //AddNodeAccount generate a new token to be used for account creation func AddNodeAccount(w http.ResponseWriter, r *http.Request) { - if code, err := ParamAdminToken(r); err != nil { + if code, err := ParamSessionToken(r); err != nil { ErrResponse(w, code, err) return } diff --git a/net/server/internal/api_addNodeAccountAccess.go b/net/server/internal/api_addNodeAccountAccess.go index 74773468..1af7f893 100644 --- a/net/server/internal/api_addNodeAccountAccess.go +++ b/net/server/internal/api_addNodeAccountAccess.go @@ -20,7 +20,7 @@ func AddNodeAccountAccess(w http.ResponseWriter, r *http.Request) { return } - if code, err := ParamAdminToken(r); err != nil { + if code, err := ParamSessionToken(r); err != nil { ErrResponse(w, code, err) return } diff --git a/net/server/internal/api_getAdminMFAuth.go b/net/server/internal/api_getAdminMFAuth.go new file mode 100644 index 00000000..fcf4789f --- /dev/null +++ b/net/server/internal/api_getAdminMFAuth.go @@ -0,0 +1,20 @@ +package databag + +import ( + "net/http" +) + +//GetAdminMFAuth checks if mfa enabled for admin +func GetAdminMFAuth(w http.ResponseWriter, r *http.Request) { + + // validate login + if code, err := ParamSessionToken(r); err != nil { + ErrResponse(w, code, err) + return + } + + enabled := getBoolConfigValue(CNFMFAEnabled, false); + confirmed := getBoolConfigValue(CNFMFAConfirmed, false); + + WriteResponse(w, enabled && confirmed) +} diff --git a/net/server/internal/api_getNodeAccountImage.go b/net/server/internal/api_getNodeAccountImage.go index 0e52edc6..fa3cbdc5 100644 --- a/net/server/internal/api_getNodeAccountImage.go +++ b/net/server/internal/api_getNodeAccountImage.go @@ -23,7 +23,7 @@ func GetNodeAccountImage(w http.ResponseWriter, r *http.Request) { return } - if code, err := ParamAdminToken(r); err != nil { + if code, err := ParamSessionToken(r); err != nil { ErrResponse(w, code, err) return } diff --git a/net/server/internal/api_getNodeAccounts.go b/net/server/internal/api_getNodeAccounts.go index 012f2fe1..87d5cd7a 100644 --- a/net/server/internal/api_getNodeAccounts.go +++ b/net/server/internal/api_getNodeAccounts.go @@ -8,7 +8,7 @@ import ( //GetNodeAccounts retrieves profiles of hosted accounts for the admin func GetNodeAccounts(w http.ResponseWriter, r *http.Request) { - if code, err := ParamAdminToken(r); err != nil { + if code, err := ParamSessionToken(r); err != nil { ErrResponse(w, code, err) return } diff --git a/net/server/internal/api_getNodeConfig.go b/net/server/internal/api_getNodeConfig.go index f95400ca..916d0f0b 100644 --- a/net/server/internal/api_getNodeConfig.go +++ b/net/server/internal/api_getNodeConfig.go @@ -8,7 +8,7 @@ import ( func GetNodeConfig(w http.ResponseWriter, r *http.Request) { // validate login - if code, err := ParamAdminToken(r); err != nil { + if code, err := ParamSessionToken(r); err != nil { ErrResponse(w, code, err) return } diff --git a/net/server/internal/api_removeAdminMFAuth.go b/net/server/internal/api_removeAdminMFAuth.go new file mode 100644 index 00000000..32403c73 --- /dev/null +++ b/net/server/internal/api_removeAdminMFAuth.go @@ -0,0 +1,40 @@ +package databag + +import ( + "net/http" + "databag/internal/store" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +//Disable multi-factor auth for admin +func RemoveAdminMFAuth(w http.ResponseWriter, r *http.Request) { + + // validate login + if code, err := ParamSessionToken(r); err != nil { + ErrResponse(w, code, err) + return + } + + err := store.DB.Transaction(func(tx *gorm.DB) error { + if res := tx.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "config_id"}}, + DoUpdates: clause.AssignmentColumns([]string{"str_value"}), + }).Create(&store.Config{ConfigID: CNFMFAConfirmed, BoolValue: false}).Error; res != nil { + return res + } + if res := tx.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "config_id"}}, + DoUpdates: clause.AssignmentColumns([]string{"str_value"}), + }).Create(&store.Config{ConfigID: CNFMFAEnabled, BoolValue: false}).Error; res != nil { + return res + } + return nil + }) + if err != nil { + ErrResponse(w, http.StatusInternalServerError, err) + return + } + + WriteResponse(w, nil) +} diff --git a/net/server/internal/api_removeNodeAccount.go b/net/server/internal/api_removeNodeAccount.go index e0c6c3c1..e5911414 100644 --- a/net/server/internal/api_removeNodeAccount.go +++ b/net/server/internal/api_removeNodeAccount.go @@ -21,7 +21,7 @@ func RemoveNodeAccount(w http.ResponseWriter, r *http.Request) { return } - if code, err := ParamAdminToken(r); err != nil { + if code, err := ParamSessionToken(r); err != nil { ErrResponse(w, code, err) return } diff --git a/net/server/internal/api_setAdminAccess.go b/net/server/internal/api_setAdminAccess.go new file mode 100644 index 00000000..d38a66f7 --- /dev/null +++ b/net/server/internal/api_setAdminAccess.go @@ -0,0 +1,45 @@ +package databag + +import ( + "encoding/hex" + "github.com/theckman/go-securerandom" + "databag/internal/store" + "gorm.io/gorm" + "gorm.io/gorm/clause" + "net/http" +) + +//SetAdminAccess begins a session for admin access +func SetAdminAccess(w http.ResponseWriter, r *http.Request) { + + // validate login + if code, err := ParamAdminToken(r); err != nil { + ErrResponse(w, code, err) + return + } + + // gernate app token + data, err := securerandom.Bytes(APPTokenSize) + if err != nil { + ErrResponse(w, http.StatusInternalServerError, err) + return + } + access := hex.EncodeToString(data) + + err = store.DB.Transaction(func(tx *gorm.DB) error { + // upsert mfa enabled + if res := tx.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "config_id"}}, + DoUpdates: clause.AssignmentColumns([]string{"str_value"}), + }).Create(&store.Config{ConfigID: CNFAdminSession, StrValue: access}).Error; res != nil { + return res + } + return nil + }) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + WriteResponse(w, access) +} diff --git a/net/server/internal/api_setAdminMFAuth.go b/net/server/internal/api_setAdminMFAuth.go new file mode 100644 index 00000000..61e2333c --- /dev/null +++ b/net/server/internal/api_setAdminMFAuth.go @@ -0,0 +1,92 @@ +package databag + +import ( + "databag/internal/store" + "github.com/pquerna/otp" + "github.com/pquerna/otp/totp" + "gorm.io/gorm" + "gorm.io/gorm/clause" + "net/http" + "errors" + "time" +) + +//SetMultiFactorAuth +func SetAdminMFAuth(w http.ResponseWriter, r *http.Request) { + + // validate login + if code, err := ParamSessionToken(r); err != nil { + ErrResponse(w, code, err) + return + } + + if !getBoolConfigValue(CNFMFAEnabled, false) { + ErrResponse(w, http.StatusMethodNotAllowed, errors.New("totp not enabled")) + return; + } + code := r.FormValue("code") + if code == "" { + ErrResponse(w, http.StatusMethodNotAllowed, errors.New("totp code required")) + return; + } + + curTime := time.Now().Unix() + failedTime := getNumConfigValue(CNFMFAFailedTime, 0); + failedCount := getNumConfigValue(CNFMFAFailedCount, 0); + if failedTime + APPMFAFailPeriod > curTime && failedCount > APPMFAFailCount { + ErrResponse(w, http.StatusTooManyRequests, errors.New("temporarily locked")) + return; + } + + secret := getStrConfigValue(CNFMFASecret, ""); + opts := totp.ValidateOpts{Period: 30, Skew: 1, Digits: otp.DigitsSix, Algorithm: otp.AlgorithmSHA256} + if valid, _ := totp.ValidateCustom(code, secret, time.Now(), opts); !valid { + err := store.DB.Transaction(func(tx *gorm.DB) error { + if failedTime + APPMFAFailPeriod > curTime { + if res := tx.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "config_id"}}, + DoUpdates: clause.AssignmentColumns([]string{"num_value"}), + }).Create(&store.Config{ConfigID: CNFMFAFailedCount, NumValue: failedCount + 1}).Error; res != nil { + return res + } + } else { + if res := tx.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "config_id"}}, + DoUpdates: clause.AssignmentColumns([]string{"num_value"}), + }).Create(&store.Config{ConfigID: CNFMFAFailedTime, NumValue: curTime}).Error; res != nil { + return res + } + if res := tx.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "config_id"}}, + DoUpdates: clause.AssignmentColumns([]string{"num_value"}), + }).Create(&store.Config{ConfigID: CNFMFAFailedCount, NumValue: failedCount + 1}).Error; res != nil { + return res + } + } + return nil + }) + if err != nil { + LogMsg("failed to increment fail count"); + } + + ErrResponse(w, http.StatusUnauthorized, errors.New("invalid code")) + return + } + + err := store.DB.Transaction(func(tx *gorm.DB) error { + // upsert mfa confirmed + if res := tx.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "config_id"}}, + DoUpdates: clause.AssignmentColumns([]string{"str_value"}), + }).Create(&store.Config{ConfigID: CNFMFAConfirmed, BoolValue: true}).Error; res != nil { + return res + } + return nil + }) + if err != nil { + ErrResponse(w, http.StatusInternalServerError, err) + return + } + + WriteResponse(w, nil) +} diff --git a/net/server/internal/api_setNodeAccountStatus.go b/net/server/internal/api_setNodeAccountStatus.go index eaa14776..680b3375 100644 --- a/net/server/internal/api_setNodeAccountStatus.go +++ b/net/server/internal/api_setNodeAccountStatus.go @@ -18,7 +18,7 @@ func SetNodeAccountStatus(w http.ResponseWriter, r *http.Request) { return } - if code, err := ParamAdminToken(r); err != nil { + if code, err := ParamSessionToken(r); err != nil { ErrResponse(w, code, err) return } diff --git a/net/server/internal/api_setNodeConfig.go b/net/server/internal/api_setNodeConfig.go index 2f23b848..fe998057 100644 --- a/net/server/internal/api_setNodeConfig.go +++ b/net/server/internal/api_setNodeConfig.go @@ -11,7 +11,7 @@ import ( func SetNodeConfig(w http.ResponseWriter, r *http.Request) { // validate login - if code, err := ParamAdminToken(r); err != nil { + if code, err := ParamSessionToken(r); err != nil { ErrResponse(w, code, err) return } diff --git a/net/server/internal/authUtil.go b/net/server/internal/authUtil.go index 4ef7c607..5484f2a2 100644 --- a/net/server/internal/authUtil.go +++ b/net/server/internal/authUtil.go @@ -95,6 +95,30 @@ func ParamAdminToken(r *http.Request) (int, error) { return http.StatusOK, nil } +//ParamSessionToken compares session token with token query param +func ParamSessionToken(r *http.Request) (int, error) { + + // parse authentication token + token := r.FormValue("token") + if token == "" { + return http.StatusUnauthorized, errors.New("token not set") + } + + // nothing to do if not configured + if !getBoolConfigValue(CNFConfigured, false) { + return http.StatusUnauthorized, errors.New("node not configured") + } + + // compare password + value := getStrConfigValue(CNFAdminSession, "") + if value != token { + return http.StatusUnauthorized, errors.New("invalid session token") + } + + return http.StatusOK, nil +} + + //GetSessionDetail retrieves account detail specified by agent query param func GetSessionDetail(r *http.Request) (*store.Session, int, error) { diff --git a/net/server/internal/configUtil.go b/net/server/internal/configUtil.go index 4064e63e..56f80786 100644 --- a/net/server/internal/configUtil.go +++ b/net/server/internal/configUtil.go @@ -63,6 +63,23 @@ const CNFIceUsername = "ice_username" //CNFIceUrl specifies the ice candidate url const CNFIcePassword = "ice_password" +//CNFMFAFailedTime start of mfa failure window +const CNFMFAFailedTime = "mfa_failed_time" + +//CNFMFAFailedCount number of failures in window +const CNFMFAFailedCount = "mfa_failed_count" + +//CNFMFARequired specified if mfa enabled for admin +const CNFMFAEnabled = "mfa_enabled" + +//CNFMFAConfirmed specified if mfa has been confirmed for admin +const CNFMFAConfirmed = "mfa_confirmed" + +//CNFMFASecret specified the mfa secret +const CNFMFASecret = "mfa_secret" + +//CNFAdminSession sepcifies the admin session token +const CNFAdminSession = "admin_session" func getStrConfigValue(configID string, empty string) string { var config store.Config diff --git a/net/server/internal/routers.go b/net/server/internal/routers.go index 56e4ea7c..a8ba8823 100644 --- a/net/server/internal/routers.go +++ b/net/server/internal/routers.go @@ -279,6 +279,41 @@ var endpoints = routes{ ImportAccount, }, + route{ + "SetAdminAccess", + strings.ToUpper("Put"), + "/admin/access", + SetAdminAccess, + }, + + route{ + "GetAdminMFAuth", + strings.ToUpper("Get"), + "/admin/mfauth", + GetAdminMFAuth, + }, + + route{ + "AddAdminMFAuth", + strings.ToUpper("Post"), + "/admin/mfauth", + AddAdminMFAuth, + }, + + route{ + "SetAdminMFAuth", + strings.ToUpper("Put"), + "/admin/mfauth", + SetAdminMFAuth, + }, + + route{ + "RemoveAdminMFAuth", + strings.ToUpper("Delete"), + "/admin/mfauth", + RemoveAdminMFAuth, + }, + route{ "RemoveNodeAccount", strings.ToUpper("Delete"), diff --git a/net/web/src/access/admin/useAdmin.hook.js b/net/web/src/access/admin/useAdmin.hook.js index 2cde10e0..1b458dad 100644 --- a/net/web/src/access/admin/useAdmin.hook.js +++ b/net/web/src/access/admin/useAdmin.hook.js @@ -3,6 +3,7 @@ import { useNavigate } from "react-router-dom"; import { getNodeStatus } from 'api/getNodeStatus'; import { setNodeStatus } from 'api/setNodeStatus'; import { getNodeConfig } from 'api/getNodeConfig'; +import { setNodeAccess } from 'api/setNodeAccess'; import { AppContext } from 'context/AppContext'; import { SettingsContext } from 'context/SettingsContext'; @@ -52,9 +53,10 @@ export function useAdmin() { if (state.unclaimed === true) { await setNodeStatus(state.password); } - await getNodeConfig(state.password); + const session = await setNodeAccess(state.password); + updateState({ busy: false }); - app.actions.setAdmin(state.password); + app.actions.setAdmin(session); } catch(err) { console.log(err); diff --git a/net/web/src/api/getAdminMFAuth.js b/net/web/src/api/getAdminMFAuth.js new file mode 100644 index 00000000..c6d9dd47 --- /dev/null +++ b/net/web/src/api/getAdminMFAuth.js @@ -0,0 +1,8 @@ +import { checkResponse, fetchWithTimeout } from './fetchUtil'; + +export async function getAdminMFAuth(token) { + const mfa = await fetchWithTimeout(`/admin/mfauth?token=${encodeURIComponent(token)}`, { method: 'GET' }); + checkResponse(mfa); + return await mfa.json(); +} + diff --git a/net/web/src/api/setNodeAccess.js b/net/web/src/api/setNodeAccess.js new file mode 100644 index 00000000..09315861 --- /dev/null +++ b/net/web/src/api/setNodeAccess.js @@ -0,0 +1,8 @@ +import { checkResponse, fetchWithTimeout } from './fetchUtil'; + +export async function setNodeAccess(token) { + const access = await fetchWithTimeout(`/admin/access?token=${encodeURIComponent(token)}`, { method: 'PUT' }); + checkResponse(access); + return access.json() +} + diff --git a/net/web/src/constants/Strings.js b/net/web/src/constants/Strings.js index b45771e6..854af302 100644 --- a/net/web/src/constants/Strings.js +++ b/net/web/src/constants/Strings.js @@ -195,6 +195,13 @@ export const en = { mfaDisabled: 'verification temporarily disabled', mfaConfirm: 'Confirm', mfaEnter: 'Enter your verification code', + + enableMultifactor: 'Enable multi-factor authentication', + disableMultifactor: 'Disable multi-factor authentication', + + disable: 'Disable', + confirmDisable: 'Disabling Multi-Factor Authentication', + disablePrompt: 'Are you sure you want to disable multi-factor authentication', }; export const fr = { @@ -395,6 +402,13 @@ export const fr = { mfaError: 'erreur de code de vérification', mfaDisabled: 'vérification temporairement désactivée', mfaConfirm: 'Confirmer', + + enableMultifactor: 'Activer l\'authentification multifacteur', + disableMultifactor: 'Désactiver l\'authentification multifacteur', + + disable: 'Désactiver', + confirmDisable: 'Désactivation de l\'authentification multi-facteurs', + disablePrompt: 'Êtes-vous sûr de vouloir désactiver l\'authentification multi-facteurs', }; export const sp = { @@ -594,6 +608,13 @@ export const sp = { mfaError: 'error de código de verificación', mfaDisabled: 'verificación temporalmente deshabilitada', mfaConfirm: 'Confirmar', + + enableMultifactor: 'Habilitar la autenticación multifactor', + disableMultifactor: 'Deshabilitar la autenticación multifactor', + + disable: 'Desactivar', + confirmDisable: 'Desactivación de la autenticación de dos factores', + disablePrompt: '¿Estás seguro de que quieres desactivar la autenticación de dos factores?', }; export const pt = { @@ -793,6 +814,13 @@ export const pt = { mfaError: 'erro de código de verificação', mfaDisabled: 'verificação temporariamente desativada', mfaConfirm: 'Confirmar', + + enableMultifactor: 'Habilitar autenticação multifator', + disableMultifactor: 'Desativar autenticação multifator', + + disable: 'Desativar', + confirmDisable: 'Desativando Autenticação de Dois Fatores', + disablePrompt: 'Tem certeza de que deseja desativar a autenticação de dois fatores?', }; export const de = { @@ -992,6 +1020,13 @@ export const de = { mfaError: 'Verifizierungscodefehler', mfaDisabled: 'Verifizierung vorübergehend deaktiviert', mfaConfirm: 'Bestätigen', + + enableMultifactor: 'Aktivieren Sie die Multi-Faktor-Authentifizierung', + disableMultifactor: 'Deaktivieren Sie die Multi-Faktor-Authentifizierung', + + disable: 'Deaktivieren', + confirmDisable: 'Deaktivierung der Zwei-Faktor-Authentifizierung', + disablePrompt: 'Sind Sie sicher, dass Sie die Zwei-Faktor-Authentifizierung deaktivieren möchten?', }; export const ru = { @@ -1191,4 +1226,11 @@ export const ru = { mfaError: 'ошибка проверочного кода', mfaDisabled: 'проверка временно отключена', mfaConfirm: 'Подтвердить', + + enableMultifactor: 'Включить многофакторную аутентификацию', + disableMultifactor: 'Отключить многофакторную аутентификацию', + + disable: 'Отключить', + confirmDisable: 'Отключение двухфакторной аутентификации', + disablePrompt: 'Вы уверены, что хотите отключить двухфакторную аутентификацию?', }; diff --git a/net/web/src/dashboard/Dashboard.jsx b/net/web/src/dashboard/Dashboard.jsx index bb0cb201..8be6658b 100644 --- a/net/web/src/dashboard/Dashboard.jsx +++ b/net/web/src/dashboard/Dashboard.jsx @@ -1,6 +1,6 @@ import { AlertIcon, DashboardWrapper, SettingsButton, AddButton, SettingsLayout, CreateLayout } from './Dashboard.styled'; import { Tooltip, Switch, Select, Button, Space, Modal, Input, InputNumber, List } from 'antd'; -import { ExclamationCircleOutlined, SettingOutlined, UserAddOutlined, LogoutOutlined, ReloadOutlined } from '@ant-design/icons'; +import { ExclamationCircleOutlined, SettingOutlined, UserAddOutlined, LogoutOutlined, ReloadOutlined, LockOutlined, UnlockOutlined } from '@ant-design/icons'; import { ThemeProvider } from "styled-components"; import { useDashboard } from './useDashboard.hook'; import { AccountItem } from './accountItem/AccountItem'; @@ -8,6 +8,7 @@ import { CopyButton } from '../copyButton/CopyButton'; export function Dashboard() { + const [ modal, modalContext ] = Modal.useModal(); const { state, actions } = useDashboard(); const onClipboard = async (value) => { @@ -18,9 +19,26 @@ export function Dashboard() { return window.location.origin + '/#/create?add=' + state.createToken; }; + const disableMFA = () => { + modal.confirm({ + title: {state.strings.confirmDisable}, + content: {state.strings.disablePrompt}, + icon: , + bodyStyle: { borderRadius: 8, padding: 16, ...state.menuStyle }, + okText: state.strings.disable, + cancelText: state.strings.cancel, + onOk() { + actions.disableMFA(); + }, + onCancel() {}, + }); + } + + return ( + { modalContext }
{ state.strings.accounts }
@@ -34,6 +52,18 @@ export function Dashboard() { } onClick={() => actions.setShowSettings(true)}>
+ { (state.mfAuthSet && state.mfaAuthEnabled) && ( +
+ } + onClick={disableMFA}> +
+ )} + { (state.mfAuthSet && !state.mfaAuthEnabled) && ( +
+ } + onClick={actions.enableMFA}> +
+ )}
} onClick={() => actions.logout()}> @@ -63,6 +93,22 @@ export function Dashboard() { onClick={() => actions.setShowSettings(true)}>
+ { (state.mfAuthSet && state.mfaAuthEnabled) && ( +
+ + } + onClick={disableMFA}> + +
+ )} + { (state.mfAuthSet && !state.mfaAuthEnabled) && ( +
+ + } + onClick={actions.enableMFA}> + +
+ )}
} diff --git a/net/web/src/dashboard/useDashboard.hook.js b/net/web/src/dashboard/useDashboard.hook.js index 448e55d0..7fcac88d 100644 --- a/net/web/src/dashboard/useDashboard.hook.js +++ b/net/web/src/dashboard/useDashboard.hook.js @@ -8,7 +8,10 @@ import { useNavigate } from 'react-router-dom'; import { AppContext } from 'context/AppContext'; import { SettingsContext } from 'context/SettingsContext'; -export function useDashboard() { +import { getAdminMFAuth } from 'api/getAdminMFAuth'; + + +export function useDashboard(token) { const [state, setState] = useState({ domain: "", @@ -38,6 +41,12 @@ export function useDashboard() { colors: {}, menuStyle: {}, strings: {}, + + mfAuthSet: false, + mfAuthEnabled: false, + mfAuthSecretText: null, + mfAuthSecretImage: null, + mfaAuthError: null, }); const navigate = useNavigate(); @@ -140,6 +149,10 @@ export function useDashboard() { await syncConfig(); await syncAccounts(); }, + enableMFA: async () => { + }, + disableMFA: async () => { + }, setSettings: async () => { if (!state.busy) { updateState({ busy: true }); @@ -161,10 +174,11 @@ export function useDashboard() { const syncConfig = async () => { try { + const enabled = await getAdminMFAuth(app.state.adminToken); const config = await getNodeConfig(app.state.adminToken); const { accountStorage, domain, keyType, pushSupported, transformSupported, allowUnsealed, enableImage, enableAudio, enableVideo, enableBinary, enableIce, iceUrl, iceUsername, icePassword, enableOpenAccess, openAccessLimit } = config; const storage = Math.ceil(accountStorage / 1073741824); - updateState({ configError: false, domain, accountStorage: storage, keyType, enableImage, enableAudio, enableVideo, enableBinary, pushSupported, transformSupported, allowUnsealed, enableIce, iceUrl, iceUsername, icePassword, enableOpenAccess, openAccessLimit }); + updateState({ mfAuthSet: true, mfaAuthEnabled: enabled, configError: false, domain, accountStorage: storage, keyType, enableImage, enableAudio, enableVideo, enableBinary, pushSupported, transformSupported, allowUnsealed, enableIce, iceUrl, iceUsername, icePassword, enableOpenAccess, openAccessLimit }); } catch(err) { console.log(err);