fix: route DI token endpoint to garmin.cn for CN accounts#360
Conversation
`is_cn=True` correctly routes the SSO and Connect API endpoints via the domain-aware `_sso` / `_connect` / `_connectapi` attributes, but the DI OAuth2 token endpoint stayed hardcoded to `diauth.garmin.com`. CN users don't exist in the .com user database, so: - Token exchange (post-SSO) succeeded for fresh logins because the SSO ticket validates server-side regardless of which DI auth host receives it (the .com endpoint accepts the ticket and issues a token whose JWT payload still carries `iss=https://diauth.garmin.cn` — confusing!) - BUT token refresh fails with 400: `{"error":"invalid_grant","error_description":"User doesn't exist in DB for GarminGUID: <uuid>"}` because the refresh path looks up the user by GUID in the host's DB. This matters in practice: the access token's TTL is ~4 hours, so any CN account that stays logged in longer than a single session will hit the refresh path and break. Fix: store `_di_token_url` as an instance attribute built from `domain` (same pattern as `_sso` / `_connect`), and use it in `_exchange_ticket()` and `_refresh_di_token()`. The module-level `DI_TOKEN_URL` constant is kept for backwards compatibility with external imports. Reproduce: `Garmin(email, password, is_cn=True).login()` then wait 4+ hours, then make any API call → 401 → 400 invalid_grant on refresh. After fix: token refresh hits `https://diauth.garmin.cn/...` → 200. Co-Authored-By: Claude Opus 4.7 <[email protected]>
WalkthroughThe OAuth token exchange and refresh logic in the Garmin Connect client now uses a domain-specific DI auth host URL computed during initialization, replacing a fixed constant. This enables token refresh for different domain environments (e.g., CN) that use Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
garminconnect/client.py (2)
894-898:⚠️ Potential issue | 🟡 MinorStale docstring references
diauth.garmin.com.Now that the endpoint is domain-aware via
self._di_token_url, the docstring claim "POST to diauth.garmin.com" is misleading for CN users — it's the one place a future reader would look to confirm the host is dynamic.📝 Suggested doc tweak
"""Exchange a CAS service ticket for native DI + IT Bearer tokens. - POST to diauth.garmin.com to get a DI OAuth2 token, then exchange - for an IT token via services.garmin.com. + POST to ``self._di_token_url`` (``diauth.{domain}``) to get a DI + OAuth2 token, then exchange for an IT token via services.{domain}. """🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@garminconnect/client.py` around lines 894 - 898, The docstring for the method that exchanges a CAS service ticket (the function which posts to obtain DI/IT tokens and uses self._di_token_url) incorrectly hardcodes "POST to diauth.garmin.com"; update the docstring to remove the static hostname and instead describe that the code POSTs to the domain-aware DI token endpoint referenced by self._di_token_url (or simply "the DI token endpoint"), and mention that the host is determined by self._di_token_url so it works for CN or other domains.
956-980: 🧹 Nitpick | 🔵 TrivialRefresh path correctly uses the instance URL — verify CN refresh under load.
_refresh_di_tokennow POSTs toself._di_token_url, which directly addresses the reportedinvalid_grant"User doesn't exist in DB for GarminGUID" failure on CN. The PR notes manual verification with agarmin.cnaccount.One thing worth noting (not blocking): on a refresh failure,
_refresh_session(line 1028) only logs at debug level and swallows the exception, so a regression here would be silent — the next API call would just continue with the (possibly stale)di_tokenuntil it 401s and triggers a re-refresh loop. Consider whether a CN-domain integration test or at least a unit test assertingself._di_token_urlis the POST target ondomain="garmin.cn"would be worth adding to lock this down.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@garminconnect/client.py` around lines 956 - 980, Add a unit test that verifies _refresh_di_token posts to the instance-specific URL by instantiating the client with domain="garmin.cn" (or mocking instance resolution), mocking the _http_post method and asserting it is called with self._di_token_url as the URL and with the expected headers/data; specifically target the _refresh_di_token method and mock _http_post so the test fails if code still posts to a hardcoded CN endpoint. Optionally, add a small test that exercises _refresh_session to confirm failures are at least logged (mocking _refresh_di_token to raise) so regressions that silently swallow DI refresh errors are detected.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@garminconnect/client.py`:
- Around line 894-898: The docstring for the method that exchanges a CAS service
ticket (the function which posts to obtain DI/IT tokens and uses
self._di_token_url) incorrectly hardcodes "POST to diauth.garmin.com"; update
the docstring to remove the static hostname and instead describe that the code
POSTs to the domain-aware DI token endpoint referenced by self._di_token_url (or
simply "the DI token endpoint"), and mention that the host is determined by
self._di_token_url so it works for CN or other domains.
- Around line 956-980: Add a unit test that verifies _refresh_di_token posts to
the instance-specific URL by instantiating the client with domain="garmin.cn"
(or mocking instance resolution), mocking the _http_post method and asserting it
is called with self._di_token_url as the URL and with the expected headers/data;
specifically target the _refresh_di_token method and mock _http_post so the test
fails if code still posts to a hardcoded CN endpoint. Optionally, add a small
test that exercises _refresh_session to confirm failures are at least logged
(mocking _refresh_di_token to raise) so regressions that silently swallow DI
refresh errors are detected.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 703aa15d-b166-4170-ae85-90b4ded40d2f
📒 Files selected for processing (1)
garminconnect/client.py
|
Thanks a lot! |
Bug
is_cn=Truecorrectly routes the SSO and Connect API endpoints via the domain-aware_sso/_connect/_connectapiattributes, but the DI OAuth2 token endpoint (DI_TOKEN_URL) stays hardcoded todiauth.garmin.com. CN users don't exist in the.comuser database, so:.comDI endpoint accepts the ticket and issues a token whose JWT payload still carriesiss=https://diauth.garmin.cn(the issuer claim correctly reflects the CN auth server, but the token was minted by.com).diauth.garmin.cn.This matters in practice: the access token's TTL is ~4 hours, so any CN account that stays logged in longer than a single session will hit the refresh path and break. Cron-driven sync jobs are the canonical case.
Reproduce
Fix
Store
_di_token_urlas an instance attribute built fromdomain(same pattern as_sso/_connect/_connectapi), and use it in_exchange_ticket()and_refresh_di_token(). The module-levelDI_TOKEN_URLconstant is kept for backwards compatibility with external imports.After fix: refresh hits
https://diauth.garmin.cn/di-oauth2-service/oauth/token→ 200.Verification
Manually patched the same code locally (monkey-patching
garminconnect.client.DI_TOKEN_URLto the CN variant before calling_refresh_di_token()) — refresh succeeds, subsequentconnectapi()calls work, persisted tokens reload cleanly across processes. Verified against a realgarmin.cnaccount.Notes
DI_GRANT_TYPEis left as the.comURI because it's a brand-namespace OAuth2 grant_type identifier (RFC 6749), not an HTTP endpoint.garmin.comusers.Summary by CodeRabbit