feat: add me.set_cookie() and me.delete_cookie() API#1376
Conversation
Adds a native cookie API that works within Mesop's SSE and WebSocket streaming architecture. Because HTTP headers are committed before event handler generators run, cookies cannot be set directly on the /__ui__ response. Instead, this implementation uses a two-phase approach: 1. me.set_cookie() / me.delete_cookie() queue PendingCookie objects on the request context. 2. After each event-handler yield in generate_data(), any pending cookies are stored in a server-side _CookieTokenCache under a one-time random token (60 s TTL), and an ApplyCookiesCommand carrying that token is appended to the render commands. 3. The TypeScript client receives the command and makes a GET request to /__apply-cookies?t=<token>. 4. The Flask endpoint retrieves and deletes the token from the cache, then returns a 204 response with the appropriate Set-Cookie headers. This approach works identically in both SSE and WebSocket modes, and the one-time token prevents replay attacks. Files changed: - mesop/protos/ui.proto: add ApplyCookiesCommand (field 8 in Command oneof) - mesop/protos/ui_pb2.py: regenerate from updated proto - mesop/runtime/context.py: add PendingCookie dataclass + cookie methods - mesop/commands/set_cookie.py: public me.set_cookie() / me.delete_cookie() - mesop/__init__.py: export the new functions - mesop/server/server.py: _CookieTokenCache, maybe_append_apply_cookies_command(), /__apply-cookies route - mesop/web/src/shell/shell.ts: handle hasApplyCookies() command with fetch Closes #1374 https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
Adds a self-contained login/logout demo that exercises me.set_cookie() and me.delete_cookie(): - mesop/examples/set_cookie.py: simulates a session cookie auth flow. On "Log in", sets demo_session cookie via me.set_cookie(). On "Log out", removes it with me.delete_cookie(). on_load reads the cookie from request.cookies so state survives hard refreshes and new tabs. - mesop/tests/e2e/set_cookie_test.ts: three Playwright tests covering * login sets cookie + state persists after reload * login state is present in a new tab (cookie shared across tabs) * logout deletes cookie and reload shows logged-out state https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
ui_pb2.py is produced by Bazel at build time from ui.proto and should not be committed. Remove it from tracking and add it to .gitignore so it is not accidentally re-added. https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
There was a problem hiding this comment.
Pull request overview
Adds a first-class me.set_cookie() / me.delete_cookie() API by queuing cookies server-side during event handling, emitting an ApplyCookiesCommand, and having the client call a dedicated __/apply-cookies endpoint that responds with Set-Cookie headers.
Changes:
- Add
ApplyCookiesCommandto the UI command protocol and handle it in the web shell by callingGET /__apply-cookies?t=.... - Add server-side pending-cookie tracking (
PendingCookie), a one-time token cache, and a new Flask route to apply cookies via a normal HTTP response. - Add an example app and Playwright e2e coverage for basic set/delete cookie behavior.
Reviewed changes
Copilot reviewed 9 out of 10 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
mesop/web/src/shell/shell.ts |
Handles new ApplyCookiesCommand by fetching the cookie-application endpoint. |
mesop/server/server.py |
Implements token cache, command emission, and GET /__apply-cookies endpoint that sets cookies. |
mesop/runtime/context.py |
Adds PendingCookie plus context APIs to queue/clear pending cookies. |
mesop/commands/set_cookie.py |
Public wrappers set_cookie() / delete_cookie() for app code (mesop as me). |
mesop/__init__.py |
Exports the new cookie APIs at the top-level mesop namespace. |
mesop/protos/ui.proto |
Extends the command oneof with ApplyCookiesCommand. |
mesop/tests/e2e/set_cookie_test.ts |
Adds e2e test for login cookie set/persist/delete. |
mesop/examples/set_cookie.py / mesop/examples/__init__.py |
Adds and registers a runnable example demonstrating the API. |
.gitignore |
Ignores generated ui_pb2.py artifact. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Addresses all Copilot review comments on #1376 and the GET vs POST security question: GET → POST for /__apply-cookies A GET puts the token in the URL, which lands in server access logs, proxy logs, and browser history. Switching to POST keeps the token in the request body where it is not routinely logged. The server now reads request.form['t'] and the TypeScript client sends it as application/x-www-form-urlencoded. shell.ts – token validation + error handling + cache: 'no-store' Validate that the token is non-empty before sending the request, and throw on any non-2xx response so failures surface in the existing error dialog rather than being silently swallowed. Also add cache: 'no-store' on the fetch call. server.py – Cache-Control: no-store on /__apply-cookies response Prevent intermediary caches from storing the tokenised response. server.py – opportunistic expiry eviction in _CookieTokenCache Call _evict_expired_locked() inside put() and pop() so stale entries are cleaned up on every write instead of accumulating indefinitely. server.py – flush pending cookies in on_load paths maybe_append_apply_cookies_command() was only called in the user_event handler loop. It is now also called before render_loop in the init on_load generator path, the init on_load non-generator path, and inside run_page_load, so cookies set during on_load are correctly flushed. context.py – validate samesite at the API boundary Raise MesopDeveloperException with a clear message for invalid samesite values, and also for samesite='None' without secure=True (required by RFC 6265bis). set_cookie_test.ts – add token-replay security test Intercept the POST to /__apply-cookies, capture the token, then replay it after the first use to confirm the server returns 400. https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
- Add mesop/commands/cookie_class.py with:
- @me.cookieclass decorator: turns a dataclass into a cookie-backed
structured store (name auto-derived as snake_case of class name)
- me.cookie(Cls): reads + deserialises from request.cookies; returns
default instance on absence or parse error
- me.save_cookie(instance, ...): JSON-serialises + calls set_cookie();
secure=None auto-detects HTTPS from request.is_secure
- me.delete_cookieclass(Cls, ...): deletes by class lookup
- Update me.delete_cookie() to accept a cookieclass type as well as str
- Export cookieclass, cookie, save_cookie, delete_cookieclass from me.*
- Rewrite set_cookie.py example to use @me.cookieclass / me.save_cookie
(removes raw request.cookies access and string prefix parsing)
- Update e2e test cookie name (demo_session → session_cookie) and value
assertion (now JSON)
- Add experimental Cookies section to docs/guides/auth.md covering both
the @me.cookieclass high-level API and the low-level set_cookie /
delete_cookie functions, with security notes
https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
…cases me.delete_cookie() already accepts a cookieclass type or a plain string, so delete_cookieclass was redundant. Remove it. https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
… SECRET_KEY
- Merge save_cookie into set_cookie — one function, two call forms:
me.set_cookie("name", "value", ...) # low-level: explicit name + raw string
me.set_cookie(SessionCookie(username="x")) # high-level: cookieclass instance
Remove save_cookie from public API entirely.
- Change secure default from True → None (auto-detects HTTPS from
request.is_secure) so the same code works in local dev and production.
- Add signed=True / encrypted=True options to @me.cookieclass:
signed — HMAC via itsdangerous (stdlib-compatible, already a Flask dep)
encrypted — Fernet via cryptography (pip install cryptography required)
Both require SECRET_KEY to be set; validated lazily at runtime.
signed and encrypted are mutually exclusive (raises MesopDeveloperException).
- Read SECRET_KEY from env and set flask_app.secret_key in server.py.
- Document SECRET_KEY in docs/api/config.md.
- Update docs/guides/auth.md: remove save_cookie references, clarify
when to use the two set_cookie call forms, add signed/encrypted section.
- Update example and e2e test accordingly.
https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
me.cookie("session_id") # returns raw str value, or "" if absent
me.cookie(SessionCookie) # returns typed instance (existing behaviour)
Removes the need to import flask.request to read raw cookies.
Update docs/guides/auth.md to document both forms.
https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 13 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
39 tests across two files; encrypted tests auto-skip when the
cryptography package is absent.
cookie_class_test.py covers:
- _to_snake_case (simple, acronym, single word, no-op)
- @cookieclass (name derivation, explicit name, signed/encrypted flags,
signed+encrypted mutual exclusion, dataclass coercion)
- me.cookie(str) — raw string form (present, absent)
- me.cookie(Cls) — cookieclass form (populated, absent, bad JSON,
unknown keys, partial fields)
- signed encode/decode roundtrip, tamper detection, wrong-key rejection,
missing SECRET_KEY error
- encrypted encode/decode roundtrip and tamper detection (skipped if
cryptography is not installed)
set_cookie_test.py covers:
- _resolve_secure (True, False, None on HTTP/HTTPS, outside request)
- set_cookie string form (calls context, explicit secure, missing value raises)
- set_cookie instance form (JSON serialisation, kwargs forwarding,
non-cookieclass raises)
- delete_cookie string and class forms, path/domain forwarding,
non-cookieclass raises
conftest.py (root) stubs Bazel-generated/Bazel-only modules so tests run
with plain pytest without a Bazel build.
https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
1. cookieclass: use dataclass_with_defaults (same as stateclass) so me.cookie(Cls) can always call cls() without arguments, even when a field has no explicit default. Previously a required field would raise TypeError on absent/unparseable cookies, contradicting the documented fallback-to-defaults behaviour. 2. context.delete_cookie: add secure parameter defaulting to False. Deletion cookies (Max-Age=0) must be visible to the browser on HTTP; the old default of secure=True (inherited from PendingCookie) silently prevented deletions in plain-HTTP environments. 3. set_cookie.delete_cookie: add secure: bool | None = None and resolve via _resolve_secure(), consistent with set_cookie(). Auto-detects HTTPS on production and HTTP on local dev; tests updated accordingly. 4. server.py generate_data: clear pending cookies in the exception handler so a failed event handler cannot leak queued cookies into a subsequent event cycle. Most important in WebSockets mode where Context is long-lived across requests. 5. docs/guides/auth.md: reword "SSE or WebSockets — neither of which supports Set-Cookie headers" which was inaccurate (SSE can carry Set-Cookie headers; the real constraint is that headers are committed before event-handler code runs). https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
- Add `from collections.abc import Callable` to cookie_class.py (F821) - Fix mutable default arg in _req_ctx (B006) — use None sentinel - Remove unused variable from pytest.importorskip call (F841) - Add strict=False to zip() calls in context.py (B905) - Apply ruff auto-format to all modified files https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
- Session2Cookie → session2_cookie: digits behave like lowercase, triggering a split before the following capital - _Session → __session: leading underscore is preserved verbatim https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
- Add @overload to cookie() so pyright knows cookie(type[T]) returns T, not T|str — fixes all the "Cannot access member X for type str" errors in tests and examples - Add cls: None = None to cookieclass() second overload and explicit return type to implementation — fixes overload consistency error and TypeVar-appears-only-once warning - bytes(key).decode() instead of key.decode() in _get_secret_key() — Flask secret_key can be memoryview in some stub versions; bytes() wrapping handles all byte-like types safely - Stack @dataclasses.dataclass on _Prefs/_Session in set_cookie_test.py and SessionCookie in set_cookie.py so pyright sees the typed __init__ through the opaque @cookieclass decorator https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
When stacked, @dataclasses.dataclass must run first (innermost) so that @cookieclass registers the already-dataclassed class. The previous order had @dataclasses.dataclass on the outside, which applied it *after* @cookieclass — creating a new class object not present in _COOKIE_CLASSES, causing me.cookie(SessionCookie) to raise "not a cookieclass" on page reload and breaking the e2e persistence test. https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
… e2e tests
The DOM update ("Logged in as: alice") arrives via the SSE response, but
the cookie is set by a separate follow-up POST to /__apply-cookies that the
Mesop client makes asynchronously afterward. Checking page.context().cookies()
immediately after the UI assertion raced against that POST and saw an empty
jar.
Fix: use page.waitForResponse('**/__apply-cookies') before asserting on
the cookie jar in the login, new-tab, and logout tests.
https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
…okies page.waitForResponse resolves when the network layer receives the response, but Playwright's CDP cookie store can lag slightly behind. Checking page.context().cookies() in that window races against the browser's internal cookie-write path and sees an empty jar. Move the cookie-jar assertions to after the page.reload() call. At that point the browser must already have the cookie stored (the on_load handler would not have restored "Logged in as: alice" without it), so there is no timing race. https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
page.context().cookies() queries the browser's cookie store via CDP, which
may not be updated synchronously after a fetch/XHR response even after
page.waitForResponse() resolves. This made the explicit cookie-jar assertion
flaky regardless of ordering.
Replace with two reliable checks instead:
1. Assert the /__apply-cookies response status is 204 — this confirms the
server sent Set-Cookie headers (a 400 would mean the token was missing
or already consumed).
2. Assert "Logged in as: alice" is still visible after a hard reload — this
proves end-to-end that the cookie was stored, sent on the reload request,
looked up by the correct name, parsed correctly, and used to restore state.
https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 16 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…_Pb2Finder modernisation - server.py: add same-site Origin check to /__apply-cookies (matching the existing guard on ui_stream) so a cross-site attacker cannot replay a valid token against a victim's browser; guard is skipped in debug mode like the other CSRF checks - conftest.py: restrict _Pb2Finder to mesop.*_pb2 so it no longer shadows real protobuf modules (e.g. google.protobuf.*_pb2); migrate from the deprecated find_module/load_module API to the modern find_spec/ create_module/exec_module importlib interface https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
…ises token check The new same-site CSRF guard on /__apply-cookies runs before the token lookup. page.request.post() sends no Origin header, so without this fix the replay returns 403 (CSRF) instead of the expected 400 (token already consumed). Sending the matching Origin lets the CSRF check pass and ensures the test verifies the actual single-use property. https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 16 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
server.py — stateless cookie token mode when SECRET_KEY is set: _CookieTokenCache now operates in two modes. When Flask's SECRET_KEY is configured, put() encodes the cookie list into an itsdangerous- signed self-contained token so any replica can verify it — multi- worker deployments don't need sticky sessions as long as SECRET_KEY is set. When SECRET_KEY is absent, it falls back to the existing per-process in-memory store (same sticky-session requirement as Mesop's in-memory state-session backend; nothing gets worse for those users). Tighten _store and put()/pop() type annotations to list[PendingCookie]. Add import dataclasses. context.py — pending_cookies() returns immutable tuple: Change return type from list[PendingCookie] to tuple[PendingCookie, ...] so callers cannot accidentally mutate internal state. Existing callers are unaffected (iteration and bool() work on tuples; the one caller that needs a list wraps it in list()). conftest.py — _Pb2Finder scoping and Bazel guard: _stub_generated_protos() now tries to import mesop.protos.ui_pb2 first and returns immediately if it succeeds, so it is a no-op inside Bazel test runs where real generated modules are available. _Pb2Finder is now appended to sys.meta_path (instead of inserted at position 0) so any real module on sys.path is still found first. https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
Remove the in-memory fallback from _CookieTokenCache. Cookie tokens are now always self-contained itsdangerous-signed blobs (URLSafeTimedSerializer), which means: - SECRET_KEY is required; a MesopDeveloperException with a clear message is raised if it is not configured. - Any server replica can verify a token — multi-worker deployments work without sticky sessions. - The class has no instance state (no lock, no dict, no eviction). playwright.config.ts: pass SECRET_KEY=mesop-e2e-test-secret-key to the test server (overridable via SECRET_KEY env var) so e2e cookie tests pass. Update the comment in configure_flask_app() and the set_cookie.py example docstring to document the SECRET_KEY requirement. https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
Use a Mesop-namespaced env var instead of the generic Flask SECRET_KEY convention. The signing code now reads directly from os.environ rather than going through flask_app.secret_key, so there is no Flask coupling. Updated: server.py, cookie_class.py (implementation + docstrings), cookie_class_test.py (patch.dict(os.environ) instead of flask_app.secret_key), playwright.config.ts, set_cookie.py example docstring. https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
Mesop's signing code reads directly from os.environ so flask_app.secret_key is unused by Mesop. Setting it would silently override any secret_key the user configured for their own Flask session/signing needs. https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 17 changed files in this pull request and generated 7 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The signed-token approach had two security problems identified in review: 1. Not single-use — pop() succeeded on every replay until TTL expired, so the replay e2e test would not pass. 2. Cookie values exposed to JS — the signed JSON blob traveled through the browser runtime, undermining httponly=True for session tokens. Revert _CookieTokenCache to the original opaque random-token design: - secrets.token_urlsafe(32) → truly random, unguessable token - cookie data stored server-side, never in the token itself - pop() removes the token on first use (strictly single-use) - per-process in-memory store (same sticky-session requirement as Mesop's in-memory state-session backend for multi-worker) Also in this commit: - Remove _get_cookie_secret_key() (no longer needed for basic cookie ops) - MESOP_COOKIE_SECRET_KEY is now only required for @cookieclass(signed=True) or @cookieclass(encrypted=True), not for basic me.set_cookie() - Fix remaining SECRET_KEY → MESOP_COOKIE_SECRET_KEY in cookie_class.py docstring (encrypted description line), docs/api/config.md (section heading + sh examples), docs/guides/auth.md (cross-reference link) - Remove MESOP_COOKIE_SECRET_KEY from playwright.config.ts (not needed for the plain-cookieclass e2e tests) - Fix e2e replay URL construction to use pathname replacement so any MESOP_BASE_URL_PATH prefix is preserved https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
…nonce tracking The previous in-memory _CookieTokenCache required sticky sessions, introducing a regression for MESOP_STATE_SESSION_BACKEND=none deployments where any worker can handle any request (client carries full state). New design: each token is an itsdangerous-signed payload containing the pending cookies and a random nonce. Any worker holding the same MESOP_COOKIE_SECRET_KEY can verify the signature, so no sticky sessions are required. Per-process nonce tracking ensures strict single-use within a process; cross-worker replay is impractical given the CSRF Origin check and HTTPS in production. Also adds MESOP_COOKIE_SECRET_KEY to playwright.config.ts so the e2e test server starts with the key set (required since me.set_cookie() now always needs it). https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
The apply-cookies token is strictly single-use within a single process but can be replayed to a different worker in multi-worker deployments. Document the mitigation (CSRF Origin check + 60s TTL) and when sticky sessions are needed for cryptographic single-use guarantees. https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 17 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- ui.proto: update ApplyCookiesCommand comment from "server-side token used to retrieve" to "one-time signed token used to apply" to match the actual itsdangerous-signed implementation - server.py: soften apply_cookies docstring — "replay attacks are not possible" overstated; now says single-use within a process, cross- worker replay mitigated by CSRF check + TTL - docs/guides/auth.md: update "How it works" section from old server- side storage description to the actual signed-token design; add link to MESOP_COOKIE_SECRET_KEY and note per-process nonce enforcement https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
- UP035: use collections.abc.Generator/Sequence instead of typing - I001: sort itsdangerous imports inside pop() https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
…uth.md - cookie_class_test.py: apply ruff-format (trailing whitespace / blank line style) — the check linter was clean but the formatter found diffs - docs/guides/auth.md: add Security notes bullet explaining that cookie values are Base64-readable in the ApplyCookiesCommand token while it transits browser JS; recommend encrypted=True for values that must never be visible to client-side code https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
The env var is required for all cookie operations (set_cookie, delete_cookie, cookie), not just signed/encrypted ones. Add an admonition block at the top of the Cookies section and update the signed/encrypted description to clarify it adds protection on top of the already-required key. https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 17 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- docs/guides/auth.md: tighten MESOP_COOKIE_SECRET_KEY admonition — write ops always need it; me.cookie() with plain cookieclass does not - conftest.py: make _stub_mesop_package() a no-op when real proto modules are importable (Bazel build), matching _stub_generated_protos() - server.py: flush pending cookies after the user-event for-loop so cookies set by a generator that yields 0 times are still sent - set_cookie.py: add note to httponly arg docstring that the value is briefly Base64-visible in the ApplyCookiesCommand token as it passes through browser JS; recommend encrypted=True for fully opaque values https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
- docs/api/config.md: shorten description to "Secret key required for using Mesop cookies" — no need to enumerate specific call sites - cookie_class.py: hoist single "Requires MESOP_COOKIE_SECRET_KEY" note to class level; remove per-feature repetition in signed/encrypted descriptions and Args - server.py + cookie_class.py: simplify error messages to "must be set to use Mesop cookies" (not specific to set/delete) https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
|
|
||
| Mesop provides a first-class cookie API so you can persist small pieces of data (session tokens, user preferences, etc.) in the browser without managing raw `Set-Cookie` headers yourself. | ||
|
|
||
| !!! note "MESOP_COOKIE_SECRET_KEY requirements" |
There was a problem hiding this comment.
This note can be removed. This is out dated since the secret key is needed to use cookies. It's true that some operations don't require the secret key, but in practice it's not usable without it. That technicalities create confusion.
Mainly we always need the secret key for the Apply Cookies Command.
|
|
||
| ### Signed and encrypted cookies | ||
|
|
||
| By default `@me.cookieclass` stores the JSON value in plain text — readable in browser DevTools. For cookies that carry sensitive data you can add tamper-protection or full encryption by setting `signed=True` or `encrypted=True` on the decorator. [`MESOP_COOKIE_SECRET_KEY`](../api/config.md#mesop_cookie_secret_key) is already required for all cookie ops; these options add extra protection on top of that. |
There was a problem hiding this comment.
Remove last sentence about Mesop Cookie Secret. It's too much repetition.
| class SessionCookie: | ||
| username: str = "" | ||
|
|
||
| Both ``signed`` and ``encrypted`` require ``MESOP_COOKIE_SECRET_KEY`` to be set. |
There was a problem hiding this comment.
Remove this line or update to say that MESOP_COOKIE_SECRET_KEY is required to use the class. I think it should be removed since it's mentioned again below in another comment.
| The login state persists across hard refreshes and new tabs because the | ||
| cookie is read in on_load. | ||
|
|
||
| Note: me.set_cookie() and me.delete_cookie() require MESOP_COOKIE_SECRET_KEY |
There was a problem hiding this comment.
Remove this line "Note: me.set_cookie() and me.delete_cookie() require MESOP_COOKIE_SECRET_KEY
to be set so that cookie tokens are cryptographically signed." Although it is true, it's too much detail. Just leave it with the example of how to start Mesop with MESOP_COOKIE_SECRET_KEY
- auth.md: simplify MESOP_COOKIE_SECRET_KEY admonition to just show how to set it; remove last sentence from signed/encrypted section - cookie_class.py: remove redundant module-level MESOP_COOKIE_SECRET_KEY note (already stated in class docstring) - set_cookie.py: add warning log in _resolve_secure RuntimeError path explaining when it occurs and that it's unexpected in production - set_cookie.py example: drop explanation sentence, keep only the start command showing how to set MESOP_COOKIE_SECRET_KEY https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
Summary
Adds a native
me.set_cookie()andme.delete_cookie()API so Mesop apps can set browser cookies from within event handlers. Closes #1374.Also closes #1304.
Problem
Mesop's
/__ui__endpoint is an SSE stream — Flask commits HTTP response headers (includingSet-Cookie) before the event-handler generator body runs. This makesafter_this_requestand direct cookie manipulation ineffective from within event handlers, leaving no built-in way to issueSet-Cookieheaders from app code.Solution: two-phase cookie application
me.set_cookie()/me.delete_cookie()queuePendingCookieobjects on the request context (Python side only — no network round-trip yet).generate_data(), any pending cookies are encoded into a short-lived itsdangerous-signed token (60 s TTL) that embeds the cookie data directly (no server-side storage). AnApplyCookiesCommandcarrying that token is appended to the render commands. Any server worker holding the sameMESOP_COOKIE_SECRET_KEYcan verify and redeem the token, so no sticky sessions are required.POST /__apply-cookiesrequest with the token in the form body (t=,credentials: 'same-origin'). Sending the token in the body (not the URL) keeps it out of server access logs and browser history./__apply-cookiesFlask endpoint validates the Origin header (same-site check, matching/__ui__), verifies the itsdangerous signature and TTL, marks the token's nonce as used (single-use within a process), then returns an empty204response with the appropriateSet-Cookieheaders.This approach works identically in both SSE and WebSocket modes. The signed token with a short TTL and per-process nonce tracking prevents replay attacks; the same-site Origin check prevents a cross-site attacker from replaying a captured token against a victim's browser.
Note on value visibility: the token payload is signed but not encrypted, so cookie values are Base64-readable as the token passes through browser JavaScript. For values that must never be visible to JS, use
@me.cookieclass(encrypted=True).Note on multi-worker replay: nonces are tracked per-process, so cross-worker replay is theoretically possible but mitigated by the CSRF Origin check and the short TTL. See
MESOP_COOKIE_SECRET_KEYdocs for details.MESOP_COOKIE_SECRET_KEYis required for all cookie write operations (me.set_cookie(),me.delete_cookie()).Files changed
mesop/protos/ui.protoApplyCookiesCommandmessage (field 8 inCommandoneof)mesop/protos/ui_pb2.pymesop/runtime/context.pyPendingCookiedataclass; addset_cookie(),delete_cookie(),pending_cookies(),clear_pending_cookies()toContextmesop/commands/set_cookie.pyme.set_cookie()/me.delete_cookie()wrappersmesop/__init__.pymesop/server/server.py_CookieTokenCache(itsdangerous-signed + nonce tracking),maybe_append_apply_cookies_command(),/__apply-cookiesroute with same-site CSRF guardmesop/web/src/shell/shell.tshasApplyCookies()command with afetch()POST to/__apply-cookiesUsage example
Test plan
me.set_cookie()from a button click handler; verifySet-Cookieheader appears in the/__apply-cookiesresponse in browser DevToolsme.delete_cookie()and verify the cookie is removed/__apply-cookiesa second time with the same token returns400/__apply-cookiesafter 60 s returns400MESOP_WEBSOCKETS_ENABLED=true— cookie should still be applied correctlyMESOP_BASE_URL_PATHset — client should call the prefixed endpoint