Skip to content

feat: add SSO embed widget login strategy to bypass 429 rate limiting#345

Open
diegoscarabelli wants to merge 4 commits intocyberjunky:masterfrom
diegoscarabelli:feat/widget-cffi-login-strategy
Open

feat: add SSO embed widget login strategy to bypass 429 rate limiting#345
diegoscarabelli wants to merge 4 commits intocyberjunky:masterfrom
diegoscarabelli:feat/widget-cffi-login-strategy

Conversation

@diegoscarabelli
Copy link
Copy Markdown

@diegoscarabelli diegoscarabelli commented Apr 4, 2026

Summary

Adds a fifth login strategy (widget+cffi) that uses the SSO embed widget HTML form flow (/sso/embed + /sso/signin) with curl_cffi TLS impersonation. This bypasses the per-clientId 429 rate limiting that affects all four existing strategies.

Closes #344

Problem

The portal and mobile login endpoints require a clientId parameter (GarminConnect, GCM_ANDROID_DARK). Garmin rate-limits these per clientId + account email. Once triggered, switching IP or User-Agent does not help. The official Garmin Connect app is unaffected because it uses a different rate limit bucket.

How this works

The SSO embed widget flow is an HTML form-based login (CSRF token + POST with username/password). It does not use a clientId parameter, so it sits in a completely different rate limit bucket. Combined with curl_cffi Chrome TLS impersonation to pass Cloudflare, it reliably authenticates even when all other strategies return 429.

The flow:

  1. GET /sso/embed (establish cookies)
  2. GET /sso/signin (obtain CSRF token from HTML)
  3. POST /sso/signin (submit credentials via form data)
  4. If MFA: POST /sso/verifyMFA/loginEnterMfaCode
  5. Extract service ticket from success page

The service ticket feeds into the existing _exchange_service_ticket for DI token exchange. No changes needed downstream.

Changes

  • Added import re for HTML parsing (CSRF token, title, ticket extraction)
  • Added _widget_login_cffi as a new login strategy (tried first when curl_cffi is available)
  • Added _complete_mfa_widget for MFA handling in the widget flow
  • Updated resume_login to support widget MFA completion
  • No new dependencies (uses existing curl_cffi)

Testing

Tested manually against two Garmin accounts (one with MFA enabled, one without):

  • Both accounts were actively 429-blocked on all four existing strategies (portal+cffi, portal+requests, mobile+cffi, mobile+requests)
  • Both authenticated successfully via the widget flow
  • Service tickets exchanged for DI tokens without issues
  • MFA flow (TOTP) verified end-to-end
  • resume_login path verified for the return_on_mfa=True case

Trade-offs

The widget flow parses HTML (CSRF token from a hidden input, success/MFA status from <title> tag, ticket from a URL in the response). This is more fragile than JSON APIs. However:

  • The garth library used this exact flow for years
  • It is one strategy in the fallback chain: if Garmin changes the HTML, the other strategies still exist
  • The JSON endpoints are currently unusable for many users due to rate limiting

Resolves #344

Summary by CodeRabbit

  • New Features

    • Added a widget-based SSO authentication flow for sign-in.
    • Added widget-based MFA completion for the new sign-in flow.
  • Improvements

    • Reordered login strategies to prefer the widget flow for faster authentication.
    • Improved MFA handling during session resume and recovery.

The portal and mobile JSON API endpoints are subject to aggressive
per-clientId rate limiting. This adds a fifth login strategy using the
SSO embed widget HTML form flow (/sso/embed + /sso/signin) with
curl_cffi TLS impersonation. This flow does not use a clientId
parameter, so it is not subject to the same rate limiting.

The strategy is tried first when curl_cffi is available, before falling
back to the existing portal and mobile strategies.

Closes cyberjunky#344
Copilot AI review requested due to automatic review settings April 4, 2026 23:21
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 4, 2026

Walkthrough

Added a new SSO web widget login strategy (widget+cffi) that implements an HTML form-based embed/signin flow with CSRF handling and widget-specific MFA completion; updated login() strategy ordering and resume_login() to complete widget MFA and establish the authenticated session when applicable.

Changes

Cohort / File(s) Summary
Widget Login Strategy
garminconnect/client.py
Added _widget_login_cffi() implementing SSO /sso/embed/sso/signin HTML form flow with CSRF extraction (regex), credentials POST, MFA detection via <title>, and service-ticket parsing. Added _complete_mfa_widget() to submit widget MFA codes and return the ticket. Updated login() ordering to include widget+cffi and extended resume_login() to prefer/complete widget MFA and clear widget state.

