Repository: huggingface/dataset-viewer

Issue:
Support JWT on cookies
private conversion: https://huggingface.slack.com/archives/D030YA5BW91/p1696507761676679

When the users goes to https://huggingface.co/datasets/emrgnt-cmplxty/sciphi-textbooks-are-all-you-need, moonlanding can put a cookie on `datasets-server.huggingface.co` with name `hf_jwt_[sha256(/datasets/emrgnt-cmplxty/sciphi-textbooks-are-all-you-need)]` and with the JWT as the value

This cookie would be read on datasets server when accessing a gated dataset

Doing so would simplify a lot the code on the Hub (moonlanding) by removing the need to refresh the JWT (remove an endpoint), and avoid the logic in the frontend code that refreshes the JWT. It would be a security improvement too, because the Hub's frontend code (javascript) would no more have access to the JWT (the browser directly adds the cookie to the HTTP request)


Discussion:
To implement that, we will need to:
- add support for JWT in cookie
- change the Hub's code
- then deprecate the JWT in header in datasets server (optional?)
See a possible issue with using cookies: https://github.com/huggingface/datasets-server/issues/2572

Resolving patch:
diff --git a/libs/libapi/src/libapi/authentication.py b/libs/libapi/src/libapi/authentication.py
--- a/libs/libapi/src/libapi/authentication.py
+++ b/libs/libapi/src/libapi/authentication.py
@@ -21,13 +21,10 @@ class RequestAuth(httpx.Auth):
     """Attaches input Request authentication headers to the given Request object."""
 
     def __init__(self, request: Optional[Request]) -> None:
-        self.cookie = request.headers.get("cookie") if request else None
         self.authorization = request.headers.get("authorization") if request else None
 
     def auth_flow(self, request: httpx.Request) -> Generator[httpx.Request, httpx.Response, None]:
         # modify and yield the request
-        if self.cookie:
-            request.headers["cookie"] = self.cookie
         if self.authorization:
             request.headers["authorization"] = self.authorization
         yield request
