Skip to content

Commit 939ce65

Browse files
authored
login: allow 2fa code input if mandated (#175)
Signed-off-by: Abhishek Kumar <[email protected]>
1 parent 94da963 commit 939ce65

File tree

3 files changed

+127
-10
lines changed

3 files changed

+127
-10
lines changed

cmd/network.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,84 @@ 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+
if !r.Config.HasShell {
88+
return nil
89+
}
90+
config.Debug("Checking if 2FA is enabled and verified for the user ", response)
91+
found, is2faEnabled := getResponseBooleanValue(response, "is2faenabled")
92+
if !found || !is2faEnabled {
93+
config.Debug("2FA is not enabled for the user, skipping 2FA validation")
94+
return nil
95+
}
96+
found, is2faVerified := getResponseBooleanValue(response, "is2faverified")
97+
if !found || is2faVerified {
98+
config.Debug("2FA is already verified for the user, skipping 2FA validation")
99+
return nil
100+
}
101+
activeSpinners := r.Config.PauseActiveSpinners()
102+
fmt.Print("Enter 2FA code: ")
103+
var code string
104+
fmt.Scanln(&code)
105+
if activeSpinners > 0 {
106+
r.Config.ResumePausedSpinners()
107+
}
108+
params := make(url.Values)
109+
params.Add("command", "validateUserTwoFactorAuthenticationCode")
110+
params.Add("codefor2fa", code)
111+
params.Add("sessionkey", sessionKey)
112+
113+
msURL, _ := url.Parse(r.Config.ActiveProfile.URL)
114+
115+
config.Debug("Validating 2FA with POST URL:", msURL, params)
116+
spinner := r.Config.StartSpinner("trying to validate 2FA...")
117+
resp, err := r.Client().PostForm(msURL.String(), params)
118+
r.Config.StopSpinner(spinner)
119+
if err != nil {
120+
return errors.New("failed to failed to validate 2FA code: " + err.Error())
121+
}
122+
config.Debug("ValidateUserTwoFactorAuthenticationCode POST response status code:", resp.StatusCode)
123+
if resp.StatusCode != http.StatusOK {
124+
r.Client().Jar, _ = cookiejar.New(nil)
125+
return errors.New("failed to validate 2FA code, please check the code. Invalidating session")
126+
}
127+
return nil
128+
}
129+
52130
// Login logs in a user based on provided request and returns http client and session key
53131
func Login(r *Request) (string, error) {
54132
params := make(url.Values)
@@ -81,6 +159,13 @@ func Login(r *Request) (string, error) {
81159
return "", e
82160
}
83161

162+
body, _ := ioutil.ReadAll(resp.Body)
163+
config.Debug("Login response body:", string(body))
164+
loginResponse, err := getLoginResponse(body)
165+
if err != nil {
166+
return "", err
167+
}
168+
84169
var sessionKey string
85170
curTime := time.Now()
86171
expiryDuration := 15 * time.Minute
@@ -98,6 +183,9 @@ func Login(r *Request) (string, error) {
98183
}()
99184

100185
config.Debug("Login sessionkey:", sessionKey)
186+
if err := checkLogin2FAPromptAndValidate(r, loginResponse, sessionKey); err != nil {
187+
return "", err
188+
}
101189
return sessionKey, nil
102190
}
103191

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)