From 27f2558bcebfa74f1e34a1d22b0c2636b7bd8052 Mon Sep 17 00:00:00 2001 From: yyy-router <1804384725@qq.com> Date: Tue, 25 Aug 2026 15:00:03 +0800 Subject: [PATCH 1/2] fix(auth): extend default JWT access TTL to 30 days and make it configurable --- backend/.env.example | 3 ++- .../timeflow/infrastructure/security/access_token.py | 6 +++--- .../infrastructure/security/test_access_token.py | 12 ++++++++++-- backend/tests/test_settings.py | 3 ++- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/backend/.env.example b/backend/.env.example index 1b76189a..36445fb6 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -18,7 +18,8 @@ TIMEFLOW_WS_MAX_CONTINUOUS_AUDIO_DURATION_MS=1800000 TIMEFLOW_JWT_SECRET= TIMEFLOW_JWT_ISSUER=timeflow-api TIMEFLOW_JWT_AUDIENCE=timeflow-app -TIMEFLOW_JWT_ACCESS_TTL_SECONDS=3600 +# 访问令牌有效期(秒),默认一个月(30 天)。 +TIMEFLOW_JWT_ACCESS_TTL_SECONDS=2592000 # Qwen realtime ASR. Replace {WorkspaceId} locally and keep the API key out of Git. TIMEFLOW_ALIYUN_ASR_WS_URL=wss://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api-ws/v1/realtime diff --git a/backend/src/timeflow/infrastructure/security/access_token.py b/backend/src/timeflow/infrastructure/security/access_token.py index 4073ab25..891ec251 100644 --- a/backend/src/timeflow/infrastructure/security/access_token.py +++ b/backend/src/timeflow/infrastructure/security/access_token.py @@ -10,7 +10,7 @@ JWT_ALGORITHM: Final = "HS256" JWT_ISSUER: Final = "timeflow-api" JWT_AUDIENCE: Final = "timeflow-app" -JWT_ACCESS_TTL_SECONDS: Final = 3600 +JWT_ACCESS_TTL_SECONDS: Final = 30 * 24 * 60 * 60 # 默认一个月(30 天) MINIMUM_SECRET_BYTES: Final = 32 REQUIRED_CLAIMS: Final = ("sub", "iat", "exp", "iss", "aud") @@ -130,8 +130,8 @@ def _validate_configuration( raise ValueError("JWT issuer must match the v1 contract") if audience != JWT_AUDIENCE: raise ValueError("JWT audience must match the v1 contract") - if access_ttl_seconds != JWT_ACCESS_TTL_SECONDS: - raise ValueError("JWT access TTL must match the v1 contract") + if access_ttl_seconds <= 0: + raise ValueError("JWT access TTL must be greater than zero") def _is_numeric_date(value: object) -> bool: diff --git a/backend/tests/infrastructure/security/test_access_token.py b/backend/tests/infrastructure/security/test_access_token.py index c7a49897..4000e9e3 100644 --- a/backend/tests/infrastructure/security/test_access_token.py +++ b/backend/tests/infrastructure/security/test_access_token.py @@ -104,8 +104,8 @@ def test_service_measures_secret_in_utf8_bytes() -> None: ({"issuer": "other-api"}, "JWT issuer must match the v1 contract"), ({"audience": ""}, "JWT audience must match the v1 contract"), ({"audience": "other-app"}, "JWT audience must match the v1 contract"), - ({"access_ttl_seconds": 0}, "JWT access TTL must match the v1 contract"), - ({"access_ttl_seconds": 7200}, "JWT access TTL must match the v1 contract"), + ({"access_ttl_seconds": 0}, "JWT access TTL must be greater than zero"), + ({"access_ttl_seconds": -1}, "JWT access TTL must be greater than zero"), ], ) def test_service_rejects_non_v1_configuration( @@ -116,6 +116,14 @@ def test_service_rejects_non_v1_configuration( build_service(**overrides) +def test_service_accepts_a_custom_access_ttl() -> None: + service = build_service(access_ttl_seconds=7200) + issued = service.issue(ACCOUNT_ID) + + assert issued.expires_in == 7200 + assert service.verify(issued.access_token) == ACCOUNT_ID + + @pytest.mark.parametrize("missing_claim", ["sub", "iat", "exp", "iss", "aud"]) def test_verify_rejects_each_missing_required_claim(missing_claim: str) -> None: token = encode_test_token(account_id=ACCOUNT_ID, omitted_claims=(missing_claim,)) diff --git a/backend/tests/test_settings.py b/backend/tests/test_settings.py index ea09e4a7..15a156e1 100644 --- a/backend/tests/test_settings.py +++ b/backend/tests/test_settings.py @@ -5,6 +5,7 @@ import pytest from pytest import MonkeyPatch +from timeflow.infrastructure.security.access_token import JWT_ACCESS_TTL_SECONDS from timeflow.infrastructure.settings import Settings, get_settings ASR_ENVIRONMENT_VARIABLES = ( @@ -226,7 +227,7 @@ def test_settings_allow_empty_jwt_secret_with_v1_defaults( assert settings.jwt_secret == "" assert settings.jwt_issuer == "timeflow-api" assert settings.jwt_audience == "timeflow-app" - assert settings.jwt_access_ttl_seconds == 3600 + assert settings.jwt_access_ttl_seconds == JWT_ACCESS_TTL_SECONDS def test_settings_carry_explicit_jwt_environment_values( From 7f0682620b5797783897a31b0e5e8907ba005d4d Mon Sep 17 00:00:00 2001 From: yyy-router <1804384725@qq.com> Date: Tue, 25 Aug 2026 15:09:37 +0800 Subject: [PATCH 2/2] fix(auth): forward JWT access TTL through Docker Compose --- .env.example | 2 ++ docker-compose.yml | 1 + 2 files changed, 3 insertions(+) diff --git a/.env.example b/.env.example index dd6f2180..6c150d56 100644 --- a/.env.example +++ b/.env.example @@ -6,4 +6,6 @@ API_PORT=8000 TIMEFLOW_ENVIRONMENT=development # Generate a private value of at least 32 UTF-8 bytes before starting the API. TIMEFLOW_JWT_SECRET= +# 访问令牌有效期(秒),默认一个月(30 天)。 +TIMEFLOW_JWT_ACCESS_TTL_SECONDS=2592000 TIMEFLOW_CORS_ALLOWED_ORIGINS=http://localhost:8081,http://127.0.0.1:8081 diff --git a/docker-compose.yml b/docker-compose.yml index df4e6fbd..81b10fc5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,6 +24,7 @@ services: TIMEFLOW_ENVIRONMENT: ${TIMEFLOW_ENVIRONMENT:-development} TIMEFLOW_DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-timeapp}:${POSTGRES_PASSWORD:-timeapp}@db:5432/${POSTGRES_DB:-timeapp} TIMEFLOW_JWT_SECRET: ${TIMEFLOW_JWT_SECRET:?Set TIMEFLOW_JWT_SECRET to at least 32 UTF-8 bytes} + TIMEFLOW_JWT_ACCESS_TTL_SECONDS: ${TIMEFLOW_JWT_ACCESS_TTL_SECONDS:-2592000} TIMEFLOW_CORS_ALLOWED_ORIGINS: ${TIMEFLOW_CORS_ALLOWED_ORIGINS:-http://localhost:8081,http://127.0.0.1:8081} depends_on: db: