From 932a64178e3ac93e68b6d460bd2f742b61c794f7 Mon Sep 17 00:00:00 2001 From: Martin Holicka Date: Wed, 29 Apr 2026 10:07:17 -0700 Subject: [PATCH 1/2] debug/debug-app-version: debug(saml2aws) : added debug flag / verbose logging and html output --- NMD_README.md | 13 +++++++++ cmd/saml2aws/commands/login.go | 5 ++++ cmd/saml2aws/main.go | 1 + pkg/cfg/cfg.go | 1 + pkg/flags/flags.go | 1 + pkg/provider/googleapps/googleapps.go | 40 +++++++++++++++++++++++++-- 6 files changed, 59 insertions(+), 2 deletions(-) diff --git a/NMD_README.md b/NMD_README.md index 2f022aa17..64fa247ba 100644 --- a/NMD_README.md +++ b/NMD_README.md @@ -11,6 +11,19 @@ 4. You may then use it as normal `saml2aws`, ensure you preface with `./saml2aws login` +### Debugging login issues + +If authentication fails unexpectedly, use the `--verbose` and `--debug-idp` flags together to diagnose the problem: + +```bash +./saml2aws --verbose login --debug-idp +``` + +- `--verbose` enables debug-level logging, showing HTTP requests/responses, page headings, and form actions from the IDP. +- `--debug-idp` writes the full IDP response HTML to a temp file (path printed in the output) so you can open it in a browser and see exactly what page the IDP returned. + +This is especially useful when Google introduces intermediate pages (password resets, account recovery, new terms of service) that saml2aws doesn't handle automatically. + ### Legacy Instructions 5. Find the AWS role and account you want cli creds for. - Go into console and find account number and role name (IAM should have the arn) diff --git a/cmd/saml2aws/commands/login.go b/cmd/saml2aws/commands/login.go index 483e43943..0f9f9109e 100644 --- a/cmd/saml2aws/commands/login.go +++ b/cmd/saml2aws/commands/login.go @@ -203,6 +203,11 @@ func buildIdpAccount(loginFlags *flags.LoginExecFlags) (*cfg.IDPAccount, error) // update username and hostname if supplied flags.ApplyFlagOverrides(loginFlags.CommonFlags, account) + // pass through debug flag (not part of CommonFlags) + if loginFlags.DebugIDP { + account.DebugIDP = true + } + err = account.Validate() if err != nil { return nil, errors.Wrap(err, "Failed to validate account.") diff --git a/cmd/saml2aws/main.go b/cmd/saml2aws/main.go index 95d5cd899..43da3d0fa 100644 --- a/cmd/saml2aws/main.go +++ b/cmd/saml2aws/main.go @@ -127,6 +127,7 @@ func main() { 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) cmdLogin.Flag("try-no-prompt", "Only prompt for credentials if they cannot be read from keyring").BoolVar(&loginFlags.TryNoPrompt) + cmdLogin.Flag("debug-idp", "Dump the IDP response HTML to a temp file when authentication fails. Useful for diagnosing login issues.").BoolVar(&loginFlags.DebugIDP) // `exec` command and settings cmdExec := app.Command("exec", "Exec the supplied command with env vars from STS token.") diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go index c2b9277f5..b78839a3a 100644 --- a/pkg/cfg/cfg.go +++ b/pkg/cfg/cfg.go @@ -70,6 +70,7 @@ type IDPAccount struct { KCAuthErrorMessage string `ini:"kc_auth_error_message,omitempty"` // used by KeyCloak; hide from user if not set KCAuthErrorElement string `ini:"kc_auth_error_element,omitempty"` // used by KeyCloak; hide from user if not set KCBroker string `ini:"kc_broker"` // used by KeyCloak; + DebugIDP bool `ini:"-"` // dump IDP response HTML on auth failure (not persisted) } func (ia IDPAccount) String() string { diff --git a/pkg/flags/flags.go b/pkg/flags/flags.go index 832fdf9be..33661d1b5 100644 --- a/pkg/flags/flags.go +++ b/pkg/flags/flags.go @@ -51,6 +51,7 @@ type LoginExecFlags struct { ExecProfile string CredentialProcess bool TryNoPrompt bool + DebugIDP bool } type ConsoleFlags struct { diff --git a/pkg/provider/googleapps/googleapps.go b/pkg/provider/googleapps/googleapps.go index 4afb168f5..22c158b92 100644 --- a/pkg/provider/googleapps/googleapps.go +++ b/pkg/provider/googleapps/googleapps.go @@ -28,7 +28,8 @@ var logger = logrus.WithField("provider", "googleapps") type Client struct { provider.ValidateBase - client *provider.HTTPClient + client *provider.HTTPClient + debugIDP bool } // New create a new Google Apps Client @@ -42,7 +43,8 @@ func New(idpAccount *cfg.IDPAccount) (*Client, error) { } return &Client{ - client: client, + client: client, + debugIDP: idpAccount.DebugIDP, }, nil } @@ -163,10 +165,44 @@ func (kc *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) samlAssertion := mustFindInputByName(responseDoc, "SAMLResponse") if samlAssertion == "" { + // Log targeted debug info when SAML assertion is missing + logger.Debug("No SAMLResponse found in response page") + + pageTitle := responseDoc.Find("title").Text() + logger.Debugf("Page title: %q", pageTitle) + + responseDoc.Find("h1, h2, h3").Each(func(i int, s *goquery.Selection) { + logger.Debugf("Heading [%s]: %q", goquery.NodeName(s), strings.TrimSpace(s.Text())) + }) + + responseDoc.Find("form").Each(func(i int, s *goquery.Selection) { + action, _ := s.Attr("action") + id, _ := s.Attr("id") + logger.Debugf("Form id=%q action=%q", id, action) + }) + + // Dump full response HTML to a temp file when --debug-idp is set + if kc.debugIDP { + htmlBody, dumpErr := responseDoc.Html() + if dumpErr == nil { + tmpFile, tmpErr := os.CreateTemp("", "saml2aws-debug-*.html") + if tmpErr == nil { + tmpFile.WriteString(htmlBody) + tmpFile.Close() + log.Printf("IDP response page written to: %s", tmpFile.Name()) + } + } + } + if responseDoc.Selection.Find("#passwordError").Text() != "" { return "", errors.New("Password error") } + // Check for forced password change page + if strings.Contains(strings.ToLower(responseDoc.Find("h2").Text()), "create a strong password") { + return "", errors.New("Google is requiring a password change for your account. Please log in at https://accounts.google.com in a browser to set a new password, then try again") + } + if err := isMissing2StepSetup(responseDoc); err != nil { return "", err } From 96d29aff12b13afcfc4032edd3d1c4b1c06841a4 Mon Sep 17 00:00:00 2001 From: Martin Holicka Date: Wed, 29 Apr 2026 10:09:46 -0700 Subject: [PATCH 2/2] debug/debug-app-version: added compiled to gitignore --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index dab5a8422..5a5da5ac8 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,7 @@ bin/ # direnv .envrc -.buildtemp \ No newline at end of file +.buildtemp + +# compiled binary +/saml2aws \ No newline at end of file