Sequence Diagram

sequenceDiagram
    participant Client
    participant SSO_Embed as SSO /embed
    participant SSO_Signin as SSO /signin
    participant SSO_MFA as SSO /verifyMFA
    participant DI as DI Token Exchange

    Client->>SSO_Embed: GET /sso/embed (establish cookies)
    SSO_Embed-->>Client: Set-Cookie

    Client->>SSO_Signin: GET /sso/signin (fetch sign-in form)
    SSO_Signin-->>Client: HTML + CSRF token

    Client->>Client: Extract CSRF from hidden input

    Client->>SSO_Signin: POST /sso/signin (email, password, _csrf, embed)
    SSO_Signin-->>Client: Response page (may indicate MFA)

    alt MFA required
        Client->>SSO_MFA: POST /sso/verifyMFA/loginEnterMfaCode (mfa-code)
        SSO_MFA-->>Client: MFA success page (contains ticket)
    end

    Client->>Client: Extract service ticket from response
    Client->>DI: Exchange ticket for DI token
    DI-->>Client: DI Token
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main change: adding a new SSO widget login strategy as a feature to bypass rate limiting, matching the primary objective of the PR.
Linked Issues check ✅ Passed The PR successfully implements the core requirements from issue #344: adds widget+cffi strategy, implements SSO embed widget HTML form flow, handles MFA via widget, and feeds service tickets into existing token exchange.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the widget+cffi login strategy and supporting MFA completion, with no unrelated modifications detected outside the linked issue scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new widget+cffi login strategy that uses the Garmin SSO embed widget HTML form flow (/sso/embed + /sso/signin) with curl_cffi TLS impersonation, intended to bypass per-clientId 429 rate limiting affecting the existing portal/mobile strategies.

Changes:

  • Introduces widget+cffi as the first attempted login strategy when curl_cffi is available.
  • Implements widget-flow MFA handling via _complete_mfa_widget and wires it into resume_login.
  • Adds regex-based HTML parsing for CSRF/title/ticket extraction.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Prevents stale _widget_session from being picked up by a subsequent
