Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add browser autofill #1183

Merged
merged 7 commits into from
Jan 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/saml2aws/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions pkg/cfg/cfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
5 changes: 5 additions & 0 deletions pkg/flags/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type CommonFlags struct {
IdpProvider string
BrowserType string
BrowserExecutablePath string
BrowserAutoFill bool
MFA string
MFAIPAddress string
MFAToken string
Expand Down Expand Up @@ -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
}
Expand Down
43 changes: 43 additions & 0 deletions pkg/provider/browser/browser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
31 changes: 31 additions & 0 deletions pkg/provider/browser/browser_test.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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)
}
41 changes: 41 additions & 0 deletions pkg/provider/browser/example/loginpage.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<html>
<head>
<title>Fake Login Page</title>
</head>
<body>
<form id="form">
<div>
<label for="username"><b>Username:</b></label>
<input type="text" name="username" />
</div>

<div>
<label for="password"><b>Password:</b></label>
<input type="password" name="password" />
</div>

<div><input type="submit">Login</button></div>
</form>

<div id="result" style="display: none">
You should not see this as the form onsubmit should override this inner
text.
</div>

<script>
const submit = document.querySelector("button[type=submit]");
const formSection = document.getElementById("form");
const resultSection = document.getElementById("result");

form.onsubmit = function (event) {
event.preventDefault();
const username = event.target.elements.username.value;
const password = event.target.elements.password.value;

formSection.style.display = "none";
resultSection.style.display = "block";
resultSection.innerText = `${username}:${password}`;
};
</script>
</body>
</html>
Loading