Spaces:
Sleeping
Sleeping
samar m commited on
Commit ·
3cefbc3
1
Parent(s): 30e2128
fix: add JWT secret warning and missing security tests
Browse files- backend/auth/jwt.py +7 -0
- tests/test_jwt.py +32 -0
backend/auth/jwt.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
import os
|
| 2 |
import secrets
|
| 3 |
import hashlib
|
|
|
|
| 4 |
from datetime import datetime, timedelta, timezone
|
| 5 |
from jose import jwt, JWTError
|
| 6 |
|
|
@@ -8,6 +9,12 @@ _SECRET = os.getenv("JWT_SECRET", "dev-secret-key-32-chars-minimum!!")
|
|
| 8 |
_ALGORITHM = "HS256"
|
| 9 |
_ACCESS_EXPIRE_MINUTES = int(os.getenv("JWT_ACCESS_EXPIRE_MINUTES", "15"))
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
def create_access_token(user_id: str, role: str, batch_id: str | None, email: str) -> str:
|
| 13 |
exp = datetime.now(timezone.utc) + timedelta(minutes=_ACCESS_EXPIRE_MINUTES)
|
|
|
|
| 1 |
import os
|
| 2 |
import secrets
|
| 3 |
import hashlib
|
| 4 |
+
import warnings
|
| 5 |
from datetime import datetime, timedelta, timezone
|
| 6 |
from jose import jwt, JWTError
|
| 7 |
|
|
|
|
| 9 |
_ALGORITHM = "HS256"
|
| 10 |
_ACCESS_EXPIRE_MINUTES = int(os.getenv("JWT_ACCESS_EXPIRE_MINUTES", "15"))
|
| 11 |
|
| 12 |
+
if _SECRET == "dev-secret-key-32-chars-minimum!!":
|
| 13 |
+
warnings.warn(
|
| 14 |
+
"JWT_SECRET is using the insecure development default. Set JWT_SECRET in your environment.",
|
| 15 |
+
stacklevel=1,
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
|
| 19 |
def create_access_token(user_id: str, role: str, batch_id: str | None, email: str) -> str:
|
| 20 |
exp = datetime.now(timezone.utc) + timedelta(minutes=_ACCESS_EXPIRE_MINUTES)
|
tests/test_jwt.py
CHANGED
|
@@ -28,3 +28,35 @@ def test_refresh_token_hashes_consistently():
|
|
| 28 |
import hashlib
|
| 29 |
raw, hashed = generate_refresh_token()
|
| 30 |
assert hashlib.sha256(raw.encode()).hexdigest() == hashed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
import hashlib
|
| 29 |
raw, hashed = generate_refresh_token()
|
| 30 |
assert hashlib.sha256(raw.encode()).hexdigest() == hashed
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def test_expired_token_raises():
|
| 34 |
+
from datetime import datetime, timedelta, timezone
|
| 35 |
+
from jose import jwt as jose_jwt
|
| 36 |
+
import os
|
| 37 |
+
secret = os.getenv("JWT_SECRET", "dev-secret-key-32-chars-minimum!!")
|
| 38 |
+
expired_payload = {
|
| 39 |
+
"user_id": "u1", "role": "student", "batch_id": None, "email": "a@b.com",
|
| 40 |
+
"exp": datetime.now(timezone.utc) - timedelta(seconds=1),
|
| 41 |
+
}
|
| 42 |
+
expired_token = jose_jwt.encode(expired_payload, secret, algorithm="HS256")
|
| 43 |
+
with pytest.raises(ValueError):
|
| 44 |
+
verify_access_token(expired_token)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def test_wrong_secret_raises():
|
| 48 |
+
token = create_access_token("u1", "student", None, "a@b.com")
|
| 49 |
+
from jose import jwt as jose_jwt
|
| 50 |
+
# Tamper: re-sign with different secret
|
| 51 |
+
import os
|
| 52 |
+
wrong_secret = "wrong-secret-completely-different!!"
|
| 53 |
+
payload = jose_jwt.decode(token, os.getenv("JWT_SECRET", "dev-secret-key-32-chars-minimum!!"), algorithms=["HS256"])
|
| 54 |
+
tampered = jose_jwt.encode(payload, wrong_secret, algorithm="HS256")
|
| 55 |
+
with pytest.raises(ValueError):
|
| 56 |
+
verify_access_token(tampered)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def test_batch_id_none_roundtrip():
|
| 60 |
+
token = create_access_token("u1", "student", None, "a@b.com")
|
| 61 |
+
payload = verify_access_token(token)
|
| 62 |
+
assert payload["batch_id"] is None
|