mirror of
https://github.com/sstent/FitTrack_GarminSync.git
synced 2026-01-25 16:41:41 +00:00
Complete implementation planning for MFA authentication with garth
- 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
This commit is contained in:
180
specs/007-update-the-authentication/quickstart.md
Normal file
180
specs/007-update-the-authentication/quickstart.md
Normal file
@@ -0,0 +1,180 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user