Switch JWT signing from HS256 to RS256 (4096-bit RSA)
- Replace symmetric SECRET_KEY with JWT_PRIVATE_KEY / JWT_PUBLIC_KEY (PEM) - Add iat claim to every token - Add expand_newlines validator in config for single-line .env PEM values - Add scripts/generate_jwt_keys.py key-generation helper - Update security-auditor agent JWT checklist with RS256 enforcement rules - Mark RS256 as done in TODO.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -21,7 +21,7 @@ You are a senior application security engineer embedded in this project. Unlike
|
||||
- **Existing security controls** (do not remove or weaken):
|
||||
- `backend/app/core/sanitize.py` — `sanitize_str`, `normalize_email`, `validate_phone`, `validate_date_of_birth` applied to all user inputs before DB
|
||||
- `backend/app/deps.py` — `get_current_admin` returns 404 (not 403) for non-admins
|
||||
- `backend/app/core/security.py` — bcrypt direct (no passlib), JWT via python-jose
|
||||
- `backend/app/core/security.py` — bcrypt direct (no passlib), JWT RS256 via python-jose; `iat` claim included; private key signs, public key verifies
|
||||
- `scripts/security_check.py` — pre-commit hook: secrets, dangerous patterns, weak crypto, SQL injection patterns, sanitization patterns, bandit
|
||||
- All SQLAlchemy queries use ORM bound parameters — no raw `text()` with string formatting
|
||||
|
||||
@@ -61,16 +61,20 @@ When reviewing any authentication code, verify all of the following:
|
||||
| Check | What to look for | Severity |
|
||||
|---|---|---|
|
||||
| Algorithm confusion | `algorithms=["none"]` or `algorithm="none"` in `jwt.decode()` | Critical |
|
||||
| Wrong algorithm | Project uses **RS256**; flag any use of `HS256`, `HS384`, `HS512`, or `none` | Critical |
|
||||
| Symmetric key used | `jwt.encode/decode` must use `JWT_PRIVATE_KEY` / `JWT_PUBLIC_KEY` (PEM); flag any use of a plain `SECRET_KEY` string for JWT | Critical |
|
||||
| Expiry enforcement | `verify_exp=False` or `options={"verify_exp": False}` | Critical |
|
||||
| Token lifetime | `ACCESS_TOKEN_EXPIRE_MINUTES` — must be ≤ 480 (8 h); flag `timedelta(days=...)` in token creation | High |
|
||||
| Secret key strength | `SECRET_KEY` must come from env var, ≥ 32 random chars; flag hardcoded strings | High |
|
||||
| Algorithm pinned | `jwt.decode()` must pass `algorithms=["HS256"]` (or project algorithm) explicitly — never a variable | High |
|
||||
| Missing claims | Token payload should include `sub`, `exp`, `iat`; flag if `iat` is absent | Medium |
|
||||
| Key loaded from env | `JWT_PRIVATE_KEY` and `JWT_PUBLIC_KEY` must come from env vars — flag hardcoded PEM strings | High |
|
||||
| Algorithm pinned | `jwt.decode()` must pass `algorithms=["RS256"]` explicitly — never a variable or list containing other algorithms | High |
|
||||
| Missing claims | Token payload must include `sub`, `exp`, `iat`; flag if any are absent | Medium |
|
||||
| Token storage | Frontend stores JWT in `localStorage` — note the XSS exposure tradeoff; recommend `httpOnly` cookie migration when hardening | Medium |
|
||||
| No refresh tokens | Project policy: no permanent sessions, no refresh tokens. Flag any `refresh_token` implementation | Medium |
|
||||
| No "remember me" | No `remember_me` or extended-expiry paths in auth flow | Medium |
|
||||
|
||||
Current project policy: **8-hour JWT, no refresh tokens, no permanent login.**
|
||||
Current project policy: **RS256 (4096-bit RSA), 8-hour JWT, no refresh tokens, no permanent login.**
|
||||
|
||||
Key management: private key (`JWT_PRIVATE_KEY`) signs tokens and must never be exposed outside the backend process. Public key (`JWT_PUBLIC_KEY`) verifies tokens and can be shared. Both are generated by `scripts/generate_jwt_keys.py`.
|
||||
|
||||
## Hard rules
|
||||
|
||||
|
||||
Reference in New Issue
Block a user