Problem
All four current login strategies (portal+cffi, portal+requests, mobile+cffi, mobile+requests) are subject to aggressive 429 rate limiting on Garmin's SSO endpoints. Once triggered, the rate limit is per-account (not per-IP or per-User-Agent), making it impossible to recover by changing network or headers. This affects issues #332 and #337.
The rate limiting appears to be keyed on the clientId parameter (GarminConnect for portal, GCM_ANDROID_DARK for mobile) combined with the account email. The official Garmin Connect mobile app is unaffected because it uses a different rate limit bucket.
Root cause analysis
We tested systematically by isolating variables:
- Changed IP (mobile hotspot): still 429. Rules out IP-only rate limiting.
- Changed User-Agent (randomized via
ua_generator): still 429. UA is not the trigger.
- Changed client ID to
GCM_IOS_DARK: bypassed rate limit for one account but not the other (which had accumulated more failed attempts). Confirms rate limiting is per clientId + account.
- Used the garth-style web widget flow (
/sso/embed + /sso/signin): bypassed rate limit completely for both accounts, even ones that were actively throttled on portal and mobile endpoints.
The web widget flow works because it uses a completely different SSO path: an HTML form-based login with CSRF tokens, no clientId parameter. Garmin's rate limiting doesn't apply to this endpoint, and curl_cffi with Chrome TLS impersonation passes Cloudflare's fingerprint check.
Proposed fix
Add a fifth login strategy, widget+cffi, that uses the SSO embed widget flow with curl_cffi. No new dependencies needed.
The flow:
# 1. Set cookies
sess.get("https://sso.garmin.com/sso/embed", params={
"id": "gauth-widget", "embedWidget": "true",
"gauthHost": "https://sso.garmin.com/sso"
})
# 2. Get CSRF token
r = sess.get("https://sso.garmin.com/sso/signin", params=SIGNIN_PARAMS)
csrf = re.search(r'name="_csrf"\s+value="(.+?)"', r.text).group(1)
# 3. POST login (HTML form, no clientId)
r = sess.post("https://sso.garmin.com/sso/signin", params=SIGNIN_PARAMS, data={
"username": email, "password": password,
"embed": "true", "_csrf": csrf
})
# 4. Handle MFA if needed (title == "Enter MFA code for login")
# POST to /sso/verifyMFA/loginEnterMfaCode with mfa-code field
# 5. Extract service ticket from success page
ticket = re.search(r'embed\?ticket=([^"]+)"', r.text).group(1)
The service ticket then feeds into the existing DI token exchange (_exchange_service_ticket), so no changes needed downstream.
Strategy order suggestion
widget+cffi should be tried first (or second after portal+cffi), since it's the only strategy that reliably bypasses both Cloudflare TLS fingerprinting and client ID rate limiting.
Trade-offs
The web widget flow parses HTML (CSRF token from a hidden input, success/MFA status from the <title> tag). This is more fragile than the JSON-based portal/mobile APIs. However, it's worth noting:
- The garth library used this exact flow successfully for years
- The JSON endpoints are currently unusable for many users due to rate limiting
- Having it as one strategy in the fallback chain means if Garmin changes the HTML, the other strategies still exist as fallback
Test results
Tested locally on 2026-04-04 against two accounts (one with MFA, one without). The web widget flow was run twice consecutively for both accounts, succeeding both times:
- Both accounts were actively 429-blocked on all four existing strategies
- Both accounts authenticated successfully via the web widget flow on both runs
- Service tickets exchanged for DI tokens without issues
- MFA (TOTP) handled end-to-end for the MFA-enabled account
- API calls (social profile, activity data) worked with the resulting tokens
Implementation: #345
Problem
All four current login strategies (
portal+cffi,portal+requests,mobile+cffi,mobile+requests) are subject to aggressive 429 rate limiting on Garmin's SSO endpoints. Once triggered, the rate limit is per-account (not per-IP or per-User-Agent), making it impossible to recover by changing network or headers. This affects issues #332 and #337.The rate limiting appears to be keyed on the
clientIdparameter (GarminConnectfor portal,GCM_ANDROID_DARKfor mobile) combined with the account email. The official Garmin Connect mobile app is unaffected because it uses a different rate limit bucket.Root cause analysis
We tested systematically by isolating variables:
ua_generator): still 429. UA is not the trigger.GCM_IOS_DARK: bypassed rate limit for one account but not the other (which had accumulated more failed attempts). Confirms rate limiting is perclientId + account./sso/embed+/sso/signin): bypassed rate limit completely for both accounts, even ones that were actively throttled on portal and mobile endpoints.The web widget flow works because it uses a completely different SSO path: an HTML form-based login with CSRF tokens, no
clientIdparameter. Garmin's rate limiting doesn't apply to this endpoint, andcurl_cffiwith Chrome TLS impersonation passes Cloudflare's fingerprint check.Proposed fix
Add a fifth login strategy,
widget+cffi, that uses the SSO embed widget flow withcurl_cffi. No new dependencies needed.The flow:
The service ticket then feeds into the existing DI token exchange (
_exchange_service_ticket), so no changes needed downstream.Strategy order suggestion
widget+cffishould be tried first (or second afterportal+cffi), since it's the only strategy that reliably bypasses both Cloudflare TLS fingerprinting and client ID rate limiting.Trade-offs
The web widget flow parses HTML (CSRF token from a hidden input, success/MFA status from the
<title>tag). This is more fragile than the JSON-based portal/mobile APIs. However, it's worth noting:Test results
Tested locally on 2026-04-04 against two accounts (one with MFA, one without). The web widget flow was run twice consecutively for both accounts, succeeding both times:
Implementation: #345