Merges Phase 4a work including: Implementation: - Metadata discovery endpoint (/api/.well-known/oauth-authorization-server) - h-app microformat parser service - Enhanced authorization endpoint with client info display - Configuration management system - Dependency injection framework Documentation: - Comprehensive gap analysis for v1.0.0 compliance - Phase 4a clarifications on development approach - Phase 4-5 critical components breakdown Testing: - Unit tests for h-app parser (308 lines, comprehensive coverage) - Unit tests for metadata endpoint (134 lines) - Unit tests for configuration system (18 lines) - Integration test updates All tests passing with high coverage. Ready for Phase 4b security hardening.
3234 lines
92 KiB
Markdown
3234 lines
92 KiB
Markdown
# Phase 4-5: Critical Components for v1.0.0 Release
|
|
|
|
**Architect**: Claude (Architect Agent)
|
|
**Date**: 2025-11-20
|
|
**Status**: Design Complete
|
|
**Design References**:
|
|
- `/docs/roadmap/v1.0.0.md` - Original release plan
|
|
- `/docs/reports/2025-11-20-gap-analysis-v1.0.0.md` - Gap analysis identifying missing components
|
|
- `/docs/architecture/security.md` - Security architecture
|
|
- `/docs/architecture/indieauth-protocol.md` - Protocol implementation details
|
|
|
|
## Overview
|
|
|
|
This design document addresses the 7 critical missing components identified in the v1.0.0 gap analysis that are blocking release. These components span Phase 3 completion (metadata endpoint, client metadata fetching), Phase 4 (security hardening), and Phase 5 (deployment, testing, documentation).
|
|
|
|
**Current Status**: Phases 1-2 are complete (100%). Phase 3 is 75% complete (missing metadata endpoint and h-app parsing). Phases 4-5 have not been started.
|
|
|
|
**Estimated Remaining Effort**: 10-15 days to reach v1.0.0 release readiness.
|
|
|
|
**Design Philosophy**: Maintain simplicity while meeting all P0 requirements. Reuse existing infrastructure where possible. Focus on production readiness and W3C IndieAuth compliance.
|
|
|
|
---
|
|
|
|
## Component 1: Metadata Endpoint
|
|
|
|
### Purpose
|
|
|
|
Provide OAuth 2.0 Authorization Server Metadata endpoint per RFC 8414 to enable IndieAuth client discovery of server capabilities and endpoints.
|
|
|
|
### Specification References
|
|
|
|
- **W3C IndieAuth**: Section on Discovery (metadata endpoint)
|
|
- **RFC 8414**: OAuth 2.0 Authorization Server Metadata
|
|
- **v1.0.0 Roadmap**: Line 62 (P0 feature), Phase 3 lines 162, 168
|
|
|
|
### Design Overview
|
|
|
|
Create a static metadata endpoint at `/.well-known/oauth-authorization-server` that returns server capabilities in JSON format. This endpoint requires no authentication and should be publicly cacheable.
|
|
|
|
### API Specification
|
|
|
|
**Endpoint**: `GET /.well-known/oauth-authorization-server`
|
|
|
|
**Request**: No parameters, no authentication required
|
|
|
|
**Response** (HTTP 200 OK):
|
|
```json
|
|
{
|
|
"issuer": "https://auth.example.com",
|
|
"authorization_endpoint": "https://auth.example.com/authorize",
|
|
"token_endpoint": "https://auth.example.com/token",
|
|
"response_types_supported": ["code"],
|
|
"grant_types_supported": ["authorization_code"],
|
|
"code_challenge_methods_supported": [],
|
|
"token_endpoint_auth_methods_supported": ["none"],
|
|
"revocation_endpoint_auth_methods_supported": ["none"],
|
|
"scopes_supported": []
|
|
}
|
|
```
|
|
|
|
**Response Headers**:
|
|
```http
|
|
Content-Type: application/json
|
|
Cache-Control: public, max-age=86400
|
|
```
|
|
|
|
### Field Definitions
|
|
|
|
| Field | Value | Rationale |
|
|
|-------|-------|-----------|
|
|
| `issuer` | Server base URL (from config) | Identifies this authorization server |
|
|
| `authorization_endpoint` | `{base_url}/authorize` | Where clients initiate auth flow |
|
|
| `token_endpoint` | `{base_url}/token` | Where clients exchange codes for tokens |
|
|
| `response_types_supported` | `["code"]` | Only authorization code flow supported |
|
|
| `grant_types_supported` | `["authorization_code"]` | Only grant type in v1.0.0 |
|
|
| `code_challenge_methods_supported` | `[]` | PKCE not supported in v1.0.0 (ADR-003) |
|
|
| `token_endpoint_auth_methods_supported` | `["none"]` | Public clients, no client secrets |
|
|
| `revocation_endpoint_auth_methods_supported` | `["none"]` | No revocation endpoint in v1.0.0 |
|
|
| `scopes_supported` | `[]` | Authentication-only, no scopes in v1.0.0 |
|
|
|
|
### Implementation Approach
|
|
|
|
**File**: `/src/gondulf/routers/metadata.py`
|
|
|
|
**Implementation Strategy**: Static JSON response generated from configuration at startup.
|
|
|
|
```python
|
|
from fastapi import APIRouter, Response
|
|
from gondulf.config import get_config
|
|
|
|
router = APIRouter()
|
|
|
|
@router.get("/.well-known/oauth-authorization-server")
|
|
async def get_metadata():
|
|
"""
|
|
OAuth 2.0 Authorization Server Metadata (RFC 8414).
|
|
|
|
Returns server capabilities for IndieAuth client discovery.
|
|
"""
|
|
config = get_config()
|
|
|
|
metadata = {
|
|
"issuer": config.BASE_URL,
|
|
"authorization_endpoint": f"{config.BASE_URL}/authorize",
|
|
"token_endpoint": f"{config.BASE_URL}/token",
|
|
"response_types_supported": ["code"],
|
|
"grant_types_supported": ["authorization_code"],
|
|
"code_challenge_methods_supported": [],
|
|
"token_endpoint_auth_methods_supported": ["none"],
|
|
"revocation_endpoint_auth_methods_supported": ["none"],
|
|
"scopes_supported": []
|
|
}
|
|
|
|
return Response(
|
|
content=json.dumps(metadata, indent=2),
|
|
media_type="application/json",
|
|
headers={
|
|
"Cache-Control": "public, max-age=86400"
|
|
}
|
|
)
|
|
```
|
|
|
|
**Configuration Change**: Add `BASE_URL` to config (e.g., `GONDULF_BASE_URL=https://auth.example.com`).
|
|
|
|
**Registration**: Add router to main.py:
|
|
```python
|
|
from gondulf.routers import metadata
|
|
app.include_router(metadata.router)
|
|
```
|
|
|
|
### Error Handling
|
|
|
|
No error conditions - endpoint always returns metadata. If configuration is invalid, application fails at startup (fail-fast principle).
|
|
|
|
### Security Considerations
|
|
|
|
- **Public Endpoint**: No authentication required (per RFC 8414)
|
|
- **Cache-Control**: Set public cache for 24 hours to reduce load
|
|
- **No Secrets**: Metadata contains no sensitive information
|
|
- **HTTPS**: Served over HTTPS in production (enforced by middleware)
|
|
|
|
### Testing Requirements
|
|
|
|
**Unit Tests** (`tests/unit/test_metadata.py`):
|
|
1. Test metadata endpoint returns 200 OK
|
|
2. Test all required fields present
|
|
3. Test correct values for each field
|
|
4. Test Cache-Control header present
|
|
5. Test Content-Type is application/json
|
|
6. Test issuer matches BASE_URL configuration
|
|
|
|
**Integration Test** (`tests/integration/test_metadata_integration.py`):
|
|
1. Test endpoint accessible via FastAPI TestClient
|
|
2. Test response can be parsed as valid JSON
|
|
3. Test endpoints in metadata are valid URLs
|
|
|
|
**Compliance Test**:
|
|
1. Verify metadata response matches RFC 8414 format
|
|
|
|
### Acceptance Criteria
|
|
|
|
- [ ] `/.well-known/oauth-authorization-server` endpoint returns valid JSON
|
|
- [ ] All required fields present per RFC 8414
|
|
- [ ] Endpoint values match actual server configuration
|
|
- [ ] Cache-Control header set to public, max-age=86400
|
|
- [ ] All tests pass (unit, integration)
|
|
- [ ] Endpoint accessible without authentication
|
|
|
|
---
|
|
|
|
## Component 2: Client Metadata Fetching (h-app Microformat)
|
|
|
|
### Purpose
|
|
|
|
Fetch and parse client application metadata from `client_id` URL to display application name, icon, and URL on the consent screen. This improves user experience by showing what application they're authorizing.
|
|
|
|
### Specification References
|
|
|
|
- **W3C IndieAuth**: Client Information Discovery
|
|
- **Microformats h-app**: http://microformats.org/wiki/h-app
|
|
- **v1.0.0 Roadmap**: Success criteria line 27, Phase 3 deliverables line 169
|
|
|
|
### Design Overview
|
|
|
|
Create a service that fetches the `client_id` URL, parses HTML for h-app microformat data, extracts application metadata, and caches results. Integrate with authorization endpoint to display app information on consent screen.
|
|
|
|
### h-app Microformat Structure
|
|
|
|
Example HTML from client application:
|
|
```html
|
|
<div class="h-app">
|
|
<img src="/icon.png" class="u-logo" alt="App Icon">
|
|
<a href="/" class="u-url p-name">My IndieAuth Client</a>
|
|
</div>
|
|
```
|
|
|
|
**Properties to Extract**:
|
|
- `p-name`: Application name (required)
|
|
- `u-logo`: Application icon URL (optional)
|
|
- `u-url`: Application URL (optional, usually same as client_id)
|
|
|
|
### Data Model
|
|
|
|
**ClientMetadata** (in-memory cache):
|
|
```python
|
|
@dataclass
|
|
class ClientMetadata:
|
|
"""Client application metadata from h-app microformat."""
|
|
client_id: str
|
|
name: str # Extracted from p-name, or domain fallback
|
|
logo_url: Optional[str] = None # Extracted from u-logo
|
|
url: Optional[str] = None # Extracted from u-url
|
|
fetched_at: datetime = None # Timestamp for cache expiry
|
|
```
|
|
|
|
### Service Design
|
|
|
|
**File**: `/src/gondulf/services/h_app_parser.py`
|
|
|
|
**Dependencies**:
|
|
- `html_fetcher.py` (already exists from Phase 2)
|
|
- `mf2py` library for microformat parsing
|
|
|
|
**Service Interface**:
|
|
```python
|
|
class HAppParser:
|
|
"""Parse h-app microformat from client_id URL."""
|
|
|
|
def __init__(self, html_fetcher: HTMLFetcher):
|
|
self.html_fetcher = html_fetcher
|
|
self.cache: Dict[str, ClientMetadata] = {}
|
|
self.cache_ttl = timedelta(hours=24)
|
|
|
|
def parse_client_metadata(self, client_id: str) -> ClientMetadata:
|
|
"""
|
|
Fetch and parse client metadata from client_id URL.
|
|
|
|
Returns ClientMetadata with name (always populated), and
|
|
optional logo_url and url.
|
|
|
|
Caches results for 24 hours to reduce HTTP requests.
|
|
"""
|
|
pass
|
|
|
|
def _parse_h_app(self, html: str, client_id: str) -> ClientMetadata:
|
|
"""
|
|
Parse h-app microformat from HTML.
|
|
|
|
Returns ClientMetadata with extracted values, or fallback
|
|
to domain name if no h-app found.
|
|
"""
|
|
pass
|
|
|
|
def _extract_domain_name(self, client_id: str) -> str:
|
|
"""Extract domain name from client_id for fallback display."""
|
|
pass
|
|
```
|
|
|
|
### Implementation Details
|
|
|
|
**Parsing Strategy**:
|
|
1. Check cache for `client_id` (if cached and not expired, return cached)
|
|
2. Fetch HTML from `client_id` using `HTMLFetcher` (reuse Phase 2 infrastructure)
|
|
3. Parse HTML with `mf2py` library to extract h-app microformat
|
|
4. Extract `p-name`, `u-logo`, `u-url` properties
|
|
5. If h-app not found, fallback to domain name extraction
|
|
6. Store in cache with 24-hour TTL
|
|
7. Return `ClientMetadata` object
|
|
|
|
**mf2py Usage**:
|
|
```python
|
|
import mf2py
|
|
from urllib.parse import urlparse, urljoin
|
|
|
|
def _parse_h_app(self, html: str, client_id: str) -> ClientMetadata:
|
|
"""Parse h-app microformat from HTML."""
|
|
# Parse microformats
|
|
parsed = mf2py.parse(doc=html, url=client_id)
|
|
|
|
# Find h-app items
|
|
h_apps = [item for item in parsed.get('items', [])
|
|
if 'h-app' in item.get('type', [])]
|
|
|
|
if not h_apps:
|
|
# Fallback: no h-app found
|
|
return ClientMetadata(
|
|
client_id=client_id,
|
|
name=self._extract_domain_name(client_id),
|
|
fetched_at=datetime.utcnow()
|
|
)
|
|
|
|
# Use first h-app
|
|
h_app = h_apps[0]
|
|
properties = h_app.get('properties', {})
|
|
|
|
# Extract properties
|
|
name = properties.get('name', [None])[0] or self._extract_domain_name(client_id)
|
|
logo_url = properties.get('logo', [None])[0]
|
|
url = properties.get('url', [None])[0] or client_id
|
|
|
|
# Resolve relative URLs
|
|
if logo_url and not logo_url.startswith('http'):
|
|
logo_url = urljoin(client_id, logo_url)
|
|
|
|
return ClientMetadata(
|
|
client_id=client_id,
|
|
name=name,
|
|
logo_url=logo_url,
|
|
url=url,
|
|
fetched_at=datetime.utcnow()
|
|
)
|
|
```
|
|
|
|
**Fallback Strategy**:
|
|
```python
|
|
def _extract_domain_name(self, client_id: str) -> str:
|
|
"""Extract domain name for fallback display."""
|
|
parsed = urlparse(client_id)
|
|
domain = parsed.hostname or client_id
|
|
|
|
# Remove 'www.' prefix if present
|
|
if domain.startswith('www.'):
|
|
domain = domain[4:]
|
|
|
|
return domain
|
|
```
|
|
|
|
**Cache Management**:
|
|
```python
|
|
def parse_client_metadata(self, client_id: str) -> ClientMetadata:
|
|
"""Fetch and parse with caching."""
|
|
# Check cache
|
|
if client_id in self.cache:
|
|
cached = self.cache[client_id]
|
|
age = datetime.utcnow() - cached.fetched_at
|
|
if age < self.cache_ttl:
|
|
logger.debug(f"Cache hit for client_id: {client_id}")
|
|
return cached
|
|
|
|
# Fetch HTML
|
|
try:
|
|
html = self.html_fetcher.fetch(client_id)
|
|
if not html:
|
|
raise ValueError("Failed to fetch client_id URL")
|
|
|
|
# Parse h-app
|
|
metadata = self._parse_h_app(html, client_id)
|
|
|
|
# Cache result
|
|
self.cache[client_id] = metadata
|
|
|
|
return metadata
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Failed to parse client metadata for {client_id}: {e}")
|
|
# Return fallback metadata
|
|
return ClientMetadata(
|
|
client_id=client_id,
|
|
name=self._extract_domain_name(client_id),
|
|
fetched_at=datetime.utcnow()
|
|
)
|
|
```
|
|
|
|
### Integration with Authorization Endpoint
|
|
|
|
**Update**: `/src/gondulf/routers/authorization.py`
|
|
|
|
**Change**: Inject `HAppParser` and fetch client metadata before rendering consent screen.
|
|
|
|
```python
|
|
from gondulf.services.h_app_parser import HAppParser, ClientMetadata
|
|
|
|
@router.get("/authorize")
|
|
async def authorize_get(
|
|
# ... existing parameters ...
|
|
h_app_parser: HAppParser = Depends(get_h_app_parser)
|
|
):
|
|
# ... existing validation ...
|
|
|
|
# Fetch client metadata
|
|
client_metadata = h_app_parser.parse_client_metadata(client_id)
|
|
|
|
# Render consent screen with client metadata
|
|
return templates.TemplateResponse(
|
|
"authorize.html",
|
|
{
|
|
"request": request,
|
|
"me": me,
|
|
"client_name": client_metadata.name,
|
|
"client_logo_url": client_metadata.logo_url,
|
|
"client_url": client_metadata.url or client_id,
|
|
"client_id": client_id,
|
|
"redirect_uri": redirect_uri,
|
|
"state": state
|
|
}
|
|
)
|
|
```
|
|
|
|
**Template Update**: `/src/gondulf/templates/authorize.html`
|
|
|
|
Add client metadata display:
|
|
```html
|
|
<div class="client-info">
|
|
{% if client_logo_url %}
|
|
<img src="{{ client_logo_url }}" alt="{{ client_name }} icon" class="client-logo">
|
|
{% endif %}
|
|
<h2>{{ client_name }}</h2>
|
|
<p class="client-url">{{ client_url }}</p>
|
|
</div>
|
|
|
|
<p>This application wants to sign you in as:</p>
|
|
<p class="user-identity"><strong>{{ me }}</strong></p>
|
|
|
|
<form method="post" action="/authorize">
|
|
<!-- ... existing form fields ... -->
|
|
<button type="submit" name="authorized" value="true">Authorize</button>
|
|
<button type="submit" name="authorized" value="false">Deny</button>
|
|
</form>
|
|
```
|
|
|
|
### Dependency Injection
|
|
|
|
**File**: `/src/gondulf/dependencies.py`
|
|
|
|
Add `get_h_app_parser()`:
|
|
```python
|
|
from gondulf.services.h_app_parser import HAppParser
|
|
|
|
@lru_cache()
|
|
def get_h_app_parser() -> HAppParser:
|
|
"""Get HAppParser singleton."""
|
|
html_fetcher = get_html_fetcher()
|
|
return HAppParser(html_fetcher)
|
|
```
|
|
|
|
### Error Handling
|
|
|
|
**Failure Modes**:
|
|
1. **HTTP fetch fails**: Return fallback metadata with domain name
|
|
2. **HTML parse fails**: Return fallback metadata with domain name
|
|
3. **h-app not found**: Return fallback metadata with domain name
|
|
4. **Invalid URLs in h-app**: Skip invalid fields, use available data
|
|
|
|
**Logging**:
|
|
- Log INFO when h-app successfully parsed
|
|
- Log WARNING when fallback used
|
|
- Log ERROR only on unexpected exceptions
|
|
|
|
### Security Considerations
|
|
|
|
- **HTTPS Only**: Reuse `HTMLFetcher` which enforces HTTPS
|
|
- **Timeout**: 5-second timeout from `HTMLFetcher`
|
|
- **Size Limit**: 5MB limit from `HTMLFetcher`
|
|
- **XSS Prevention**: HTML escape all client metadata in templates (Jinja2 auto-escaping)
|
|
- **Logo URL Validation**: Only display logo if HTTPS URL
|
|
- **Cache Poisoning**: Cache keyed by client_id, no user input
|
|
|
|
### Testing Requirements
|
|
|
|
**Unit Tests** (`tests/unit/test_h_app_parser.py`):
|
|
1. Test h-app parsing with complete metadata
|
|
2. Test h-app parsing with missing logo
|
|
3. Test h-app parsing with missing url
|
|
4. Test h-app not found (fallback to domain)
|
|
5. Test relative logo URL resolution
|
|
6. Test domain name extraction fallback
|
|
7. Test cache hit (no HTTP request)
|
|
8. Test cache expiry (new HTTP request)
|
|
9. Test HTML fetch failure (fallback)
|
|
10. Test multiple h-app items (use first)
|
|
|
|
**Integration Tests** (`tests/integration/test_authorization_with_client_metadata.py`):
|
|
1. Test authorization endpoint displays client name
|
|
2. Test authorization endpoint displays client logo
|
|
3. Test authorization endpoint with fallback metadata
|
|
|
|
### Acceptance Criteria
|
|
|
|
- [ ] `HAppParser` service created with caching
|
|
- [ ] h-app microformat parsing working with mf2py
|
|
- [ ] Fallback to domain name when h-app not found
|
|
- [ ] Cache working with 24-hour TTL
|
|
- [ ] Integration with authorization endpoint complete
|
|
- [ ] Consent screen displays client name, logo, URL
|
|
- [ ] All tests pass (unit, integration)
|
|
- [ ] HTML escaping prevents XSS
|
|
|
|
---
|
|
|
|
## Component 3: Security Hardening
|
|
|
|
### Purpose
|
|
|
|
Implement security best practices required for production deployment: security headers, HTTPS enforcement, input sanitization audit, and PII logging audit.
|
|
|
|
### Specification References
|
|
|
|
- **v1.0.0 Roadmap**: Line 65 (P0 feature), Phase 4 lines 198-203
|
|
- **OWASP Top 10**: Security header recommendations
|
|
- **OAuth 2.0 Security Best Practices**: HTTPS enforcement
|
|
|
|
### Design Overview
|
|
|
|
Create security middleware to add HTTP security headers to all responses. Implement HTTPS enforcement for production environments. Conduct comprehensive audit of input sanitization and PII logging practices.
|
|
|
|
### 3.1: Security Headers Middleware
|
|
|
|
**File**: `/src/gondulf/middleware/security_headers.py`
|
|
|
|
**Headers to Implement**:
|
|
|
|
| Header | Value | Purpose |
|
|
|--------|-------|---------|
|
|
| `X-Frame-Options` | `DENY` | Prevent clickjacking attacks |
|
|
| `X-Content-Type-Options` | `nosniff` | Prevent MIME sniffing |
|
|
| `X-XSS-Protection` | `1; mode=block` | Enable XSS filter (legacy browsers) |
|
|
| `Referrer-Policy` | `strict-origin-when-cross-origin` | Control referrer information |
|
|
| `Strict-Transport-Security` | `max-age=31536000; includeSubDomains` | Force HTTPS (production only) |
|
|
| `Content-Security-Policy` | `default-src 'self'; style-src 'self' 'unsafe-inline'` | Restrict resource loading |
|
|
| `Cache-Control` | `no-store, no-cache, must-revalidate` | Prevent caching of sensitive pages (auth endpoints only) |
|
|
| `Pragma` | `no-cache` | HTTP/1.0 cache control |
|
|
|
|
**Implementation**:
|
|
```python
|
|
from fastapi import Request, Response
|
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
from gondolf.config import get_config
|
|
|
|
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
|
"""Add security headers to all responses."""
|
|
|
|
async def dispatch(self, request: Request, call_next):
|
|
response = await call_next(request)
|
|
|
|
config = get_config()
|
|
|
|
# Always add these headers
|
|
response.headers["X-Frame-Options"] = "DENY"
|
|
response.headers["X-Content-Type-Options"] = "nosniff"
|
|
response.headers["X-XSS-Protection"] = "1; mode=block"
|
|
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
|
|
|
|
# CSP: Allow self and inline styles (for minimal CSS in templates)
|
|
response.headers["Content-Security-Policy"] = (
|
|
"default-src 'self'; "
|
|
"style-src 'self' 'unsafe-inline'; "
|
|
"img-src 'self' https:; " # Allow HTTPS images (client logos)
|
|
"frame-ancestors 'none'" # Equivalent to X-Frame-Options: DENY
|
|
)
|
|
|
|
# HSTS: Only in production with HTTPS
|
|
if not config.DEBUG and request.url.scheme == "https":
|
|
response.headers["Strict-Transport-Security"] = (
|
|
"max-age=31536000; includeSubDomains"
|
|
)
|
|
|
|
# Cache control for sensitive endpoints
|
|
if request.url.path in ["/authorize", "/token", "/api/verify/code"]:
|
|
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate"
|
|
response.headers["Pragma"] = "no-cache"
|
|
|
|
return response
|
|
```
|
|
|
|
**Registration**: Add to `main.py`:
|
|
```python
|
|
from gondulf.middleware.security_headers import SecurityHeadersMiddleware
|
|
|
|
app.add_middleware(SecurityHeadersMiddleware)
|
|
```
|
|
|
|
### 3.2: HTTPS Enforcement Middleware
|
|
|
|
**File**: `/src/gondulf/middleware/https_enforcement.py`
|
|
|
|
**Implementation**:
|
|
```python
|
|
from fastapi import Request, Response
|
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
from starlette.responses import RedirectResponse
|
|
from gondolf.config import get_config
|
|
|
|
class HTTPSEnforcementMiddleware(BaseHTTPMiddleware):
|
|
"""Enforce HTTPS in production (redirect HTTP to HTTPS)."""
|
|
|
|
async def dispatch(self, request: Request, call_next):
|
|
config = get_config()
|
|
|
|
# Only enforce in production
|
|
if not config.DEBUG:
|
|
# Allow localhost HTTP for local testing
|
|
if request.url.hostname not in ["localhost", "127.0.0.1"]:
|
|
# Check if HTTP (not HTTPS)
|
|
if request.url.scheme != "https":
|
|
# Redirect to HTTPS
|
|
https_url = request.url.replace(scheme="https")
|
|
return RedirectResponse(url=str(https_url), status_code=301)
|
|
|
|
return await call_next(request)
|
|
```
|
|
|
|
**Registration**: Add to `main.py` BEFORE security headers middleware:
|
|
```python
|
|
from gondulf.middleware.https_enforcement import HTTPSEnforcementMiddleware
|
|
|
|
# Add HTTPS enforcement first (before security headers)
|
|
app.add_middleware(HTTPSEnforcementMiddleware)
|
|
app.add_middleware(SecurityHeadersMiddleware)
|
|
```
|
|
|
|
**Configuration**: Add `DEBUG` flag to config:
|
|
```python
|
|
# config.py
|
|
DEBUG = os.getenv("GONDULF_DEBUG", "false").lower() == "true"
|
|
```
|
|
|
|
### 3.3: Input Sanitization Audit
|
|
|
|
**Scope**: Review all user input handling for proper validation and sanitization.
|
|
|
|
**Audit Checklist**:
|
|
|
|
| Input Source | Current Validation | Additional Sanitization Needed |
|
|
|--------------|-------------------|-------------------------------|
|
|
| `me` parameter | Pydantic HttpUrl, custom validation | ✅ Adequate (URL validation comprehensive) |
|
|
| `client_id` parameter | Pydantic HttpUrl | ✅ Adequate |
|
|
| `redirect_uri` parameter | Pydantic HttpUrl, domain validation | ✅ Adequate |
|
|
| `state` parameter | Pydantic str, max_length=512 | ✅ Adequate (opaque, not interpreted) |
|
|
| `code` parameter | str, checked against storage | ✅ Adequate (constant-time hash comparison) |
|
|
| Email addresses | Regex validation in `validation.py` | ✅ Adequate (from rel=me, not user input) |
|
|
| HTML templates | Jinja2 auto-escaping | ✅ Adequate (all {{ }} escaped by default) |
|
|
| SQL queries | SQLAlchemy parameterized queries | ✅ Adequate (no string interpolation) |
|
|
| DNS queries | dnspython library | ✅ Adequate (library handles escaping) |
|
|
|
|
**Action Items**:
|
|
1. **Add HTML escaping test**: Verify Jinja2 auto-escaping works
|
|
2. **Add SQL injection test**: Verify parameterized queries prevent injection
|
|
3. **Document validation patterns**: Update security.md with validation approach
|
|
|
|
**No Code Changes Required**: Existing validation is adequate.
|
|
|
|
### 3.4: PII Logging Audit
|
|
|
|
**Scope**: Ensure no Personally Identifiable Information (PII) is logged.
|
|
|
|
**PII Definition**:
|
|
- Email addresses
|
|
- Full tokens (access tokens, authorization codes, verification codes)
|
|
- IP addresses (in production)
|
|
|
|
**Audit Checklist**:
|
|
|
|
| Service | Logs Email? | Logs Tokens? | Logs IP? | Action Required |
|
|
|---------|-------------|--------------|----------|-----------------|
|
|
| `email.py` | ❌ No (only domain) | N/A | ❌ No | ✅ Compliant |
|
|
| `token_service.py` | N/A | ⚠️ Token prefix only (8 chars) | ❌ No | ✅ Compliant (prefix OK) |
|
|
| `domain_verification.py` | ⚠️ **May log email** | ⚠️ **May log code** | ❌ No | 🔍 **Review Required** |
|
|
| `authorization.py` | ❌ No | ⚠️ Code prefix only | ⚠️ **May log IP in errors** | 🔍 **Review Required** |
|
|
| `verification.py` | ❌ No | ⚠️ Code in errors? | ❌ No | 🔍 **Review Required** |
|
|
|
|
**Action Items**:
|
|
1. **Review all logger.info/warning/error calls** in services and routers
|
|
2. **Remove email addresses** from log messages (use domain only)
|
|
3. **Remove full codes/tokens** from log messages (use prefix or hash)
|
|
4. **Remove IP addresses** from production logs (OK in DEBUG mode)
|
|
5. **Add logging best practices** to coding standards
|
|
|
|
**Example Fix**:
|
|
```python
|
|
# BAD: Logs email (PII)
|
|
logger.info(f"Verification sent to {email}")
|
|
|
|
# GOOD: Logs domain only
|
|
logger.info(f"Verification sent to user at domain {domain}")
|
|
|
|
# BAD: Logs full token
|
|
logger.debug(f"Generated token: {token}")
|
|
|
|
# GOOD: Logs token prefix for correlation
|
|
logger.debug(f"Generated token: {token[:8]}...")
|
|
```
|
|
|
|
### 3.5: Security Configuration
|
|
|
|
**Add to config.py**:
|
|
```python
|
|
# Security settings
|
|
DEBUG = os.getenv("GONDULF_DEBUG", "false").lower() == "true"
|
|
HSTS_MAX_AGE = int(os.getenv("GONDULF_HSTS_MAX_AGE", "31536000")) # 1 year
|
|
CSP_POLICY = os.getenv(
|
|
"GONDULF_CSP_POLICY",
|
|
"default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' https:; frame-ancestors 'none'"
|
|
)
|
|
```
|
|
|
|
**Add to .env.example**:
|
|
```bash
|
|
# Security Settings (Production)
|
|
GONDULF_DEBUG=false
|
|
GONDULF_HSTS_MAX_AGE=31536000 # HSTS max-age in seconds (1 year default)
|
|
# GONDULF_CSP_POLICY=... # Custom CSP policy (optional)
|
|
```
|
|
|
|
### Error Handling
|
|
|
|
**Middleware Errors**:
|
|
- Log errors but DO NOT block requests
|
|
- If middleware fails, continue without headers (fail-open for availability)
|
|
- Log ERROR level for middleware failures
|
|
|
|
### Security Considerations
|
|
|
|
- **HSTS Preloading**: Consider submitting domain to HSTS preload list (future)
|
|
- **CSP Reporting**: Consider adding CSP report-uri (future)
|
|
- **Security.txt**: Consider adding /.well-known/security.txt (future)
|
|
|
|
### Testing Requirements
|
|
|
|
**Unit Tests** (`tests/unit/test_security_middleware.py`):
|
|
1. Test security headers present on all responses
|
|
2. Test HSTS header only in production
|
|
3. Test HSTS header not present in DEBUG mode
|
|
4. Test HTTPS enforcement redirects HTTP to HTTPS
|
|
5. Test HTTPS enforcement allows localhost HTTP
|
|
6. Test Cache-Control headers on sensitive endpoints
|
|
7. Test CSP header allows self and inline styles
|
|
|
|
**Integration Tests** (`tests/integration/test_security_integration.py`):
|
|
1. Test authorization endpoint has security headers
|
|
2. Test token endpoint has cache control headers
|
|
3. Test metadata endpoint does NOT have cache control (should be cacheable)
|
|
|
|
**Security Tests** (`tests/security/test_input_validation.py`):
|
|
1. Test HTML escaping in templates (XSS prevention)
|
|
2. Test SQL injection prevention (parameterized queries)
|
|
3. Test URL validation rejects malicious URLs
|
|
|
|
**PII Tests** (`tests/security/test_pii_logging.py`):
|
|
1. Test no email addresses in logs (mock logger, verify calls)
|
|
2. Test no full tokens in logs
|
|
3. Test no full codes in logs
|
|
|
|
### Acceptance Criteria
|
|
|
|
- [ ] Security headers middleware implemented and registered
|
|
- [ ] HTTPS enforcement middleware implemented and registered (production only)
|
|
- [ ] All security headers present on responses
|
|
- [ ] HSTS header only in production
|
|
- [ ] Input sanitization audit complete (documented)
|
|
- [ ] PII logging audit complete (issues fixed)
|
|
- [ ] All tests pass (unit, integration, security)
|
|
- [ ] No email addresses in logs
|
|
- [ ] No full tokens/codes in logs
|
|
|
|
---
|
|
|
|
## Component 4: Deployment Configuration
|
|
|
|
### Purpose
|
|
|
|
Provide production-ready deployment artifacts: Dockerfile, docker-compose.yml, database backup script, and comprehensive environment variable documentation.
|
|
|
|
### Specification References
|
|
|
|
- **v1.0.0 Roadmap**: Line 66 (P0 feature), Phase 5 lines 233-236
|
|
- **Docker Best Practices**: Multi-stage builds, non-root user, minimal base images
|
|
|
|
### Design Overview
|
|
|
|
Create Docker deployment configuration using multi-stage build for minimal image size. Provide docker-compose.yml for easy local testing. Create SQLite backup script with GPG encryption support. Document all environment variables comprehensively.
|
|
|
|
### 4.1: Dockerfile
|
|
|
|
**File**: `/Dockerfile`
|
|
|
|
**Strategy**: Multi-stage build to minimize final image size.
|
|
|
|
**Implementation**:
|
|
```dockerfile
|
|
# Stage 1: Builder
|
|
FROM python:3.11-slim AS builder
|
|
|
|
# Install uv for fast dependency resolution
|
|
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
|
|
|
# Set working directory
|
|
WORKDIR /app
|
|
|
|
# Copy dependency files
|
|
COPY pyproject.toml uv.lock ./
|
|
|
|
# Install dependencies to a virtual environment
|
|
RUN uv sync --frozen --no-dev
|
|
|
|
# Stage 2: Runtime
|
|
FROM python:3.11-slim
|
|
|
|
# Create non-root user
|
|
RUN useradd --create-home --shell /bin/bash gondulf
|
|
|
|
# Set working directory
|
|
WORKDIR /app
|
|
|
|
# Copy virtual environment from builder
|
|
COPY --from=builder /app/.venv /app/.venv
|
|
|
|
# Copy application code
|
|
COPY src/ /app/src/
|
|
COPY migrations/ /app/migrations/
|
|
|
|
# Create data directory for SQLite
|
|
RUN mkdir -p /app/data && chown gondulf:gondulf /app/data
|
|
|
|
# Switch to non-root user
|
|
USER gondulf
|
|
|
|
# Set environment variables
|
|
ENV PATH="/app/.venv/bin:$PATH"
|
|
ENV PYTHONPATH="/app/src"
|
|
ENV GONDULF_DATABASE_URL="sqlite:////app/data/gondulf.db"
|
|
|
|
# Expose port
|
|
EXPOSE 8000
|
|
|
|
# Health check
|
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
|
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
|
|
|
|
# Run application
|
|
CMD ["uvicorn", "gondulf.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
|
```
|
|
|
|
**Build Instructions**:
|
|
```bash
|
|
# Build image
|
|
docker build -t gondulf:1.0.0 .
|
|
|
|
# Tag as latest
|
|
docker tag gondulf:1.0.0 gondulf:latest
|
|
```
|
|
|
|
**Image Properties**:
|
|
- Base: `python:3.11-slim` (Debian-based, ~150MB)
|
|
- User: Non-root `gondulf` user
|
|
- Port: 8000 (Uvicorn default)
|
|
- Health check: `/health` endpoint every 30 seconds
|
|
- Data volume: `/app/data` (for SQLite database)
|
|
|
|
### 4.2: docker-compose.yml
|
|
|
|
**File**: `/docker-compose.yml`
|
|
|
|
**Purpose**: Local testing and development deployment.
|
|
|
|
**Implementation**:
|
|
```yaml
|
|
version: "3.8"
|
|
|
|
services:
|
|
gondulf:
|
|
build: .
|
|
image: gondulf:latest
|
|
container_name: gondulf
|
|
restart: unless-stopped
|
|
ports:
|
|
- "8000:8000"
|
|
volumes:
|
|
- ./data:/app/data
|
|
- ./logs:/app/logs
|
|
environment:
|
|
# Required
|
|
- GONDULF_SECRET_KEY=${GONDULF_SECRET_KEY}
|
|
- GONDULF_BASE_URL=${GONDULF_BASE_URL:-http://localhost:8000}
|
|
|
|
# SMTP Configuration (required for email verification)
|
|
- GONDULF_SMTP_HOST=${GONDULF_SMTP_HOST}
|
|
- GONDULF_SMTP_PORT=${GONDULF_SMTP_PORT:-587}
|
|
- GONDULF_SMTP_USERNAME=${GONDULF_SMTP_USERNAME}
|
|
- GONDULF_SMTP_PASSWORD=${GONDULF_SMTP_PASSWORD}
|
|
- GONDULF_SMTP_FROM_EMAIL=${GONDULF_SMTP_FROM_EMAIL}
|
|
- GONDULF_SMTP_USE_TLS=${GONDULF_SMTP_USE_TLS:-true}
|
|
|
|
# Optional Configuration
|
|
- GONDULF_DEBUG=${GONDULF_DEBUG:-false}
|
|
- GONDULF_LOG_LEVEL=${GONDULF_LOG_LEVEL:-INFO}
|
|
- GONDULF_TOKEN_TTL=${GONDULF_TOKEN_TTL:-3600}
|
|
|
|
healthcheck:
|
|
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 10s
|
|
|
|
networks:
|
|
- gondulf-network
|
|
|
|
networks:
|
|
gondulf-network:
|
|
driver: bridge
|
|
|
|
volumes:
|
|
data:
|
|
logs:
|
|
```
|
|
|
|
**Usage**:
|
|
```bash
|
|
# Create .env file with required variables
|
|
cp .env.example .env
|
|
nano .env # Edit with your values
|
|
|
|
# Start service
|
|
docker-compose up -d
|
|
|
|
# View logs
|
|
docker-compose logs -f gondulf
|
|
|
|
# Stop service
|
|
docker-compose down
|
|
|
|
# Restart service
|
|
docker-compose restart gondulf
|
|
```
|
|
|
|
### 4.3: Backup Script
|
|
|
|
**File**: `/scripts/backup_database.sh`
|
|
|
|
**Purpose**: Backup SQLite database with optional GPG encryption.
|
|
|
|
**Implementation**:
|
|
```bash
|
|
#!/bin/bash
|
|
set -euo pipefail
|
|
|
|
# Configuration
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
DATA_DIR="${DATA_DIR:-$PROJECT_ROOT/data}"
|
|
BACKUP_DIR="${BACKUP_DIR:-$PROJECT_ROOT/backups}"
|
|
DB_FILE="${DB_FILE:-$DATA_DIR/gondulf.db}"
|
|
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
|
BACKUP_FILE="$BACKUP_DIR/gondulf_${TIMESTAMP}.db"
|
|
RETENTION_DAYS="${RETENTION_DAYS:-30}"
|
|
|
|
# GPG encryption (optional)
|
|
GPG_RECIPIENT="${GPG_RECIPIENT:-}"
|
|
|
|
# Colors for output
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
RED='\033[0;31m'
|
|
NC='\033[0m' # No Color
|
|
|
|
echo "========================================="
|
|
echo "Gondulf Database Backup"
|
|
echo "========================================="
|
|
echo
|
|
|
|
# Check if database exists
|
|
if [ ! -f "$DB_FILE" ]; then
|
|
echo -e "${RED}ERROR: Database file not found: $DB_FILE${NC}"
|
|
exit 1
|
|
fi
|
|
|
|
# Create backup directory
|
|
mkdir -p "$BACKUP_DIR"
|
|
|
|
# SQLite backup (using .backup command for consistency)
|
|
echo -e "${YELLOW}Backing up database...${NC}"
|
|
sqlite3 "$DB_FILE" ".backup $BACKUP_FILE"
|
|
|
|
if [ $? -eq 0 ]; then
|
|
echo -e "${GREEN}✓ Database backed up to: $BACKUP_FILE${NC}"
|
|
|
|
# Get file size
|
|
SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
|
|
echo -e " Size: $SIZE"
|
|
else
|
|
echo -e "${RED}ERROR: Backup failed${NC}"
|
|
exit 1
|
|
fi
|
|
|
|
# GPG encryption (optional)
|
|
if [ -n "$GPG_RECIPIENT" ]; then
|
|
echo -e "${YELLOW}Encrypting backup with GPG...${NC}"
|
|
gpg --encrypt --recipient "$GPG_RECIPIENT" --output "${BACKUP_FILE}.gpg" "$BACKUP_FILE"
|
|
|
|
if [ $? -eq 0 ]; then
|
|
echo -e "${GREEN}✓ Backup encrypted: ${BACKUP_FILE}.gpg${NC}"
|
|
|
|
# Remove unencrypted backup
|
|
rm "$BACKUP_FILE"
|
|
BACKUP_FILE="${BACKUP_FILE}.gpg"
|
|
else
|
|
echo -e "${RED}WARNING: Encryption failed, keeping unencrypted backup${NC}"
|
|
fi
|
|
fi
|
|
|
|
# Cleanup old backups
|
|
echo -e "${YELLOW}Cleaning up old backups (older than $RETENTION_DAYS days)...${NC}"
|
|
find "$BACKUP_DIR" -name "gondulf_*.db*" -type f -mtime +$RETENTION_DAYS -delete
|
|
CLEANED=$(find "$BACKUP_DIR" -name "gondulf_*.db*" -type f -mtime +$RETENTION_DAYS | wc -l)
|
|
echo -e "${GREEN}✓ Removed $CLEANED old backup(s)${NC}"
|
|
|
|
# List recent backups
|
|
echo
|
|
echo "Recent backups:"
|
|
ls -lh "$BACKUP_DIR"/gondulf_*.db* 2>/dev/null | tail -5 || echo " No backups found"
|
|
|
|
echo
|
|
echo -e "${GREEN}Backup complete!${NC}"
|
|
```
|
|
|
|
**Restore Script**: `/scripts/restore_database.sh`
|
|
|
|
```bash
|
|
#!/bin/bash
|
|
set -euo pipefail
|
|
|
|
# Configuration
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
DATA_DIR="${DATA_DIR:-$PROJECT_ROOT/data}"
|
|
DB_FILE="${DB_FILE:-$DATA_DIR/gondulf.db}"
|
|
|
|
# Colors
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
RED='\033[0;31m'
|
|
NC='\033[0m'
|
|
|
|
echo "========================================="
|
|
echo "Gondulf Database Restore"
|
|
echo "========================================="
|
|
echo
|
|
|
|
# Check arguments
|
|
if [ $# -ne 1 ]; then
|
|
echo "Usage: $0 <backup_file>"
|
|
echo "Example: $0 backups/gondulf_20251120_143000.db"
|
|
exit 1
|
|
fi
|
|
|
|
BACKUP_FILE="$1"
|
|
|
|
# Check if backup exists
|
|
if [ ! -f "$BACKUP_FILE" ]; then
|
|
echo -e "${RED}ERROR: Backup file not found: $BACKUP_FILE${NC}"
|
|
exit 1
|
|
fi
|
|
|
|
# Check if encrypted
|
|
if [[ "$BACKUP_FILE" == *.gpg ]]; then
|
|
echo -e "${YELLOW}Decrypting backup...${NC}"
|
|
DECRYPTED_FILE="${BACKUP_FILE%.gpg}"
|
|
gpg --decrypt --output "$DECRYPTED_FILE" "$BACKUP_FILE"
|
|
|
|
if [ $? -ne 0 ]; then
|
|
echo -e "${RED}ERROR: Decryption failed${NC}"
|
|
exit 1
|
|
fi
|
|
|
|
BACKUP_FILE="$DECRYPTED_FILE"
|
|
echo -e "${GREEN}✓ Backup decrypted${NC}"
|
|
fi
|
|
|
|
# Backup current database (if exists)
|
|
if [ -f "$DB_FILE" ]; then
|
|
CURRENT_BACKUP="${DB_FILE}.before_restore_$(date +%Y%m%d_%H%M%S)"
|
|
echo -e "${YELLOW}Backing up current database to: $CURRENT_BACKUP${NC}"
|
|
cp "$DB_FILE" "$CURRENT_BACKUP"
|
|
echo -e "${GREEN}✓ Current database backed up${NC}"
|
|
fi
|
|
|
|
# Restore backup
|
|
echo -e "${YELLOW}Restoring database...${NC}"
|
|
cp "$BACKUP_FILE" "$DB_FILE"
|
|
|
|
if [ $? -eq 0 ]; then
|
|
echo -e "${GREEN}✓ Database restored from: $BACKUP_FILE${NC}"
|
|
echo
|
|
echo -e "${YELLOW}NOTE: Please restart the Gondulf service${NC}"
|
|
else
|
|
echo -e "${RED}ERROR: Restore failed${NC}"
|
|
|
|
# Restore from backup if exists
|
|
if [ -f "$CURRENT_BACKUP" ]; then
|
|
cp "$CURRENT_BACKUP" "$DB_FILE"
|
|
echo -e "${YELLOW}Restored previous database${NC}"
|
|
fi
|
|
|
|
exit 1
|
|
fi
|
|
|
|
# Cleanup decrypted file if it was created
|
|
if [[ "$1" == *.gpg ]] && [ -f "${1%.gpg}" ]; then
|
|
rm "${1%.gpg}"
|
|
fi
|
|
|
|
echo -e "${GREEN}Restore complete!${NC}"
|
|
```
|
|
|
|
**Make scripts executable**:
|
|
```bash
|
|
chmod +x scripts/backup_database.sh
|
|
chmod +x scripts/restore_database.sh
|
|
```
|
|
|
|
**Cron Setup** (example):
|
|
```bash
|
|
# Backup daily at 2 AM
|
|
0 2 * * * /app/scripts/backup_database.sh >> /app/logs/backup.log 2>&1
|
|
```
|
|
|
|
### 4.4: Environment Variable Documentation
|
|
|
|
**File**: `/docs/deployment/environment-variables.md`
|
|
|
|
**Content**:
|
|
```markdown
|
|
# Environment Variables
|
|
|
|
All Gondulf configuration is done via environment variables with the `GONDULF_` prefix.
|
|
|
|
## Required Variables
|
|
|
|
### SECRET_KEY (Required)
|
|
**Variable**: `GONDULF_SECRET_KEY`
|
|
**Description**: Secret key for cryptographic operations (token generation, session security)
|
|
**Format**: String, minimum 32 characters
|
|
**Example**: `GONDULF_SECRET_KEY=$(python -c "import secrets; print(secrets.token_urlsafe(32))")`
|
|
**Security**: NEVER commit to version control. Generate unique value per deployment.
|
|
|
|
### BASE_URL (Required)
|
|
**Variable**: `GONDULF_BASE_URL`
|
|
**Description**: Base URL of the Gondulf server (used for metadata endpoint)
|
|
**Format**: URL with scheme (https://...)
|
|
**Example**: `GONDULF_BASE_URL=https://auth.example.com`
|
|
**Production**: Must be HTTPS
|
|
|
|
### SMTP Configuration (Required for Email Verification)
|
|
**Variable**: `GONDULF_SMTP_HOST`
|
|
**Description**: SMTP server hostname
|
|
**Example**: `GONDULF_SMTP_HOST=smtp.gmail.com`
|
|
|
|
**Variable**: `GONDULF_SMTP_PORT`
|
|
**Description**: SMTP server port
|
|
**Default**: `587`
|
|
**Common Values**: `587` (STARTTLS), `465` (implicit TLS), `25` (unencrypted)
|
|
|
|
**Variable**: `GONDULF_SMTP_USERNAME`
|
|
**Description**: SMTP authentication username
|
|
**Example**: `GONDULF_SMTP_USERNAME=noreply@example.com`
|
|
|
|
**Variable**: `GONDULF_SMTP_PASSWORD`
|
|
**Description**: SMTP authentication password
|
|
**Security**: Use app-specific password if available (Gmail, Outlook)
|
|
|
|
**Variable**: `GONDULF_SMTP_FROM_EMAIL`
|
|
**Description**: From address for verification emails
|
|
**Example**: `GONDULF_SMTP_FROM_EMAIL=noreply@example.com`
|
|
|
|
**Variable**: `GONDULF_SMTP_USE_TLS`
|
|
**Description**: Enable STARTTLS (port 587)
|
|
**Default**: `true`
|
|
**Values**: `true`, `false`
|
|
|
|
## Optional Variables
|
|
|
|
### Database Configuration
|
|
**Variable**: `GONDULF_DATABASE_URL`
|
|
**Description**: Database connection URL
|
|
**Default**: `sqlite:///./data/gondulf.db`
|
|
**Format**: SQLAlchemy connection string
|
|
**PostgreSQL Example**: `postgresql://user:pass@localhost/gondulf` (future)
|
|
|
|
### Security Settings
|
|
**Variable**: `GONDULF_DEBUG`
|
|
**Description**: Enable debug mode (disables HTTPS enforcement)
|
|
**Default**: `false`
|
|
**Values**: `true`, `false`
|
|
**Warning**: NEVER enable in production
|
|
|
|
**Variable**: `GONDULF_HSTS_MAX_AGE`
|
|
**Description**: HSTS max-age header value (seconds)
|
|
**Default**: `31536000` (1 year)
|
|
|
|
### Token Configuration
|
|
**Variable**: `GONDULF_TOKEN_TTL`
|
|
**Description**: Access token time-to-live (seconds)
|
|
**Default**: `3600` (1 hour)
|
|
**Range**: `300` (5 min) to `86400` (24 hours)
|
|
|
|
**Variable**: `GONDULF_TOKEN_CLEANUP_ENABLED`
|
|
**Description**: Enable automatic cleanup of expired tokens
|
|
**Default**: `true`
|
|
**Values**: `true`, `false`
|
|
|
|
**Variable**: `GONDULF_TOKEN_CLEANUP_INTERVAL`
|
|
**Description**: Token cleanup interval (seconds)
|
|
**Default**: `3600` (1 hour)
|
|
|
|
### Logging Configuration
|
|
**Variable**: `GONDULF_LOG_LEVEL`
|
|
**Description**: Logging level
|
|
**Default**: `INFO`
|
|
**Values**: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`
|
|
|
|
## Docker Environment Variables
|
|
|
|
When running in Docker, use `-e` flag or `--env-file`:
|
|
|
|
```bash
|
|
# Using -e flag
|
|
docker run -e GONDULF_SECRET_KEY=xxx -e GONDULF_BASE_URL=https://auth.example.com gondulf:latest
|
|
|
|
# Using --env-file
|
|
docker run --env-file .env gondulf:latest
|
|
```
|
|
|
|
## Docker Compose
|
|
|
|
Create `.env` file in project root:
|
|
```env
|
|
GONDULF_SECRET_KEY=your-secret-key-here
|
|
GONDULF_BASE_URL=https://auth.example.com
|
|
GONDULF_SMTP_HOST=smtp.gmail.com
|
|
GONDULF_SMTP_PORT=587
|
|
GONDULF_SMTP_USERNAME=noreply@example.com
|
|
GONDULF_SMTP_PASSWORD=your-password-here
|
|
GONDULF_SMTP_FROM_EMAIL=noreply@example.com
|
|
```
|
|
|
|
Then run:
|
|
```bash
|
|
docker-compose up -d
|
|
```
|
|
|
|
## Security Recommendations
|
|
|
|
1. **Generate SECRET_KEY**: Use `secrets.token_urlsafe(32)` or similar
|
|
2. **SMTP Password**: Use app-specific passwords (Gmail, Outlook)
|
|
3. **HTTPS Only**: Never expose HTTP in production
|
|
4. **Environment Files**: Add `.env` to `.gitignore`
|
|
5. **Docker Secrets**: Use Docker secrets in production (not environment variables)
|
|
|
|
## Validation
|
|
|
|
Gondulf validates configuration at startup. Invalid configuration will prevent application from starting (fail-fast).
|
|
|
|
Missing required variables will produce clear error messages:
|
|
```
|
|
ERROR: Required environment variable GONDULF_SECRET_KEY is not set
|
|
```
|
|
```
|
|
|
|
### Testing Requirements
|
|
|
|
**Dockerfile Tests** (`tests/docker/test_dockerfile.sh`):
|
|
1. Test image builds successfully
|
|
2. Test image runs as non-root user
|
|
3. Test health check passes
|
|
4. Test application starts
|
|
5. Test data volume persists
|
|
|
|
**docker-compose Tests** (`tests/docker/test_docker_compose.sh`):
|
|
1. Test docker-compose up succeeds
|
|
2. Test service accessible on port 8000
|
|
3. Test /health endpoint returns 200
|
|
4. Test docker-compose down cleans up
|
|
|
|
**Backup Script Tests** (`tests/scripts/test_backup.sh`):
|
|
1. Test backup creates file
|
|
2. Test backup with GPG encryption
|
|
3. Test backup cleanup removes old files
|
|
4. Test restore from backup
|
|
5. Test restore from encrypted backup
|
|
|
|
### Acceptance Criteria
|
|
|
|
- [ ] Dockerfile builds successfully
|
|
- [ ] Docker image runs as non-root user
|
|
- [ ] Health check passes
|
|
- [ ] docker-compose.yml starts service successfully
|
|
- [ ] Backup script creates backup successfully
|
|
- [ ] Backup script with GPG encryption works
|
|
- [ ] Restore script restores database successfully
|
|
- [ ] Environment variable documentation complete
|
|
- [ ] All tests pass (Docker, backup/restore)
|
|
|
|
---
|
|
|
|
## Component 5: Integration & End-to-End Tests
|
|
|
|
### Purpose
|
|
|
|
Implement comprehensive integration and end-to-end tests to verify complete authentication flows, endpoint integration, and W3C IndieAuth compliance.
|
|
|
|
### Specification References
|
|
|
|
- **v1.0.0 Roadmap**: Line 67 (P0 feature), Testing Strategy lines 275-287, Phase 5 lines 230-232
|
|
- **Testing Standards**: `/docs/standards/testing.md`
|
|
|
|
### Design Overview
|
|
|
|
Create integration test suite (20% of total tests) covering endpoint interactions and multi-component workflows. Create end-to-end test suite (10% of total tests) covering complete user journeys. Use FastAPI TestClient for HTTP testing, mock external dependencies (SMTP, DNS).
|
|
|
|
### Test Organization
|
|
|
|
**Directory Structure**:
|
|
```
|
|
tests/
|
|
├── unit/ # Existing unit tests (70%)
|
|
│ ├── test_*.py
|
|
│ └── ...
|
|
├── integration/ # NEW: Integration tests (20%)
|
|
│ ├── __init__.py
|
|
│ ├── conftest.py # Shared fixtures
|
|
│ ├── test_authorization_flow.py
|
|
│ ├── test_token_flow.py
|
|
│ ├── test_verification_flow.py
|
|
│ ├── test_metadata_endpoint.py
|
|
│ └── test_error_handling.py
|
|
├── e2e/ # NEW: End-to-end tests (10%)
|
|
│ ├── __init__.py
|
|
│ ├── conftest.py
|
|
│ ├── test_complete_auth_flow.py
|
|
│ ├── test_email_verification_flow.py
|
|
│ └── test_dns_verification_flow.py
|
|
└── security/ # NEW: Security tests
|
|
├── __init__.py
|
|
├── test_input_validation.py
|
|
├── test_timing_attacks.py
|
|
└── test_pii_logging.py
|
|
```
|
|
|
|
### 5.1: Integration Tests
|
|
|
|
**Purpose**: Test endpoint interactions and multi-component workflows without mocking application logic.
|
|
|
|
#### Test: Authorization Flow Integration
|
|
|
|
**File**: `tests/integration/test_authorization_flow.py`
|
|
|
|
**Scenarios**:
|
|
1. Valid authorization request returns consent page
|
|
2. User approval generates authorization code
|
|
3. User denial redirects with error
|
|
4. Missing parameters return error
|
|
5. Invalid redirect_uri returns error
|
|
6. State parameter preserved through flow
|
|
|
|
**Implementation Pattern**:
|
|
```python
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from gondulf.main import app
|
|
from gondulf.dependencies import get_html_fetcher, get_dns_service
|
|
|
|
@pytest.fixture
|
|
def client():
|
|
"""TestClient with mocked external dependencies."""
|
|
# Mock HTML fetcher (no real HTTP requests)
|
|
def mock_html_fetcher():
|
|
class MockHTMLFetcher:
|
|
def fetch(self, url):
|
|
return '<html><link rel="me" href="mailto:user@example.com"></html>'
|
|
return MockHTMLFetcher()
|
|
|
|
# Mock DNS service (no real DNS queries)
|
|
def mock_dns_service():
|
|
class MockDNSService:
|
|
def verify_txt_record(self, domain):
|
|
return True
|
|
return MockDNSService()
|
|
|
|
# Override dependencies
|
|
app.dependency_overrides[get_html_fetcher] = mock_html_fetcher
|
|
app.dependency_overrides[get_dns_service] = mock_dns_service
|
|
|
|
yield TestClient(app)
|
|
|
|
# Cleanup
|
|
app.dependency_overrides.clear()
|
|
|
|
def test_authorization_request_valid(client):
|
|
"""Test valid authorization request returns consent page."""
|
|
response = client.get(
|
|
"/authorize",
|
|
params={
|
|
"me": "https://example.com",
|
|
"client_id": "https://client.example.com",
|
|
"redirect_uri": "https://client.example.com/callback",
|
|
"state": "random-state-value",
|
|
"response_type": "code"
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert "authorize" in response.text.lower()
|
|
assert "example.com" in response.text
|
|
assert "client.example.com" in response.text
|
|
|
|
def test_authorization_approval_generates_code(client):
|
|
"""Test user approval generates authorization code and redirects."""
|
|
# First, get to consent page (this creates session/state)
|
|
response = client.get("/authorize", params={...})
|
|
|
|
# Then submit approval
|
|
response = client.post(
|
|
"/authorize",
|
|
data={
|
|
"authorized": "true",
|
|
"me": "https://example.com",
|
|
"client_id": "https://client.example.com",
|
|
"redirect_uri": "https://client.example.com/callback",
|
|
"state": "random-state-value"
|
|
},
|
|
allow_redirects=False
|
|
)
|
|
|
|
assert response.status_code == 302
|
|
assert "code=" in response.headers["location"]
|
|
assert "state=random-state-value" in response.headers["location"]
|
|
```
|
|
|
|
#### Test: Token Flow Integration
|
|
|
|
**File**: `tests/integration/test_token_flow.py`
|
|
|
|
**Scenarios**:
|
|
1. Valid code exchange returns token
|
|
2. Expired code returns error
|
|
3. Used code returns error
|
|
4. Mismatched client_id returns error
|
|
5. Mismatched redirect_uri returns error
|
|
6. Token response includes correct fields
|
|
|
|
**Implementation Pattern**:
|
|
```python
|
|
def test_token_exchange_valid(client, create_auth_code):
|
|
"""Test valid authorization code exchanges for token."""
|
|
# create_auth_code is a fixture that creates a valid code
|
|
code, metadata = create_auth_code(
|
|
me="https://example.com",
|
|
client_id="https://client.example.com",
|
|
redirect_uri="https://client.example.com/callback"
|
|
)
|
|
|
|
response = client.post(
|
|
"/token",
|
|
data={
|
|
"grant_type": "authorization_code",
|
|
"code": code,
|
|
"client_id": "https://client.example.com",
|
|
"redirect_uri": "https://client.example.com/callback",
|
|
"me": "https://example.com"
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
json = response.json()
|
|
assert "access_token" in json
|
|
assert json["token_type"] == "Bearer"
|
|
assert json["me"] == "https://example.com"
|
|
assert len(json["access_token"]) == 43 # base64url(32 bytes)
|
|
```
|
|
|
|
#### Test: Verification Flow Integration
|
|
|
|
**File**: `tests/integration/test_verification_flow.py`
|
|
|
|
**Scenarios**:
|
|
1. Start verification sends email and returns success
|
|
2. Valid code verifies successfully
|
|
3. Invalid code returns error
|
|
4. Expired code returns error
|
|
5. Rate limiting prevents abuse
|
|
|
|
#### Test: Metadata Endpoint
|
|
|
|
**File**: `tests/integration/test_metadata_endpoint.py`
|
|
|
|
**Scenarios**:
|
|
1. Metadata endpoint returns valid JSON
|
|
2. All required fields present
|
|
3. URLs are valid
|
|
4. Cache-Control header present
|
|
|
|
#### Test: Error Handling
|
|
|
|
**File**: `tests/integration/test_error_handling.py`
|
|
|
|
**Scenarios**:
|
|
1. OAuth error responses formatted correctly
|
|
2. Error redirection preserves state parameter
|
|
3. Invalid redirect_uri shows error page (no redirect)
|
|
4. Server errors return 500 with error page
|
|
|
|
### 5.2: End-to-End Tests
|
|
|
|
**Purpose**: Test complete user journeys from start to finish.
|
|
|
|
#### Test: Complete Authentication Flow
|
|
|
|
**File**: `tests/e2e/test_complete_auth_flow.py`
|
|
|
|
**Scenario**: Simulate complete IndieAuth flow:
|
|
1. Client initiates authorization request
|
|
2. Server checks domain verification (DNS + email)
|
|
3. User verifies email (enters code)
|
|
4. User approves authorization
|
|
5. Client exchanges code for token
|
|
6. Token is valid and can be used
|
|
|
|
**Implementation**:
|
|
```python
|
|
@pytest.mark.e2e
|
|
def test_complete_authentication_flow(client, mock_smtp, mock_dns):
|
|
"""Test complete IndieAuth authentication flow end-to-end."""
|
|
# 1. Client initiates authorization
|
|
auth_response = client.get("/authorize", params={
|
|
"me": "https://user.example.com",
|
|
"client_id": "https://app.example.com",
|
|
"redirect_uri": "https://app.example.com/callback",
|
|
"state": "client-random-state",
|
|
"response_type": "code"
|
|
})
|
|
assert auth_response.status_code == 200
|
|
|
|
# 2. Server discovers email from rel=me (mocked)
|
|
# 3. Server sends verification email (captured by mock_smtp)
|
|
verify_response = client.post("/api/verify/start", json={
|
|
"domain": "user.example.com"
|
|
})
|
|
assert verify_response.json()["success"] is True
|
|
|
|
# Extract verification code from mock SMTP
|
|
verification_code = mock_smtp.get_last_email_code()
|
|
|
|
# 4. User submits verification code
|
|
code_response = client.post("/api/verify/code", json={
|
|
"domain": "user.example.com",
|
|
"code": verification_code
|
|
})
|
|
assert code_response.json()["success"] is True
|
|
|
|
# 5. User approves authorization
|
|
approve_response = client.post("/authorize", data={
|
|
"authorized": "true",
|
|
"me": "https://user.example.com",
|
|
"client_id": "https://app.example.com",
|
|
"redirect_uri": "https://app.example.com/callback",
|
|
"state": "client-random-state"
|
|
}, allow_redirects=False)
|
|
|
|
assert approve_response.status_code == 302
|
|
location = approve_response.headers["location"]
|
|
assert "code=" in location
|
|
assert "state=client-random-state" in location
|
|
|
|
# Extract authorization code from redirect
|
|
from urllib.parse import urlparse, parse_qs
|
|
parsed = urlparse(location)
|
|
params = parse_qs(parsed.query)
|
|
auth_code = params["code"][0]
|
|
|
|
# 6. Client exchanges code for token
|
|
token_response = client.post("/token", data={
|
|
"grant_type": "authorization_code",
|
|
"code": auth_code,
|
|
"client_id": "https://app.example.com",
|
|
"redirect_uri": "https://app.example.com/callback",
|
|
"me": "https://user.example.com"
|
|
})
|
|
|
|
assert token_response.status_code == 200
|
|
token_json = token_response.json()
|
|
assert "access_token" in token_json
|
|
assert token_json["me"] == "https://user.example.com"
|
|
|
|
# 7. Token is valid (verify in database)
|
|
# This would be a token introspection endpoint in future
|
|
token = token_json["access_token"]
|
|
assert len(token) == 43
|
|
```
|
|
|
|
#### Test: Email Verification Flow
|
|
|
|
**File**: `tests/e2e/test_email_verification_flow.py`
|
|
|
|
**Scenario**: Test email-based domain verification:
|
|
1. User starts verification
|
|
2. Email sent with code
|
|
3. User enters code
|
|
4. Domain marked as verified
|
|
|
|
#### Test: DNS Verification Flow
|
|
|
|
**File**: `tests/e2e/test_dns_verification_flow.py`
|
|
|
|
**Scenario**: Test DNS TXT record verification:
|
|
1. User adds TXT record
|
|
2. Server queries DNS
|
|
3. Domain verified via TXT record
|
|
4. Email verification still required (two-factor)
|
|
|
|
### 5.3: Security Tests
|
|
|
|
**Purpose**: Test security properties and attack resistance.
|
|
|
|
#### Test: Input Validation
|
|
|
|
**File**: `tests/security/test_input_validation.py`
|
|
|
|
**Scenarios**:
|
|
1. Malformed URLs rejected
|
|
2. SQL injection attempts blocked
|
|
3. XSS attempts escaped in templates
|
|
4. Open redirect attempts blocked
|
|
5. URL fragment in me parameter rejected
|
|
|
|
**Implementation**:
|
|
```python
|
|
@pytest.mark.security
|
|
def test_sql_injection_prevention(client):
|
|
"""Test SQL injection attempts are blocked."""
|
|
# Attempt SQL injection in me parameter
|
|
response = client.get("/authorize", params={
|
|
"me": "https://example.com'; DROP TABLE tokens; --",
|
|
"client_id": "https://app.example.com",
|
|
"redirect_uri": "https://app.example.com/callback",
|
|
"state": "state",
|
|
"response_type": "code"
|
|
})
|
|
|
|
# Should return error (invalid URL), not execute SQL
|
|
assert response.status_code in [400, 422]
|
|
|
|
# Verify tokens table still exists
|
|
from gondulf.dependencies import get_database
|
|
db = get_database()
|
|
result = db.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='tokens'")
|
|
assert result.fetchone() is not None
|
|
|
|
@pytest.mark.security
|
|
def test_xss_prevention_in_templates(client):
|
|
"""Test XSS attempts are escaped in HTML templates."""
|
|
xss_payload = "<script>alert('XSS')</script>"
|
|
|
|
response = client.get("/authorize", params={
|
|
"me": f"https://example.com/{xss_payload}",
|
|
"client_id": "https://app.example.com",
|
|
"redirect_uri": "https://app.example.com/callback",
|
|
"state": xss_payload,
|
|
"response_type": "code"
|
|
})
|
|
|
|
# XSS payload should be HTML-escaped in response
|
|
assert "<script>" not in response.text
|
|
assert "<script>" in response.text or response.status_code in [400, 422]
|
|
```
|
|
|
|
#### Test: Timing Attacks
|
|
|
|
**File**: `tests/security/test_timing_attacks.py`
|
|
|
|
**Scenarios**:
|
|
1. Token verification takes constant time
|
|
2. Code verification takes constant time
|
|
3. Email verification takes constant time
|
|
|
|
**Implementation**:
|
|
```python
|
|
import time
|
|
import statistics
|
|
|
|
@pytest.mark.security
|
|
def test_token_verification_constant_time(client, create_token):
|
|
"""Test token verification is resistant to timing attacks."""
|
|
valid_token, _ = create_token()
|
|
invalid_token = "x" * 43
|
|
|
|
# Measure verification time for valid token
|
|
valid_times = []
|
|
for _ in range(100):
|
|
start = time.perf_counter()
|
|
# Simulate token verification
|
|
response = client.get("/some-protected-endpoint",
|
|
headers={"Authorization": f"Bearer {valid_token}"})
|
|
end = time.perf_counter()
|
|
valid_times.append(end - start)
|
|
|
|
# Measure verification time for invalid token
|
|
invalid_times = []
|
|
for _ in range(100):
|
|
start = time.perf_counter()
|
|
response = client.get("/some-protected-endpoint",
|
|
headers={"Authorization": f"Bearer {invalid_token}"})
|
|
end = time.perf_counter()
|
|
invalid_times.append(end - start)
|
|
|
|
# Compare timing distributions
|
|
valid_mean = statistics.mean(valid_times)
|
|
invalid_mean = statistics.mean(invalid_times)
|
|
|
|
# Times should be similar (within 10% tolerance for noise)
|
|
ratio = valid_mean / invalid_mean
|
|
assert 0.9 < ratio < 1.1, f"Timing difference detected: {ratio:.2f}x"
|
|
```
|
|
|
|
#### Test: PII Logging
|
|
|
|
**File**: `tests/security/test_pii_logging.py`
|
|
|
|
**Scenarios**:
|
|
1. Email addresses not logged
|
|
2. Full tokens not logged
|
|
3. Full codes not logged
|
|
4. Only prefixes/hashes logged
|
|
|
|
**Implementation**:
|
|
```python
|
|
import logging
|
|
from unittest.mock import patch
|
|
|
|
@pytest.mark.security
|
|
def test_email_not_logged(client, caplog):
|
|
"""Test email addresses are not logged."""
|
|
with caplog.at_level(logging.DEBUG):
|
|
# Trigger email verification
|
|
response = client.post("/api/verify/start", json={
|
|
"domain": "example.com"
|
|
})
|
|
|
|
# Check all log messages
|
|
for record in caplog.records:
|
|
# Email should never appear in logs
|
|
assert "user@example.com" not in record.message
|
|
assert "@" not in record.message or "example.com" in record.message
|
|
|
|
@pytest.mark.security
|
|
def test_full_tokens_not_logged(client, create_token, caplog):
|
|
"""Test full access tokens are not logged."""
|
|
token, _ = create_token()
|
|
|
|
with caplog.at_level(logging.DEBUG):
|
|
# Trigger token usage
|
|
response = client.get("/some-endpoint",
|
|
headers={"Authorization": f"Bearer {token}"})
|
|
|
|
# Full token should never appear in logs
|
|
for record in caplog.records:
|
|
assert token not in record.message
|
|
# Prefix (first 8 chars) is acceptable
|
|
```
|
|
|
|
### 5.4: Shared Test Fixtures
|
|
|
|
**File**: `tests/integration/conftest.py`
|
|
|
|
**Fixtures**:
|
|
```python
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from gondulf.main import app
|
|
from gondulf.storage import CodeStore
|
|
from gondulf.services.token_service import TokenService
|
|
import secrets
|
|
|
|
@pytest.fixture
|
|
def client():
|
|
"""FastAPI TestClient with mocked dependencies."""
|
|
# Setup mocks
|
|
# ...
|
|
yield TestClient(app)
|
|
# Cleanup
|
|
app.dependency_overrides.clear()
|
|
|
|
@pytest.fixture
|
|
def mock_smtp():
|
|
"""Mock SMTP server that captures sent emails."""
|
|
class MockSMTP:
|
|
def __init__(self):
|
|
self.sent_emails = []
|
|
|
|
def sendmail(self, from_addr, to_addrs, msg):
|
|
self.sent_emails.append({
|
|
"from": from_addr,
|
|
"to": to_addrs,
|
|
"message": msg
|
|
})
|
|
|
|
def get_last_email_code(self):
|
|
"""Extract verification code from last email."""
|
|
import re
|
|
last_email = self.sent_emails[-1]
|
|
match = re.search(r'\b\d{6}\b', last_email["message"])
|
|
return match.group(0) if match else None
|
|
|
|
return MockSMTP()
|
|
|
|
@pytest.fixture
|
|
def mock_dns():
|
|
"""Mock DNS resolver that always returns valid."""
|
|
class MockDNSResolver:
|
|
def verify_txt_record(self, domain):
|
|
return True
|
|
|
|
return MockDNSResolver()
|
|
|
|
@pytest.fixture
|
|
def create_auth_code():
|
|
"""Fixture to create authorization codes for testing."""
|
|
def _create(me, client_id, redirect_uri, state="test-state"):
|
|
code = secrets.token_urlsafe(32)
|
|
metadata = {
|
|
"me": me,
|
|
"client_id": client_id,
|
|
"redirect_uri": redirect_uri,
|
|
"state": state,
|
|
"created_at": time.time(),
|
|
"expires_at": time.time() + 600,
|
|
"used": False
|
|
}
|
|
# Store in CodeStore
|
|
code_store = CodeStore()
|
|
code_store.store(code, metadata, ttl=600)
|
|
return code, metadata
|
|
|
|
return _create
|
|
|
|
@pytest.fixture
|
|
def create_token():
|
|
"""Fixture to create access tokens for testing."""
|
|
def _create(me="https://example.com", client_id="https://app.example.com"):
|
|
token_service = TokenService(database=get_database())
|
|
token = token_service.generate_token(
|
|
me=me,
|
|
client_id=client_id,
|
|
scope=""
|
|
)
|
|
return token, {"me": me, "client_id": client_id}
|
|
|
|
return _create
|
|
```
|
|
|
|
### Test Execution
|
|
|
|
**Pytest Markers**:
|
|
```python
|
|
# pytest.ini or pyproject.toml
|
|
[tool.pytest.ini_options]
|
|
markers = [
|
|
"unit: Unit tests (fast, no external dependencies)",
|
|
"integration: Integration tests (TestClient, mocked external deps)",
|
|
"e2e: End-to-end tests (complete user journeys)",
|
|
"security: Security-focused tests (timing, injection, etc.)"
|
|
]
|
|
```
|
|
|
|
**Run Tests**:
|
|
```bash
|
|
# All tests
|
|
uv run pytest
|
|
|
|
# Unit tests only
|
|
uv run pytest -m unit
|
|
|
|
# Integration tests only
|
|
uv run pytest -m integration
|
|
|
|
# E2E tests only
|
|
uv run pytest -m e2e
|
|
|
|
# Security tests only
|
|
uv run pytest -m security
|
|
|
|
# With coverage
|
|
uv run pytest --cov=src/gondulf --cov-report=html --cov-report=term-missing
|
|
```
|
|
|
|
### Acceptance Criteria
|
|
|
|
- [ ] Integration test suite created (20+ tests)
|
|
- [ ] End-to-end test suite created (10+ tests)
|
|
- [ ] Security test suite created (15+ tests)
|
|
- [ ] All tests pass
|
|
- [ ] Test coverage ≥80% overall
|
|
- [ ] Critical path coverage ≥95% (auth, token, security)
|
|
- [ ] Fixtures for common test scenarios created
|
|
- [ ] Pytest markers configured
|
|
|
|
---
|
|
|
|
## Component 6: Real Client Testing
|
|
|
|
### Purpose
|
|
|
|
Verify IndieAuth server interoperability by testing with real IndieAuth clients to ensure W3C specification compliance and production readiness.
|
|
|
|
### Specification References
|
|
|
|
- **v1.0.0 Roadmap**: Phase 5 lines 239, 330, Success metrics line 535
|
|
- **W3C IndieAuth**: Full specification compliance required
|
|
|
|
### Design Overview
|
|
|
|
Test Gondulf with at least 2 real IndieAuth clients to verify protocol compliance and identify interoperability issues. Document testing methodology and results.
|
|
|
|
### Client Selection
|
|
|
|
**Criteria for Client Selection**:
|
|
1. Publicly available and documented
|
|
2. Known to be W3C IndieAuth compliant
|
|
3. Different implementations (diversity of clients)
|
|
4. Active maintenance
|
|
|
|
**Recommended Clients**:
|
|
|
|
1. **IndieLogin.com** (https://indielogin.com)
|
|
- Reference client by Aaron Parecki (IndieAuth spec author)
|
|
- PHP implementation
|
|
- Most authoritative for compliance testing
|
|
|
|
2. **Quill** (https://quill.p3k.io)
|
|
- Micropub client with IndieAuth support
|
|
- Real-world usage scenario
|
|
|
|
3. **Indigenous** (iOS/Android app)
|
|
- Mobile app with IndieAuth
|
|
- Tests mobile client scenarios
|
|
|
|
4. **Monocle** (https://monocle.p3k.io)
|
|
- Microsub client with IndieAuth
|
|
- Tests feed reader scenario
|
|
|
|
**Minimum Requirement**: Test with at least 2 different clients (recommendation: IndieLogin + one other).
|
|
|
|
### Testing Approach
|
|
|
|
#### Phase 1: Local Development Testing
|
|
|
|
**Setup**:
|
|
1. Run Gondulf locally with ngrok for HTTPS tunnel
|
|
2. Configure test domain with DNS TXT record and rel=me link
|
|
3. Use ngrok URL as `GONDULF_BASE_URL`
|
|
|
|
**Steps**:
|
|
```bash
|
|
# Start Gondulf locally
|
|
uv run uvicorn gondulf.main:app --reload --port 8000
|
|
|
|
# In another terminal, start ngrok
|
|
ngrok http 8000
|
|
|
|
# Note ngrok URL (e.g., https://abc123.ngrok.io)
|
|
# Set as BASE_URL:
|
|
export GONDULF_BASE_URL=https://abc123.ngrok.io
|
|
```
|
|
|
|
**Test Domain Setup**:
|
|
```bash
|
|
# Add DNS TXT record:
|
|
# _gondulf.test.example.com TXT "verified"
|
|
|
|
# Add to test.example.com homepage:
|
|
# <link rel="me" href="mailto:admin@test.example.com">
|
|
```
|
|
|
|
#### Phase 2: Test with IndieLogin
|
|
|
|
**Test Procedure**:
|
|
1. Go to https://indielogin.com
|
|
2. Enter test domain (https://test.example.com)
|
|
3. Click "Sign In"
|
|
4. Observe authorization flow:
|
|
- IndieLogin discovers Gondulf as authorization server
|
|
- IndieLogin redirects to Gondulf /authorize
|
|
- Gondulf displays consent screen
|
|
- User approves
|
|
- IndieLogin receives authorization code
|
|
- IndieLogin exchanges code for token
|
|
- IndieLogin displays success
|
|
|
|
**Expected Results**:
|
|
- [ ] IndieLogin discovers authorization endpoint via metadata
|
|
- [ ] Authorization request has all required parameters
|
|
- [ ] Consent screen displays correctly
|
|
- [ ] Authorization code generated and returned
|
|
- [ ] Token exchange succeeds
|
|
- [ ] Token response includes correct `me` value
|
|
- [ ] IndieLogin shows "Signed in as test.example.com"
|
|
|
|
**Debugging**:
|
|
- Monitor Gondulf logs for errors
|
|
- Check IndieLogin error messages
|
|
- Use browser dev tools to inspect redirects
|
|
- Verify DNS TXT record resolvable
|
|
|
|
#### Phase 3: Test with Secondary Client
|
|
|
|
**Test Procedure**: (Using Quill as example)
|
|
1. Go to https://quill.p3k.io
|
|
2. Click "Sign In"
|
|
3. Enter test domain
|
|
4. Follow authorization flow
|
|
5. Verify successful sign-in
|
|
|
|
**Expected Results**:
|
|
- [ ] Client discovers Gondulf endpoints
|
|
- [ ] Authorization flow completes
|
|
- [ ] Token issued successfully
|
|
- [ ] Client can use token (if applicable)
|
|
|
|
#### Phase 4: Error Scenario Testing
|
|
|
|
**Test Error Handling**:
|
|
1. **Invalid redirect_uri**: Client sends non-matching redirect_uri
|
|
- Expected: Error response
|
|
2. **Missing state**: Client omits state parameter
|
|
- Expected: Error response
|
|
3. **User denial**: User clicks "Deny" on consent screen
|
|
- Expected: Error redirect with `access_denied`
|
|
4. **Expired code**: Client delays token exchange beyond 10 minutes
|
|
- Expected: `invalid_grant` error
|
|
|
|
### Testing Checklist
|
|
|
|
**Discovery**:
|
|
- [ ] Metadata endpoint accessible
|
|
- [ ] Metadata contains correct endpoint URLs
|
|
- [ ] Clients can discover endpoints automatically
|
|
|
|
**Authorization Flow**:
|
|
- [ ] Client redirects to /authorize correctly
|
|
- [ ] All required parameters present
|
|
- [ ] Consent screen renders correctly
|
|
- [ ] User approval generates code
|
|
- [ ] User denial returns error
|
|
- [ ] State parameter preserved
|
|
- [ ] Redirect back to client succeeds
|
|
|
|
**Token Exchange**:
|
|
- [ ] Token endpoint accepts POST requests
|
|
- [ ] Valid code exchanges for token
|
|
- [ ] Token response has all required fields
|
|
- [ ] Token response `me` value matches request
|
|
- [ ] Used code returns error on second exchange
|
|
- [ ] Invalid code returns error
|
|
|
|
**Error Handling**:
|
|
- [ ] OAuth 2.0 error format correct
|
|
- [ ] Error descriptions helpful
|
|
- [ ] Error redirection preserves state
|
|
|
|
**Security**:
|
|
- [ ] HTTPS enforced (or localhost allowed in dev)
|
|
- [ ] Security headers present
|
|
- [ ] Open redirect prevented
|
|
- [ ] CSRF protection via state parameter
|
|
|
|
### Documentation
|
|
|
|
**File**: `/docs/testing/real-client-testing.md`
|
|
|
|
**Content**:
|
|
```markdown
|
|
# Real Client Testing Results
|
|
|
|
## Test Environment
|
|
- **Date**: YYYY-MM-DD
|
|
- **Gondulf Version**: 1.0.0
|
|
- **Test Domain**: test.example.com
|
|
- **DNS TXT Record**: _gondulf.test.example.com TXT "verified"
|
|
- **rel=me Link**: <link rel="me" href="mailto:admin@test.example.com">
|
|
|
|
## Client 1: IndieLogin.com
|
|
|
|
**Client**: https://indielogin.com
|
|
**Version**: Latest (as of test date)
|
|
**Implementation**: PHP (reference implementation)
|
|
|
|
### Test Results
|
|
- ✅ Discovery via metadata endpoint
|
|
- ✅ Authorization flow complete
|
|
- ✅ Token exchange successful
|
|
- ✅ Signed in as test.example.com
|
|
- ✅ Error handling correct (tested user denial)
|
|
|
|
### Issues Found
|
|
- None
|
|
|
|
### Screenshots
|
|
- [Authorization screen](screenshots/indielogin-authorize.png)
|
|
- [Success screen](screenshots/indielogin-success.png)
|
|
|
|
## Client 2: Quill
|
|
|
|
**Client**: https://quill.p3k.io
|
|
**Version**: Latest
|
|
**Implementation**: PHP
|
|
|
|
### Test Results
|
|
- ✅ Discovery successful
|
|
- ✅ Authorization flow complete
|
|
- ✅ Token issued
|
|
- ✅ Can create micropub posts (token works)
|
|
|
|
### Issues Found
|
|
- None
|
|
|
|
### Screenshots
|
|
- [Quill sign-in](screenshots/quill-signin.png)
|
|
- [Quill authorized](screenshots/quill-authorized.png)
|
|
|
|
## Compliance Summary
|
|
|
|
✅ Gondulf is fully compliant with W3C IndieAuth specification
|
|
✅ Successfully interoperates with 2 different clients
|
|
✅ No protocol deviations detected
|
|
✅ Ready for production use
|
|
|
|
## Recommendations
|
|
- Monitor logs for authentication errors
|
|
- Consider testing with additional clients (Indigenous, Monocle)
|
|
- Set up automated compliance testing (future)
|
|
```
|
|
|
|
### Troubleshooting Guide
|
|
|
|
**File**: `/docs/testing/troubleshooting-client-testing.md`
|
|
|
|
**Common Issues**:
|
|
|
|
| Issue | Cause | Solution |
|
|
|-------|-------|----------|
|
|
| Client can't discover endpoints | Metadata endpoint not accessible | Check BASE_URL, ensure metadata endpoint registered |
|
|
| "Invalid redirect_uri" error | Domain mismatch | Verify redirect_uri matches client_id domain |
|
|
| "Domain not verified" error | DNS TXT record missing | Add TXT record: `_gondulf.{domain}` = `verified` |
|
|
| "Email not found" error | rel=me link missing | Add `<link rel="me" href="mailto:...">` to homepage |
|
|
| Token exchange fails | Code expired or used | Check code TTL (10 minutes), ensure single-use |
|
|
| HTTPS errors | ngrok not running or BASE_URL wrong | Verify ngrok running, update BASE_URL in config |
|
|
|
|
### Acceptance Criteria
|
|
|
|
- [ ] Tested with at least 2 different IndieAuth clients
|
|
- [ ] All authorization flows complete successfully
|
|
- [ ] Token exchange successful with all clients
|
|
- [ ] Error scenarios tested and handled correctly
|
|
- [ ] Testing documentation created with results
|
|
- [ ] Screenshots/evidence of successful testing
|
|
- [ ] No W3C IndieAuth compliance issues found
|
|
- [ ] Troubleshooting guide created
|
|
|
|
---
|
|
|
|
## Component 7: Deployment Documentation
|
|
|
|
### Purpose
|
|
|
|
Provide comprehensive documentation for operators to install, configure, deploy, and troubleshoot Gondulf in production environments.
|
|
|
|
### Specification References
|
|
|
|
- **v1.0.0 Roadmap**: Phase 5 lines 229, 253, Release Checklist lines 443-451
|
|
|
|
### Design Overview
|
|
|
|
Create four deployment guides: Installation, Configuration, Deployment, and Troubleshooting. Each guide should be clear, complete, and tested by following the steps.
|
|
|
|
### 7.1: Installation Guide
|
|
|
|
**File**: `/docs/deployment/installation.md`
|
|
|
|
**Content**:
|
|
```markdown
|
|
# Installation Guide
|
|
|
|
This guide covers installing Gondulf on a fresh server.
|
|
|
|
## Prerequisites
|
|
|
|
- Linux server (Ubuntu 22.04 LTS recommended)
|
|
- Domain name with DNS access
|
|
- Email account with SMTP access (Gmail, Mailgun, etc.)
|
|
- Docker and Docker Compose installed
|
|
|
|
## System Requirements
|
|
|
|
**Minimum**:
|
|
- 1 CPU core
|
|
- 1 GB RAM
|
|
- 10 GB disk space
|
|
- Ubuntu 22.04 LTS or similar
|
|
|
|
**Recommended**:
|
|
- 2 CPU cores
|
|
- 2 GB RAM
|
|
- 20 GB disk space
|
|
- Ubuntu 22.04 LTS
|
|
|
|
## Step 1: Install Docker
|
|
|
|
```bash
|
|
# Update package index
|
|
sudo apt update
|
|
|
|
# Install Docker
|
|
sudo apt install -y docker.io docker-compose
|
|
|
|
# Add user to docker group
|
|
sudo usermod -aG docker $USER
|
|
|
|
# Reload group membership (or logout/login)
|
|
newgrp docker
|
|
|
|
# Verify installation
|
|
docker --version
|
|
docker-compose --version
|
|
```
|
|
|
|
## Step 2: Clone Repository
|
|
|
|
```bash
|
|
# Create directory
|
|
mkdir -p /opt/gondulf
|
|
cd /opt/gondulf
|
|
|
|
# Clone repository
|
|
git clone https://github.com/yourusername/gondulf.git .
|
|
|
|
# Or download release
|
|
wget https://github.com/yourusername/gondulf/releases/download/v1.0.0/gondulf-1.0.0.tar.gz
|
|
tar xzf gondulf-1.0.0.tar.gz
|
|
```
|
|
|
|
## Step 3: Generate Secret Key
|
|
|
|
```bash
|
|
# Generate SECRET_KEY
|
|
python3 -c "import secrets; print(secrets.token_urlsafe(32))"
|
|
|
|
# Save output for next step
|
|
```
|
|
|
|
## Step 4: Configure Environment
|
|
|
|
```bash
|
|
# Copy example config
|
|
cp .env.example .env
|
|
|
|
# Edit configuration
|
|
nano .env
|
|
```
|
|
|
|
Set required variables:
|
|
```env
|
|
GONDULF_SECRET_KEY=<generated-in-step-3>
|
|
GONDULF_BASE_URL=https://auth.yourdomain.com
|
|
GONDULF_SMTP_HOST=smtp.gmail.com
|
|
GONDULF_SMTP_PORT=587
|
|
GONDULF_SMTP_USERNAME=noreply@yourdomain.com
|
|
GONDULF_SMTP_PASSWORD=<your-app-password>
|
|
GONDULF_SMTP_FROM_EMAIL=noreply@yourdomain.com
|
|
```
|
|
|
|
See [Configuration Guide](configuration.md) for all options.
|
|
|
|
## Step 5: Configure DNS
|
|
|
|
Add DNS TXT record for domain verification:
|
|
|
|
```
|
|
Type: TXT
|
|
Name: _gondulf.yourdomain.com
|
|
Value: verified
|
|
TTL: 3600
|
|
```
|
|
|
|
Verify DNS propagation:
|
|
```bash
|
|
dig TXT _gondulf.yourdomain.com
|
|
```
|
|
|
|
## Step 6: Build Docker Image
|
|
|
|
```bash
|
|
# Build image
|
|
docker-compose build
|
|
|
|
# Verify image created
|
|
docker images | grep gondulf
|
|
```
|
|
|
|
## Step 7: Start Service
|
|
|
|
```bash
|
|
# Start in background
|
|
docker-compose up -d
|
|
|
|
# Check logs
|
|
docker-compose logs -f gondulf
|
|
|
|
# Verify health
|
|
curl http://localhost:8000/health
|
|
```
|
|
|
|
Expected response:
|
|
```json
|
|
{"status": "healthy"}
|
|
```
|
|
|
|
## Step 8: Configure Reverse Proxy
|
|
|
|
### Using Nginx
|
|
|
|
```nginx
|
|
server {
|
|
listen 80;
|
|
server_name auth.yourdomain.com;
|
|
|
|
# Redirect HTTP to HTTPS
|
|
return 301 https://$server_name$request_uri;
|
|
}
|
|
|
|
server {
|
|
listen 443 ssl http2;
|
|
server_name auth.yourdomain.com;
|
|
|
|
# SSL configuration (use Let's Encrypt)
|
|
ssl_certificate /etc/letsencrypt/live/auth.yourdomain.com/fullchain.pem;
|
|
ssl_certificate_key /etc/letsencrypt/live/auth.yourdomain.com/privkey.pem;
|
|
|
|
# Proxy to Gondulf
|
|
location / {
|
|
proxy_pass http://localhost:8000;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Using Caddy
|
|
|
|
```caddyfile
|
|
auth.yourdomain.com {
|
|
reverse_proxy localhost:8000
|
|
}
|
|
```
|
|
|
|
Caddy automatically handles HTTPS with Let's Encrypt.
|
|
|
|
## Step 9: Verify Installation
|
|
|
|
```bash
|
|
# Check metadata endpoint
|
|
curl https://auth.yourdomain.com/.well-known/oauth-authorization-server
|
|
|
|
# Should return JSON with endpoints
|
|
```
|
|
|
|
## Step 10: Set Up Backups
|
|
|
|
```bash
|
|
# Test backup script
|
|
./scripts/backup_database.sh
|
|
|
|
# Add to crontab (daily at 2 AM)
|
|
crontab -e
|
|
```
|
|
|
|
Add line:
|
|
```
|
|
0 2 * * * /opt/gondulf/scripts/backup_database.sh >> /opt/gondulf/logs/backup.log 2>&1
|
|
```
|
|
|
|
## Next Steps
|
|
|
|
- Read [Configuration Guide](configuration.md) for advanced options
|
|
- Read [Deployment Guide](deployment.md) for production best practices
|
|
- Read [Troubleshooting Guide](troubleshooting.md) for common issues
|
|
- Test authentication with IndieLogin.com
|
|
|
|
## Upgrading
|
|
|
|
```bash
|
|
# Pull latest code
|
|
git pull
|
|
|
|
# Rebuild image
|
|
docker-compose build
|
|
|
|
# Restart service
|
|
docker-compose up -d
|
|
|
|
# Check logs
|
|
docker-compose logs -f gondulf
|
|
```
|
|
```
|
|
|
|
### 7.2: Configuration Guide
|
|
|
|
**File**: `/docs/deployment/configuration.md`
|
|
|
|
**Content**: (Expand environment-variables.md with examples and best practices)
|
|
|
|
**Sections**:
|
|
1. Required Configuration
|
|
2. Optional Configuration
|
|
3. Security Configuration
|
|
4. SMTP Configuration (detailed examples for Gmail, Mailgun, SendGrid)
|
|
5. DNS Configuration
|
|
6. Performance Tuning
|
|
7. Monitoring Configuration
|
|
8. Example Configurations (development, staging, production)
|
|
|
|
### 7.3: Deployment Guide
|
|
|
|
**File**: `/docs/deployment/deployment.md`
|
|
|
|
**Content**:
|
|
```markdown
|
|
# Deployment Guide
|
|
|
|
This guide covers deploying Gondulf to production.
|
|
|
|
## Production Checklist
|
|
|
|
Before deploying to production, verify:
|
|
|
|
- [ ] SECRET_KEY is unique and strong (32+ characters)
|
|
- [ ] BASE_URL uses HTTPS
|
|
- [ ] SMTP credentials are valid and tested
|
|
- [ ] DNS TXT record is set and verified
|
|
- [ ] Reverse proxy configured with SSL/TLS
|
|
- [ ] Firewall configured (only 80/443 exposed)
|
|
- [ ] Backups scheduled (daily recommended)
|
|
- [ ] Logs monitored
|
|
- [ ] Health check passing
|
|
|
|
## Deployment Strategies
|
|
|
|
### Single Server Deployment
|
|
|
|
Simplest deployment for small scale (<1000 users):
|
|
|
|
```
|
|
┌─────────────────────┐
|
|
│ Reverse Proxy │ (Nginx/Caddy with SSL)
|
|
│ (Port 80/443) │
|
|
└──────────┬──────────┘
|
|
│
|
|
┌──────────▼──────────┐
|
|
│ Gondulf Docker │ (Port 8000)
|
|
│ Container │
|
|
└──────────┬──────────┘
|
|
│
|
|
┌──────────▼──────────┐
|
|
│ SQLite Database │ (Volume mount)
|
|
│ /app/data/ │
|
|
└─────────────────────┘
|
|
```
|
|
|
|
**Pros**:
|
|
- Simple setup
|
|
- Low cost
|
|
- Easy to maintain
|
|
|
|
**Cons**:
|
|
- Single point of failure
|
|
- Limited scalability
|
|
|
|
### High Availability Deployment (Future)
|
|
|
|
For larger scale or high availability requirements:
|
|
|
|
```
|
|
┌─────────────────────┐
|
|
│ Load Balancer │
|
|
└──────────┬──────────┘
|
|
│
|
|
┌──────┴──────┐
|
|
│ │
|
|
┌───▼───┐ ┌───▼───┐
|
|
│Gondulf│ │Gondulf│
|
|
│ 1 │ │ 2 │
|
|
└───┬───┘ └───┬───┘
|
|
│ │
|
|
└──────┬──────┘
|
|
│
|
|
┌──────────▼──────────┐
|
|
│ PostgreSQL │
|
|
│ (Shared DB) │
|
|
└─────────────────────┘
|
|
```
|
|
|
|
**Note**: Requires PostgreSQL support (planned for v1.2.0).
|
|
|
|
## Security Best Practices
|
|
|
|
### 1. HTTPS Only
|
|
- Never expose HTTP in production
|
|
- Use valid SSL/TLS certificates (Let's Encrypt recommended)
|
|
- Enable HSTS headers (Gondulf does this automatically)
|
|
|
|
### 2. Firewall Configuration
|
|
```bash
|
|
# Allow SSH (careful!)
|
|
sudo ufw allow 22/tcp
|
|
|
|
# Allow HTTP/HTTPS
|
|
sudo ufw allow 80/tcp
|
|
sudo ufw allow 443/tcp
|
|
|
|
# Enable firewall
|
|
sudo ufw enable
|
|
```
|
|
|
|
### 3. Secret Management
|
|
- Never commit .env to version control
|
|
- Use Docker secrets in production (not environment variables)
|
|
- Rotate SECRET_KEY periodically (requires token regeneration)
|
|
|
|
### 4. Database Security
|
|
```bash
|
|
# Set restrictive permissions
|
|
chmod 600 /opt/gondulf/data/gondulf.db
|
|
chown gondulf:gondulf /opt/gondulf/data/gondulf.db
|
|
|
|
# Enable encrypted filesystem (LUKS recommended)
|
|
```
|
|
|
|
### 5. Monitoring
|
|
- Monitor logs for authentication failures
|
|
- Monitor disk space (SQLite database growth)
|
|
- Monitor backup success
|
|
- Set up alerts for service downtime
|
|
|
|
## Backup Strategy
|
|
|
|
### Daily Automated Backups
|
|
```bash
|
|
# Backup script runs daily at 2 AM
|
|
0 2 * * * /opt/gondulf/scripts/backup_database.sh
|
|
```
|
|
|
|
### Backup Retention
|
|
- Keep 7 daily backups
|
|
- Keep 4 weekly backups
|
|
- Keep 12 monthly backups
|
|
|
|
### Backup Testing
|
|
- Test restore procedure quarterly
|
|
- Verify backup integrity monthly
|
|
|
|
### Off-site Backups
|
|
```bash
|
|
# Sync to S3 (example)
|
|
aws s3 sync /opt/gondulf/backups/ s3://my-bucket/gondulf-backups/
|
|
```
|
|
|
|
## Monitoring
|
|
|
|
### Health Check
|
|
```bash
|
|
# Check every minute
|
|
* * * * * curl -f http://localhost:8000/health || systemctl restart docker-compose
|
|
```
|
|
|
|
### Log Monitoring
|
|
```bash
|
|
# Monitor for errors
|
|
docker-compose logs -f gondulf | grep ERROR
|
|
|
|
# Monitor authentication activity
|
|
docker-compose logs -f gondulf | grep "Authorization granted"
|
|
```
|
|
|
|
### Disk Usage
|
|
```bash
|
|
# Check database size
|
|
du -h /opt/gondulf/data/gondulf.db
|
|
|
|
# Check backup size
|
|
du -sh /opt/gondulf/backups/
|
|
```
|
|
|
|
## Scaling Considerations
|
|
|
|
### Current Limits (v1.0.0)
|
|
- **Users**: 10-100 domains (SQLite limit ~100 concurrent users)
|
|
- **Requests**: ~100 requests/second (single server)
|
|
- **Database**: ~100 MB typical (1000 tokens)
|
|
|
|
### When to Upgrade
|
|
- Database file >1 GB
|
|
- Response time >500ms
|
|
- Need high availability
|
|
- >1000 domains
|
|
|
|
### Upgrade Path (v1.2.0+)
|
|
- Migrate to PostgreSQL
|
|
- Add load balancer
|
|
- Add Redis for session storage
|
|
- Add metrics (Prometheus + Grafana)
|
|
|
|
## Troubleshooting
|
|
|
|
See [Troubleshooting Guide](troubleshooting.md) for common issues.
|
|
|
|
## Maintenance
|
|
|
|
### Regular Tasks
|
|
- **Daily**: Check logs for errors
|
|
- **Weekly**: Verify backup success
|
|
- **Monthly**: Review disk usage
|
|
- **Quarterly**: Test restore procedure
|
|
- **Annually**: Review security (update dependencies, rotate secrets)
|
|
|
|
### Upgrades
|
|
```bash
|
|
# Backup before upgrade
|
|
./scripts/backup_database.sh
|
|
|
|
# Pull latest version
|
|
git pull
|
|
|
|
# Rebuild image
|
|
docker-compose build
|
|
|
|
# Restart
|
|
docker-compose up -d
|
|
|
|
# Verify
|
|
curl https://auth.yourdomain.com/health
|
|
```
|
|
```
|
|
|
|
### 7.4: Troubleshooting Guide
|
|
|
|
**File**: `/docs/deployment/troubleshooting.md`
|
|
|
|
**Content**:
|
|
```markdown
|
|
# Troubleshooting Guide
|
|
|
|
Common issues and solutions for Gondulf.
|
|
|
|
## Service Won't Start
|
|
|
|
### Error: "Required environment variable GONDULF_SECRET_KEY is not set"
|
|
|
|
**Cause**: Missing or invalid .env file
|
|
|
|
**Solution**:
|
|
```bash
|
|
# Verify .env file exists
|
|
ls -la .env
|
|
|
|
# Check SECRET_KEY is set
|
|
grep SECRET_KEY .env
|
|
|
|
# Generate new SECRET_KEY if needed
|
|
python3 -c "import secrets; print(secrets.token_urlsafe(32))"
|
|
```
|
|
|
|
### Error: "Database connection failed"
|
|
|
|
**Cause**: Database file not accessible or corrupted
|
|
|
|
**Solution**:
|
|
```bash
|
|
# Check database file exists
|
|
ls -la data/gondulf.db
|
|
|
|
# Check permissions
|
|
chmod 600 data/gondulf.db
|
|
|
|
# If corrupted, restore from backup
|
|
./scripts/restore_database.sh backups/gondulf_YYYYMMDD_HHMMSS.db
|
|
```
|
|
|
|
### Error: "Port 8000 already in use"
|
|
|
|
**Cause**: Another service using port 8000
|
|
|
|
**Solution**:
|
|
```bash
|
|
# Find process using port
|
|
sudo lsof -i :8000
|
|
|
|
# Kill process or change Gondulf port in docker-compose.yml
|
|
```
|
|
|
|
## Authentication Issues
|
|
|
|
### Error: "Domain not verified"
|
|
|
|
**Cause**: DNS TXT record not found or incorrect
|
|
|
|
**Solution**:
|
|
```bash
|
|
# Verify TXT record exists
|
|
dig TXT _gondulf.yourdomain.com
|
|
|
|
# Expected output:
|
|
# _gondulf.yourdomain.com. 3600 IN TXT "verified"
|
|
|
|
# If missing, add to DNS and wait for propagation (up to 24 hours)
|
|
```
|
|
|
|
### Error: "Email not found"
|
|
|
|
**Cause**: rel=me link missing from homepage
|
|
|
|
**Solution**:
|
|
```bash
|
|
# Check homepage for rel=me link
|
|
curl https://yourdomain.com | grep 'rel="me"'
|
|
|
|
# Expected: <link rel="me" href="mailto:admin@yourdomain.com">
|
|
|
|
# Add to <head> of homepage if missing
|
|
```
|
|
|
|
### Error: "Failed to send verification email"
|
|
|
|
**Cause**: SMTP configuration incorrect or credentials invalid
|
|
|
|
**Solution**:
|
|
```bash
|
|
# Check SMTP settings in .env
|
|
grep SMTP .env
|
|
|
|
# Test SMTP connection
|
|
python3 -c "
|
|
import smtplib
|
|
smtp = smtplib.SMTP('smtp.gmail.com', 587)
|
|
smtp.starttls()
|
|
smtp.login('user@gmail.com', 'password')
|
|
print('SMTP connection successful')
|
|
smtp.quit()
|
|
"
|
|
|
|
# For Gmail, ensure "App Passwords" used (not account password)
|
|
# https://support.google.com/accounts/answer/185833
|
|
```
|
|
|
|
## Client Compatibility Issues
|
|
|
|
### Error: "Client can't discover authorization endpoint"
|
|
|
|
**Cause**: Metadata endpoint not accessible
|
|
|
|
**Solution**:
|
|
```bash
|
|
# Test metadata endpoint
|
|
curl https://auth.yourdomain.com/.well-known/oauth-authorization-server
|
|
|
|
# Should return JSON with authorization_endpoint, token_endpoint
|
|
|
|
# If 404, verify BASE_URL is correct and metadata router registered
|
|
```
|
|
|
|
### Error: "invalid_redirect_uri"
|
|
|
|
**Cause**: redirect_uri doesn't match client_id domain
|
|
|
|
**Solution**:
|
|
- Verify client_id and redirect_uri have same domain
|
|
- Or client_id is parent domain of redirect_uri
|
|
- Example: client_id=https://app.example.com, redirect_uri=https://app.example.com/callback (OK)
|
|
- Example: client_id=https://app.example.com, redirect_uri=https://evil.com (NOT OK)
|
|
|
|
### Error: "Token exchange failed: invalid_grant"
|
|
|
|
**Cause**: Authorization code expired or already used
|
|
|
|
**Solution**:
|
|
- Authorization codes expire after 10 minutes
|
|
- Codes are single-use only
|
|
- Client must exchange code immediately after receiving it
|
|
- Check client isn't caching and reusing old codes
|
|
|
|
## Performance Issues
|
|
|
|
### Slow Response Times
|
|
|
|
**Cause**: Database growing too large or disk I/O slow
|
|
|
|
**Solution**:
|
|
```bash
|
|
# Check database size
|
|
du -h data/gondulf.db
|
|
|
|
# Cleanup expired tokens
|
|
docker-compose exec gondulf python -c "
|
|
from gondulf.dependencies import get_token_service
|
|
token_service = get_token_service()
|
|
deleted = token_service.cleanup_expired_tokens()
|
|
print(f'Deleted {deleted} expired tokens')
|
|
"
|
|
|
|
# Consider migrating to PostgreSQL if database >1 GB
|
|
```
|
|
|
|
### High Memory Usage
|
|
|
|
**Cause**: In-memory storage (authorization codes, verification codes) growing
|
|
|
|
**Solution**:
|
|
```bash
|
|
# Restart service to clear in-memory storage
|
|
docker-compose restart gondulf
|
|
|
|
# Check for code leaks (codes not expiring properly)
|
|
# Review logs for "cleanup" messages
|
|
docker-compose logs gondulf | grep cleanup
|
|
```
|
|
|
|
## Security Issues
|
|
|
|
### Warning: "HTTP request in production"
|
|
|
|
**Cause**: HTTPS enforcement not working
|
|
|
|
**Solution**:
|
|
```bash
|
|
# Verify DEBUG mode is disabled
|
|
grep DEBUG .env
|
|
# Should be: GONDULF_DEBUG=false
|
|
|
|
# Verify reverse proxy is forwarding X-Forwarded-Proto header
|
|
# Nginx: proxy_set_header X-Forwarded-Proto $scheme;
|
|
|
|
# Restart service
|
|
docker-compose restart gondulf
|
|
```
|
|
|
|
### Error: "HSTS header not present"
|
|
|
|
**Cause**: Not using HTTPS or DEBUG mode enabled
|
|
|
|
**Solution**:
|
|
- HSTS only added when HTTPS and DEBUG=false
|
|
- Verify reverse proxy is terminating SSL and forwarding to Gondulf
|
|
- Check `curl -I https://auth.yourdomain.com | grep Strict-Transport-Security`
|
|
|
|
## Backup and Restore Issues
|
|
|
|
### Error: "Backup failed: database locked"
|
|
|
|
**Cause**: Database in use during backup
|
|
|
|
**Solution**:
|
|
```bash
|
|
# Stop service before backup
|
|
docker-compose stop gondulf
|
|
|
|
# Run backup
|
|
./scripts/backup_database.sh
|
|
|
|
# Restart service
|
|
docker-compose start gondulf
|
|
|
|
# Or use SQLite online backup (script already does this)
|
|
```
|
|
|
|
### Error: "Restore failed: permission denied"
|
|
|
|
**Cause**: Incorrect file permissions
|
|
|
|
**Solution**:
|
|
```bash
|
|
# Fix permissions
|
|
sudo chown gondulf:gondulf data/gondulf.db
|
|
chmod 600 data/gondulf.db
|
|
|
|
# Retry restore
|
|
./scripts/restore_database.sh backups/gondulf_YYYYMMDD_HHMMSS.db
|
|
```
|
|
|
|
## Logs and Debugging
|
|
|
|
### Enable Debug Logging
|
|
|
|
```bash
|
|
# Set log level to DEBUG in .env
|
|
GONDULF_LOG_LEVEL=DEBUG
|
|
|
|
# Restart service
|
|
docker-compose restart gondulf
|
|
|
|
# View debug logs
|
|
docker-compose logs -f gondulf
|
|
```
|
|
|
|
### Common Log Messages
|
|
|
|
**Normal**:
|
|
```
|
|
[INFO] Authorization granted for example.com to client.example.com
|
|
[INFO] Token generated: abc12345...
|
|
[INFO] Email verification sent for domain example.com
|
|
```
|
|
|
|
**Errors**:
|
|
```
|
|
[ERROR] Failed to send email: SMTP authentication failed
|
|
[ERROR] Database connection lost
|
|
[ERROR] DNS verification failed for domain example.com
|
|
```
|
|
|
|
**Security Events**:
|
|
```
|
|
[WARNING] Code replay attack detected: abc12345...
|
|
[WARNING] Invalid redirect_uri: https://evil.com
|
|
[ERROR] Failed authentication attempt for domain example.com
|
|
```
|
|
|
|
## Getting Help
|
|
|
|
### Before Asking for Help
|
|
|
|
1. Check logs: `docker-compose logs gondulf`
|
|
2. Verify configuration: `cat .env`
|
|
3. Check health endpoint: `curl http://localhost:8000/health`
|
|
4. Review this troubleshooting guide
|
|
|
|
### Where to Get Help
|
|
|
|
- GitHub Issues: https://github.com/yourusername/gondulf/issues
|
|
- Documentation: https://gondulf.readthedocs.io/
|
|
- Email: support@gondulf.example.com (if applicable)
|
|
|
|
### Information to Include
|
|
|
|
When reporting issues, include:
|
|
1. Gondulf version (`docker images | grep gondulf`)
|
|
2. Operating system and version
|
|
3. Relevant logs (redact sensitive information)
|
|
4. Steps to reproduce
|
|
5. Expected vs actual behavior
|
|
```
|
|
|
|
### API Documentation
|
|
|
|
**File**: `/docs/api/openapi.json`
|
|
|
|
**Generation**: Use FastAPI's built-in OpenAPI generation:
|
|
```bash
|
|
# Generate OpenAPI spec
|
|
curl http://localhost:8000/openapi.json > docs/api/openapi.json
|
|
|
|
# Or use FastAPI to serve docs
|
|
# Available at: http://localhost:8000/docs (Swagger UI)
|
|
# Available at: http://localhost:8000/redoc (ReDoc)
|
|
```
|
|
|
|
**Manual Enhancement**: Add descriptions, examples, and schemas to routers:
|
|
```python
|
|
@router.post(
|
|
"/token",
|
|
response_model=TokenResponse,
|
|
responses={
|
|
400: {"description": "Invalid request or expired code"},
|
|
401: {"description": "Client authentication failed"}
|
|
},
|
|
summary="Exchange authorization code for access token",
|
|
description="""
|
|
OAuth 2.0 token endpoint for authorization code exchange.
|
|
|
|
The client exchanges a valid authorization code for an access token.
|
|
Codes are single-use and expire after 10 minutes.
|
|
"""
|
|
)
|
|
async def token_endpoint(...):
|
|
pass
|
|
```
|
|
|
|
### Acceptance Criteria
|
|
|
|
- [ ] Installation guide created and tested
|
|
- [ ] Configuration guide created with examples
|
|
- [ ] Deployment guide created with best practices
|
|
- [ ] Troubleshooting guide created with common issues
|
|
- [ ] API documentation generated (OpenAPI)
|
|
- [ ] All guides reviewed for clarity and accuracy
|
|
- [ ] External reviewer can install and deploy following guides
|
|
- [ ] Screenshots/examples included where helpful
|
|
|
|
---
|
|
|
|
## Implementation Plan
|
|
|
|
### Phase 4a: Complete Phase 3 (Estimated: 2-3 days)
|
|
|
|
**Tasks**:
|
|
1. Implement metadata endpoint (0.5 day)
|
|
2. Implement h-app parser service (1 day)
|
|
3. Integrate h-app with authorization endpoint (0.5 day)
|
|
4. Test metadata and h-app functionality (0.5 day)
|
|
5. Update documentation (0.5 day)
|
|
|
|
**Dependencies**: None (can start immediately)
|
|
|
|
**Deliverable**: Phase 3 100% complete
|
|
|
|
### Phase 4b: Security Hardening (Estimated: 3-5 days)
|
|
|
|
**Tasks**:
|
|
1. Implement security headers middleware (0.5 day)
|
|
2. Implement HTTPS enforcement middleware (0.5 day)
|
|
3. Conduct input sanitization audit (1 day)
|
|
4. Conduct PII logging audit and fixes (1 day)
|
|
5. Write security tests (1-2 days)
|
|
6. Document security measures (0.5 day)
|
|
|
|
**Dependencies**: None (can start immediately)
|
|
|
|
**Deliverable**: Phase 4 100% complete
|
|
|
|
### Phase 5a: Deployment Configuration (Estimated: 2-3 days)
|
|
|
|
**Tasks**:
|
|
1. Create Dockerfile with multi-stage build (1 day)
|
|
2. Create docker-compose.yml (0.5 day)
|
|
3. Create backup/restore scripts (0.5 day)
|
|
4. Write environment variable documentation (0.5 day)
|
|
5. Test Docker deployment (0.5 day)
|
|
|
|
**Dependencies**: None (can start immediately)
|
|
|
|
**Deliverable**: Deployment configuration complete
|
|
|
|
### Phase 5b: Comprehensive Testing (Estimated: 5-7 days)
|
|
|
|
**Tasks**:
|
|
1. Write integration tests (2-3 days)
|
|
2. Write end-to-end tests (2 days)
|
|
3. Write security tests (1 day)
|
|
4. Run all tests and fix failures (1 day)
|
|
5. Achieve coverage goals (1 day)
|
|
|
|
**Dependencies**: Phases 4a, 4b complete (for accurate testing)
|
|
|
|
**Deliverable**: Test suite complete with 80%+ coverage
|
|
|
|
### Phase 5c: Real Client Testing (Estimated: 2-3 days)
|
|
|
|
**Tasks**:
|
|
1. Set up test environment with ngrok (0.5 day)
|
|
2. Test with IndieLogin (1 day)
|
|
3. Test with second client (Quill or other) (1 day)
|
|
4. Document testing results (0.5 day)
|
|
|
|
**Dependencies**: Phases 4a, 4b, 5a complete (for working deployment)
|
|
|
|
**Deliverable**: Successful testing with 2+ real clients
|
|
|
|
### Phase 5d: Documentation (Estimated: 2-3 days)
|
|
|
|
**Tasks**:
|
|
1. Write installation guide (0.5 day)
|
|
2. Write configuration guide (0.5 day)
|
|
3. Write deployment guide (0.5 day)
|
|
4. Write troubleshooting guide (0.5 day)
|
|
5. Generate API documentation (0.25 day)
|
|
6. Review and test all documentation (0.25 day)
|
|
|
|
**Dependencies**: All previous phases complete (for accurate documentation)
|
|
|
|
**Deliverable**: Complete deployment documentation
|
|
|
|
### Critical Path
|
|
|
|
**Parallel Work Possible**:
|
|
- Phase 4a, 4b, 5a can be done in parallel (independent tasks)
|
|
- Phase 5b depends on 4a, 4b (need complete functionality to test)
|
|
- Phase 5c depends on 4a, 4b, 5a (need working deployment)
|
|
- Phase 5d depends on all phases (documents everything)
|
|
|
|
**Fastest Path** (with parallelization):
|
|
- Week 1: Phases 4a, 4b, 5a in parallel (3-5 days)
|
|
- Week 2: Phase 5b (5-7 days)
|
|
- Week 3: Phase 5c and 5d in parallel (2-3 days)
|
|
|
|
**Total Estimated Time**: 10-15 days (2-3 weeks calendar time)
|
|
|
|
---
|
|
|
|
## Summary of Deliverables
|
|
|
|
| Component | Files Created | Estimated Effort |
|
|
|-----------|---------------|------------------|
|
|
| 1. Metadata Endpoint | `/src/gondulf/routers/metadata.py` | XS (<1 day) |
|
|
| 2. h-app Parser | `/src/gondulf/services/h_app_parser.py` | S (1-2 days) |
|
|
| 3. Security Hardening | `/src/gondulf/middleware/*.py`, PII fixes | S (3-5 days) |
|
|
| 4. Deployment Config | `/Dockerfile`, `/docker-compose.yml`, `/scripts/*.sh` | S (2-3 days) |
|
|
| 5. Integration/E2E Tests | `/tests/integration/*.py`, `/tests/e2e/*.py` | M (5-7 days) |
|
|
| 6. Real Client Testing | `/docs/testing/*.md` | M (2-3 days) |
|
|
| 7. Deployment Docs | `/docs/deployment/*.md` | M (2-3 days) |
|
|
|
|
**Total Estimated Effort**: 15-24 days
|
|
**Realistic with Parallelization**: 10-15 days
|
|
**Conservative Estimate**: 15-18 days
|
|
|
|
---
|
|
|
|
## Risk Assessment
|
|
|
|
### Technical Risks
|
|
|
|
| Risk | Likelihood | Impact | Mitigation |
|
|
|------|------------|--------|------------|
|
|
| Real client testing reveals protocol issues | Medium | High | Early testing with IndieLogin (reference implementation) |
|
|
| h-app parsing fails on real sites | Medium | Medium | Robust fallback to domain name |
|
|
| Docker deployment issues | Low | High | Test early, document thoroughly |
|
|
| Security test failures | Medium | High | Comprehensive security review before tests |
|
|
| Integration test coverage gaps | Medium | Medium | Systematic coverage of all endpoints |
|
|
|
|
### Schedule Risks
|
|
|
|
| Risk | Likelihood | Impact | Mitigation |
|
|
|------|------------|--------|------------|
|
|
| Testing takes longer than estimated | High | Medium | Allocate buffer time, prioritize P0 tests |
|
|
| Real client testing reveals bugs | Medium | High | Early testing, iterative fixes |
|
|
| Documentation takes longer than estimated | Medium | Low | Start documentation in parallel with development |
|
|
|
|
### Quality Risks
|
|
|
|
| Risk | Likelihood | Impact | Mitigation |
|
|
|------|------------|--------|------------|
|
|
| Test coverage doesn't reach 80% | Low | High | Systematic testing, coverage monitoring |
|
|
| Security vulnerabilities discovered | Medium | Critical | Security-focused testing, external review recommended |
|
|
| Documentation incomplete or incorrect | Low | Medium | External reviewer tests documentation |
|
|
|
|
---
|
|
|
|
## Success Criteria
|
|
|
|
This design is successful when:
|
|
|
|
- [ ] All 7 critical components implemented and tested
|
|
- [ ] v1.0.0 Release Checklist 100% complete
|
|
- [ ] Test coverage ≥80% overall, ≥95% critical paths
|
|
- [ ] Security tests pass (no vulnerabilities found)
|
|
- [ ] Successfully tested with ≥2 real IndieAuth clients
|
|
- [ ] Docker deployment successful
|
|
- [ ] Documentation complete and verified by external reviewer
|
|
- [ ] No P0 gaps remaining from gap analysis
|
|
|
|
---
|
|
|
|
## Appendix: Design Decisions
|
|
|
|
### Why mf2py for h-app Parsing?
|
|
|
|
**Decision**: Use mf2py library instead of manual BeautifulSoup parsing.
|
|
|
|
**Rationale**:
|
|
- Standard microformats parser (used by IndieWeb community)
|
|
- Handles edge cases (nested microformats, multiple h-apps)
|
|
- Actively maintained
|
|
- Small dependency (no security concerns)
|
|
|
|
**Alternative Considered**: Manual BeautifulSoup parsing
|
|
- **Rejected**: Would need to reimplement microformat parsing logic
|
|
|
|
### Why Multi-Stage Dockerfile?
|
|
|
|
**Decision**: Use multi-stage build with builder and runtime stages.
|
|
|
|
**Rationale**:
|
|
- Smaller final image (no build tools in runtime)
|
|
- Faster deployments (smaller image size)
|
|
- Security (fewer packages in production image)
|
|
- Standard Docker best practice
|
|
|
|
**Alternative Considered**: Single-stage build
|
|
- **Rejected**: Larger image, slower deployments
|
|
|
|
### Why SQLite .backup Instead of File Copy?
|
|
|
|
**Decision**: Use SQLite's .backup command for backups.
|
|
|
|
**Rationale**:
|
|
- Handles locked database (consistent snapshot)
|
|
- No downtime required
|
|
- Recommended by SQLite documentation
|
|
- Reliable and tested
|
|
|
|
**Alternative Considered**: File copy with service stop
|
|
- **Rejected**: Requires downtime
|
|
|
|
### Why Integration Tests with TestClient?
|
|
|
|
**Decision**: Use FastAPI TestClient for integration tests instead of full HTTP server.
|
|
|
|
**Rationale**:
|
|
- Faster execution (no network overhead)
|
|
- Easier to run in CI/CD
|
|
- Full control over environment
|
|
- Same code paths as production (ASGI)
|
|
|
|
**Alternative Considered**: Real HTTP server with requests library
|
|
- **Rejected**: Slower, more complex setup
|
|
|
|
---
|
|
|
|
## Next Steps
|
|
|
|
**For Developer**:
|
|
|
|
1. Review this design document thoroughly
|
|
2. Ask clarification questions if any design is ambiguous
|
|
3. Implement components in order suggested by Implementation Plan
|
|
4. Follow testing requirements for each component
|
|
5. Create implementation report after completion
|
|
6. Signal: "IMPLEMENTATION COMPLETE: Critical Components for v1.0.0"
|
|
|
|
**For Architect**:
|
|
|
|
1. Answer any clarification questions from Developer
|
|
2. Review implementation report when complete
|
|
3. Verify all acceptance criteria met
|
|
4. Approve or request changes
|
|
5. Update roadmap for v1.0.0 release
|