diff --git a/.env.example b/.env.example index 65da9ae..84763b5 100644 --- a/.env.example +++ b/.env.example @@ -31,6 +31,11 @@ REDIS_URL=redis://:changeme_redis@redis:6379/0 # JWT signing secret — generate with: python3 -c "import secrets; print(secrets.token_hex(64))" SECRET_KEY=CHANGEME-replace-with-64-char-random-hex +# ── JWT Key Pair (Phase 7.3 — ES256) ───────────────────────────────────────── +# Generated by running the Python snippet in README.md JWT Key Generation section +JWT_PRIVATE_KEY= +JWT_PUBLIC_KEY= + # ── Admin Bootstrap (Phase 2 — D-04) ───────────────────────────────────────── # First admin account created on startup if users table is empty. # Both vars must be set; if missing, a WARNING is logged but app starts normally. diff --git a/README.md b/README.md index 4a0b5e0..9da0283 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,8 @@ Copy `.env.example` to `.env`. Only the fields marked **Required** must be set b | Variable | Description | |----------|-------------| | `SECRET_KEY` | JWT signing secret — generate with `openssl rand -hex 32` | +| `JWT_PRIVATE_KEY` | Base64-encoded PEM private key for ES256 JWT signing (required) — see [JWT Key Generation](#jwt-key-generation) | +| `JWT_PUBLIC_KEY` | Base64-encoded PEM public key for ES256 JWT verification (required) — see [JWT Key Generation](#jwt-key-generation) | | `CLOUD_CREDS_KEY` | Master key for cloud credential encryption — generate with `openssl rand -hex 16` (pad to 32 chars) | | `ADMIN_EMAIL` | Bootstrap admin email | | `ADMIN_PASSWORD` | Bootstrap admin password (must pass strength check) | @@ -159,6 +161,29 @@ Copy `.env.example` to `.env`. Only the fields marked **Required** must be set b | `GOOGLE_CLIENT_ID/SECRET` | *(unset)* | Required only if using Google Drive backend | | `ONEDRIVE_CLIENT_ID/SECRET` | *(unset)* | Required only if using OneDrive backend | +### JWT Key Generation + +Phase 7.3 uses ES256 (ECDSA P-256) for JWT signing. Unlike HS256, ES256 is asymmetric — the private key signs tokens and the public key verifies them. A leaked public key cannot forge tokens. + +Generate the key pair with this Python one-liner (requires `cryptography`, already in `requirements.txt`): + +```bash +python3 -c " +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives import serialization +import base64 +k = ec.generate_private_key(ec.SECP256R1()) +priv = base64.b64encode(k.private_bytes(serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption())).decode() +pub = base64.b64encode(k.public_key().public_bytes(serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo)).decode() +print(f'JWT_PRIVATE_KEY={priv}') +print(f'JWT_PUBLIC_KEY={pub}') +" +``` + +Paste the two output lines into your `.env` file at the project root. + +> **Warning:** Rotating these keys invalidates every active session — the startup rotation hook will bulk-revoke all refresh tokens on the next boot. + --- ## Development diff --git a/backend/main.py b/backend/main.py index 24c65d0..8f7e7c6 100644 --- a/backend/main.py +++ b/backend/main.py @@ -244,7 +244,7 @@ async def lifespan(app: FastAPI): # ── Application factory ─────────────────────────────────────────────────────── -app = FastAPI(title="Document Scanner API", version="0.1.1", lifespan=lifespan) +app = FastAPI(title="Document Scanner API", version="0.1.2", lifespan=lifespan) # Rate limiter state (slowapi) app.state.limiter = auth_limiter diff --git a/docker-compose.yml b/docker-compose.yml index 3c625d7..3ba7f4f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -62,6 +62,8 @@ services: - MINIO_PUBLIC_ENDPOINT=${MINIO_PUBLIC_ENDPOINT:-localhost:9000} - REDIS_URL=${REDIS_URL} - SECRET_KEY=${SECRET_KEY} + - JWT_PRIVATE_KEY=${JWT_PRIVATE_KEY} + - JWT_PUBLIC_KEY=${JWT_PUBLIC_KEY} - ADMIN_EMAIL=${ADMIN_EMAIL} - ADMIN_PASSWORD=${ADMIN_PASSWORD} - CORS_ORIGINS=${CORS_ORIGINS:-http://localhost:5173} @@ -101,6 +103,8 @@ services: - REDIS_URL=${REDIS_URL} - CLOUD_CREDS_KEY=${CLOUD_CREDS_KEY} - SECRET_KEY=${SECRET_KEY} + - JWT_PRIVATE_KEY=${JWT_PRIVATE_KEY} + - JWT_PUBLIC_KEY=${JWT_PUBLIC_KEY} - PYTHONDONTWRITEBYTECODE=1 - PYTHONPATH=/app labels: diff --git a/frontend/package.json b/frontend/package.json index 91a6092..b18eb21 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "document-scanner-frontend", - "version": "0.1.1", + "version": "0.1.2", "type": "module", "scripts": { "dev": "vite",