diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 1528e6896..6622e3f08 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -24,7 +24,6 @@ jobs: - name: Test run: | - go run github.com/playwright-community/playwright-go/cmd/playwright install go test -v ./... -coverprofile=${{ matrix.os }}_coverage.txt -covermode=atomic - name: Upload coverage report diff --git a/README.md b/README.md index de530ae3f..5a94e24e7 100644 --- a/README.md +++ b/README.md @@ -248,6 +248,7 @@ Commands: The file that will cache the credentials retrieved from AWS. When not specified, will use the default AWS credentials file location. (env: SAML2AWS_CREDENTIALS_FILE) --cache-saml Caches the SAML response (env: SAML2AWS_CACHE_SAML) --cache-file=CACHE-FILE The location of the SAML cache file (env: SAML2AWS_SAML_CACHE_FILE) + --download-browser-driver Automatically download browsers for Browser IDP. (env: SAML2AWS_AUTO_BROWSER_DOWNLOAD) --disable-sessions Do not use Okta sessions. Uses Okta sessions by default. (env: SAML2AWS_OKTA_DISABLE_SESSIONS) --disable-remember-device Do not remember Okta MFA device. Remembers MFA device by default. (env: SAML2AWS_OKTA_DISABLE_REMEMBER_DEVICE) @@ -544,6 +545,18 @@ region = us-east-1 To use this you will need to export `AWS_DEFAULT_PROFILE=customer-test` environment variable to target `test`. +### Playwright Browser Drivers for Browser IDP + +If you are using the Browser Identity Provider, on first invocation of `saml2aws login` you need to remember to install +the browser drivers in order for playwright-go to work. Otherwise you will see the following error message: + +`Error authenticating to IDP.: could not start driver: fork/exec ... no such file or directory` + +To install the drivers, you can: +* Pass `--download-browser-driver` to `saml2aws login` +* Set in your shell environment `SAML2AWS_AUTO_BROWSER_DOWNLOAD=true` +* Set `download_browser_driver = true` in your saml2aws config file, i.e. `~/.saml2aws` + ## Advanced Configuration (Multiple AWS account access but SAML authenticate against a single 'SSO' AWS account) Example: diff --git a/cmd/saml2aws/commands/login.go b/cmd/saml2aws/commands/login.go index 97bbb7bd9..0a7766a4e 100644 --- a/cmd/saml2aws/commands/login.go +++ b/cmd/saml2aws/commands/login.go @@ -224,6 +224,12 @@ func resolveLoginDetails(account *cfg.IDPAccount, loginFlags *flags.LoginExecFla loginDetails.MFAIPAddress = loginFlags.CommonFlags.MFAIPAddress } + if loginFlags.DownloadBrowser { + loginDetails.DownloadBrowser = loginFlags.DownloadBrowser + } else if account.DownloadBrowser { + loginDetails.DownloadBrowser = account.DownloadBrowser + } + // log.Printf("loginDetails %+v", loginDetails) // if skip prompt was passed just pass back the flag values diff --git a/cmd/saml2aws/main.go b/cmd/saml2aws/main.go index e974e40a4..328400df3 100644 --- a/cmd/saml2aws/main.go +++ b/cmd/saml2aws/main.go @@ -114,6 +114,7 @@ func main() { cmdLogin.Flag("credentials-file", "The file that will cache the credentials retrieved from AWS. When not specified, will use the default AWS credentials file location. (env: SAML2AWS_CREDENTIALS_FILE)").Envar("SAML2AWS_CREDENTIALS_FILE").StringVar(&commonFlags.CredentialsFile) cmdLogin.Flag("cache-saml", "Caches the SAML response (env: SAML2AWS_CACHE_SAML)").Envar("SAML2AWS_CACHE_SAML").BoolVar(&commonFlags.SAMLCache) cmdLogin.Flag("cache-file", "The location of the SAML cache file (env: SAML2AWS_SAML_CACHE_FILE)").Envar("SAML2AWS_SAML_CACHE_FILE").StringVar(&commonFlags.SAMLCacheFile) + cmdLogin.Flag("download-browser-driver", "Automatically download browsers for Browser IDP. (env: SAML2AWS_AUTO_BROWSER_DOWNLOAD)").Envar("SAML2AWS_AUTO_BROWSER_DOWNLOAD").BoolVar(&loginFlags.DownloadBrowser) cmdLogin.Flag("disable-sessions", "Do not use Okta sessions. Uses Okta sessions by default. (env: SAML2AWS_OKTA_DISABLE_SESSIONS)").Envar("SAML2AWS_OKTA_DISABLE_SESSIONS").BoolVar(&commonFlags.DisableSessions) cmdLogin.Flag("disable-remember-device", "Do not remember Okta MFA device. Remembers MFA device by default. (env: SAML2AWS_OKTA_DISABLE_REMEMBER_DEVICE)").Envar("SAML2AWS_OKTA_DISABLE_REMEMBER_DEVICE").BoolVar(&commonFlags.DisableRememberDevice) diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go index aac86f275..d547edf78 100644 --- a/pkg/cfg/cfg.go +++ b/pkg/cfg/cfg.go @@ -56,9 +56,11 @@ type IDPAccount struct { SAMLCache bool `ini:"saml_cache"` SAMLCacheFile string `ini:"saml_cache_file"` TargetURL string `ini:"target_url"` - DisableRememberDevice bool `ini:"disable_remember_device"` // used by Okta - DisableSessions bool `ini:"disable_sessions"` // used by Okta - Headless bool `ini:"headless"` // used by browser + DisableRememberDevice bool `ini:"disable_remember_device"` // used by Okta + DisableSessions bool `ini:"disable_sessions"` // used by Okta + DownloadBrowser bool `ini:"download_browser_driver"` // used by browser + BrowserDriverDir string `ini:"browser_driver_dir,omitempty"` // used by browser; hide from user if not set + Headless bool `ini:"headless"` // used by browser Prompter string `ini:"prompter"` } diff --git a/pkg/creds/creds.go b/pkg/creds/creds.go index 0f846ff02..5aa697103 100644 --- a/pkg/creds/creds.go +++ b/pkg/creds/creds.go @@ -4,6 +4,7 @@ package creds type LoginDetails struct { ClientID string // used by OneLogin ClientSecret string // used by OneLogin + DownloadBrowser bool // used by Browser MFAIPAddress string // used by OneLogin Username string Password string diff --git a/pkg/flags/flags.go b/pkg/flags/flags.go index 9c741cb94..60d86408a 100644 --- a/pkg/flags/flags.go +++ b/pkg/flags/flags.go @@ -39,6 +39,7 @@ type CommonFlags struct { // LoginExecFlags flags for the Login / Exec commands type LoginExecFlags struct { CommonFlags *CommonFlags + DownloadBrowser bool Force bool DuoMFAOption string ExecProfile string diff --git a/pkg/provider/browser/browser.go b/pkg/provider/browser/browser.go index 83314c971..4bea17373 100644 --- a/pkg/provider/browser/browser.go +++ b/pkg/provider/browser/browser.go @@ -17,16 +17,33 @@ var logger = logrus.WithField("provider", "browser") // Client client for browser based Identity Provider type Client struct { Headless bool + // Setup alternative directory to download playwright browsers to + BrowserDriverDir string } // New create new browser based client func New(idpAccount *cfg.IDPAccount) (*Client, error) { - return &Client{Headless: idpAccount.Headless}, nil + return &Client{ + Headless: idpAccount.Headless, + BrowserDriverDir: idpAccount.BrowserDriverDir, + }, nil } func (cl *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) { + runOptions := playwright.RunOptions{} + if cl.BrowserDriverDir != "" { + runOptions.DriverDirectory = cl.BrowserDriverDir + } + + // Optionally download browser drivers if specified + if loginDetails.DownloadBrowser { + err := playwright.Install(&runOptions) + if err != nil { + return "", err + } + } - pw, err := playwright.Run() + pw, err := playwright.Run(&runOptions) if err != nil { return "", err } diff --git a/pkg/provider/browser/browser_test.go b/pkg/provider/browser/browser_test.go index edd2aeda0..c21cde716 100644 --- a/pkg/provider/browser/browser_test.go +++ b/pkg/provider/browser/browser_test.go @@ -39,13 +39,29 @@ func TestValidate(t *testing.T) { client, err := New(account) assert.Nil(t, err) loginDetails := &creds.LoginDetails{ - URL: "https://google.com/", + URL: "https://google.com/", + DownloadBrowser: true, } resp, err := client.Authenticate(loginDetails) assert.Nil(t, err) assert.Equal(t, resp, response) } +// Test that if download directory does not have browsers, it fails with expected error message +func TestNoBrowserDriverFail(t *testing.T) { + account := &cfg.IDPAccount{ + Headless: true, + BrowserDriverDir: t.TempDir(), // set up a directory we know won't have drivers + } + loginDetails := &creds.LoginDetails{ + URL: "https://google.com/", + } + client, _ := New(account) + _, err := client.Authenticate(loginDetails) + assert.Error(t, err) + assert.ErrorContains(t, err, "could not start driver") +} + func fakeSAMLResponse(page playwright.Page, loginDetails *creds.LoginDetails) (string, error) { return response, nil }