diff --git a/datasette/app.py b/datasette/app.py index e4e544c6..0c2bb809 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -67,6 +67,7 @@ from .utils import ( StartupError, async_call_with_supported_arguments, await_me_maybe, + baseconv, call_with_supported_arguments, detect_json1, display_actor, @@ -1430,6 +1431,18 @@ class Datasette: return await template.render_async(template_context) + def set_actor_cookie( + self, response: Response, actor: dict, expire_after: Optional[int] = None + ): + data = {"a": actor} + if expire_after: + expires_at = int(time.time()) + (24 * 60 * 60) + data["e"] = baseconv.base62.encode(expires_at) + response.set_cookie("ds_actor", self.sign(data, "actor")) + + def delete_actor_cookie(self, response: Response): + response.set_cookie("ds_actor", "", expires=0, max_age=0) + async def _asset_urls(self, key, template, context, request, view_name): # Flatten list-of-lists from plugins: seen_urls = set() diff --git a/datasette/views/special.py b/datasette/views/special.py index e997b788..1db24d74 100644 --- a/datasette/views/special.py +++ b/datasette/views/special.py @@ -85,7 +85,7 @@ class AuthTokenView(BaseView): self.ds._root_token = None response = Response.redirect(self.ds.urls.instance()) root_actor = {"id": "root"} - response.set_cookie("ds_actor", self.ds.sign({"a": root_actor}, "actor")) + self.ds.set_actor_cookie(response, root_actor) await self.ds.track_event(LoginEvent(actor=root_actor)) return response else: @@ -107,7 +107,7 @@ class LogoutView(BaseView): async def post(self, request): response = Response.redirect(self.ds.urls.instance()) - response.set_cookie("ds_actor", "", expires=0, max_age=0) + self.ds.delete_actor_cookie(response) self.ds.add_message(request, "You are now logged out", self.ds.WARNING) await self.ds.track_event(LogoutEvent(actor=request.actor)) return response diff --git a/docs/authentication.rst b/docs/authentication.rst index 5452b9c3..0343dc94 100644 --- a/docs/authentication.rst +++ b/docs/authentication.rst @@ -1062,19 +1062,25 @@ Authentication plugins can set signed ``ds_actor`` cookies themselves like so: .. code-block:: python response = Response.redirect("/") - response.set_cookie( - "ds_actor", - datasette.sign({"a": {"id": "cleopaws"}}, "actor"), - ) + datasette.set_actor_cookie(response, {"id": "cleopaws"}) -Note that you need to pass ``"actor"`` as the namespace to :ref:`datasette_sign`. +The shape of data encoded in the cookie is as follows: -The shape of data encoded in the cookie is as follows:: +.. code-block:: json { - "a": {... actor ...} + "a": { + "id": "cleopaws" + } } +To implement logout in a plugin, use the ``delete_actor_cookie()`` method: + +.. code-block:: python + + response = Response.redirect("/") + datasette.delete_actor_cookie(response) + .. _authentication_ds_actor_expiry: Including an expiry time @@ -1082,25 +1088,13 @@ Including an expiry time ``ds_actor`` cookies can optionally include a signed expiry timestamp, after which the cookies will no longer be valid. Authentication plugins may chose to use this mechanism to limit the lifetime of the cookie. For example, if a plugin implements single-sign-on against another source it may decide to set short-lived cookies so that if the user is removed from the SSO system their existing Datasette cookies will stop working shortly afterwards. -To include an expiry, add a ``"e"`` key to the cookie value containing a base62-encoded integer representing the timestamp when the cookie should expire. For example, here's how to set a cookie that expires after 24 hours: +To include an expiry pass ``expire_after=`` to ``datasette.set_actor_cookie()`` with a number of seconds. For example, to expire in 24 hours: .. code-block:: python - import time - from datasette.utils import baseconv - - expires_at = int(time.time()) + (24 * 60 * 60) - response = Response.redirect("/") - response.set_cookie( - "ds_actor", - datasette.sign( - { - "a": {"id": "cleopaws"}, - "e": baseconv.base62.encode(expires_at), - }, - "actor", - ), + datasette.set_actor_cookie( + response, {"id": "cleopaws"}, expire_after=60 * 60 * 24 ) The resulting cookie will encode data that looks something like this: @@ -1108,13 +1102,12 @@ The resulting cookie will encode data that looks something like this: .. code-block:: json { - "a": { - "id": "cleopaws" - }, - "e": "1jjSji" + "a": { + "id": "cleopaws" + }, + "e": "1jjSji" } - .. _LogoutView: The /-/logout page diff --git a/tests/plugins/my_plugin.py b/tests/plugins/my_plugin.py index 54c59227..2aa57e69 100644 --- a/tests/plugins/my_plugin.py +++ b/tests/plugins/my_plugin.py @@ -279,9 +279,7 @@ def register_routes(): # Mainly for the latest.datasette.io demo if request.method == "POST": response = Response.redirect("/") - response.set_cookie( - "ds_actor", datasette.sign({"a": {"id": "root"}}, "actor") - ) + datasette.set_actor_cookie(response, {"id": "root"}) return response return Response.html( """