mirror of
https://github.com/sstent/FitTrack_GarminSync.git
synced 2026-01-25 08:35:23 +00:00
- Created detailed implementation plan with technical context - Developed data models for GarthToken, MFAChallenge, and UserSession entities - Defined API contracts for MFA authentication flow - Created quickstart guide for implementation - Updated agent context with new technology stack - Verified constitution compliance for all design decisions
6.0 KiB
6.0 KiB
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:
- CLI sends username/password to backend service
- Backend service uses garth to initiate authentication with Garmin
- If MFA is required, backend responds with MFA challenge details
- CLI prompts user for MFA code
- CLI sends MFA code to backend
- 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:
# 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:
@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:
# 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:
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:
# 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:
@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