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
180 lines
6.0 KiB
Markdown
180 lines
6.0 KiB
Markdown
# 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 |