# Task Recon: Implement login/logout/status methods with proper error handling

**Task ID:** 6.4
**Status:** pending
**Generated:** 2026-01-28T12:00:00Z
**Agent:** claude-opus-4-5-20251101

---

## Task Summary

Complete the AuthService public interface by implementing `login`, `logout`, `is_authenticated`, and `get_status` methods. These methods provide the high-level authentication API that CLI commands will use, building on top of the token caching and refresh infrastructure already implemented in subtasks 6.1-6.3.

### 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 complete token lifecycle management.
**This Subtask's Role:** Provides the final public API layer for authentication operations - the methods that CLI commands and other services will call directly. This subtask completes the AuthService implementation by adding user-facing operations on top of the core token management already implemented.

### Dependencies
- 6.1: MSAL ConfidentialClientApplication setup for certificate authentication (done)
- 6.2: Client-secret authentication with environment variable support (done)
- 6.3: Token caching and refresh logic (done)

### Acceptance Criteria
From task details:
1. `login(tenant_name, force)` - Returns existing token if authenticated (unless force=True), otherwise acquires new token
2. `logout(tenant_name, all_tenants)` - Clears tokens for one or all tenants
3. `is_authenticated(tenant_name)` - Returns True/False without throwing exceptions
4. `get_status(tenant_name)` - Returns AuthStatus with AUTHENTICATED/UNAUTHENTICATED/EXPIRED state
5. Create AuthToken model if needed (note: subtask 6.3 decided to use CachedToken instead)
6. Use AuthenticationError for credential issues, ConfigurationError for missing tenant

---

## Insights from Previous Tasks

### Dependency Task Notes
| Task ID | Title | Key Insight |
|---------|-------|-------------|
| 6.1 | MSAL ConfidentialClientApplication setup | SHA-1 thumbprint format is uppercase hex; MSAL validates tenant at instantiation (must mock in tests) |
| 6.2 | Client-secret authentication | Empty env var treated as not set; whitespace preserved in secrets |
| 6.3 | Token caching and refresh logic | Decided to return `CachedToken` from `refresh_token()` rather than creating separate `AuthToken` model |

### Reusable Patterns from Dependencies
- Tenant resolution: `_resolve_tenant(tenant_name)` at `auth_service.py:221-248` - use for all public methods
- Token needs refresh check: `_needs_refresh(token)` at `auth_service.py:250-272` - use in `is_authenticated`
- MSAL mocking pattern: See `test_auth_service.py:754-770` - mock `ConfidentialClientApplication`
- ConfigurationError for no default tenant: See `auth_service.py:239-245`

### Warnings and Gotchas
- **From Task 6.3**: `get_token()` returns only the access token string; use `refresh_token()` if you need the full `CachedToken`
- **From Task 6.3**: Cache key is the resolved tenant `profile.name`, not the original `tenant_name` parameter
- **From Task 6.1**: MSAL `ConfidentialClientApplication` makes a network call in `__init__` - unit tests must mock it
- **From Task 6.3**: Naive datetimes in `expires_at` are assumed to be UTC

### Open Items Inherited
- [x] Subtask 6.4: Implement `login`, `logout`, `get_status`, `is_authenticated` (this task)

---

## Architecture Context

### Relevant Design Decisions
- **Exception Hierarchy**: All errors inherit from `TeamsPhoneError` with specific exit codes (2-8)
- **AuthenticationError (exit 2)**: For credential issues (invalid token, cert problems)
- **ConfigurationError (exit 7)**: For missing tenant configuration
- **NotFoundError (exit 4)**: For tenant not found (from `_resolve_tenant`)
- **Return Types**: Subtask 6.3 decided to use `CachedToken` for token returns rather than creating a separate `AuthToken` model

### Module Structure
```
src/teams_phone/
├── services/
│   └── auth_service.py  # AuthService class - add methods here
├── models/
│   └── auth.py          # AuthStatus, AuthStatusType, CachedToken models
├── infrastructure/
│   ├── config_manager.py  # ConfigManager - tenant config access
│   └── cache_manager.py   # CacheManager - token storage
└── exceptions.py          # AuthenticationError, ConfigurationError, NotFoundError
```