@@ -66,7 +63,7 @@ async def auth_check(
           which will be replaced with the dataset name, for example: https://huggingface.co/api/datasets/%s/auth-check
           The authentication service must return 200, 401, 403 or 404.
           If None, the dataset is always authorized.
-        request (`Request`, *optional*): the request which optionally bears authentication headers: "cookie",
+        request (`Request`, *optional*): the request which optionally bears authentication headers:
           "authorization" or "X-Api-Key"
         hf_jwt_public_keys (`list[str]`, *optional*): the public keys to use to decode the JWT token
         hf_jwt_algorithm (`str`): the algorithm to use to decode the JWT token
diff --git a/services/admin/src/admin/authentication.py b/services/admin/src/admin/authentication.py
--- a/services/admin/src/admin/authentication.py
+++ b/services/admin/src/admin/authentication.py
@@ -20,8 +20,7 @@ async def auth_check(
     Args:
         external_auth_url (`str`, *optional*): the URL of an external authentication service. If None, the dataset is always
           authorized.
-        request (`Request`, *optional*): the request which optionally bears authentication headers: "cookie" or
-          "authorization"
+        request (`Request`, *optional*): the request which optionally bears authentication header: "authorization"
         organization (`str`, *optional*): the organization name. If None, the dataset is always
           authorized.
         hf_timeout_seconds (`float`, *optional*): the timeout in seconds for the HTTP request to the external authentication


Test patch:
diff --git a/e2e/tests/test_11_api.py b/e2e/tests/test_11_api.py
--- a/e2e/tests/test_11_api.py
+++ b/e2e/tests/test_11_api.py
@@ -23,6 +23,7 @@
 
 
 def get_auth_headers(auth_type: str) -> dict[str, str]:
+    # cookie authentication is not supported anymore
     return (
         {"Authorization": f"Bearer {NORMAL_USER_TOKEN}"}
         if auth_type == "token"
@@ -90,8 +91,8 @@ def normal_user_gated_dataset(csv_path: str) -> Iterator[str]:
     "auth_type,expected_status_code,expected_error_code",
     [
         (None, 401, "ExternalUnauthenticatedError"),
+        ("cookie", 401, "ExternalUnauthenticatedError"),
         ("token", 200, None),
-        ("cookie", 200, None),
     ],
 )
 def test_auth_gated(
@@ -120,8 +121,8 @@ def normal_user_private_dataset(csv_path: str) -> Iterator[str]:
     "auth_type,expected_status_code,expected_error_code",
     [
         (None, 401, "ExternalUnauthenticatedError"),
+        ("cookie", 401, "ExternalUnauthenticatedError"),
         ("token", 501, "NotSupportedPrivateRepositoryError"),
-        ("cookie", 501, "NotSupportedPrivateRepositoryError"),
     ],
 )
 def test_auth_private(
diff --git a/libs/libapi/tests/test_authentication.py b/libs/libapi/tests/test_authentication.py
--- a/libs/libapi/tests/test_authentication.py
+++ b/libs/libapi/tests/test_authentication.py
@@ -46,8 +46,7 @@
 dataset_inexistent = "dataset_inexistent"
 dataset_throttled = "dataset_throttled"
 
-cookie_ok = "cookie ok"
-cookie_wrong = "cookie wrong"
+cookie_is_not_supported_anymore = "cookie is not supported anymore"
 api_token_ok = "api token ok"
 api_token_wrong = "api token wrong"
 
@@ -75,7 +74,7 @@ def auth_callback(request: WerkzeugRequest) -> WerkzeugResponse:
     if dataset == dataset_public:
         # a public dataset always has read access
         return WerkzeugResponse(status=200)
-    if request.headers.get("cookie") != cookie_ok and request.headers.get("authorization") != f"Bearer {api_token_ok}":
+    if request.headers.get("authorization") != f"Bearer {api_token_ok}":
         # the user is not authenticated
         return WerkzeugResponse(status=401)
     if dataset == dataset_protected_with_access:
@@ -208,9 +207,8 @@ async def assert_auth_headers(
         ({}, does_not_raise()),
         ({"Authorization": f"Bearer {api_token_wrong}"}, does_not_raise()),
         ({"Authorization": api_token_ok}, does_not_raise()),
-        ({"Cookie": cookie_wrong}, does_not_raise()),
+        ({"Cookie": cookie_is_not_supported_anymore}, does_not_raise()),
         ({"Authorization": f"Bearer {api_token_ok}"}, does_not_raise()),
-        ({"Cookie": cookie_ok}, does_not_raise()),
         ({"X-Api-Key": get_jwt(dataset_public)}, does_not_raise()),
         ({"Authorization": f"Bearer {get_jwt(dataset_public)}"}, does_not_raise()),
     ],
@@ -231,9 +229,8 @@ async def test_external_auth_service_dataset_public(
         ({}, pytest.raises(ExternalUnauthenticatedError)),
         ({"Authorization": f"Bearer {api_token_wrong}"}, pytest.raises(ExternalUnauthenticatedError)),
         ({"Authorization": api_token_ok}, pytest.raises(ExternalUnauthenticatedError)),
-        ({"Cookie": cookie_wrong}, pytest.raises(ExternalUnauthenticatedError)),
+        ({"Cookie": cookie_is_not_supported_anymore}, pytest.raises(ExternalUnauthenticatedError)),
         ({"Authorization": f"Bearer {api_token_ok}"}, does_not_raise()),
-        ({"Cookie": cookie_ok}, does_not_raise()),
         ({"X-Api-Key": get_jwt(dataset_protected_with_access)}, does_not_raise()),
         ({"Authorization": f"Bearer jwt:{get_jwt(dataset_protected_with_access)}"}, does_not_raise()),
     ],
@@ -256,9 +253,8 @@ async def test_external_auth_service_dataset_protected_with_access(
         ({}, pytest.raises(ExternalUnauthenticatedError)),
         ({"Authorization": f"Bearer {api_token_wrong}"}, pytest.raises(ExternalUnauthenticatedError)),
         ({"Authorization": api_token_ok}, pytest.raises(ExternalUnauthenticatedError)),
-        ({"Cookie": cookie_wrong}, pytest.raises(ExternalUnauthenticatedError)),
+        ({"Cookie": cookie_is_not_supported_anymore}, pytest.raises(ExternalUnauthenticatedError)),
         ({"Authorization": f"Bearer {api_token_ok}"}, pytest.raises(ExternalAuthenticatedError)),
-        ({"Cookie": cookie_ok}, pytest.raises(ExternalAuthenticatedError)),
         ({"X-Api-Key": get_jwt(dataset_protected_without_access)}, does_not_raise()),
         ({"Authorization": f"Bearer jwt:{get_jwt(dataset_protected_without_access)}"}, does_not_raise()),
     ],
@@ -281,9 +277,8 @@ async def test_external_auth_service_dataset_protected_without_access(
         ({}, pytest.raises(ExternalUnauthenticatedError)),
         ({"Authorization": f"Bearer {api_token_wrong}"}, pytest.raises(ExternalUnauthenticatedError)),
         ({"Authorization": api_token_ok}, pytest.raises(ExternalUnauthenticatedError)),
-        ({"Cookie": cookie_wrong}, pytest.raises(ExternalUnauthenticatedError)),
+        ({"Cookie": cookie_is_not_supported_anymore}, pytest.raises(ExternalUnauthenticatedError)),
         ({"Authorization": f"Bearer {api_token_ok}"}, pytest.raises(ExternalAuthenticatedError)),
-        ({"Cookie": cookie_ok}, pytest.raises(ExternalAuthenticatedError)),
         ({"X-Api-Key": get_jwt(dataset_inexistent)}, does_not_raise()),
         ({"Authorization": f"Bearer jwt:{get_jwt(dataset_inexistent)}"}, does_not_raise()),
     ],
@@ -304,9 +299,8 @@ async def test_external_auth_service_dataset_inexistent(
         ({}, pytest.raises(ExternalUnauthenticatedError)),
         ({"Authorization": f"Bearer {api_token_wrong}"}, pytest.raises(ExternalUnauthenticatedError)),
         ({"Authorization": api_token_ok}, pytest.raises(ExternalUnauthenticatedError)),
-        ({"Cookie": cookie_wrong}, pytest.raises(ExternalUnauthenticatedError)),
+        ({"Cookie": cookie_is_not_supported_anymore}, pytest.raises(ExternalUnauthenticatedError)),
         ({"Authorization": f"Bearer {api_token_ok}"}, pytest.raises(ValueError)),
-        ({"Cookie": cookie_ok}, pytest.raises(ValueError)),
         ({"X-Api-Key": get_jwt(dataset_throttled)}, does_not_raise()),
         ({"Authorization": f"Bearer jwt:{get_jwt(dataset_throttled)}"}, does_not_raise()),
     ],