Files
sstent eebedaa57c 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
2025-12-19 13:08:28 -08:00

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:

  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:

# 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