Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 2 additions & 1 deletion backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
yyy-router marked this conversation as resolved.

# 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
Expand Down
6 changes: 3 additions & 3 deletions backend/src/timeflow/infrastructure/security/access_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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:
Expand Down
12 changes: 10 additions & 2 deletions backend/tests/infrastructure/security/test_access_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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,))
Expand Down
3 changes: 2 additions & 1 deletion backend/tests/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading