# Task Recon: Implement MSAL ConfidentialClientApplication setup for certificate authentication

**Task ID:** 6.1
**Status:** pending
**Generated:** 2026-01-28
**Agent:** claude-opus-4-5-20251101

---

## Task Summary

Create the core MSAL integration with certificate-based authentication, including PEM file loading and thumbprint extraction. This subtask establishes the foundation for the AuthService by implementing:
1. AuthService class with `__init__` accepting ConfigManager and CacheManager dependencies
2. SCOPES class constant for Graph API access
3. `_load_certificate()` method for PEM file loading and SHA-1 thumbprint extraction
4. `_create_msal_app()` method for creating ConfidentialClientApplication with certificate credentials
5. Error handling with AuthenticationError for file and certificate issues

### Parent Task Context

**Parent Task:** Implement AuthService with MSAL (ID: 6)
**Parent Goal:** Create the AuthService for Microsoft Entra ID authentication supporting certificate and client-secret methods, with token caching and status management.
**This Subtask's Role:** Establishes the foundational certificate authentication mechanism that subsequent subtasks (6.2-6.4) will build upon for client-secret auth, token caching, and login/logout functionality.

### Dependencies
- Depends on: Task 3 (Pydantic Models for Configuration - provides TenantProfile, AuthMethod, CachedToken)
- Depends on: Task 4 (ConfigManager - provides tenant profile retrieval)
- Depends on: Task 5 (CacheManager - provides token storage, though not directly used in 6.1)

### Acceptance Criteria
1. AuthService class created in `src/teams_phone/services/auth_service.py`
2. `SCOPES` constant set to `["https://graph.microsoft.com/.default"]`
3. `_load_certificate(cert_path: str)` returns dict with `private_key` and `thumbprint` keys
4. PEM file loading uses cryptography library for x509 certificate parsing
5. Thumbprint is SHA-1 hash of certificate, base64url encoded
6. `_create_msal_app(profile: TenantProfile)` creates ConfidentialClientApplication for CERTIFICATE auth method
7. Authority URL pattern: `https://login.microsoftonline.com/{tenant_id}`
8. FileNotFoundError for missing certificates raises AuthenticationError with remediation guidance
9. All tests pass with `pytest tests/ -m unit`
10. Type checking passes with `mypy src/ tests/`

---

## Insights from Previous Tasks

### Dependency Task Notes

| Task ID | Title | Key Insight |
|---------|-------|-------------|
| 3 | Pydantic Models for Configuration | `AuthMethod(str, Enum)` pattern makes enums directly usable as strings; `ConfigDict(extra="forbid")` required on all models |
| 4 | ConfigManager | Load-modify-save pattern for config; `NotFoundError` raised for missing tenants with remediation text |
| 5 | CacheManager | `ConfigurationError` with `from e` for chained exceptions on PermissionError; graceful degradation returns None for invalid data |

### Reusable Patterns from Dependencies
- **Permission error handling**: `try: ... except PermissionError as e: raise ConfigurationError(...) from e` - see `config_manager.py:48-57`
- **Pydantic JSON serialization**: `model.model_dump_json()` for saving, `Model.model_validate_json(content)` for loading - see `cache_manager.py:104-108`
- **Path expansion pattern**: `Path(str_or_none or DEFAULT).expanduser()` - see `cache_manager.py:38-42`

### Warnings and Gotchas
- **mypy strict mode**: Doesn't allow direct enum-to-string comparison (use `.value` instead)
- **datetime.utcnow() deprecated**: Use `datetime.now(timezone.utc)` in Python 3.12+
- **extra="forbid" on Pydantic models**: Any extra fields cause validation failures

### Open Items Inherited
- [ ] Config model may need `populate_by_name=True` if TOML uses different casing (from Task 3)
- [ ] Consider adding validation for cert_path existence when auth_method is CERTIFICATE (from Task 3 - relevant to this task)

---

## Architecture Context

