# Quickstart Guide: Updated Authentication Flow for MFA with garth ## Overview This guide provides essential information for developers implementing the updated MFA authentication flow using garth. The updated flow properly handles multi-factor authentication challenges when users have MFA enabled on their Garmin Connect accounts. ## Architecture Overview The authentication flow follows this sequence: 1. CLI sends username/password to backend service 2. Backend service uses garth to initiate authentication with Garmin 3. If MFA is required, backend responds with MFA challenge details 4. CLI prompts user for MFA code 5. CLI sends MFA code to backend 6. Backend completes authentication with garth/Garmin and returns tokens ## Implementation Steps ### 1. Backend Service Updates The backend service needs to be updated to handle the multi-step MFA flow: ```python # In your authentication endpoint async def garmin_login(): # Step 1: Attempt initial authentication result = await garth_client.login(username, password) # Check if MFA is required if garth_client.mfa_required: # Return MFA challenge details return { "success": False, "mfa_required": True, "mfa_challenge_id": generate_challenge_id(), "mfa_type": determine_mfa_type(username) } # If no MFA required, complete authentication if result: token = await store_tokens(result) return { "success": True, "session_id": generate_session_id(), "access_token": token.access_token, "expires_in": token.expires_in } ``` ### 2. MFA Challenge Completion Endpoint Create an endpoint for completing the authentication after MFA code is provided: ```python @app.post("/api/garmin/login/mfa-complete") async def complete_mfa_login(mfa_data: MFACompletionRequest): # Complete authentication with MFA code using garth result = await garth_client.enter_two_step(mfa_data.mfa_code, mfa_data.challenge_id) if result: token = await store_tokens(result) return { "success": True, "session_id": generate_session_id(), "access_token": token.access_token, "expires_in": token.expires_in } else: return {"success": False, "error": "Invalid MFA code"} ``` ### 3. CLI Updates Update the CLI to handle the two-step flow: ```python # First call - initial authentication response = await api_client.post("/api/garmin/login", json=payload) if response.get("mfa_required"): # Get MFA code from user mfa_code = input(f"Enter {response.get('mfa_type')} code: ") # Complete authentication with MFA code completion_payload = { "mfa_code": mfa_code, "challenge_id": response.get("mfa_challenge_id") } response = await api_client.post("/api/garmin/login/mfa-complete", json=completion_payload) # Handle successful authentication if response.get("success"): # Store tokens locally token_manager.save_tokens(response) ``` ### 4. Token Management Implement secure storage and retrieval of garth tokens in CentralDB: ```python class TokenRepository: async def save_garth_tokens(self, user_id: str, tokens: dict): """Save garth tokens (OAuth1, OAuth2) to CentralDB""" # Encrypt tokens before storing encrypted_tokens = encrypt(tokens) # Store in database await db.execute( insert(GarthToken).values( user_id=user_id, encrypted_tokens=encrypted_tokens, created_at=datetime.utcnow(), expires_at=self.calculate_expiry(tokens.get('expires_in')) ) ) async def load_garth_tokens(self, user_id: str): """Load and decrypt garth tokens from CentralDB""" result = await db.execute( select(GarthToken).where(GarthToken.user_id == user_id) ) token_record = result.fetchone() if token_record and self.is_token_valid(token_record): return decrypt(token_record.encrypted_tokens) return None ``` ## Testing MFA Flow ### Unit Tests Test the individual components of the MFA flow: ```python # Test authentication without MFA def test_auth_without_mfa(): # Mock garth to return successful authentication without MFA with patch('garth.login', return_value=MOCK_SUCCESS_RESPONSE): response = await auth_endpoint({"username": "test", "password": "pass"}) assert response["success"] == True # Test MFA challenge initiation def test_auth_with_mfa_required(): # Mock garth to indicate MFA is required with patch('garth.login') as mock_login: mock_login.side_effect = MFARequiredException() response = await auth_endpoint({"username": "test", "password": "pass"}) assert response["mfa_required"] == True assert "mfa_challenge_id" in response ``` ### Integration Tests Test the complete MFA flow: ```python @pytest.mark.asyncio async def test_complete_mfa_flow(): # Test the full flow: initiate auth -> receive MFA challenge -> complete with code -> get tokens # 1. Initiate authentication (should return MFA required) init_resp = await client.post("/api/garmin/login", json={"username": "mfa_user", "password": "pass"}) assert init_resp.json()["mfa_required"] == True # 2. Complete MFA (in mock environment, use known code) mfa_resp = await client.post("/api/garmin/login/mfa-complete", json={"mfa_code": "123456", "challenge_id": init_resp.json()["challenge_id"]}) assert mfa_resp.json()["success"] == True assert "access_token" in mfa_resp.json() ``` ## Security Considerations - Store garth tokens encrypted in CentralDB - Implement rate limiting for authentication attempts to prevent brute force - Use secure session tokens with appropriate expiration times - Log authentication attempts for security monitoring - Implement secure handling of MFA codes (don't log them) - Validate MFA codes have not expired before accepting them