resume_login call if a different login strategy is used next.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@garminconnect/client.py`:
- Around line 364-370: The current check uses title_match/_TITLE_RE and raises
GarminConnectAuthenticationError with a generic message when title != "Success";
instead, examine the extracted title string for known failure values (e.g.,
"Invalid code", "Code expired", "Too many attempts") and map them to clearer,
specific errors or messages before raising—either raise distinct exceptions
(e.g., GarminConnectMFAInvalidCodeError) or include a descriptive message that
differentiates "invalid MFA code" vs. "unexpected server response"; update the
block that references title_match, title, and the
GarminConnectAuthenticationError to perform this lookup and raise the
appropriate, more informative exception.
- Around line 352-362: The POST to "/sso/verifyMFA/loginEnterMfaCode" (the
sess.post call in garminconnect/client.py that sends the MFA code) is missing a
timeout; add a timeout keyword argument to that sess.post invocation so the
request cannot hang indefinitely (use an existing attribute like
self._request_timeout if one exists, otherwise add a sensible default such as
timeout=30 and surface it as a configurable client option). Make sure to update
any constructor or config to allow setting the timeout if you introduce a new
attribute and keep the same parameter name when used elsewhere.
- Around line 285-295: The POST that submits credentials (the sess.post call
that posts to f"{sso_base}/signin" with signin_params and data including
"_csrf") must include an explicit timeout argument so it cannot hang
indefinitely; update that sess.post invocation to pass a numeric timeout (or
reuse a configured value like self.timeout or a module-level DEFAULT_TIMEOUT)
and ensure any related callers expect a requests timeout exception
(requests.exceptions.Timeout) if needed.
- Around line 269-277: Check the response and handle errors for the initial
cookie-establishment call sess.get(sso_embed, params=embed_params) and the
subsequent CSRF fetch r = sess.get(f"{sso_base}/signin", ...): add an explicit
timeout argument to both calls, wrap them in try/except to catch
requests.exceptions.RequestException, verify r.status_code (treat non-2xx as
failure) and surface a clear error or raise an exception that includes the URL,
status_code and the underlying exception message before proceeding to CSRF
extraction; update any logging or error text so failures point to
sess.get(sso_embed, ...) or the signin GET rather than a generic "could not find
CSRF token."
- Around line 325-328: The strict title equality check (if title != "Success")
can false-negative if the success page is localized or changed; update the logic
in the same block (where title and GarminConnectConnectionError are referenced)
to try a fallback detection: if title isn't exactly "Success", attempt to locate
the service ticket (e.g., check for a parsed variable like service_ticket, look
for a "ticket" query param in the redirect URL, or search the page HTML for a
service ticket token) and only raise GarminConnectConnectionError when neither
the title matches nor a valid service ticket is found; keep the original
exception message but include the fallback branch so presence of a service
ticket allows success to proceed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f7389bd9-ccec-4f2a-826b-0ade911cb6cc

📥 Commits

Reviewing files that changed from the base of the PR and between c7fcade and 7cdeea3.

📒 Files selected for processing (1)
  • garminconnect/client.py

Address Copilot review feedback:
- Check status codes on embed and sign-in GET requests
- Add explicit timeout=30 to credentials POST and MFA POST
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

garminconnect/client.py:1151

  • resume_login(self, _client_state, mfa_code) still ignores the _client_state argument and relies on internal attributes (_widget_session, _widget_signin_params, _widget_last_resp) to complete MFA. This makes the "state" value returned from login(return_on_mfa=True) effectively unused for the widget flow and prevents resuming MFA on a fresh Client instance. Consider using _client_state as the source of truth (or documenting/removing the parameter) so MFA resumption behaves consistently.
    def put(self, _domain: str, path: str, **kwargs: Any) -> Any:
        api = kwargs.pop("api", False)
        resp = self._run_request("PUT", path, **kwargs)
        if api:
            return resp.json() if hasattr(resp, "json") else None
        return resp


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
garminconnect/client.py (1)

280-294: ⚠️ Potential issue | 🟡 Minor

Raise on /sso/signin GET failures before parsing CSRF.

This branch still skips r.ok handling, so a 403/5xx is reported as “could not find CSRF token” instead of the actual HTTP failure.

Suggested fix
         if r.status_code == 429:
             raise GarminConnectTooManyRequestsError(
                 "Widget login returned 429 on sign-in page"
             )
+        if not r.ok:
+            raise GarminConnectConnectionError(
+                f"Widget login: sign-in page returned HTTP {r.status_code}"
+            )
         csrf_match = self._CSRF_RE.search(r.text)
         if not csrf_match:
             raise GarminConnectConnectionError(
                 "Widget login: could not find CSRF token in sign-in page"
             )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@garminconnect/client.py` around lines 280 - 294, The GET to /sso/signin
