File size: 5,616 Bytes
17b22d9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e14044c
 
 
 
 
 
 
17b22d9
 
 
 
e14044c
 
17b22d9
 
 
 
 
 
 
 
 
 
e14044c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17b22d9
 
 
 
 
 
 
 
 
 
 
 
e14044c
17b22d9
 
 
 
 
e14044c
 
17b22d9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
"""The sign-in flow, exercised without ever reaching Hugging Face.

Everything here is about refusing bad input. The happy path needs a real
provider and a real Space, and is verified against the deployment rather than
faked here — a mocked token exchange would only assert that the mock works.
"""

from __future__ import annotations

import pytest
from fastapi.testclient import TestClient

from server import oauth
from server.main import app


@pytest.fixture(autouse=True)
def clean_env(monkeypatch):
    for var in ("OAUTH_CLIENT_ID", "OAUTH_CLIENT_SECRET", "OAUTH_SCOPES",
                "OPENID_PROVIDER_URL", "SPACE_ID", "SPACE_HOST",
                "HF_TOKEN", "HUGGING_FACE_HUB_TOKEN", "ASE_ALLOW_SPACE_CREDITS"):
        monkeypatch.delenv(var, raising=False)
    monkeypatch.setenv("ASE_KEY_FILE", "/nonexistent/ase-glm-test-key")


@pytest.fixture
def client():
    return TestClient(app)


def test_not_configured_without_a_space(client):
    assert oauth.configured() is False
    r = client.get("/api/auth/login", follow_redirects=False)
    assert r.status_code == 503
    assert "Space" in r.json()["error"]


def test_config_says_sign_in_is_unavailable(client):
    auth = client.get("/api/config").json()["auth"]
    assert auth["canSignIn"] is False
    assert auth["profile"] is None


def test_login_redirects_to_the_provider_with_the_billing_scope(client, monkeypatch):
    monkeypatch.setenv("OAUTH_CLIENT_ID", "cid")
    monkeypatch.setenv("OAUTH_CLIENT_SECRET", "shh")
    monkeypatch.setenv("SPACE_HOST", "example.hf.space")

    r = client.get("/api/auth/login", follow_redirects=False)
    assert r.status_code == 302
    url = r.headers["location"]
    assert url.startswith("https://huggingface.co/oauth/authorize?")
    # inference-api is the whole point: without it we learn who someone is and
    # still cannot bill them.
    assert "inference-api" in url
    assert "example.hf.space%2Fauth%2Fcallback" in url
    assert "state=" in url
    assert "shh" not in url


def start_login(client) -> str:
    """Begin a sign-in and return the state we were issued."""
    r = client.get("/api/auth/login", follow_redirects=False)
    query = r.headers["location"].split("?", 1)[1]
    return dict(p.split("=", 1) for p in query.split("&"))["state"]


def test_the_callback_refuses_a_state_it_did_not_issue(client, monkeypatch):
    monkeypatch.setenv("OAUTH_CLIENT_ID", "cid")
    monkeypatch.setenv("OAUTH_CLIENT_SECRET", "shh")

    # A real session, so the rejection is about the state and not the cookie.
    start_login(client)
    r = client.get("/auth/callback", params={"code": "abc", "state": "forged"},
                   follow_redirects=False)
    assert r.status_code == 200
    assert "state did not match" in r.text
    # And nothing was stored.
    assert client.get("/api/config").json()["auth"]["mode"] == "none"


def test_the_callback_refuses_a_missing_state(client):
    r = client.get("/auth/callback", params={"code": "abc"}, follow_redirects=False)
    # No session came back at all: reported as a cookie problem, because that
    # is what it almost always is and it has a different fix from a forgery.
    assert "not returned by your browser" in r.text


def test_the_session_cookie_crosses_sites_on_a_space(monkeypatch):
    # A Space is viewed inside an iframe on huggingface.co, so this cookie is
    # third-party. Under SameSite=Lax the browser withholds it on the way back
    # from the provider and every sign-in fails as a state mismatch.
    monkeypatch.setenv("SPACE_ID", "someone/ase-glm")
    monkeypatch.setenv("OAUTH_CLIENT_ID", "cid")
    monkeypatch.setenv("OAUTH_CLIENT_SECRET", "shh")
    import importlib
    from server import main as main_mod
    importlib.reload(main_mod)
    r = TestClient(main_mod.app).get("/api/auth/login", follow_redirects=False)
    cookie = r.headers.get("set-cookie", "").lower()
    assert "samesite=none" in cookie
    # SameSite=None is only honoured on a Secure cookie.
    assert "secure" in cookie
    importlib.reload(main_mod)


def test_the_callback_reports_a_provider_error(client):
    r = client.get("/auth/callback", params={"error": "access_denied"})
    assert "access_denied" in r.text


def test_a_state_cannot_be_replayed(client, monkeypatch):
    monkeypatch.setenv("OAUTH_CLIENT_ID", "cid")
    monkeypatch.setenv("OAUTH_CLIENT_SECRET", "shh")
    monkeypatch.setenv("SPACE_HOST", "example.hf.space")

    state = start_login(client)

    # The first use consumes it (and then fails at the token exchange, which is
    # fine — what matters is that a second attempt cannot reuse the same state).
    client.get("/auth/callback", params={"code": "abc", "state": state})
    again = client.get("/auth/callback", params={"code": "abc", "state": state})
    assert "Signed in" not in again.text
    assert client.get("/api/config").json()["auth"]["mode"] == "none"


def test_logout_is_a_post_and_clears_the_session(client):
    assert client.get("/auth/callback", params={"error": "x"}).status_code == 200
    assert client.post("/api/auth/logout").json() == {"ok": True}
    # GET must not log anyone out: a prefetched link would sign them out.
    assert client.get("/api/auth/logout").status_code in (404, 405)


def test_the_session_cookie_is_not_readable_by_script(client, monkeypatch):
    monkeypatch.setenv("OAUTH_CLIENT_ID", "cid")
    monkeypatch.setenv("OAUTH_CLIENT_SECRET", "shh")
    r = client.get("/api/auth/login", follow_redirects=False)
    cookie = r.headers.get("set-cookie", "")
    assert "httponly" in cookie.lower()
    assert "samesite=lax" in cookie.lower()