File size: 3,070 Bytes
bbe01fe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# backend/tests/test_jwt_auth.py
# Tests JWT verification — the only security gate between the internet and the chat endpoint.
# Every path through verify_jwt() is covered.

import time
import pytest
from fastapi import HTTPException
from fastapi.security import HTTPAuthorizationCredentials
from jose import jwt
from unittest.mock import patch

from tests.conftest import TEST_JWT_SECRET, TEST_ALGORITHM, make_jwt


def _make_credentials(token: str) -> HTTPAuthorizationCredentials:
    return HTTPAuthorizationCredentials(scheme="Bearer", credentials=token)


def _verify(token: str):
    """Call verify_jwt synchronously (it's not async)."""
    from app.security.jwt_auth import verify_jwt
    return verify_jwt(_make_credentials(token))


class TestVerifyJWT:
    def test_valid_token_passes(self, valid_token):
        payload = _verify(valid_token)
        assert payload["sub"] == "test-user"

    def test_expired_token_rejected(self, expired_token):
        with pytest.raises(HTTPException) as exc_info:
            _verify(expired_token)
        assert exc_info.value.status_code == 401
        assert "expired" in exc_info.value.detail.lower()

    def test_wrong_secret_rejected(self, wrong_secret_token):
        with pytest.raises(HTTPException) as exc_info:
            _verify(wrong_secret_token)
        assert exc_info.value.status_code == 401

    def test_malformed_token_rejected(self):
        with pytest.raises(HTTPException) as exc_info:
            _verify("not.a.jwt")
        assert exc_info.value.status_code == 401

    def test_empty_token_rejected(self):
        with pytest.raises(HTTPException):
            _verify("")

    def test_algorithm_none_attack_rejected(self):
        # Attacker crafts a token with alg=none to bypass signature verification.
        # jose refuses to decode alg=none tokens by default — this test confirms it.
        payload = {"sub": "attacker", "exp": int(time.time()) + 3600}
        # We can't easily craft an alg=none token with jose, but we can verify
        # that a tampered token (modified signature) is rejected.
        valid = make_jwt()
        tampered = valid[: valid.rfind(".")] + ".invalidsignature"
        with pytest.raises(HTTPException) as exc_info:
            _verify(tampered)
        assert exc_info.value.status_code == 401

    def test_missing_jwt_secret_raises_500(self):
        # If JWT_SECRET is not configured on the server, the endpoint returns 500
        # rather than accidentally accepting all tokens.
        from app.core.config import get_settings, Settings
        settings = get_settings()
        original = settings.JWT_SECRET
        settings.JWT_SECRET = None
        try:
            with pytest.raises(HTTPException) as exc_info:
                _verify(make_jwt())
            assert exc_info.value.status_code == 500
        finally:
            settings.JWT_SECRET = original

    def test_token_payload_fields_preserved(self):
        token = make_jwt(role="guest")
        payload = _verify(token)
        assert payload.get("role") == "guest"