# Task Recon: Implement client-secret authentication with environment variable support

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

---

## Task Summary

Add client-secret authentication method to AuthService that reads credentials from the `TEAMS_PHONE_CLIENT_SECRET` environment variable. This extends the existing `_create_msal_app` method to handle `AuthMethod.CLIENT_SECRET` in addition to the already-implemented certificate authentication.

### 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:** Adds the client-secret authentication path to `_create_msal_app()`, complementing the certificate auth implemented in subtask 6.1. This enables developers to use simpler (but less secure) client-secret auth for dev/test scenarios.

### Dependencies
- Depends on: Subtask 6.1 (MSAL ConfidentialClientApplication setup for certificate authentication) - **DONE**

### Acceptance Criteria
1. Define constant `CLIENT_SECRET_ENV_VAR = "TEAMS_PHONE_CLIENT_SECRET"` in auth_service.py
2. Update `_create_msal_app` to handle `AuthMethod.CLIENT_SECRET`:
   - Read client secret from `os.environ.get(CLIENT_SECRET_ENV_VAR)`
   - If env var is not set, raise `AuthenticationError` with remediation: "Set the TEAMS_PHONE_CLIENT_SECRET environment variable"
   - Create `ConfidentialClientApplication` with `client_credential=client_secret` (string)
3. Add `_get_client_secret() -> str` helper method for cleaner code organization
4. Both auth methods use the same authority URL pattern: `https://login.microsoftonline.com/{tenant_id}`
5. Docstrings document that client-secret is for dev/test only (less secure than certificates)
6. All tests pass with `pytest tests/ -m unit`
7. Type checking passes with `mypy src/ tests/`

---

## Insights from Previous Tasks

### Dependency Task Notes

| Task ID | Title | Key Insight |
|---------|-------|-------------|
| 6.1 | MSAL ConfidentialClientApplication setup for certificate auth | SHA-1 thumbprint uses uppercase hex (40 chars); MSAL type ignore needed; Combined PEM file approach; Authority URL format established |
| 3 | Pydantic Models for Configuration | `AuthMethod(str, Enum)` pattern makes enums usable as strings |
| 4 | ConfigManager | Load-modify-save pattern; `NotFoundError` with remediation for missing tenants |
| 5 | CacheManager | Exception chaining with `from e`; graceful degradation returns None |

### Reusable Patterns from Dependencies

- **MSAL ConfidentialClientApplication creation**: See `auth_service.py:160-164` for certificate auth pattern
- **Authority URL format**: `f"https://login.microsoftonline.com/{profile.tenant_id}"` - see `auth_service.py:146`
- **Exception with remediation**: See `auth_service.py:64-71` for FileNotFoundError handling pattern

### Warnings and Gotchas

- **MSAL makes network call on init**: `ConfidentialClientApplication` validates tenant in `__init__` - unit tests MUST mock it
- **Type ignore for MSAL**: Add `# type: ignore[import-untyped]` since MSAL lacks py.typed marker
- **mypy strict mode**: All functions need return type annotations
- **Currently raises "not yet implemented"**: `auth_service.py:166-173` needs to be replaced with actual client-secret logic

### Open Items Inherited

- [ ] Subtask 6.3: Implement token caching with `get_token`, `refresh_token` (next)
- [ ] Subtask 6.4: Implement `login`, `logout`, `get_status`, `is_authenticated` (after 6.3)

---

## Architecture Context

### Relevant Design Decisions

- **Layered Monolith**: Services layer handles business logic (AuthService)
- **Exception hierarchy**: All errors inherit from `TeamsPhoneError` with exit codes; use `AuthenticationError` (exit code 2) for auth failures
- **Environment variables for secrets**: Client secrets read from env vars to avoid hardcoding in config files
- **Dev/test focus for client-secret**: Less secure than certificates, documented as for development use only

