From 7574f61d04ef41b75db9255d949c3ea7d7b945c1 Mon Sep 17 00:00:00 2001 From: SESA826635 Date: Fri, 12 Sep 2025 10:41:16 +0200 Subject: [PATCH 1/3] feat: add account lockout and login failure detection for PingFederate provider --- pkg/provider/pingfed/pingfed.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/pkg/provider/pingfed/pingfed.go b/pkg/provider/pingfed/pingfed.go index 1e3357698..d931946b7 100644 --- a/pkg/provider/pingfed/pingfed.go +++ b/pkg/provider/pingfed/pingfed.go @@ -8,6 +8,7 @@ import ( "log" "net/http" "net/url" + "strings" "time" "github.com/PuerkitoBio/goquery" @@ -74,6 +75,15 @@ func (ac *Client) follow(ctx context.Context, req *http.Request) (string, error) return "", errors.Wrap(err, "failed to build document from response") } + // Check for authentication errors first + if msg, ok := docIsLoginFail(doc); ok { + logger.WithField("type", "authentication-error").Debug("doc detect") + return "", fmt.Errorf(msg) + } else if msg, ok := docIsAccountLocked(doc); ok { + logger.WithField("type", "account-locked").Debug("doc detect") + return "", fmt.Errorf(msg) + } + var handler func(context.Context, *goquery.Document, *url.URL) (context.Context, *http.Request, error) if docIsFormRedirectToTarget(doc, ac.idpAccount.TargetURL) { @@ -358,6 +368,27 @@ func extractSAMLResponse(doc *goquery.Document) (v string, ok bool) { return doc.Find("input[name=\"SAMLResponse\"]").Attr("value") } +// docIsLoginFail checks for login authentication failures +func docIsLoginFail(doc *goquery.Document) (v string, ok bool) { + isLoginFail := doc.Find(".ping-error").Size() >= 1 + if isLoginFail { + errorText := doc.Find(".ping-error").Text() + return strings.Join(strings.Fields(errorText), " "), true + } + return "", false + +} + +// docIsAccountLocked checks if user account is locked or blocked +func docIsAccountLocked(doc *goquery.Document) (v string, ok bool) { + isAccountLocked := doc.Find(".window.settings.blocked").Size() > 0 + if isAccountLocked { + errorText := strings.TrimSpace(doc.Find(".window.settings.blocked .error-text .text").Text()) + return strings.Join(strings.Fields(errorText), " "), true + } + return "", false +} + // ensures given url is an absolute URL. if not, it will be combined with the base URL func makeAbsoluteURL(v string, base string) string { if u, err := url.ParseRequestURI(v); err == nil && !u.IsAbs() { From 44ed06888ba4ad700c6f6fe35891a11924b82fd0 Mon Sep 17 00:00:00 2001 From: SESA826635 Date: Fri, 12 Sep 2025 14:55:17 +0200 Subject: [PATCH 2/3] feat: allow OTP to be passed via MFAToken parameter --- pkg/provider/pingfed/pingfed.go | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/pkg/provider/pingfed/pingfed.go b/pkg/provider/pingfed/pingfed.go index d931946b7..061c4db17 100644 --- a/pkg/provider/pingfed/pingfed.go +++ b/pkg/provider/pingfed/pingfed.go @@ -179,7 +179,12 @@ func (ac *Client) handleCheckWebAuthn(ctx context.Context, doc *goquery.Document return ctx, req, err } +// Improved OTP handling in handleOTP function func (ac *Client) handleOTP(ctx context.Context, doc *goquery.Document, requestURL *url.URL) (context.Context, *http.Request, error) { + loginDetails, ok := ctx.Value(ctxKey("login")).(*creds.LoginDetails) + if !ok { + return ctx, nil, fmt.Errorf("no context value for 'login'") + } form, err := page.NewFormFromDocument(doc, "#otp-form") if err != nil { return ctx, nil, errors.Wrap(err, "error extracting OTP form") @@ -191,9 +196,20 @@ func (ac *Client) handleOTP(ctx context.Context, doc *goquery.Document, requestU break } } - - token := prompter.StringRequired("Enter passcode") - form.Values.Set("otp", token) + // Improved MFA token handling with retry capability + var mfaToken string + if loginDetails.MFAToken != "" { + mfaToken = loginDetails.MFAToken + // Clear the token to allow for retry on failure + loginDetails.MFAToken = "" + } else { + mfaToken = prompter.StringRequired("Enter passcode") + if mfaToken == "" { + // User cancelled (Ctrl+C) or provided empty input + return ctx, nil, fmt.Errorf("OTP entry cancelled by user") + } + } + form.Values.Set("otp", mfaToken) req, err := form.BuildRequest() return ctx, req, err } From e85f62516b472ad6ee25ba3ffc59372c8654dbd7 Mon Sep 17 00:00:00 2001 From: SESA826635 Date: Wed, 24 Sep 2025 14:41:30 +0200 Subject: [PATCH 3/3] fix(pingfed): fix error polling swipe status --- pkg/provider/pingfed/pingfed.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/provider/pingfed/pingfed.go b/pkg/provider/pingfed/pingfed.go index 061c4db17..4b7689d89 100644 --- a/pkg/provider/pingfed/pingfed.go +++ b/pkg/provider/pingfed/pingfed.go @@ -234,7 +234,8 @@ func (ac *Client) handleSwipe(ctx context.Context, doc *goquery.Document, _ *url for { time.Sleep(3 * time.Second) - res, err := ac.client.Do(req) + clonedReq := req.Clone(req.Context()) + res, err := ac.client.Do(clonedReq) if err != nil { return ctx, nil, errors.Wrap(err, "error polling swipe status") }