Skip to content

Commit c4852f3

Browse files
committed
Detect MFA via page title and handle CSRF
Instead of scanning the entire response body for "MFA", detect MFA by parsing the HTML <title> (matching the Python garth approach) to avoid false positives from bodies that contain "MFA" text. Extract the page title early, check for "MFA" case-insensitively, and if detected update m_lastError, refresh cookies, extract a new CSRF token using two regex patterns, emit mfaRequired (unless suppressed), and abort the login flow. Also adjust the success check to rely on the title == "Success" and remove the legacy body-based MFA detection block. Added debugging logs for the title, CSRF token, and MFA signal paths.
1 parent 28ae8d8 commit c4852f3

1 file changed

Lines changed: 44 additions & 33 deletions

File tree

src/garminconnect.cpp

Lines changed: 44 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -455,15 +455,54 @@ bool GarminConnect::performLogin(const QString &email, const QString &password,
455455
qDebug() << "GarminConnect: Login response length:" << response.length();
456456
qDebug() << "GarminConnect: Response snippet:" << response.left(300);
457457

458-
// Check for success title (like Python garth library)
458+
// Check page title (like Python garth library)
459+
// garth checks ONLY the title for MFA detection, not the body
460+
// This is important because some servers (like garmin.cn) may have "MFA" text
461+
// in their Success page HTML body, which would cause false positives
462+
QString pageTitle;
459463
QRegularExpression titleRegex("<title>(.+?)</title>");
460464
QRegularExpressionMatch titleMatch = titleRegex.match(response);
461465
if (titleMatch.hasMatch()) {
462-
QString title = titleMatch.captured(1);
463-
qDebug() << "GarminConnect: Page title:" << title;
464-
if (title == "Success") {
465-
qDebug() << "GarminConnect: Login successful (Success page detected)";
466+
pageTitle = titleMatch.captured(1);
467+
qDebug() << "GarminConnect: Page title:" << pageTitle;
468+
}
469+
470+
// Check if MFA is required by looking at the TITLE (garth approach)
471+
// This is more reliable than checking the body which may contain "MFA" in scripts/URLs
472+
if (pageTitle.contains("MFA", Qt::CaseInsensitive)) {
473+
m_lastError = "MFA Required";
474+
qDebug() << "GarminConnect: MFA detected in page title";
475+
476+
// Extract new CSRF token from MFA page - try multiple patterns
477+
QRegularExpression csrfRegex1("name=\"_csrf\"[^>]*value=\"([^\"]+)\"");
478+
QRegularExpression csrfRegex2("value=\"([^\"]+)\"[^>]*name=\"_csrf\"");
479+
480+
QRegularExpressionMatch match = csrfRegex1.match(response);
481+
if (!match.hasMatch()) {
482+
match = csrfRegex2.match(response);
483+
}
484+
if (match.hasMatch()) {
485+
m_csrfToken = match.captured(1);
486+
qDebug() << "GarminConnect: CSRF token from MFA page:" << m_csrfToken.left(20) << "...";
466487
}
488+
489+
// Update cookies
490+
m_cookies = m_manager->cookieJar()->cookiesForUrl(url);
491+
492+
if (!suppressMfaSignal) {
493+
qDebug() << "GarminConnect: Emitting mfaRequired signal";
494+
emit mfaRequired();
495+
} else {
496+
qDebug() << "GarminConnect: MFA required but signal suppressed (retrying with MFA code)";
497+
}
498+
reply->deleteLater();
499+
return false;
500+
}
501+
502+
// Check if login was successful (title is "Success")
503+
if (pageTitle == "Success") {
504+
qDebug() << "GarminConnect: Login successful (Success page detected)";
505+
// Continue to extract ticket below
467506
}
468507

469508
// Check for error messages in response
@@ -552,34 +591,6 @@ bool GarminConnect::performLogin(const QString &email, const QString &password,
552591
return false;
553592
}
554593

555-
// Check if MFA is required (legacy check for non-redirect MFA)
556-
if (response.contains("MFA", Qt::CaseInsensitive) ||
557-
response.contains("Enter MFA Code", Qt::CaseInsensitive)) {
558-
m_lastError = "MFA Required";
559-
qDebug() << "GarminConnect: MFA content detected in response";
560-
561-
// Extract new CSRF token from MFA page - try multiple patterns
562-
QRegularExpression csrfRegex1("name=\"_csrf\"[^>]*value=\"([^\"]+)\"");
563-
QRegularExpression csrfRegex2("value=\"([^\"]+)\"[^>]*name=\"_csrf\"");
564-
565-
QRegularExpressionMatch match = csrfRegex1.match(response);
566-
if (!match.hasMatch()) {
567-
match = csrfRegex2.match(response);
568-
}
569-
if (match.hasMatch()) {
570-
m_csrfToken = match.captured(1);
571-
}
572-
573-
// Update cookies
574-
m_cookies = m_manager->cookieJar()->cookiesForUrl(url);
575-
576-
if (!suppressMfaSignal) {
577-
emit mfaRequired();
578-
}
579-
reply->deleteLater();
580-
return false;
581-
}
582-
583594
// Extract ticket from response URL (already declared above)
584595
if (responseUrl.isEmpty()) {
585596
responseUrl = reply->url();

0 commit comments

Comments
 (0)