### Module Structure
```
src/teams_phone/
├── services/
│   ├── __init__.py       # Exports AuthService, SCOPES
│   └── auth_service.py   # AuthService implementation - MODIFY THIS FILE
├── infrastructure/
│   ├── config_manager.py # Provides TenantProfile
│   └── cache_manager.py  # Token storage (for later subtasks)
├── models/
│   ├── tenant.py         # TenantProfile, AuthMethod enum
│   └── auth.py           # CachedToken, AuthStatus
└── exceptions.py         # AuthenticationError
```

### Conventions to Follow

- Use `os` module for environment variable access (`os.environ.get()`)
- Raise `AuthenticationError` with `remediation` parameter for user guidance
- Document security implications in docstrings
- Helper methods prefixed with `_` for private methods
- MSAL ConfidentialClientApplication accepts `client_credential` as either dict (certificate) or string (secret)

---

## Related Code References

### Primary References (Must Read)

| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/services/auth_service.py` | 128-173 | `_create_msal_app` method - MODIFY THIS |
| `src/teams_phone/services/auth_service.py` | 166-173 | Current "not yet implemented" block to replace |
| `src/teams_phone/models/tenant.py` | 10-19 | `AuthMethod` enum with CERTIFICATE and CLIENT_SECRET values |
| `src/teams_phone/exceptions.py` | 47-58 | `AuthenticationError` class definition |

### Secondary References (Helpful Context)

| File | Lines | Purpose |
|------|-------|---------|
| `tests/unit/test_auth_service.py` | 334-353 | Existing test for CLIENT_SECRET raising "not yet implemented" - UPDATE THIS |
| `tests/unit/test_auth_service.py` | 252-283 | Test pattern for mocking MSAL ConfidentialClientApplication |
| `.taskmaster/tasks/task_006.md` | 45-88 | Implementation notes from subtask 6.1 |

### Code Patterns to Follow

```python
# Current _create_msal_app structure (auth_service.py:128-173)
def _create_msal_app(
    self, profile: TenantProfile
) -> ConfidentialClientApplication:
    authority = f"https://login.microsoftonline.com/{profile.tenant_id}"

    if profile.auth_method == AuthMethod.CERTIFICATE:
        # ... certificate handling ...
        return ConfidentialClientApplication(
            client_id=str(profile.client_id),
            client_credential=cert_credential,
            authority=authority,
        )

    # Client-secret support will be added in subtask 6.2
    raise AuthenticationError(
        f"Auth method '{profile.auth_method.value}' not yet implemented",
        # ...
    )
```

```python
# Pattern for MSAL with client secret (from MSAL documentation)
# When client_credential is a string, MSAL treats it as a client secret
app = ConfidentialClientApplication(
    client_id="client-id",
    client_credential="client-secret-string",  # Simple string for secret
    authority="https://login.microsoftonline.com/tenant-id",
)
```

```python
# Environment variable reading pattern
import os

CLIENT_SECRET_ENV_VAR = "TEAMS_PHONE_CLIENT_SECRET"

def _get_client_secret(self) -> str:
    """Get client secret from environment variable.

    Returns:
        The client secret string.

    Raises:
        AuthenticationError: If environment variable is not set.
    """
    secret = os.environ.get(CLIENT_SECRET_ENV_VAR)
    if secret is None:
        raise AuthenticationError(
            f"Client secret not found in environment variable {CLIENT_SECRET_ENV_VAR}",
            remediation=f"Set the {CLIENT_SECRET_ENV_VAR} environment variable."
        )
    return secret