currently parses the response body before verifying HTTP success, causing
403/5xx to be misreported as a missing CSRF; update the block that performs
sess.get(...) to first check r.ok (or r.status_code) and raise a
GarminConnectConnectionError (including r.status_code and a short message) for
non-2xx responses (handle 429 as before using
GarminConnectTooManyRequestsError), then proceed to run
self._CSRF_RE.search(r.text) and raise the existing CSRF-related
GarminConnectConnectionError only if the token is absent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@garminconnect/client.py`:
- Around line 329-331: The widget login call sites pass service_url=sso_embed to
Client._establish_session but _establish_session still redeems the fallback
ticket against the hard-coded MOBILE_SSO_SERVICE_URL, breaking the JWT fallback;
update _establish_session to use the passed service_url parameter when
performing the fallback/JWT redemption (instead of the constant
MOBILE_SSO_SERVICE_URL), ensure any internal variable/branch that builds the
DI/JWT redemption endpoint uses service_url, and leave callers like the ones
that call _complete_mfa_widget(...)/_establish_session(ticket, sess=sess,
service_url=sso_embed) unchanged so widget logins can properly fall back to
JWT_WEB.
- Around line 315-341: The widget branch currently treats any non-"Success"
title as a connection error which causes Client.login() to continue trying other
strategies; modify the post-MFA check in the widget handling (around
title_match, title, _widget_session, _widget_signin_params, _widget_last_resp,
and _complete_mfa_widget) so that known widget credential-rejection titles
(e.g., titles containing "Invalid", "Incorrect", "Bad credentials", or a
specific rejection phrase your tests show) raise
GarminConnectAuthenticationError immediately, while leaving the generic
GarminConnectConnectionError for other unexpected titles; ensure return_on_mfa
and prompt_mfa flows remain unchanged.
- Around line 327-333: After successful inline MFA completion in the prompt_mfa
path, clear the stored widget state to avoid reusing stale widget sessions:
after calling self._establish_session(ticket, sess=sess, service_url=sso_embed)
(inside the prompt_mfa branch) reset self._widget_session,
self._widget_signin_params, and self._widget_last_resp to None so subsequent
resume_login() calls won't pick up old widget state from the same Client
instance.
- Around line 365-384: The _complete_mfa_widget() flow parses the HTML title
without checking the HTTP response code, so transport errors (429/5xx) get
misclassified as invalid MFA; update the POST handling to first inspect
r.status_code (and treat non-2xx as transport failures) and raise a distinct
error or re-raise an HTTP/transport exception before attempting to parse the
title; ensure the change references the POST response variable r, the method
_complete_mfa_widget, and use GarminConnectAuthenticationError (or a new
Transport/HTTP error) to signal network/server failures rather than treating
them as "Widget MFA verification failed".

---

Duplicate comments:
In `@garminconnect/client.py`:
- Around line 280-294: The GET to /sso/signin currently parses the response body
before verifying HTTP success, causing 403/5xx to be misreported as a missing
CSRF; update the block that performs sess.get(...) to first check r.ok (or
r.status_code) and raise a GarminConnectConnectionError (including r.status_code
and a short message) for non-2xx responses (handle 429 as before using
GarminConnectTooManyRequestsError), then proceed to run
self._CSRF_RE.search(r.text) and raise the existing CSRF-related
GarminConnectConnectionError only if the token is absent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2c2d4463-46d6-471b-805f-ef60e8cbb4d0

📥 Commits

Reviewing files that changed from the base of the PR and between 7cdeea3 and 8aa2c8d.

📒 Files selected for processing (1)
  • garminconnect/client.py

- Detect credential failures in widget title and raise
  AuthenticationError instead of ConnectionError to prevent
  falling through to other strategies with bad credentials
- Clear widget MFA state in the prompt_mfa inline path (not just
  resume_login) to prevent stale session reuse
- Check HTTP status on MFA POST before parsing HTML to correctly
  classify 429/5xx as transport failures
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@garminconnect/client.py`:
- Around line 169-180: The widget MFA state (_widget_session and related
_widget_* attributes) must be cleared before starting a fresh login so
resume_login() won't incorrectly prefer the stale widget path; update the
beginning of login() (or just before resume_login() selection) to reset/clear
_widget_session, _widget_challenge, _widget_result (or any _widget_* attributes
used) to None/empty so that _complete_mfa_widget() is not chosen for new
portal/mobile MFA flows; ensure you reference and clear all widget-related
fields present on the Client instance so the new login proceeds through the
correct strategy selection.
- Around line 297-354: The response body title-based authentication heuristic is
being applied even for transport errors; before inspecting title/title_lower
(the block using title_match/title_lower), check r.ok (HTTP 2xx) and if not ok
raise a GarminConnectConnectionError (or re-raise appropriate transport error)
so server-side 4xx/5xx pages don't get misclassified as credential failures;
mirror the guard used in _complete_mfa_widget(), and keep the existing MFA
handling (references: _complete_mfa_widget, _establish_session, _widget_session,
_widget_signin_params, _widget_last_resp) intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 56cac49c-1646-4031-8d37-728a243cc166

📥 Commits

Reviewing files that changed from the base of the PR and between 8aa2c8d and 97fee47.

📒 Files selected for processing (1)
  • garminconnect/client.py

Comment on lines +169 to +180
1. SSO embed widget with curl_cffi (HTML form, no clientId)
2. Portal web flow with curl_cffi (desktop browser TLS + UA)
3. Portal web flow with plain requests (desktop browser UA)
4. Mobile SSO with curl_cffi (Android WebView TLS)
5. Mobile SSO with plain requests (last resort)
"""
strategies: list[tuple[str, Any]] = []

# SSO embed widget — uses /sso/embed + /sso/signin HTML form flow.
# No clientId parameter, so not subject to per-client rate limiting.
if HAS_CFFI:
strategies.append(("widget+cffi", self._widget_login_cffi))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reset stale widget MFA state before starting a new login() attempt.

Line 1182 now gives _widget_session highest priority in resume_login(). If a widget MFA challenge is abandoned or fails, and the same Client later runs a fresh login() that reaches portal/mobile MFA, the old _widget_* attributes are still present and the new code will be routed through _complete_mfa_widget() instead of the current flow.

🛠️ Suggested fix
     def login(
         self,
         email: str,
         password: str,
         prompt_mfa: Any = None,
         return_on_mfa: bool = False,
     ) -> tuple[str | None, Any]:
         """Log in to Garmin Connect.