### Conventions to Follow
- All public methods accept `tenant_name: str | None = None` - resolve to default if None
- Use `_resolve_tenant()` for tenant resolution (raises ConfigurationError or NotFoundError)
- Methods that can fail throw specific exceptions; `is_authenticated` returns bool without throwing
- Docstrings follow Google style with Args, Returns, Raises sections

---

## Related Code References

### Primary References (Must Read)
| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/services/auth_service.py` | 221-248 | `_resolve_tenant()` - tenant resolution pattern to reuse |
| `src/teams_phone/services/auth_service.py` | 250-272 | `_needs_refresh()` - check if token is valid |
| `src/teams_phone/services/auth_service.py` | 274-323 | `refresh_token()` - acquires new token, returns CachedToken |
| `src/teams_phone/services/auth_service.py` | 378-407 | `get_token()` - returns access token string |
| `src/teams_phone/models/auth.py` | 10-22 | `AuthStatusType` enum - AUTHENTICATED, UNAUTHENTICATED, EXPIRED |
| `src/teams_phone/models/auth.py` | 58-78 | `AuthStatus` model - return type for get_status |
| `src/teams_phone/infrastructure/cache_manager.py` | 129-148 | `clear_tokens()` - for logout implementation |

### Secondary References (Helpful Context)
| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/exceptions.py` | 47-57 | `AuthenticationError` definition |
| `src/teams_phone/exceptions.py` | 112-121 | `ConfigurationError` definition |
| `src/teams_phone/exceptions.py` | 72-82 | `NotFoundError` definition |
| `src/teams_phone/models/config.py` | 46-68 | `Config` model with `tenants: dict[str, TenantProfile]` |
| `src/teams_phone/infrastructure/config_manager.py` | 59-95 | `load_config()` - returns Config with tenants dict |

### Code Patterns to Follow

```python
# Pattern from auth_service.py:221-248 - Tenant resolution
def _resolve_tenant(self, tenant_name: str | None) -> TenantProfile:
    """Resolve tenant name to a TenantProfile."""
    if tenant_name is None:
        default_name = self.config_manager.get_default_tenant()
        if default_name is None:
            raise ConfigurationError(
                "No tenant specified and no default tenant configured",
                remediation=(
                    "Either specify a tenant name or set a default tenant:\n"
                    "  teams-phone tenants default <tenant-name>"
                ),
            )
        tenant_name = default_name
    return self.config_manager.get_tenant(tenant_name)
```

```python
# Pattern from cache_manager.py:129-148 - Clear tokens
def clear_tokens(self, tenant_name: str | None = None) -> None:
    """Clear cached tokens."""
    if tenant_name is not None:
        token_path = self._get_token_path(tenant_name)
        token_path.unlink(missing_ok=True)
    else:
        if self._token_dir.exists():
            for token_file in self._token_dir.glob("*.json"):
                token_file.unlink(missing_ok=True)
```

---

## Dependencies

### Internal Modules
- `teams_phone.models.AuthStatus` - Return type for get_status
- `teams_phone.models.AuthStatusType` - Enum values for status
- `teams_phone.models.CachedToken` - Token model (used instead of AuthToken per 6.3 decision)
- `teams_phone.exceptions.AuthenticationError` - For auth failures
- `teams_phone.exceptions.ConfigurationError` - For missing tenant config
- `teams_phone.exceptions.NotFoundError` - For tenant not found (from _resolve_tenant)

### External Packages
- `msal` - Already imported, not needed for new methods
- `datetime` - Already imported

### New Dependencies Needed
None required - all dependencies are already in place from subtasks 6.1-6.3.

---

## Testing Guidance

### Test File Location
`tests/unit/test_auth_service.py`

### Test Patterns in Use
```python
# Example test structure (tests/unit/test_auth_service.py:917-948)
class TestGetToken:
    """Tests for get_token method."""

    @pytest.mark.unit
    def test_returns_cached_token_when_valid(self, tmp_path: Path) -> None:
        """get_token returns cached token when it's still valid."""
        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)

        # Setup tenant and cache...
        # Assert expected behavior...
```

### Fixtures Available
- `tmp_path` (pytest built-in) - Temporary directory for test files
- `monkeypatch` (pytest built-in) - For environment variable manipulation

### Test Utilities
- `generate_test_certificate(tmp_path)` at `test_auth_service.py:22-69` - Creates test PEM files
- Pattern: Create ConfigManager/CacheManager with tmp_path for isolation

