diff --git a/backend/services/auth.py b/backend/services/auth.py index a2bbca1..bf1e26e 100644 --- a/backend/services/auth.py +++ b/backend/services/auth.py @@ -84,10 +84,25 @@ def validate_password_strength(password: str) -> None: # ── JWT helpers ───────────────────────────────────────────────────────────────── -def create_access_token(user_id: str, role: str) -> str: +def _compute_fgp(user_agent: str, accept_lang: str) -> str: + """Return 16-char hex fingerprint binding a token to its client context (D-04).""" + return hmac.new( + settings.secret_key.encode(), + (user_agent + accept_lang).encode(), + hashlib.sha256, + ).hexdigest()[:16] + + +def create_access_token( + user_id: str, + role: str, + user_agent: str = "", + accept_lang: str = "", +) -> str: """Return a signed JWT access token. - Claims: sub=user_id, role=role, typ='access', exp=now+access_token_expire_minutes. + Claims: sub=user_id, role=role, typ='access', exp=now+access_token_expire_minutes, + fgp=fingerprint computed from client User-Agent + Accept-Language headers (D-05). """ now = datetime.now(timezone.utc) payload = { @@ -97,6 +112,7 @@ def create_access_token(user_id: str, role: str) -> str: "iat": now, "exp": now + timedelta(minutes=settings.access_token_expire_minutes), "jti": str(uuid.uuid4()), + "fgp": _compute_fgp(user_agent, accept_lang), } private_pem = base64.b64decode(settings.jwt_private_key).decode() return jwt.encode(payload, private_pem, algorithm="ES256")