fischer-agentkit/tests/unit/auth/test_jwt_utils.py

145 lines
4.6 KiB
Python

"""Unit tests for jwt_utils (V2 sid/jti claims)."""
from __future__ import annotations
from datetime import datetime, timedelta, timezone
import jwt
import pytest
from agentkit.server.auth.jwt_utils import (
ACCESS_TOKEN_TTL,
JWT_ALGORITHM,
REFRESH_TOKEN_TTL,
REFRESH_TOKEN_TTL_REMEMBER_ME,
create_token_pair,
verify_token,
)
SECRET = "test-secret-with-at-least-32-bytes-1234"
def test_create_token_pair_has_required_v1_claims():
pair = create_token_pair(
user_id="u-1", username="alice", role="member", secret=SECRET
)
for token in (pair.access_token, pair.refresh_token):
payload = jwt.decode(token, SECRET, algorithms=[JWT_ALGORITHM])
assert payload["sub"] == "u-1"
assert payload["username"] == "alice"
assert payload["role"] == "member"
assert payload["iat"] and payload["exp"]
def test_v1_pair_has_no_sid_or_jti():
pair = create_token_pair(
user_id="u-1", username="alice", role="member", secret=SECRET
)
access = jwt.decode(pair.access_token, SECRET, algorithms=[JWT_ALGORITHM])
refresh = jwt.decode(pair.refresh_token, SECRET, algorithms=[JWT_ALGORITHM])
assert "sid" not in access
assert "jti" not in access
assert "sid" not in refresh
def test_v2_pair_includes_sid_on_both_tokens_and_jti_on_access():
pair = create_token_pair(
user_id="u-1",
username="alice",
role="member",
secret=SECRET,
session_id="sess-abc",
)
access = jwt.decode(pair.access_token, SECRET, algorithms=[JWT_ALGORITHM])
refresh = jwt.decode(pair.refresh_token, SECRET, algorithms=[JWT_ALGORITHM])
assert access["sid"] == "sess-abc"
assert refresh["sid"] == "sess-abc"
assert access["jti"] and isinstance(access["jti"], str)
# Refresh intentionally has no jti — rotation uses the token hash.
assert "jti" not in refresh
def test_refresh_token_type_is_refresh():
pair = create_token_pair(
user_id="u-1", username="alice", role="member", secret=SECRET
)
refresh = jwt.decode(pair.refresh_token, SECRET, algorithms=[JWT_ALGORITHM])
assert refresh["type"] == "refresh"
def test_access_token_type_is_access():
pair = create_token_pair(
user_id="u-1", username="alice", role="member", secret=SECRET
)
access = jwt.decode(pair.access_token, SECRET, algorithms=[JWT_ALGORITHM])
assert access["type"] == "access"
def test_default_refresh_ttl_is_7_days():
now = datetime(2026, 6, 20, 0, 0, 0, tzinfo=timezone.utc)
pair = create_token_pair(
user_id="u-1",
username="alice",
role="member",
secret=SECRET,
now=now,
)
assert (pair.access_expires_at - now) == ACCESS_TOKEN_TTL
assert (pair.refresh_expires_at - now) == REFRESH_TOKEN_TTL
def test_remember_me_extends_refresh_ttl_to_30_days():
now = datetime(2026, 6, 20, 0, 0, 0, tzinfo=timezone.utc)
pair = create_token_pair(
user_id="u-1",
username="alice",
role="member",
secret=SECRET,
now=now,
remember_me=True,
)
assert (pair.refresh_expires_at - now) == REFRESH_TOKEN_TTL_REMEMBER_ME
def test_verify_token_accepts_access_and_refresh_by_default():
pair = create_token_pair(
user_id="u-1", username="alice", role="member", secret=SECRET
)
a = verify_token(pair.access_token, SECRET)
r = verify_token(pair.refresh_token, SECRET)
assert a["type"] == "access"
assert r["type"] == "refresh"
def test_verify_token_expected_type_filters_other_type():
pair = create_token_pair(
user_id="u-1", username="alice", role="member", secret=SECRET
)
with pytest.raises(jwt.InvalidTokenError):
verify_token(pair.access_token, SECRET, expected_type="refresh")
with pytest.raises(jwt.InvalidTokenError):
verify_token(pair.refresh_token, SECRET, expected_type="access")
def test_verify_token_rejects_expired():
# Use a "now" far in the past so the access token is definitely expired.
past = datetime.now(timezone.utc) - timedelta(hours=1)
pair = create_token_pair(
user_id="u-1", username="alice", role="member", secret=SECRET, now=past
)
with pytest.raises(jwt.ExpiredSignatureError):
verify_token(pair.access_token, SECRET)
def test_verify_token_rejects_wrong_secret():
pair = create_token_pair(
user_id="u-1", username="alice", role="member", secret=SECRET
)
with pytest.raises(jwt.InvalidTokenError):
verify_token(pair.access_token, "other-secret")
def test_create_token_pair_rejects_empty_secret():
with pytest.raises(ValueError):
create_token_pair(user_id="u-1", username="alice", role="member", secret="")