9.5 KiB
9.5 KiB
Phase 7.3: Security — ES256 Algorithm Upgrade - Context
Gathered: 2026-06-05 Status: Ready for planning
## Phase BoundaryPhase 7.3 delivers three auth-security upgrades in one cohesive change:
- ES256 algorithm upgrade — Replace HS256 with ECDSA P-256 (ES256) across all four JWT signing/verification sites in
services/auth.py. Generate a P-256 key pair; store base64-encoded PEM keys asJWT_PRIVATE_KEYandJWT_PUBLIC_KEYenv vars. A leaked public key cannot forge tokens. - Startup token rotation — On first boot after the ES256 deploy, bulk-revoke all existing
RefreshTokenrows (setrevoked=True). Detect the migration via thesystem_settingstable; update the storedjwt_algorithmvalue after rotation so subsequent restarts do not re-revoke. - "Remember me" session duration — Default refresh token TTL changes from 30 days to 16 hours. A
remember_me: bool = Falseparam on the login endpoint (and a checkbox inLoginView.vue) opts the user into the existing 30-day TTL. Access token TTL stays at 15 minutes (CLAUDE.md non-negotiable).
No other changes are in scope for this phase.
## Implementation DecisionsKey Format and Storage
- D-01: Both
JWT_PRIVATE_KEYandJWT_PUBLIC_KEYare required env vars. Both store the PEM key base64-encoded as a single line — shell-safe, no newline-escape complications.config.pydecodes each value before passing to PyJWT. - D-02:
create_access_tokenandcreate_password_reset_tokensign with the private key usingalgorithm="ES256".decode_access_tokenanddecode_password_reset_tokenverify with the public key usingalgorithms=["ES256"]. - D-03:
settings.secret_keyis removed from all JWT code after ES256 is in place. It is no longer used for JWT signing. TheSECRET_KEYenv var stays in docker-compose for any future non-JWT HMAC needs (Phase 7.4 fingerprinting), butservices/auth.pymust not reference it.
Startup Token Rotation
- D-04: On FastAPI
lifespanstartup, compare thejwt_algorithmkey in thesystem_settingstable (or its absence) against the current algorithm"ES256". If the stored value differs (e.g., was"HS256"or is missing), bulk-revoke allRefreshTokenrows by executingUPDATE refresh_tokens SET revoked = true WHERE revoked = false, then upsertjwt_algorithm = "ES256"intosystem_settings. This is idempotent — safe to restart. - D-05: Use the existing
system_settingsupsert pattern frombackend/services/ai_config.py(seed_system_settings_from_env) as the reference for reading/writing system settings in the startup hook.
Key Generation Workflow
- D-06: Document key generation as a Python one-liner in
README.mdusing thecryptographylibrary (already a project dependency). The snippet prints both base64-encoded keys ready to paste into.env. No extra tooling required. - D-07:
docker-compose.ymlreferences${JWT_PRIVATE_KEY}and${JWT_PUBLIC_KEY}from the.envfile — same pattern as the existing${SECRET_KEY}. No placeholder values in the repo.
Session Duration / "Remember Me"
- D-08: Access token TTL: 15 minutes — unchanged and non-negotiable (CLAUDE.md).
- D-09: Default refresh token TTL: 16 hours (new config setting
refresh_token_expire_hours: int = 16). This guarantees session expiry overnight and covers a full workday. - D-10: "Remember me" opt-in: 30 days (existing
refresh_token_expire_days: int = 30kept as-is). Whenremember_me=Trueis passed to the login endpoint,create_refresh_tokenuses the 30-day TTL; otherwise it uses 16 hours. - D-11:
POST /api/auth/loginrequest body gainsremember_me: bool = False.create_refresh_tokengainsremember_me: bool = Falseparam — selects betweentimedelta(hours=settings.refresh_token_expire_hours)andtimedelta(days=settings.refresh_token_expire_days). - D-12:
LoginView.vuegains a "Remember me for 30 days" checkbox that passesremember_metoauthStore.login(). The store passes it through to the API call.
<canonical_refs>
Canonical References
Downstream agents MUST read these before planning or implementing.
JWT signing — target code (4 sites to change)
backend/services/auth.pylines 84–150 —create_access_token(line 86),decode_access_token(line 102),create_password_reset_token(line 121),decode_password_reset_token(line 135); all currently usesettings.secret_key/"HS256"
Config — where new settings land
backend/config.pyline 31 — currentsecret_key: str = "CHANGEME"; addjwt_private_key: str,jwt_public_key: str,refresh_token_expire_hours: int = 16here
Startup hook — where rotation logic goes
backend/main.pyline 136 —lifespanfunction; the ES256 rotation check goes inside this existing startup block, after the Redis check and before the AI seedbackend/services/ai_config.py— reference implementation for reading/writingsystem_settingskey-value pairs at startup (seed_system_settings_from_envpattern)
Models — token and settings tables
backend/db/models.pyline 87 —RefreshTokenmodel withrevoked: Mapped[bool]column (line 102) — target for bulk-revoke UPDATEbackend/db/models.pyline 340 —SystemSettingsmodel — storage forjwt_algorithmdetection key
Login endpoint — remember_me param
backend/api/auth.py—POST /api/auth/loginhandler; addremember_me: bool = Falseto the request schema and pass it through tocreate_refresh_token
Frontend login form
frontend/src/views/auth/LoginView.vueline 228 —authStore.login(email, password)call; extend to passremember_mebool from new checkbox
Security requirement
CLAUDE.md§"Login token hardening" — access token TTL 15 min max, ES256 asymmetric algorithm, refresh token rotation; this phase enforces all three.planning/codebase/CONCERNS.md§"JWT Algorithm Downgrade: HS256 Instead of ES256" — full risk description and fix approach
docker-compose env var pattern
docker-compose.ymllines ~60–65 — existingSECRET_KEY=${SECRET_KEY}pattern;JWT_PRIVATE_KEYandJWT_PUBLIC_KEYfollow the same form
</canonical_refs>
<code_context>
Existing Code Insights
Reusable Assets
lifespanfunction inbackend/main.py:136— already has a startup block with DB session, Redis check, and AI seed; the ES256 rotation check slots in as another startup task in the same blockSystemSettingsmodel + upsert pattern inbackend/services/ai_config.py— exact pattern for reading and writing a key-value entry insystem_settingsat startup; copy this to avoid re-inventing the upsertRefreshToken.revokedcolumn — already exists (migrations/0001); no schema change needed for bulk revoke
Established Patterns
- Base64-encoded keys:
config.pyalready handlesminio_secret_keyand similar string secrets from env — apply the samebase64.b64decode(settings.jwt_private_key).decode()pattern before constructing theEllipticCurvePrivateKeyfor PyJWT - Token hash: refresh tokens stored as
sha256(raw.encode()).hexdigest()— unchanged by this phase; only the access token algorithm changes create_refresh_tokeninservices/auth.pycurrently usestimedelta(days=settings.refresh_token_expire_days)— add aremember_meparam that selects between hours and days TTL
Integration Points
backend/services/auth.py— primary change site; 4 signing/decode functions +create_refresh_tokenTTL parambackend/config.py— add 3 new settings;secret_keystays but JWT code stops referencing itbackend/main.py:lifespan— add startup rotation check (5–10 lines)backend/api/auth.py— login endpoint gainsremember_me: bool = False;create_refresh_tokencall passes it throughfrontend/src/views/auth/LoginView.vue— add checkbox, passremember_meto store/APIfrontend/src/stores/auth.js(or.ts) —login()action gainsremember_meparam and passes it to the API
</code_context>
## Specific Ideas- The startup rotation bulk UPDATE should use a raw SQL
UPDATE refresh_tokens SET revoked = true WHERE revoked = falseexecuted viasession.execute(text(...))for efficiency — avoids loading all rows into Python (same pattern as other bulk updates in the codebase). - The
system_settingskey for algorithm tracking: usekey="jwt_algorithm",value="ES256", provider/model fieldsNULLor""(check existing SystemSettings schema for nullable constraints before inserting). - PyJWT ES256 usage:
jwt.encode(payload, private_key_pem_str, algorithm="ES256")andjwt.decode(token, public_key_pem_str, algorithms=["ES256"])— PyJWT accepts PEM strings directly; no need to use thecryptographykey objects directly in the jwt calls. - The "Remember me" checkbox label: "Stay signed in for 30 days" — clearer than "Remember me" about what it actually does.
- Phase 7.4 — Token fingerprinting / token binding: Add
fgp(fingerprint) claim =hmac(key, User-Agent + Accept-Language)[:16]; validate inget_current_user.SECRET_KEYmay serve as the HMAC key here. Tracked in.planning/codebase/CONCERNS.md§"No Token Fingerprint / Token Binding". - Key rotation ceremony: A process for rotating the P-256 key pair in production (dual-key overlap period, gradual rollout) — not needed for the initial upgrade but worth documenting when this goes to production.
Phase: 07.3-security-es256-algorithm-upgrade-inserted Context gathered: 2026-06-05