ProvenPin Install

Starlette SessionMiddleware signed cookies with itsdangerous: working versions (tested 2026-10-15)

Add cookie sessions to a Starlette app via SessionMiddleware, then test login, tampering, logout, expiry and JSON safety fully offline over httpx ASGITransport.

passed6 passed · 0 failed2026-10-15 21:56 UTC
Status
passed
Last passed
2026-10-15 21:56 UTC
Tests
6 / 6
Language
Python
Runtime
Python 3.13.16
pinned versions5
starlette@1.8.0itsdangerous@2.2.0httpx@0.28.1pytest@9.1.1pytest-asyncio@1.4.0
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt
.venv/bin/python -m pytest -q
test environment
Python 3.13.16
linux 6.17.0-1022-azure
x64
runner: github-actions 38813530570

The code

Exactly the files that passed. Your agent can pull them with get_recipe("starlette-itsdangerous-sessions") (install), or see them on GitHub.

requirements.txt
starlette==1.8.0
itsdangerous==2.2.0
httpx==0.28.1
pytest==9.1.1
pytest-asyncio==1.4.0
app.py
import json
from base64 import b64decode, b64encode

from itsdangerous import TimestampSigner
from starlette.applications import Starlette
from starlette.middleware.sessions import SessionMiddleware
from starlette.responses import JSONResponse
from starlette.routing import Route


async def login(request):
    request.session["user"] = request.path_params["user"]
    return JSONResponse({"ok": True})


async def me(request):
    return JSONResponse({"user": request.session.get("user")})


async def logout(request):
    request.session.clear()
    return JSONResponse({"ok": True})


async def stash_non_json(request):
    request.session["tags"] = {"a", "b"}
    return JSONResponse({"ok": True})


def create_app(secret_key="dev-secret", max_age=14 * 24 * 3600):
    app = Starlette(
        routes=[
            Route("/login/{user}", login),
            Route("/me", me),
            Route("/logout", logout),
            Route("/stash-non-json", stash_non_json),
        ]
    )
    app.add_middleware(SessionMiddleware, secret_key=secret_key, max_age=max_age)
    return app


def encode_session(session, secret_key, timestamp=None):
    # same wire format SessionMiddleware produces: base64(json) + sig
    data = b64encode(json.dumps(session).encode("utf-8"))
    signer = TimestampSigner(str(secret_key))
    if timestamp is not None:
        signer.get_timestamp = lambda: timestamp
    return signer.sign(data).decode("utf-8")
tests/test_sessions.py
import json
import time
from base64 import b64decode

import httpx
import pytest
from itsdangerous import TimestampSigner

from app import create_app, encode_session


def client_for(app):
    transport = httpx.ASGITransport(app=app)
    return httpx.AsyncClient(transport=transport, base_url="http://test")


@pytest.fixture
def app():
    return create_app(secret_key="s3cr3t")


async def test_session_roundtrip(app):
    async with client_for(app) as client:
        assert (await client.get("/login/alice")).status_code == 200
        assert client.cookies.get("session")
        assert (await client.get("/me")).json() == {"user": "alice"}


async def test_session_is_signed_not_encrypted(app):
    async with client_for(app) as client:
        await client.get("/login/alice")
        raw = client.cookies["session"]
    payload = TimestampSigner("s3cr3t").unsign(raw, max_age=60)
    assert json.loads(b64decode(payload)) == {"user": "alice"}


async def test_tampered_cookie_is_ignored(app):
    async with client_for(app) as client:
        r = await client.get("/me", headers={"Cookie": "session=not-a-real-signature"})
    assert r.json() == {"user": None}


async def test_non_json_value_raises(app):
    async with client_for(app) as client:
        with pytest.raises(TypeError):
            await client.get("/stash-non-json")


async def test_logout_clears_cookie(app):
    async with client_for(app) as client:
        await client.get("/login/alice")
        assert (await client.get("/me")).json() == {"user": "alice"}
        await client.get("/logout")
        assert client.cookies.get("session") is None
        assert (await client.get("/me")).json() == {"user": None}


async def test_max_age_expiry():
    app = create_app(secret_key="s3cr3t", max_age=60)
    old = encode_session({"user": "alice"}, "s3cr3t", timestamp=int(time.time()) - 3600)
    async with client_for(app) as client:
        r = await client.get("/me", headers={"Cookie": f"session={old}"})
    assert r.json() == {"user": None}
pytest.ini
[pytest]
pythonpath = .
asyncio_mode = auto

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.