### Tests to Write
Per test strategy from task details:
1. `test_login_returns_cached_token_when_authenticated` - force=False, is_authenticated=True
2. `test_login_acquires_new_token_when_force_true` - force=True bypasses cache
3. `test_login_acquires_token_when_not_authenticated` - no cached token
4. `test_logout_clears_single_tenant_token` - specific tenant
5. `test_logout_clears_all_tenant_tokens` - all_tenants=True
6. `test_logout_handles_nonexistent_tenant` - NotFoundError handling
7. `test_is_authenticated_returns_true_for_valid_token`
8. `test_is_authenticated_returns_false_for_expired_token`
9. `test_is_authenticated_returns_false_for_no_token`
10. `test_is_authenticated_does_not_throw` - even for config errors
11. `test_get_status_returns_authenticated_with_details`
12. `test_get_status_returns_unauthenticated_when_no_token`
13. `test_get_status_returns_expired_when_token_expired`

---

## Risks and Blockers

### Technical Risks
- [ ] **is_authenticated must not throw**: Needs careful error handling to catch ConfigurationError and NotFoundError, returning False instead
- [ ] **logout all_tenants iteration**: Need to access `config.tenants` dict to iterate tenant names; ConfigManager doesn't have a `list_tenants()` method - must call `load_config().tenants.keys()`

### Dependency Risks
None - all dependencies (6.1, 6.2, 6.3) are complete.

### Open Questions
- [ ] **AuthToken vs CachedToken**: Task details mention "Create AuthToken model if not existing". Subtask 6.3 decided to use CachedToken. Should login return CachedToken or do we need AuthToken? Recommendation: Follow 6.3 decision and return CachedToken.
- [ ] **get_status error handling**: Should get_status throw for invalid tenant, or return UNAUTHENTICATED with error_message? Recommendation: Throw for ConfigurationError/NotFoundError (user needs to fix config), similar to other methods.

---

## Implementation Recommendations

### Suggested Approach
1. Start with `is_authenticated` - simplest method, establishes error-swallowing pattern
2. Implement `get_status` - uses similar logic but returns structured data
3. Implement `login` - builds on existing `refresh_token` and `is_authenticated`
4. Implement `logout` - standalone method using CacheManager

### Files to Create/Modify
| Action | File | Reason |
|--------|------|--------|
| Modify | `src/teams_phone/services/auth_service.py` | Add 4 public methods to AuthService class |
| Modify | `tests/unit/test_auth_service.py` | Add test classes for new methods |

### Quick Wins
- `is_authenticated` is a good first method - simple logic that wraps existing `_needs_refresh` check
- All methods follow the same tenant resolution pattern via `_resolve_tenant`

---

## Cheat Sheet

### Key Imports
```python
# Already in auth_service.py - no new imports needed
from teams_phone.exceptions import AuthenticationError, ConfigurationError
from teams_phone.models import AuthMethod, CachedToken, TenantProfile

# Need to add for get_status:
from teams_phone.models import AuthStatus, AuthStatusType
```

### Key Functions to Use
- `self._resolve_tenant(tenant_name)` - Resolves tenant name to TenantProfile, handles default
- `self._needs_refresh(token)` - Returns True if token is expired or within buffer
- `self.refresh_token(tenant_name)` - Acquires and caches new token, returns CachedToken
- `self.cache_manager.get_token(tenant_name)` - Returns CachedToken or None
- `self.cache_manager.clear_tokens(tenant_name)` - Clears specific or all tokens
- `self.config_manager.load_config()` - Returns Config with `tenants` dict

### Common Patterns
- Tenant resolution: See `auth_service.py:221-248`
- Token validation: See `auth_service.py:250-272`
- Test setup: See `test_auth_service.py:917-948`

### Method Signatures to Implement
```python
def login(self, tenant_name: str | None = None, force: bool = False) -> CachedToken:
    """Login and acquire authentication token."""
    ...

def logout(self, tenant_name: str | None = None, all_tenants: bool = False) -> None:
    """Logout and clear cached tokens."""
    ...

def is_authenticated(self, tenant_name: str | None = None) -> bool:
    """Check if currently authenticated with valid token."""
    ...

def get_status(self, tenant_name: str | None = None) -> AuthStatus:
    """Get current authentication status."""
    ...
```

---

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