# Story 2.1: API Key Authentication

Status: done

## Story

As an **API consumer**,
I want **to authenticate my requests with an API key**,
So that **my documents are secure and usage is tracked**.

## Acceptance Criteria

1. **AC1:** Valid API key in `X-API-Key` header authenticates request successfully

2. **AC2:** Missing API key returns 401 Unauthorized with error:
   ```json
   {"error": {"code": "MISSING_API_KEY", "message": "X-API-Key header required"}}
   ```

3. **AC3:** Invalid API key returns 401 Unauthorized with error:
   ```json
   {"error": {"code": "INVALID_API_KEY", "message": "API key not found or inactive"}}
   ```

4. **AC4:** API keys are stored as SHA-256 hashes (never plaintext)

5. **AC5:** Rate limiting applies per API key (default: 60 requests/minute)

6. **AC6:** Rate limit exceeded returns 429 Too Many Requests with `Retry-After` header

7. **AC7:** `last_used_at` timestamp is updated on each authenticated request

8. **AC8:** Script `scripts/create_api_key.py` generates new API keys and outputs the plaintext key (only shown once)

## Tasks / Subtasks

- [x] **Task 1: Create auth service module** (AC: 1, 3, 4, 7)
  - [x] Create `src/services/auth_service.py`
  - [x] Implement `hash_api_key(key: str) -> str` using SHA-256
  - [x] Implement `validate_api_key(key: str) -> APIKey | None` that queries DB by hash
  - [x] Implement `update_last_used(api_key_id: UUID)` async function
  - [x] Add unit tests for hash function

- [x] **Task 2: Create rate limiter service** (AC: 5, 6)
  - [x] Create `src/services/rate_limiter.py`
  - [x] Implement sliding window rate limiting using Redis
  - [x] Key pattern: `rate_limit:{key_hash}:requests`
  - [x] Use INCR with EXPIRE for atomic operations
  - [x] Return remaining requests and reset time
  - [x] Add `Retry-After` header calculation

- [x] **Task 3: Create auth middleware/dependency** (AC: 1, 2, 3, 5, 6, 7)
  - [x] Update `src/api/dependencies.py` with `get_current_api_key()` dependency
  - [x] Extract `X-API-Key` header from request
  - [x] Validate key using auth_service
  - [x] Check rate limit using rate_limiter
  - [x] Update last_used_at on successful auth
  - [x] Raise appropriate HTTPException for auth failures

- [x] **Task 4: Create API key generation script** (AC: 4, 8)
  - [x] Create `scripts/create_api_key.py`
  - [x] Generate secure random key (32 bytes, hex encoded)
  - [x] Hash and store in database
  - [x] Output plaintext key to console (shown only once)
  - [x] Accept optional `--name` parameter for key identification
  - [x] Accept optional `--rate-limit` parameter (default 60)

- [x] **Task 5: Create Pydantic schemas for auth** (AC: 2, 3, 6)
  - [x] Create `src/api/schemas/auth.py`
  - [x] Define `AuthError` response model
  - [x] Define `RateLimitError` response model with retry_after field

- [x] **Task 6: Verify authentication flow** (AC: 1-8)
  - [x] Create test API key using script
  - [x] Test valid key → 200 OK
  - [x] Test missing key → 401 MISSING_API_KEY
  - [x] Test invalid key → 401 INVALID_API_KEY
  - [x] Test rate limiting (61 requests) → 429 with Retry-After
  - [x] Verify last_used_at updates in database

## Dev Notes

### Architecture Patterns and Constraints

- **Hash Algorithm:** SHA-256 via `hashlib.sha256()` for API key storage
- **Rate Limiting:** Redis sliding window algorithm (INCR + EXPIRE pattern)
- **Dependency Injection:** Use FastAPI `Depends()` for auth middleware
- **Error Format:** Standard DSOLError with code/message/details structure
- **Async Pattern:** All DB and Redis operations must be async

### Project Structure Notes

From previous stories, the following exist:
- `src/api/dependencies.py` - Has placeholder `get_current_api_key()` (needs full implementation)
- `src/api/middleware.py` - CORS and exception handlers
- `src/db/models.py` - APIKey model with key_hash, is_active, rate_limit_per_minute
- `src/services/` - Services package created in Story 1.5
- `src/api/schemas/` - Schemas package created in Story 1.5

