Skip to content

Commit 70593ff

Browse files
committed
login: allow 2fa code input if mandated
Fixes #145 Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
1 parent b7eb34a commit 70593ff

File tree

3 files changed

+124
-10
lines changed

3 files changed

+124
-10
lines changed

cmd/network.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,81 @@ func findSessionCookie(cookies []*http.Cookie) *http.Cookie {
4949
return nil
5050
}
5151

52+
func getLoginResponse(responseBody []byte) (map[string]interface{}, error) {
53+
var responseMap map[string]interface{}
54+
err := json.Unmarshal(responseBody, &responseMap)
55+
if err != nil {
56+
return nil, errors.New("failed to parse login response: " + err.Error())
57+
}
58+
loginRespRaw, ok := responseMap["loginresponse"]
59+
if !ok {
60+
return nil, errors.New("failed to parse login response, expected 'loginresponse' key not found")
61+
}
62+
loginResponse, ok := loginRespRaw.(map[string]interface{})
63+
if !ok {
64+
return nil, errors.New("failed to parse login response, expected 'loginresponse' to be a map")
65+
}
66+
return loginResponse, nil
67+
}
68+
69+
func getResponseBooleanValue(response map[string]interface{}, key string) (bool, bool) {
70+
v, found := response[key]
71+
if !found {
72+
return false, false
73+
}
74+
switch value := v.(type) {
75+
case bool:
76+
return true, value
77+
case string:
78+
return true, strings.ToLower(value) == "true"
79+
case float64:
80+
return true, value != 0
81+
default:
82+
return true, false
83+
}
84+
}
85+
86+
func checkLogin2FAPromptAndValidate(r *Request, response map[string]interface{}, sessionKey string) error {
87+
config.Debug("Checking if 2FA is enabled and verified for the user ", response)
88+
found, is2faEnabled := getResponseBooleanValue(response, "is2faenabled")
89+
if !found || !is2faEnabled {
90+
config.Debug("2FA is not enabled for the user, skipping 2FA validation")
91+
return nil
92+
}
93+
found, is2faVerified := getResponseBooleanValue(response, "is2faverified")
94+
if !found || is2faVerified {
95+
config.Debug("2FA is already verified for the user, skipping 2FA validation")
96+
return nil
97+
}
98+
activeSpinners := r.Config.PauseActiveSpinners()
99+
fmt.Print("Enter 2FA code: ")
100+
var code string
101+
fmt.Scanln(&code)
102+
if activeSpinners > 0 {
103+
r.Config.ResumePausedSpinners()
104+
}
105+
params := make(url.Values)
106+
params.Add("command", "validateUserTwoFactorAuthenticationCode")
107+
params.Add("codefor2fa", code)
108+
params.Add("sessionkey", sessionKey)
109+
110+
msURL, _ := url.Parse(r.Config.ActiveProfile.URL)
111+
112+
config.Debug("Validating 2FA with POST URL:", msURL, params)
113+
spinner := r.Config.StartSpinner("trying to validate 2FA...")
114+
resp, err := r.Client().PostForm(msURL.String(), params)
115+
r.Config.StopSpinner(spinner)
116+
if err != nil {
117+
return errors.New("failed to failed to validate 2FA code: " + err.Error())
118+
}
119+
config.Debug("ValidateUserTwoFactorAuthenticationCode POST response status code:", resp.StatusCode)
120+
if resp.StatusCode != http.StatusOK {
121+
r.Client().Jar, _ = cookiejar.New(nil)
122+
return errors.New("failed to validate 2FA code, please check the code. Invalidating session")
123+
}
124+
return nil
125+
}
126+
52127
// Login logs in a user based on provided request and returns http client and session key
53128
func Login(r *Request) (string, error) {
54129
params := make(url.Values)
@@ -81,6 +156,13 @@ func Login(r *Request) (string, error) {
81156
return "", e
82157
}
83158

159+
body, _ := ioutil.ReadAll(resp.Body)
160+
config.Debug("Login response body:", string(body))
161+
loginResponse, err := getLoginResponse(body)
162+
if err != nil {
163+
return "", err
164+
}
165+
84166
var sessionKey string
85167
curTime := time.Now()
86168
expiryDuration := 15 * time.Minute
@@ -98,6 +180,9 @@ func Login(r *Request) (string, error) {
98180
}()
99181

100182
config.Debug("Login sessionkey:", sessionKey)
183+
if err := checkLogin2FAPromptAndValidate(r, loginResponse, sessionKey); err != nil {
184+
return "", err
185+
}
101186
return sessionKey, nil
102187
}
103188

config/config.go

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"strconv"
3131
"time"
3232

33+
"github.com/briandowns/spinner"
3334
"github.com/gofrs/flock"
3435
homedir "github.com/mitchellh/go-homedir"
3536
ini "gopkg.in/ini.v1"
@@ -73,16 +74,17 @@ type Core struct {
7374

7475
// Config describes CLI config file and default options
7576
type Config struct {
76-
Dir string
77-
ConfigFile string
78-
HistoryFile string
79-
LogFile string
80-
HasShell bool
81-
Core *Core
82-
ActiveProfile *ServerProfile
83-
Context *context.Context
84-
Cancel context.CancelFunc
85-
C chan bool
77+
Dir string
78+
ConfigFile string
79+
HistoryFile string
80+
LogFile string
81+
HasShell bool
82+
Core *Core
83+
ActiveProfile *ServerProfile
84+
Context *context.Context
85+
Cancel context.CancelFunc
86+
C chan bool
87+
activeSpinners []*spinner.Spinner
8688
}
8789

8890
// GetOutputFormats returns the supported output formats.

config/spinner.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,39 @@ func (c *Config) StartSpinner(suffix string) *spinner.Spinner {
4040
waiter := spinner.New(cursor, 200*time.Millisecond)
4141
waiter.Suffix = " " + suffix
4242
waiter.Start()
43+
c.activeSpinners = append(c.activeSpinners, waiter)
4344
return waiter
4445
}
4546

4647
// StopSpinner stops the provided spinner if it is valid
4748
func (c *Config) StopSpinner(waiter *spinner.Spinner) {
4849
if waiter != nil {
4950
waiter.Stop()
51+
for i, s := range c.activeSpinners {
52+
if s == waiter {
53+
c.activeSpinners = append(c.activeSpinners[:i], c.activeSpinners[i+1:]...)
54+
break
55+
}
56+
}
57+
}
58+
}
59+
60+
// PauseActiveSpinners stops the spinners without removing them from the acive spinners list, allowing resume.
61+
func (c *Config) PauseActiveSpinners() int {
62+
count := len(c.activeSpinners)
63+
for _, s := range c.activeSpinners {
64+
if s != nil && s.Active() {
65+
s.Stop()
66+
}
67+
}
68+
return count
69+
}
70+
71+
// ResumePausedSpinners restarts the spinners from the active spinners list if they are not already running.
72+
func (c *Config) ResumePausedSpinners() {
73+
for _, s := range c.activeSpinners {
74+
if s != nil && !s.Active() {
75+
s.Start()
76+
}
5077
}
5178
}

0 commit comments

Comments
 (0)