pkg/provider/browser/browser.go (173 lines of code) (raw):

package browser import ( "errors" "fmt" "net/url" "regexp" "strings" "github.com/aliyun/saml2alibabacloud/pkg/cfg" "github.com/aliyun/saml2alibabacloud/pkg/creds" "github.com/playwright-community/playwright-go" "github.com/sirupsen/logrus" ) var logger = logrus.WithField("provider", "browser") const DEFAULT_TIMEOUT float64 = 300000 // Client client for browser based Identity Provider type Client struct { BrowserType string BrowserExecutablePath string Headless bool // Setup alternative directory to download playwright browsers to BrowserDriverDir string Timeout int BrowserAutoFill 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, }, nil } // contains checks if a string is present in a slice func contains(s []string, str string) bool { for _, v := range s { if v == str { return true } } return false } func (cl *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) { runOptions := playwright.RunOptions{} if cl.BrowserDriverDir != "" { runOptions.DriverDirectory = cl.BrowserDriverDir } // Optionally download browser drivers if loginDetails.DownloadBrowser { err := playwright.Install(&runOptions) if err != nil { return "", err } } pw, err := playwright.Run(&runOptions) if err != nil { return "", err } // TODO: provide some overrides for this window launchOptions := playwright.BrowserTypeLaunchOptions{ Headless: playwright.Bool(cl.Headless), } validBrowserTypes := []string{"chromium", "firefox", "webkit", "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", "msedge-canary"} if len(cl.BrowserType) > 0 && !contains(validBrowserTypes, cl.BrowserType) { return "", fmt.Errorf("invalid browser-type: '%s', only %s are allowed", cl.BrowserType, validBrowserTypes) } if cl.BrowserType != "" { logger.Info(fmt.Sprintf("Setting browser type: %s", cl.BrowserType)) launchOptions.Channel = playwright.String(cl.BrowserType) } // Default browser is Chromium as it is widely supported for Identity providers, // It can also be set to the other playwright browsers: Firefox and WebKit browserType := pw.Chromium if cl.BrowserType == "firefox" { browserType = pw.Firefox } else if cl.BrowserType == "webkit" { browserType = pw.WebKit } // You can set the path to a browser executable to run instead of the playwright-go bundled one. If `executablePath` // is a relative path, then it is resolved relative to the current working directory. // Note that Playwright only works with the bundled Chromium, Firefox or WebKit, use at your own risk. see: if len(cl.BrowserExecutablePath) > 0 { logger.Info(fmt.Sprintf("Setting browser executable path: %s", cl.BrowserExecutablePath)) launchOptions.ExecutablePath = &cl.BrowserExecutablePath } // currently using the main browsers supported by Playwright: Chromium, Firefox or Webkit // // this is a sandboxed browser window so password managers and addons are separate browser, err := browserType.Launch(launchOptions) if err != nil { return "", err } page, err := browser.NewPage() if err != nil { return "", err } defer func() { logger.Info("clean up browser") if err := browser.Close(); err != nil { logger.Info("Error when closing browser", err) } if err := pw.Stop(); err != nil { logger.Info("Error when stopping pm", err) } }() return getSAMLResponse(page, loginDetails, cl) } var getSAMLResponse = func(page playwright.Page, loginDetails *creds.LoginDetails, client *Client) (string, error) { logger.WithField("URL", loginDetails.URL).Info("opening browser") if _, err := page.Goto(loginDetails.URL); err != nil { return "", err } if client.BrowserAutoFill { err := autoFill(page, loginDetails) if err != nil { logger.Error("error when auto filling", err) } } signin_re, err := aliyunSigninRegex() if err != nil { return "", err } logger.Info("waiting ...") r, _ := page.ExpectRequest(signin_re, nil, client.expectRequestTimeout()) data, err := r.PostData() if err != nil { return "", err } values, err := url.ParseQuery(data) if err != nil { return "", err } 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 or input of the form that the password field is in submitLocator := page.Locator("form", playwright.PageLocatorOptions{ Has: passwordField, }).Locator("[type='submit']") count, err := submitLocator.Count() if err != nil { return err } // when submit locator exists, Click it if count > 0 { 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) return err } } func aliyunSigninRegex() (*regexp.Regexp, error) { return regexp.Compile(`https:\/\/signin\.(alibabacloud|aliyun)\.com\/saml-role\/sso`) } func (cl *Client) Validate(loginDetails *creds.LoginDetails) error { if loginDetails.URL == "" { return errors.New("empty URL") } return nil } func (cl *Client) expectRequestTimeout() playwright.PageExpectRequestOptions { timeout := float64(cl.Timeout) if timeout < 30000 { timeout = DEFAULT_TIMEOUT } return playwright.PageExpectRequestOptions{Timeout: &timeout} }