### Relevant Design Decisions
- **Layered Monolith**: Services layer handles business logic, Infrastructure layer handles external integrations
- **AuthService lives in services/**: Even though it uses MSAL (external), it's business logic for auth
- **Exception hierarchy**: All errors inherit from `TeamsPhoneError` with exit codes; use `AuthenticationError` for auth failures
- **Pydantic strict mode**: Fail-fast on API contract changes

### Module Structure
```
src/teams_phone/
├── services/
│   ├── __init__.py       # Export AuthService
│   └── auth_service.py   # NEW FILE - AuthService implementation
├── infrastructure/
│   ├── config_manager.py # Used to get TenantProfile
│   └── cache_manager.py  # Used for token storage (later subtasks)
├── models/
│   ├── tenant.py         # TenantProfile, AuthMethod
│   └── auth.py           # CachedToken, AuthStatus, AuthStatusType
└── exceptions.py         # AuthenticationError
```

### Conventions to Follow
- Use `ConfigDict(extra="forbid")` if creating new Pydantic models
- Docstrings follow Google style (see existing files)
- All public methods need type hints
- Raise custom exceptions with `remediation` parameter for user guidance
- Use `from e` for exception chaining

---

## Related Code References

### Primary References (Must Read)

| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/models/tenant.py` | 1-47 | TenantProfile and AuthMethod enum definitions |
| `src/teams_phone/models/auth.py` | 1-79 | CachedToken and AuthStatus models for auth state |
| `src/teams_phone/exceptions.py` | 47-58 | AuthenticationError class with exit code 2 |
| `src/teams_phone/infrastructure/config_manager.py` | 19-38 | ConfigManager __init__ pattern with path handling |
| `src/teams_phone/constants.py` | 1-94 | Constants including GRAPH_API_BASE, exit codes |

### Secondary References (Helpful Context)

| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/infrastructure/cache_manager.py` | 54-71 | Permission error handling pattern for ensure_*_dir |
| `tests/unit/test_config_manager.py` | 67-78 | Test pattern for permission errors using patch |
| `tests/unit/test_cache_manager.py` | 87-108 | Test patterns for get_token (similar to what we need) |

### Code Patterns to Follow

```python
# Exception raising pattern with remediation (src/teams_phone/exceptions.py:26-35)
class AuthenticationError(TeamsPhoneError):
    """Authentication failure (exit code 2)."""
    exit_code: int = ExitCode.AUTH_ERROR

# Usage:
raise AuthenticationError(
    f"Cannot load certificate from {cert_path}: {e}",
    remediation="Verify the certificate file exists and is a valid PEM file."
) from e
```

```python
# Permission/File error handling pattern (config_manager.py:48-57)
try:
    self.config_path.parent.mkdir(parents=True, exist_ok=True)
except PermissionError as e:
    raise ConfigurationError(
        f"Cannot create configuration directory {self.config_path.parent}: {e}",
        remediation=(
            f"Check that you have write permissions to {self.config_path.parent}.\n"
            "You may need to create the directory manually or run with elevated privileges."
        ),
    ) from e
```

---

## Dependencies

### Internal Modules
- `teams_phone.infrastructure.ConfigManager` - retrieves TenantProfile for auth
- `teams_phone.infrastructure.CacheManager` - token storage (used in later subtasks)
- `teams_phone.models.TenantProfile` - tenant configuration with auth_method, cert_path
- `teams_phone.models.AuthMethod` - enum for CERTIFICATE vs CLIENT_SECRET
- `teams_phone.models.CachedToken` - token storage model (used in later subtasks)
- `teams_phone.exceptions.AuthenticationError` - auth failure exception

### External Packages
- `msal>=1.24.0` - Microsoft Authentication Library (already in pyproject.toml)
- `cryptography` - for PEM certificate parsing and SHA-1 thumbprint (NEEDS TO BE ADDED)

### New Dependencies Needed
- `cryptography>=41.0.0` - Required for X.509 certificate loading and SHA-1 fingerprint calculation. Add to `pyproject.toml` dependencies.

---

## Testing Guidance

### Test File Location
`tests/unit/test_auth_service.py` (new file to create)

### Test Patterns in Use
```python
# Test class organization pattern (tests/unit/test_config_manager.py:20-41)
class TestAuthServiceInit:
    """Tests for AuthService initialization."""

    @pytest.mark.unit
    def test_accepts_dependencies(self, tmp_path: Path) -> None:
        """AuthService accepts ConfigManager and CacheManager."""
        # Create managers with tmp_path
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))

        service = AuthService(config_manager, cache_manager)

        assert service is not None
```

```python
# Test pattern for file operations with tmp_path (tests/unit/test_cache_manager.py)
@pytest.mark.unit
def test_load_certificate_valid_pem(self, tmp_path: Path) -> None:
    """_load_certificate loads valid PEM certificate."""
    # Create test certificate file
    cert_path = tmp_path / "test.pem"
    cert_path.write_text(VALID_PEM_CONTENT)  # Define test constant

    result = service._load_certificate(str(cert_path))

    assert "private_key" in result
    assert "thumbprint" in result
```

```python
# Test pattern for exception assertions (tests/unit/test_config_manager.py:67-78)
@pytest.mark.unit
def test_missing_certificate_raises_auth_error(self, tmp_path: Path) -> None:
    """_load_certificate raises AuthenticationError for missing file."""
    with pytest.raises(AuthenticationError) as exc_info:
        service._load_certificate("/nonexistent/path.pem")

    assert "Cannot load certificate" in str(exc_info.value)
    assert exc_info.value.remediation is not None
```

### Fixtures Available
- `tmp_path` - pytest built-in for temporary directory
- Consider creating `conftest.py` fixtures for:
  - Sample valid PEM certificate (use cryptography to generate)
  - ConfigManager and CacheManager instances

### Test Utilities
- `tests/conftest.py` - Add fixtures for auth service testing
- Use `unittest.mock.patch` for MSAL mocking in later subtasks

---

## Risks and Blockers

### Technical Risks
- [ ] **cryptography dependency not yet added**: Must add `cryptography>=41.0.0` to `pyproject.toml` before implementation
- [ ] **PEM file format variations**: PEM files may contain certificate + private key combined, or just private key. MSAL requires specific format. Test with real-world certificate formats.
- [ ] **SHA-1 thumbprint encoding**: Azure expects specific encoding (hex uppercase? base64?). Verify against Azure portal thumbprint display.

### Dependency Risks
- [ ] Tasks 3, 4, 5 marked as pending in TaskMaster but implementation notes show they are complete. Verify code exists before proceeding.

### Open Questions
- [ ] **Thumbprint encoding format**: MSAL documentation shows hex string for thumbprint. Azure portal shows hex (uppercase, colon-separated). Need to verify exact format expected by MSAL. Based on Context7 docs, MSAL expects the thumbprint as a hex string.
- [ ] **Combined PEM handling**: Should `_load_certificate` handle PEM files with both cert and key, or require separate files? Task details say "PEM file" singular, suggesting combined format.
- [ ] **passphrase support**: Should we support encrypted private keys? Task details don't mention this. Start without, add if needed.

---

## Implementation Recommendations

### Suggested Approach
1. Add `cryptography>=41.0.0` to `pyproject.toml` dependencies
2. Create `src/teams_phone/services/auth_service.py` with class skeleton
3. Implement `_load_certificate()` method:
   - Read PEM file content
   - Parse private key using cryptography
   - Parse certificate using cryptography
   - Calculate SHA-1 fingerprint using `cert.fingerprint(hashes.SHA1())`
   - Return dict with `private_key` (PEM string) and `thumbprint` (hex string)
4. Implement `_create_msal_app()` method for CERTIFICATE auth only
5. Update `src/teams_phone/services/__init__.py` to export AuthService
6. Create `tests/unit/test_auth_service.py` with unit tests
7. Run `pytest tests/ -m unit`, `mypy src/ tests/`, `ruff check src/ tests/`

### Files to Create/Modify
| Action | File | Reason |
|--------|------|--------|
| Modify | `pyproject.toml` | Add cryptography dependency |
| Create | `src/teams_phone/services/auth_service.py` | Main implementation |
| Modify | `src/teams_phone/services/__init__.py` | Export AuthService |
| Create | `tests/unit/test_auth_service.py` | Unit tests |

### Quick Wins
- Start with `_load_certificate()` implementation - it's self-contained and testable
- Use Python's `cryptography` library which is well-documented and type-hinted
- MSAL ConfidentialClientApplication is straightforward once certificate dict is correct

---

## Cheat Sheet

### Key Imports
```python
from msal import ConfidentialClientApplication
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.serialization import Encoding

from teams_phone.exceptions import AuthenticationError
from teams_phone.infrastructure import CacheManager, ConfigManager
from teams_phone.models import AuthMethod, TenantProfile
```

### Key Functions to Use
- `x509.load_pem_x509_certificate(pem_data)` - Load X509 cert from PEM bytes
- `serialization.load_pem_private_key(key_data, password=None)` - Load private key
- `cert.fingerprint(hashes.SHA1())` - Get SHA-1 fingerprint as bytes
- `fingerprint.hex()` - Convert bytes to hex string
- `ConfidentialClientApplication(client_id, client_credential={"private_key": ..., "thumbprint": ...}, authority=...)` - Create MSAL app

### Common Patterns
- **MSAL authority URL**: `f"https://login.microsoftonline.com/{profile.tenant_id}"`
- **Certificate dict for MSAL**: `{"private_key": pem_string, "thumbprint": hex_string}`
- **File reading with encoding**: `Path(cert_path).read_bytes()` for PEM data

### MSAL Certificate Credential Format
```python
# From Context7 MSAL documentation
client_credential = {
    "private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----",
    "thumbprint": "A1B2C3D4E5F6...",  # SHA-1 hex string (uppercase)
    # "public_certificate": "..." # Optional, for Subject Name/Issuer auth
}
```

### Certificate Loading Pattern
```python
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization

def _load_certificate(self, cert_path: str) -> dict[str, str]:
    """Load certificate and extract private key and thumbprint."""
    try:
        pem_data = Path(cert_path).read_bytes()
    except FileNotFoundError as e:
        raise AuthenticationError(
            f"Certificate file not found: {cert_path}",
            remediation="Verify the certificate path in your tenant configuration."
        ) from e

    # Load certificate for thumbprint
    cert = x509.load_pem_x509_certificate(pem_data)
    thumbprint = cert.fingerprint(hashes.SHA1()).hex().upper()

    # Load private key (may be in same file or we need the whole PEM)
    private_key = serialization.load_pem_private_key(pem_data, password=None)
    private_key_pem = private_key.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.PKCS8,
        encryption_algorithm=serialization.NoEncryption()
    ).decode("utf-8")

    return {
        "private_key": private_key_pem,
        "thumbprint": thumbprint,
    }
```

---

*Generated by task-recon (claude-opus-4-5-20251101) | Ready for implementation*