Files to create/modify in this story:
- `src/services/auth_service.py` - NEW: API key validation service
- `src/services/rate_limiter.py` - NEW: Redis rate limiting service
- `src/api/schemas/auth.py` - NEW: Auth error response schemas
- `src/api/dependencies.py` - MODIFY: Full auth dependency implementation
- `scripts/create_api_key.py` - NEW: CLI script for key generation

### Learnings from Previous Story

**From Story 1-5-health-check-endpoint (Status: done)**

- **Redis Client Available:** `RedisDep` dependency already configured in `src/api/dependencies.py` with singleton pattern - reuse for rate limiting
- **Schemas Pattern:** Use `src/api/schemas/` package structure established (see `health.py` for Literal types pattern)
- **Services Pattern:** Use `src/services/` package for business logic (see `health.py` for async function patterns)
- **Dependency Pattern:** Follow `AsyncSessionDep`, `RedisDep` typing pattern for new dependencies
- **Async Timeout:** Use `asyncio.timeout()` pattern from health checks if needed

[Source: docs/sprint-artifacts/1-5-health-check-endpoint.md#Dev-Agent-Record]

### References

- [Source: docs/sprint-artifacts/tech-spec-epic-2.md#Story-2.1-API-Key-Authentication] - Acceptance criteria
- [Source: docs/sprint-artifacts/tech-spec-epic-2.md#Security] - Security requirements (SHA-256, rate limiting)
- [Source: docs/sprint-artifacts/tech-spec-epic-2.md#Workflows-and-Sequencing] - Rate limiting flow diagram
- [Source: docs/architecture.md#Security-Architecture] - API key authentication pattern
- [Source: docs/epics.md#Story-2.1] - Original story definition

## Dev Agent Record

### Context Reference

N/A - Story context will be generated by story-context workflow

### Agent Model Used

Claude Opus 4.5 (claude-opus-4-5-20251101)

### Debug Log References

N/A

### Completion Notes List

**Implementation Summary:**

1. **Auth Service (`src/services/auth_service.py`):**
   - `hash_api_key()`: SHA-256 hashing with hex encoding
   - `validate_api_key()`: Async DB lookup by hash with is_active check
   - `update_last_used()`: Updates last_used_at timestamp atomically

2. **Rate Limiter (`src/services/rate_limiter.py`):**
   - Sliding window algorithm using Redis INCR + EXPIRE
   - Key pattern: `rate_limit:{key_hash}:requests`
   - Returns `RateLimitResult` dataclass with allowed, remaining, reset_at, retry_after

3. **Auth Dependency (`src/api/dependencies.py`):**
   - Full `get_current_api_key()` implementation
   - Extracts X-API-Key header
   - Validates key, checks rate limit, updates last_used_at
   - Returns 401 MISSING_API_KEY, 401 INVALID_API_KEY, or 429 RATE_LIMIT_EXCEEDED

4. **API Key Generator (`scripts/create_api_key.py`):**
   - Generates 32-byte (64 hex char) random keys using `secrets.token_hex()`
   - CLI with --name and --rate-limit options
   - Outputs plaintext key (shown only once)

5. **Auth Schemas (`src/api/schemas/auth.py`):**
   - `ErrorDetail`: code + message
   - `ErrorResponse`: wrapper with error field
   - `RateLimitErrorResponse`: error + retry_after

**Verification Results:**
- All unit tests pass (4/4)
- Missing API key → 401 MISSING_API_KEY ✓
- Invalid API key → 401 INVALID_API_KEY ✓
- Valid API key → 200 OK with authenticated response ✓
- Rate limiting → 429 with retry_after field ✓
- last_used_at updates in database ✓

**Additional Changes:**
- Updated `src/api/routes/documents.py` to use `ApiKeyDep` for authentication testing

### File List

| Status | File Path |
|--------|-----------|
| NEW | src/services/auth_service.py |
| NEW | src/services/rate_limiter.py |
| NEW | src/api/schemas/auth.py |
| NEW | scripts/create_api_key.py |
| NEW | tests/services/test_auth_service.py |
| MODIFIED | src/api/dependencies.py |
| MODIFIED | src/api/schemas/__init__.py |
| MODIFIED | src/api/routes/documents.py |
