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:
2025-12-19 13:08:28 -08:00
parent 05c41ff1fe
commit eebedaa57c
5 changed files with 540 additions and 0 deletions

View 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