From 9e096e6f6eee7a218fe9b5be24602b4680f8f7b9 Mon Sep 17 00:00:00 2001 From: sstent Date: Mon, 22 Dec 2025 06:12:29 -0800 Subject: [PATCH] docs: Add spec for fixing garminconnect login and implementing garth MFA --- .../spec.md | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 specs/009-fix-garminconnect-login-with-garth/spec.md diff --git a/specs/009-fix-garminconnect-login-with-garth/spec.md b/specs/009-fix-garminconnect-login-with-garth/spec.md new file mode 100644 index 0000000..2df3782 --- /dev/null +++ b/specs/009-fix-garminconnect-login-with-garth/spec.md @@ -0,0 +1,54 @@ +--- +description: "Specification for resolving garminconnect login failure and implementing garth MFA" +--- + +# 009: Fix Garmin Connect Login Failure and Implement Garth MFA + +## 1. Problem Statement + +The GarminSync backend service has been encountering persistent login failures with `garminconnect` due to recent changes in Garmin's Single Sign-On (SSO) process. The specific error observed in the logs is `Login failed: Unexpected title: GARMIN Authentication Application`. This issue prevents users from authenticating with Garmin Connect, especially those with Multi-Factor Authentication (MFA) enabled, severely impacting the service's core functionality. The existing implementation in `backend/src/services/garmin_auth_service.py` was relying on `garminconnect`'s internal login method, which proved brittle against Garmin's evolving authentication flow. + +## 2. Proposed Solution + +The solution involves refactoring the authentication mechanism within the `GarminAuthService` to primarily leverage the `garth` library for direct login and robust MFA handling. `garth` is known for its resilience to Garmin's authentication changes and its explicit support for MFA flows. Once `garth` successfully establishes a session, `garminconnect` will implicitly pick up this session, thereby bypassing `garminconnect`'s problematic internal login process. + +## 3. Technical Details + +### 3.1. Modified Files + +- `backend/src/services/garmin_auth_service.py`: + - **Imports**: Replaced `garminconnect.Garmin` import with `garth` and `garth.exc.GarthException`. + - **`_perform_login` method**: Refactored to use `garth.Client().login(email=username, password=password)`. This method now returns a `garth.Client` instance and is responsible for initiating the core `garth` login. It also raises `GarthException` if MFA is required, which is then handled by the calling method. + - **`initial_login` method**: Modified to call the refactored `_perform_login`. It now handles `GarthException` to detect when MFA is required, returning a structured dictionary response (`{"success": False, "mfa_required": True, ...}`) to indicate the need for MFA input. The return type was updated from `Optional[GarminCredentials]` to `Dict[str, Any]`. + - **`complete_mfa_login` method**: A new asynchronous method added to `GarminAuthService`. This method takes `username`, `password`, and `mfa_code`, and uses `garth.Client().login(email=username, password=password, mfa_token=mfa_code)` to complete the MFA-enabled login. It returns structured dictionary responses for success or failure, including `GarminCredentials` on successful authentication. + - **`GarminCredentials` Instantiation**: Accesses to `display_name`, `full_name`, and `unit_system` attributes are now directly from the `garth.Client` instance (e.g., `client.display_name`) rather than a `garminconnect.Garmin` instance, as `garth` populates these. + - **Static Analysis Fixes**: Corrected `typing` imports to include `Any` and `Dict`, and removed unused `TextIO`. Suppressed `mypy` `attr-defined` errors for `garth.Client` attributes using `# type: ignore` comments. +- `backend/src/api/garmin_sync.py`: Sorted imports using `ruff`. + +### 3.2. Authentication Flow Changes + +The new authentication flow in the backend service is as follows: +1. **Initial Login Attempt**: The `initial_login` method attempts a login using `garth`. +2. **MFA Detection**: If `garth` detects that MFA is required, `initial_login` returns a response indicating this, prompting the client (e.g., CLI) to request an MFA code from the user. +3. **MFA Completion**: The client then calls the `complete_mfa_login` method with the provided MFA code. This method attempts to finalize the `garth` login. +4. **Session Establishment**: Upon successful login (either initial or after MFA), `garth` automatically manages the session tokens. `garminconnect.Garmin` instances, when initialized without credentials, will then implicitly use this established `garth` session. + +## 4. Acceptance Criteria + +### 4.1. Functional Requirements + +- **FR1**: Users with Garmin Connect accounts (both with and without MFA enabled) shall be able to successfully authenticate with the GarminSync backend service. +- **FR2**: The `initial_login` endpoint/method shall correctly detect and indicate when MFA is required for a user account. +- **FR3**: The `complete_mfa_login` endpoint/method shall successfully process a provided MFA code to complete the authentication for MFA-enabled accounts. +- **FR4**: Upon successful authentication, the backend service shall return `GarminCredentials` containing valid session and token information. +- **FR5**: The `garminconnect` library, when used for subsequent API calls (e.g., fetching activities), shall successfully utilize the session established by `garth` without requiring a separate login. + +### 4.2. Quality Attributes + +- **QA1 - Robustness**: The authentication flow shall be resilient to changes in Garmin's SSO page structure (as handled by `garth`). +- **QA2 - Security**: While `garmin_password_plaintext` is still present, the change ensures the primary authentication uses `garth`'s secure methods. (Note: Removal of plaintext password storage is a future task). +- **QA3 - Maintainability**: The code changes adhere to Python best practices and pass static analysis checks (`ruff`, `mypy`). + +## 5. Verification + +The fix can be verified by deploying the updated backend service and attempting to log in with various Garmin Connect accounts, including those protected by MFA, using a compatible client (e.g., the CLI). Success is measured by the ability to authenticate and subsequently fetch data via `garminconnect` methods. Static analysis with `ruff check` and `mypy` passed.