```

---

## Dependencies

### Internal Modules

- `teams_phone.models.AuthMethod` - enum with `CLIENT_SECRET` value
- `teams_phone.models.TenantProfile` - tenant configuration with `auth_method` field
- `teams_phone.exceptions.AuthenticationError` - auth failure exception with `remediation`

### External Packages

- `msal>=1.24.0` - Microsoft Authentication Library (already in pyproject.toml)
- `os` - Python standard library for environment variables (no install needed)

### New Dependencies Needed

- None required - all dependencies are already available

---

## Testing Guidance

### Test File Location

`tests/unit/test_auth_service.py` (existing file - add new tests)

### Test Patterns in Use

```python
# Existing test that currently expects "not yet implemented" (tests/unit/test_auth_service.py:334-353)
# This test needs to be REPLACED with tests for the actual implementation
@pytest.mark.unit
def test_raises_for_client_secret_auth(self, tmp_path: Path) -> None:
    """_create_msal_app raises AuthenticationError for CLIENT_SECRET auth method."""
    # ... creates profile with AuthMethod.CLIENT_SECRET ...
    with pytest.raises(AuthenticationError) as exc_info:
        service._create_msal_app(profile)
    assert "not yet implemented" in str(exc_info.value)
```

```python
# Pattern for testing environment variables with monkeypatch (pytest built-in)
@pytest.mark.unit
def test_reads_secret_from_env_var(
    self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    """_get_client_secret reads from TEAMS_PHONE_CLIENT_SECRET env var."""
    monkeypatch.setenv("TEAMS_PHONE_CLIENT_SECRET", "test-secret-value")

    service = AuthService(config_manager, cache_manager)
    secret = service._get_client_secret()

    assert secret == "test-secret-value"

@pytest.mark.unit
def test_missing_env_var_raises_auth_error(
    self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    """_get_client_secret raises AuthenticationError when env var not set."""
    monkeypatch.delenv("TEAMS_PHONE_CLIENT_SECRET", raising=False)

    service = AuthService(config_manager, cache_manager)

    with pytest.raises(AuthenticationError) as exc_info:
        service._get_client_secret()

    assert "TEAMS_PHONE_CLIENT_SECRET" in str(exc_info.value)
    assert exc_info.value.remediation is not None
```

```python
# Pattern for testing _create_msal_app with client secret
@pytest.mark.unit
def test_creates_app_with_client_secret_auth(
    self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    """_create_msal_app creates ConfidentialClientApplication for client-secret auth."""
    monkeypatch.setenv("TEAMS_PHONE_CLIENT_SECRET", "my-secret")

    profile = TenantProfile(
        name="test-tenant",
        tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
        client_id=UUID("87654321-4321-4321-4321-210987654321"),
        display_name="Test Tenant",
        auth_method=AuthMethod.CLIENT_SECRET,
    )

    with patch("teams_phone.services.auth_service.ConfidentialClientApplication") as mock_app:
        mock_instance = MagicMock()
        mock_app.return_value = mock_instance

        app = service._create_msal_app(profile)

        mock_app.assert_called_once()
        call_kwargs = mock_app.call_args[1]
        assert call_kwargs["client_credential"] == "my-secret"
```

### Fixtures Available

- `tmp_path` - pytest built-in for temporary directory
- `monkeypatch` - pytest built-in for environment variable manipulation

### Test Utilities

- `tests/conftest.py` - Contains marker configuration
- `unittest.mock.patch` - For mocking MSAL ConfidentialClientApplication
- `unittest.mock.MagicMock` - For creating mock instances

---

## Risks and Blockers

### Technical Risks

- [ ] **Empty string handling**: Decide if empty string env var should be treated as "not set" or as an actual (invalid) secret. Recommend treating empty string as not set.
- [ ] **Security considerations**: Document in docstrings that client-secret auth is less secure than certificates and intended for dev/test only.

### Dependency Risks

- None - Subtask 6.1 is complete and all required code is in place.

### Open Questions

- [ ] **Should empty string be valid?**: An empty `TEAMS_PHONE_CLIENT_SECRET` is likely an error. Consider checking `if not secret:` instead of `if secret is None:`.
- [ ] **Whitespace handling**: Should leading/trailing whitespace be stripped from the secret? Recommend: No, preserve as-is since the secret value must match exactly what's registered in Azure.

---

## Implementation Recommendations

### Suggested Approach

1. Add `import os` to auth_service.py imports
2. Add constant `CLIENT_SECRET_ENV_VAR = "TEAMS_PHONE_CLIENT_SECRET"` after SCOPES
3. Add `_get_client_secret(self) -> str` helper method with:
   - Read from `os.environ.get(CLIENT_SECRET_ENV_VAR)`
   - Raise `AuthenticationError` if None or empty string
   - Return the secret value
4. Update `_create_msal_app` method:
   - Add `elif profile.auth_method == AuthMethod.CLIENT_SECRET:` block
   - Call `_get_client_secret()` to get the secret
   - Create `ConfidentialClientApplication` with `client_credential=secret`
   - Return the app instance
5. Remove the "not yet implemented" `raise AuthenticationError` at the end
6. Update/replace the existing test that expects "not yet implemented"
7. Add new tests for:
   - `_get_client_secret` reading from env var
   - `_get_client_secret` raising when env var missing
   - `_create_msal_app` with CLIENT_SECRET auth method
   - MSAL app created with correct client_credential

### Files to Create/Modify

| Action | File | Reason |
|--------|------|--------|
| Modify | `src/teams_phone/services/auth_service.py` | Add CLIENT_SECRET_ENV_VAR, _get_client_secret(), update _create_msal_app() |
| Modify | `tests/unit/test_auth_service.py` | Replace/update CLIENT_SECRET test, add new tests for env var handling |

### Quick Wins

- The `_get_client_secret()` method is self-contained and easy to test first
- The MSAL ConfidentialClientApplication accepts `client_credential` as a simple string for client secrets
- Existing test mocking patterns for MSAL can be reused exactly

---

## Cheat Sheet

### Key Imports

```python
import os  # Add this import

from msal import ConfidentialClientApplication  # type: ignore[import-untyped]

from teams_phone.exceptions import AuthenticationError
from teams_phone.models import AuthMethod, TenantProfile
```

### Key Constants

```python
# Add after SCOPES constant
CLIENT_SECRET_ENV_VAR: str = "TEAMS_PHONE_CLIENT_SECRET"
"""Environment variable name for client secret authentication.

Note: Client-secret authentication is less secure than certificate-based
authentication and should only be used for development and testing purposes.
"""
```

### Key Functions to Implement

```python
def _get_client_secret(self) -> str:
    """Get client secret from environment variable.

    Client-secret authentication is less secure than certificate-based
    authentication and is intended for development/testing environments only.

    Returns:
        The client secret string from the environment variable.

    Raises:
        AuthenticationError: If the environment variable is not set or is empty.
    """
    secret = os.environ.get(CLIENT_SECRET_ENV_VAR)
    if not secret:  # Handles None and empty string
        raise AuthenticationError(
            f"Client secret not found in environment variable {CLIENT_SECRET_ENV_VAR}",
            remediation=(
                f"Set the {CLIENT_SECRET_ENV_VAR} environment variable:\n"
                f"  export {CLIENT_SECRET_ENV_VAR}='your-client-secret'\n"
                "Note: Client-secret auth is for dev/test only. "
                "Use certificate auth in production."
            ),
        )
    return secret
```

### MSAL Client Secret Pattern

```python
# In _create_msal_app, add elif block for CLIENT_SECRET:
elif profile.auth_method == AuthMethod.CLIENT_SECRET:
    client_secret = self._get_client_secret()

    return ConfidentialClientApplication(
        client_id=str(profile.client_id),
        client_credential=client_secret,  # String for client secret
        authority=authority,
    )
```

### Test Patterns

```python
# Using monkeypatch for environment variables
def test_with_env_var(self, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("TEAMS_PHONE_CLIENT_SECRET", "test-value")
    # ... test code ...

def test_without_env_var(self, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.delenv("TEAMS_PHONE_CLIENT_SECRET", raising=False)
    # ... test code ...
```

---

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