From 9187fbb970fc5e8b338b59389e829646558d90ab Mon Sep 17 00:00:00 2001 From: hexdev Date: Fri, 24 Jul 2026 12:59:41 +0700 Subject: [PATCH] feat(auth): implement JWT TTL and expiration added access_token_ttl setting fixed public and private key mismatch while decoding jwt decode_jwt now returns None on expired tokens instead of raising exc generate_pair converts sub to string and uses dynamic expiration --- config.py | 2 ++ core/exp.py | 7 +++++++ core/secrets.py | 9 ++++++--- schemas/jwt.py | 7 +++---- 4 files changed, 18 insertions(+), 7 deletions(-) create mode 100644 core/exp.py diff --git a/config.py b/config.py index db6adcb..7345e29 100644 --- a/config.py +++ b/config.py @@ -16,6 +16,8 @@ class Settings(BaseSettings): private_key_fp: str = Field() public_key_fp: str = Field() + access_token_ttl: int = Field(description="Access token TTL (minutes)") + @computed_field @property def db_url(self) -> str: diff --git a/core/exp.py b/core/exp.py new file mode 100644 index 0000000..d94c8b7 --- /dev/null +++ b/core/exp.py @@ -0,0 +1,7 @@ +from datetime import UTC, datetime, timedelta + +from config import cfg + + +def get_exp(minutes: int = cfg.access_token_ttl) -> float: + return (datetime.now(UTC) + timedelta(minutes=minutes)).timestamp() diff --git a/core/secrets.py b/core/secrets.py index 604306f..0c74e78 100644 --- a/core/secrets.py +++ b/core/secrets.py @@ -35,12 +35,15 @@ def generate_jwt(payload: dict[str, Any]) -> str: return jwt.encode(payload, cfg.private_key, "RS256") -def decode_jwt(token: str) -> dict[str, Any]: - return jwt.decode(token, cfg.public_key, "RS256") +def decode_jwt(token: str) -> dict[str, Any] | None: + try: + return jwt.decode(token, cfg.public_key, "RS256") + except jwt.ExpiredSignatureError: + return def generate_pair(user_id: int, iss: ProvidersType) -> KeyPair: - payload = JWTPayload(sub=user_id, iss=iss) + payload = JWTPayload(sub=str(user_id), iss=iss) access_token = generate_jwt(payload.model_dump()) refresh_token = secrets.token_urlsafe(32) diff --git a/schemas/jwt.py b/schemas/jwt.py index d3c8ce7..539d985 100644 --- a/schemas/jwt.py +++ b/schemas/jwt.py @@ -1,11 +1,10 @@ -from datetime import UTC, datetime - from pydantic import BaseModel, Field +from core.exp import get_exp from schemas.providers import ProvidersType class JWTPayload(BaseModel): - sub: int = Field(description="User ID") + sub: str = Field(description="User ID") iss: ProvidersType = Field(description="Issuer") - exp: float = Field(default_factory=lambda: datetime.now(UTC).timestamp()) + exp: float = Field(default_factory=get_exp)