Spaces:
Running on Zero
Running on Zero
File size: 5,268 Bytes
6143b69 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | """Auth primitives: password hashing, JWT, and a small file-based user store.
Prefers ``passlib`` / ``python-jose`` when installed (see requirements.txt) and
falls back to stdlib implementations (PBKDF2-HMAC + a hand-rolled HS256 token) so
the backend runs and tests pass without those optional dependencies.
"""
from __future__ import annotations
import base64
import hashlib
import hmac
import json
import os
import time
from pathlib import Path
from backend.config import settings
# ββ password hashing βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
try:
import bcrypt
def hash_password(password: str) -> str:
return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("ascii")
def verify_password(password: str, hashed: str) -> bool:
try:
return bcrypt.checkpw(password.encode("utf-8"), hashed.encode("ascii"))
except Exception: # noqa: BLE001
return False
except ImportError: # bcrypt unavailable β PBKDF2 fallback
def hash_password(password: str) -> str:
salt = os.urandom(16)
dk = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, 120_000)
return f"pbkdf2_sha256$120000${salt.hex()}${dk.hex()}"
def verify_password(password: str, hashed: str) -> bool:
try:
algo, iters, salt_hex, dk_hex = hashed.split("$")
except ValueError:
return False
if algo != "pbkdf2_sha256":
return False
dk = hashlib.pbkdf2_hmac(
"sha256", password.encode("utf-8"), bytes.fromhex(salt_hex), int(iters)
)
return hmac.compare_digest(dk.hex(), dk_hex)
# ββ JWT ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _b64url(data: bytes) -> str:
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
def _b64url_decode(data: str) -> bytes:
pad = "=" * (-len(data) % 4)
return base64.urlsafe_b64decode(data + pad)
try:
from jose import jwt as _jose_jwt
def create_token(tenant_id: str) -> str:
payload = {
"sub": tenant_id,
"exp": int(time.time()) + settings.jwt_expiry_minutes * 60,
}
return _jose_jwt.encode(
payload, settings.jwt_secret, algorithm=settings.jwt_algorithm
)
def decode_token(token: str) -> dict:
return _jose_jwt.decode(
token, settings.jwt_secret, algorithms=[settings.jwt_algorithm]
)
except Exception: # noqa: BLE001 - python-jose unavailable β HS256 fallback
def create_token(tenant_id: str) -> str:
header = _b64url(json.dumps({"alg": "HS256", "typ": "JWT"}).encode())
payload = _b64url(
json.dumps(
{
"sub": tenant_id,
"exp": int(time.time()) + settings.jwt_expiry_minutes * 60,
}
).encode()
)
signing_input = f"{header}.{payload}".encode()
sig = hmac.new(
settings.jwt_secret.encode(), signing_input, hashlib.sha256
).digest()
return f"{header}.{payload}.{_b64url(sig)}"
def decode_token(token: str) -> dict:
try:
header_b64, payload_b64, sig_b64 = token.split(".")
except ValueError as exc:
raise ValueError("Malformed token") from exc
signing_input = f"{header_b64}.{payload_b64}".encode()
expected = hmac.new(
settings.jwt_secret.encode(), signing_input, hashlib.sha256
).digest()
if not hmac.compare_digest(expected, _b64url_decode(sig_b64)):
raise ValueError("Invalid signature")
payload = json.loads(_b64url_decode(payload_b64))
if int(payload.get("exp", 0)) < int(time.time()):
raise ValueError("Token expired")
return payload
# ββ user store βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _users_path() -> Path:
p = settings.data_dir_path / "users.json"
p.parent.mkdir(parents=True, exist_ok=True)
return p
def _load_users() -> dict[str, str]:
path = _users_path()
if not path.is_file():
return {}
try:
return json.loads(path.read_text(encoding="utf-8"))
except Exception: # noqa: BLE001
return {}
def _save_users(users: dict[str, str]) -> None:
_users_path().write_text(json.dumps(users), encoding="utf-8")
def register_user(tenant_id: str, password: str) -> bool:
"""Create a tenant. Returns False if it already exists."""
users = _load_users()
if tenant_id in users:
return False
users[tenant_id] = hash_password(password)
_save_users(users)
return True
def authenticate(tenant_id: str, password: str) -> bool:
users = _load_users()
hashed = users.get(tenant_id)
return bool(hashed and verify_password(password, hashed))
|