@@
         4. Mobile SSO with curl_cffi (Android WebView TLS)
         5. Mobile SSO with plain requests (last resort)
         """
+        for attr in ("_widget_session", "_widget_signin_params", "_widget_last_resp"):
+            if hasattr(self, attr):
+                delattr(self, attr)
+
         strategies: list[tuple[str, Any]] = []
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@garminconnect/client.py` around lines 169 - 180, The widget MFA state
(_widget_session and related _widget_* attributes) must be cleared before
starting a fresh login so resume_login() won't incorrectly prefer the stale
widget path; update the beginning of login() (or just before resume_login()
selection) to reset/clear _widget_session, _widget_challenge, _widget_result (or
any _widget_* attributes used) to None/empty so that _complete_mfa_widget() is
not chosen for new portal/mobile MFA flows; ensure you reference and clear all
widget-related fields present on the Client instance so the new login proceeds
through the correct strategy selection.

Comment on lines +297 to +354
r = sess.post(
f"{sso_base}/signin",
params=signin_params,
headers={"Referer": r.url},
data={
"username": email,
"password": password,
"embed": "true",
"_csrf": csrf_match.group(1),
},
timeout=30,
)

if r.status_code == 429:
raise GarminConnectTooManyRequestsError(
"Widget login returned 429"
)

title_match = self._TITLE_RE.search(r.text)
title = title_match.group(1) if title_match else ""

# Step 4: Handle MFA
if "MFA" in title:
self._widget_session = sess
self._widget_signin_params = signin_params
self._widget_last_resp = r

if return_on_mfa:
return "needs_mfa", sess

if prompt_mfa:
mfa_code = prompt_mfa()
ticket = self._complete_mfa_widget(mfa_code)
self._establish_session(
ticket, sess=sess, service_url=sso_embed
)
del self._widget_session
del self._widget_signin_params
del self._widget_last_resp
return None, None
raise GarminConnectAuthenticationError(
"MFA Required but no prompt_mfa mechanism supplied"
)

if title != "Success":
# Detect credential failures to prevent falling through
# to other strategies with bad credentials
title_lower = title.lower()
if any(
hint in title_lower
for hint in ("locked", "invalid", "error", "incorrect")
):
raise GarminConnectAuthenticationError(
f"Widget login: authentication failed ('{title}')"
)
raise GarminConnectConnectionError(
f"Widget login: unexpected title '{title}'"
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Check the credential POST status before the title-based auth heuristic.

After Line 310, any non-429 4xx/5xx HTML page still flows into the title_lower check. A server-side error page titled "Error" or "Invalid request" will now raise GarminConnectAuthenticationError and stop the remaining strategies, even though the failure was transport-side. Mirror the r.ok guard from _complete_mfa_widget() here and only use the title hints on successful responses.

🛠️ Suggested fix
         if r.status_code == 429:
             raise GarminConnectTooManyRequestsError(
                 "Widget login returned 429"
             )
+        if not r.ok:
+            raise GarminConnectConnectionError(
+                f"Widget login: credential POST returned HTTP {r.status_code}"
+            )
 
         title_match = self._TITLE_RE.search(r.text)
         title = title_match.group(1) if title_match else ""
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@garminconnect/client.py` around lines 297 - 354, The response body
title-based authentication heuristic is being applied even for transport errors;
before inspecting title/title_lower (the block using title_match/title_lower),
check r.ok (HTTP 2xx) and if not ok raise a GarminConnectConnectionError (or
re-raise appropriate transport error) so server-side 4xx/5xx pages don't get
misclassified as credential failures; mirror the guard used in
_complete_mfa_widget(), and keep the existing MFA handling (references:
_complete_mfa_widget, _establish_session, _widget_session,
_widget_signin_params, _widget_last_resp) intact.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add SSO web widget login strategy to bypass 429 rate limiting

2 participants