From 06c0dbc9b141076d9f351cce410a454ccaf6873b Mon Sep 17 00:00:00 2001 From: Christopher Virtucio Date: Thu, 14 Nov 2024 13:18:25 -0500 Subject: [PATCH 1/4] added browser_autofill_no_username to disable inputting username in login page --- pkg/cfg/cfg.go | 1 + pkg/provider/browser/browser.go | 40 ++++++++++--------- pkg/provider/browser/browser_test.go | 33 ++++++++++++++- .../example/loginpage-without-username.html | 36 +++++++++++++++++ 4 files changed, 91 insertions(+), 19 deletions(-) create mode 100644 pkg/provider/browser/example/loginpage-without-username.html diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go index 101491c04..bc4cbb105 100644 --- a/pkg/cfg/cfg.go +++ b/pkg/cfg/cfg.go @@ -42,6 +42,7 @@ type IDPAccount struct { BrowserType string `ini:"browser_type,omitempty"` // used by 'Browser' Provider BrowserExecutablePath string `ini:"browser_executable_path,omitempty"` // used by 'Browser' Provider BrowserAutoFill bool `ini:"browser_autofill,omitempty"` // used by 'Browser' Provider + BrowserAutoFillNoUsername bool `ini:"browser_autofill_no_username,omitempty"` // used by 'Browser' Provider MFA string `ini:"mfa"` MFAIPAddress string `ini:"mfa_ip_address"` // used by OneLogin SkipVerify bool `ini:"skip_verify"` diff --git a/pkg/provider/browser/browser.go b/pkg/provider/browser/browser.go index 0681589b1..67c61812c 100644 --- a/pkg/provider/browser/browser.go +++ b/pkg/provider/browser/browser.go @@ -27,17 +27,20 @@ type Client struct { BrowserDriverDir string Timeout int BrowserAutoFill bool + // If true, avoid auto-filling the username in the login form on the browser. + BrowserAutoFillNoUsername bool } // New create new browser based client func New(idpAccount *cfg.IDPAccount) (*Client, error) { return &Client{ - Headless: idpAccount.Headless, - BrowserDriverDir: idpAccount.BrowserDriverDir, - BrowserType: strings.ToLower(idpAccount.BrowserType), - BrowserExecutablePath: idpAccount.BrowserExecutablePath, - Timeout: idpAccount.Timeout, - BrowserAutoFill: idpAccount.BrowserAutoFill, + Headless: idpAccount.Headless, + BrowserDriverDir: idpAccount.BrowserDriverDir, + BrowserType: strings.ToLower(idpAccount.BrowserType), + BrowserExecutablePath: idpAccount.BrowserExecutablePath, + Timeout: idpAccount.Timeout, + BrowserAutoFill: idpAccount.BrowserAutoFill, + BrowserAutoFillNoUsername: idpAccount.BrowserAutoFillNoUsername, }, nil } @@ -176,7 +179,7 @@ var getSAMLResponse = func(page playwright.Page, loginDetails *creds.LoginDetail } if client.BrowserAutoFill { - err := autoFill(page, loginDetails) + err := autoFill(page, loginDetails, client.BrowserAutoFillNoUsername) if err != nil { logger.Error("error when auto filling", err) } @@ -202,7 +205,7 @@ var getSAMLResponse = func(page playwright.Page, loginDetails *creds.LoginDetail return values.Get("SAMLResponse"), nil } -var autoFill = func(page playwright.Page, loginDetails *creds.LoginDetails) error { +var autoFill = func(page playwright.Page, loginDetails *creds.LoginDetails, browserAutoFillNoUsername bool) error { passwordField := page.Locator("input[type='password']") err := passwordField.WaitFor(playwright.LocatorWaitForOptions{ State: playwright.WaitForSelectorStateVisible, @@ -217,17 +220,18 @@ var autoFill = func(page playwright.Page, loginDetails *creds.LoginDetails) erro return err } - keyboard := page.Keyboard() - - // move to username field which is above password field - err = keyboard.Press("Shift+Tab") - if err != nil { - return err - } + if !browserAutoFillNoUsername { + keyboard := page.Keyboard() + // move to username field which is above password field + err = keyboard.Press("Shift+Tab") + if err != nil { + return err + } - err = keyboard.InsertText(loginDetails.Username) - if err != nil { - return err + err = keyboard.InsertText(loginDetails.Username) + if err != nil { + return err + } } // Find the submit button or input of the form that the password field is in diff --git a/pkg/provider/browser/browser_test.go b/pkg/provider/browser/browser_test.go index af702db2a..b0b9034f5 100644 --- a/pkg/provider/browser/browser_test.go +++ b/pkg/provider/browser/browser_test.go @@ -203,6 +203,37 @@ func TestExpectRequestOptionsDefaultTimeout(t *testing.T) { } func TestAutoFill(t *testing.T) { + pageLocations := []string{ + "example/loginpage-without-username.html", + } + // iterate over each login page + for _, pageLocation := range pageLocations { + data, err := os.ReadFile(pageLocation) + require.Nil(t, err) + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write(data) + })) + defer ts.Close() + + pw, _ := playwright.Run() + browser, _ := pw.Chromium.Launch() + context, _ := browser.NewContext() + page, _ := context.NewPage() + _, _ = page.Goto(ts.URL) + + loginDetails := &creds.LoginDetails{URL: ts.URL, Username: "golang", Password: "gopher"} + _ = autoFill(page, loginDetails, true) + + password, _ := page.Locator("input[type='password']").First().InputValue() + assert.Equal(t, "gopher", password) + + result, _ := page.Locator("div#result").Evaluate("el => el.innerText", nil) + assert.Equal(t, "golang:gopher", result) + } +} + +func TestAutoFillNoUsername(t *testing.T) { // 3 different login pages pageLocations := []string{ "example/loginpage.html", @@ -226,7 +257,7 @@ func TestAutoFill(t *testing.T) { _, _ = page.Goto(ts.URL) loginDetails := &creds.LoginDetails{URL: ts.URL, Username: "golang", Password: "gopher"} - _ = autoFill(page, loginDetails) + _ = autoFill(page, loginDetails, false) username, _ := page.Locator("input[name='username']").First().InputValue() assert.Equal(t, "golang", username) diff --git a/pkg/provider/browser/example/loginpage-without-username.html b/pkg/provider/browser/example/loginpage-without-username.html new file mode 100644 index 000000000..c0e2c935c --- /dev/null +++ b/pkg/provider/browser/example/loginpage-without-username.html @@ -0,0 +1,36 @@ + + + Fake Login Page + + +
+
+ + +
+ +
Login
+
+ + + + + + \ No newline at end of file From faf5b4dc0f6e3db647e3ed1a7e50d710bc19e669 Mon Sep 17 00:00:00 2001 From: Christopher Virtucio Date: Thu, 14 Nov 2024 14:57:50 -0500 Subject: [PATCH 2/4] best effort input of username and password instead of disabling username input completely --- pkg/cfg/cfg.go | 1 - pkg/provider/browser/browser.go | 57 ++++++++++++++++++---------- pkg/provider/browser/browser_test.go | 4 +- 3 files changed, 40 insertions(+), 22 deletions(-) diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go index bc4cbb105..101491c04 100644 --- a/pkg/cfg/cfg.go +++ b/pkg/cfg/cfg.go @@ -42,7 +42,6 @@ type IDPAccount struct { BrowserType string `ini:"browser_type,omitempty"` // used by 'Browser' Provider BrowserExecutablePath string `ini:"browser_executable_path,omitempty"` // used by 'Browser' Provider BrowserAutoFill bool `ini:"browser_autofill,omitempty"` // used by 'Browser' Provider - BrowserAutoFillNoUsername bool `ini:"browser_autofill_no_username,omitempty"` // used by 'Browser' Provider MFA string `ini:"mfa"` MFAIPAddress string `ini:"mfa_ip_address"` // used by OneLogin SkipVerify bool `ini:"skip_verify"` diff --git a/pkg/provider/browser/browser.go b/pkg/provider/browser/browser.go index 67c61812c..0cabc3691 100644 --- a/pkg/provider/browser/browser.go +++ b/pkg/provider/browser/browser.go @@ -27,8 +27,6 @@ type Client struct { BrowserDriverDir string Timeout int BrowserAutoFill bool - // If true, avoid auto-filling the username in the login form on the browser. - BrowserAutoFillNoUsername bool } // New create new browser based client @@ -40,7 +38,6 @@ func New(idpAccount *cfg.IDPAccount) (*Client, error) { BrowserExecutablePath: idpAccount.BrowserExecutablePath, Timeout: idpAccount.Timeout, BrowserAutoFill: idpAccount.BrowserAutoFill, - BrowserAutoFillNoUsername: idpAccount.BrowserAutoFillNoUsername, }, nil } @@ -179,7 +176,7 @@ var getSAMLResponse = func(page playwright.Page, loginDetails *creds.LoginDetail } if client.BrowserAutoFill { - err := autoFill(page, loginDetails, client.BrowserAutoFillNoUsername) + err := autoFill(page, loginDetails) if err != nil { logger.Error("error when auto filling", err) } @@ -205,30 +202,52 @@ var getSAMLResponse = func(page playwright.Page, loginDetails *creds.LoginDetail return values.Get("SAMLResponse"), nil } -var autoFill = func(page playwright.Page, loginDetails *creds.LoginDetails, browserAutoFillNoUsername bool) error { - passwordField := page.Locator("input[type='password']") - err := passwordField.WaitFor(playwright.LocatorWaitForOptions{ - State: playwright.WaitForSelectorStateVisible, - }) - +func locatedExists(locator playwright.Locator) (bool, error) { + count, err := locator.Count() if err != nil { - return err + return false, err } - err = passwordField.Fill(loginDetails.Password) + return count > 0, nil +} + +func float64Ptr(n int) *float64 { + f64 := float64(n) + return &f64 +} + +var autoFill = func(page playwright.Page, loginDetails *creds.LoginDetails) error { + passwordField := page.Locator("input[type='password']") + _ = passwordField.WaitFor(playwright.LocatorWaitForOptions{ + State: playwright.WaitForSelectorStateVisible, + Timeout: float64Ptr(10000), + }) + + passwordFieldExists, err := locatedExists(passwordField) if err != nil { return err } - if !browserAutoFillNoUsername { - keyboard := page.Keyboard() - // move to username field which is above password field - err = keyboard.Press("Shift+Tab") + if passwordFieldExists { + err = passwordField.Fill(loginDetails.Password) if err != nil { return err } + } + + usernameField := page.Locator("input[name='username']") + _ = usernameField.WaitFor(playwright.LocatorWaitForOptions{ + State: playwright.WaitForSelectorStateVisible, + Timeout: float64Ptr(10000), + }) + + usernameFieldExists, err := locatedExists(usernameField) + if err != nil { + return err + } - err = keyboard.InsertText(loginDetails.Username) + if usernameFieldExists { + err = usernameField.Fill(loginDetails.Username) if err != nil { return err } @@ -238,13 +257,13 @@ var autoFill = func(page playwright.Page, loginDetails *creds.LoginDetails, brow submitLocator := page.Locator("form", playwright.PageLocatorOptions{ Has: passwordField, }).Locator("[type='submit']") - count, err := submitLocator.Count() + submitLocatorExists, err := locatedExists(submitLocator) if err != nil { return err } // when submit locator exists, Click it - if count > 0 { + if submitLocatorExists { return submitLocator.Click() } else { // Use javascript to submit the form when no submit input or button is found _, err := page.Evaluate(`document.querySelector('input[type="password"]').form.submit()`, nil) diff --git a/pkg/provider/browser/browser_test.go b/pkg/provider/browser/browser_test.go index b0b9034f5..f5020f51e 100644 --- a/pkg/provider/browser/browser_test.go +++ b/pkg/provider/browser/browser_test.go @@ -223,7 +223,7 @@ func TestAutoFill(t *testing.T) { _, _ = page.Goto(ts.URL) loginDetails := &creds.LoginDetails{URL: ts.URL, Username: "golang", Password: "gopher"} - _ = autoFill(page, loginDetails, true) + _ = autoFill(page, loginDetails) password, _ := page.Locator("input[type='password']").First().InputValue() assert.Equal(t, "gopher", password) @@ -257,7 +257,7 @@ func TestAutoFillNoUsername(t *testing.T) { _, _ = page.Goto(ts.URL) loginDetails := &creds.LoginDetails{URL: ts.URL, Username: "golang", Password: "gopher"} - _ = autoFill(page, loginDetails, false) + _ = autoFill(page, loginDetails) username, _ := page.Locator("input[name='username']").First().InputValue() assert.Equal(t, "golang", username) From 0ae760233489109ad4a5b8966e8022d8cb76ae81 Mon Sep 17 00:00:00 2001 From: Christopher Virtucio Date: Thu, 14 Nov 2024 16:04:13 -0500 Subject: [PATCH 3/4] reduced timeout to 5 seconds per node --- pkg/provider/browser/browser.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/provider/browser/browser.go b/pkg/provider/browser/browser.go index 0cabc3691..fd3e8bd2a 100644 --- a/pkg/provider/browser/browser.go +++ b/pkg/provider/browser/browser.go @@ -220,7 +220,7 @@ var autoFill = func(page playwright.Page, loginDetails *creds.LoginDetails) erro passwordField := page.Locator("input[type='password']") _ = passwordField.WaitFor(playwright.LocatorWaitForOptions{ State: playwright.WaitForSelectorStateVisible, - Timeout: float64Ptr(10000), + Timeout: float64Ptr(5000), }) passwordFieldExists, err := locatedExists(passwordField) @@ -238,7 +238,7 @@ var autoFill = func(page playwright.Page, loginDetails *creds.LoginDetails) erro usernameField := page.Locator("input[name='username']") _ = usernameField.WaitFor(playwright.LocatorWaitForOptions{ State: playwright.WaitForSelectorStateVisible, - Timeout: float64Ptr(10000), + Timeout: float64Ptr(5000), }) usernameFieldExists, err := locatedExists(usernameField) From fcc087c31740b930bbbe119e0d1dd55963271e92 Mon Sep 17 00:00:00 2001 From: Christopher Virtucio Date: Fri, 15 Nov 2024 12:27:02 -0500 Subject: [PATCH 4/4] reduced timeout for password field; no longer waiting for username field --- pkg/provider/browser/browser.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/pkg/provider/browser/browser.go b/pkg/provider/browser/browser.go index fd3e8bd2a..40661b53e 100644 --- a/pkg/provider/browser/browser.go +++ b/pkg/provider/browser/browser.go @@ -220,7 +220,7 @@ var autoFill = func(page playwright.Page, loginDetails *creds.LoginDetails) erro passwordField := page.Locator("input[type='password']") _ = passwordField.WaitFor(playwright.LocatorWaitForOptions{ State: playwright.WaitForSelectorStateVisible, - Timeout: float64Ptr(5000), + Timeout: float64Ptr(3000), }) passwordFieldExists, err := locatedExists(passwordField) @@ -235,12 +235,7 @@ var autoFill = func(page playwright.Page, loginDetails *creds.LoginDetails) erro } } - usernameField := page.Locator("input[name='username']") - _ = usernameField.WaitFor(playwright.LocatorWaitForOptions{ - State: playwright.WaitForSelectorStateVisible, - Timeout: float64Ptr(5000), - }) - + usernameField := page.Locator("input[name='username']") // no need to wait since we can assume at this point that the form has been loaded usernameFieldExists, err := locatedExists(usernameField) if err != nil { return err