145 lines
4.6 KiB
Python
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="")
|