diff --git a/cmd/saml2aws/main.go b/cmd/saml2aws/main.go index 2a41c653e..7cba1d555 100644 --- a/cmd/saml2aws/main.go +++ b/cmd/saml2aws/main.go @@ -74,6 +74,7 @@ func main() { app.Flag("idp-provider", "The configured IDP provider. (env: SAML2AWS_IDP_PROVIDER)").Envar("SAML2AWS_IDP_PROVIDER").EnumVar(&commonFlags.IdpProvider, "Akamai", "AzureAD", "ADFS", "ADFS2", "Browser", "GoogleApps", "Ping", "JumpCloud", "Okta", "OneLogin", "PSU", "KeyCloak", "F5APM", "Shibboleth", "ShibbolethECP", "NetIQ", "Auth0") app.Flag("browser-type", "The configured browser type when the IDP provider is set to Browser. if not set 'chromium' will be used. (env: SAML2AWS_BROWSER_TYPE)").Envar("SAML2AWS_BROWSER_TYPE").EnumVar(&commonFlags.BrowserType, "chromium", "firefox", "webkit", "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", "msedge-canary") app.Flag("browser-executable-path", "The configured browser full path when the IDP provider is set to Browser. If set, no browser download will be performed and the executable path will be used instead. (env: SAML2AWS_BROWSER_EXECUTABLE_PATH)").Envar("SAML2AWS_BROWSER_EXECUTABLE_PATH").StringVar(&commonFlags.BrowserExecutablePath) + app.Flag("browser-autofill", "Configures browser to autofill the username and password. (env: SAML2AWS_BROWSER_AUTOFILL)").Envar("SAML2AWS_BROWSER_AUTOFILL").BoolVar(&commonFlags.BrowserAutoFill) app.Flag("mfa", "The name of the mfa. (env: SAML2AWS_MFA)").Envar("SAML2AWS_MFA").StringVar(&commonFlags.MFA) app.Flag("skip-verify", "Skip verification of server certificate. (env: SAML2AWS_SKIP_VERIFY)").Envar("SAML2AWS_SKIP_VERIFY").Short('s').BoolVar(&commonFlags.SkipVerify) app.Flag("url", "The URL of the SAML IDP server used to login. (env: SAML2AWS_URL)").Envar("SAML2AWS_URL").StringVar(&commonFlags.URL) diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go index 8051c8976..01644303c 100644 --- a/pkg/cfg/cfg.go +++ b/pkg/cfg/cfg.go @@ -41,6 +41,7 @@ type IDPAccount struct { Provider string `ini:"provider"` 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 MFA string `ini:"mfa"` MFAIPAddress string `ini:"mfa_ip_address"` // used by OneLogin SkipVerify bool `ini:"skip_verify"` diff --git a/pkg/flags/flags.go b/pkg/flags/flags.go index 1848358c3..0e7607ef7 100644 --- a/pkg/flags/flags.go +++ b/pkg/flags/flags.go @@ -14,6 +14,7 @@ type CommonFlags struct { IdpProvider string BrowserType string BrowserExecutablePath string + BrowserAutoFill bool MFA string MFAIPAddress string MFAToken string @@ -83,6 +84,10 @@ func ApplyFlagOverrides(commonFlags *CommonFlags, account *cfg.IDPAccount) { account.BrowserExecutablePath = commonFlags.BrowserExecutablePath } + if commonFlags.BrowserAutoFill { + account.BrowserAutoFill = commonFlags.BrowserAutoFill + } + if commonFlags.MFA != "" { account.MFA = commonFlags.MFA } diff --git a/pkg/provider/browser/browser.go b/pkg/provider/browser/browser.go index faa605368..5292af6de 100644 --- a/pkg/provider/browser/browser.go +++ b/pkg/provider/browser/browser.go @@ -25,6 +25,7 @@ type Client struct { // Setup alternative directory to download playwright browsers to BrowserDriverDir string Timeout int + BrowserAutoFill bool } // New create new browser based client @@ -35,6 +36,7 @@ func New(idpAccount *cfg.IDPAccount) (*Client, error) { BrowserType: strings.ToLower(idpAccount.BrowserType), BrowserExecutablePath: idpAccount.BrowserExecutablePath, Timeout: idpAccount.Timeout, + BrowserAutoFill: idpAccount.BrowserAutoFill, }, nil } @@ -132,6 +134,13 @@ var getSAMLResponse = func(page playwright.Page, loginDetails *creds.LoginDetail return "", err } + if client.BrowserAutoFill { + err := autoFill(page, loginDetails) + if err != nil { + logger.Error("error when auto filling", err) + } + } + // https://docs.aws.amazon.com/general/latest/gr/signin-service.html // https://docs.amazonaws.cn/en_us/aws/latest/userguide/endpoints-Ningxia.html // https://docs.amazonaws.cn/en_us/aws/latest/userguide/endpoints-Beijing.html @@ -155,6 +164,40 @@ var getSAMLResponse = func(page playwright.Page, loginDetails *creds.LoginDetail return values.Get("SAMLResponse"), nil } +var autoFill = func(page playwright.Page, loginDetails *creds.LoginDetails) error { + passwordField := page.Locator("input[type='password']") + err := passwordField.WaitFor(playwright.LocatorWaitForOptions{ + State: playwright.WaitForSelectorStateVisible, + }) + + if err != nil { + return err + } + + err = passwordField.Fill(loginDetails.Password) + if err != nil { + return err + } + + 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 + } + + // Find the submit button of the form that the password field is in + return page.Locator("form", playwright.PageLocatorOptions{ + Has: passwordField, + }).Locator("input[type='submit']").Click() +} + func signinRegex() (*regexp.Regexp, error) { // https://docs.aws.amazon.com/general/latest/gr/signin-service.html // https://docs.amazonaws.cn/en_us/aws/latest/userguide/endpoints-Ningxia.html diff --git a/pkg/provider/browser/browser_test.go b/pkg/provider/browser/browser_test.go index d74cc0567..a6744af5c 100644 --- a/pkg/provider/browser/browser_test.go +++ b/pkg/provider/browser/browser_test.go @@ -1,11 +1,15 @@ package browser import ( + "net/http" + "net/http/httptest" "net/url" + "os" "testing" "github.com/playwright-community/playwright-go" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/versent/saml2aws/v2/mocks" "github.com/versent/saml2aws/v2/pkg/cfg" "github.com/versent/saml2aws/v2/pkg/creds" @@ -197,3 +201,30 @@ func TestExpectRequestOptionsDefaultTimeout(t *testing.T) { t.Errorf("Unexpected value for timeout [%.0f]: expected [%.0f]", *options.Timeout, DEFAULT_TIMEOUT) } } + +func TestAutoFill(t *testing.T) { + data, err := os.ReadFile("example/loginpage.html") + 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) + + username, _ := page.Locator("input[name='username']").First().InputValue() + assert.Equal(t, "golang", username) + 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) +} diff --git a/pkg/provider/browser/example/loginpage.html b/pkg/provider/browser/example/loginpage.html new file mode 100644 index 000000000..86c0f74b4 --- /dev/null +++ b/pkg/provider/browser/example/loginpage.html @@ -0,0 +1,41 @@ + + + Fake Login Page + + +
+
+ + +
+ +
+ + +
+ +
Login
+
+ + + + + +