Skip to content
Open
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
56 changes: 52 additions & 4 deletions pkg/provider/pingfed/pingfed.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"log"
"net/http"
"net/url"
"strings"
"time"

"github.com/PuerkitoBio/goquery"
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -169,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")
Expand All @@ -181,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
}
Expand All @@ -208,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")
}
Expand Down Expand Up @@ -358,6 +385,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() {
Expand Down