Skip to content

Authentication Middleware Pattern broke in 0.19+ #8451

@wlach

Description

@wlach

Describe the bug

It appears that the authentication middleware approach described here broke in 0.19.0 and above (issue still present on latest as of this writing 0.20.2): https://docs.marimo.io/guides/deploying/programmatically/#authentication-middleware-example

See attached minimal reproduction example (run with uv run name_of_file.py). If you downgrade marimo to 0.18.4 via the header you'll see the state propagated into the notebook correctly. Does not work with later versions.

Will you submit a PR?

  • Yes

Environment

Details
{
  "marimo": "0.20.2",
  "editable": false,
  "location": "/Users/wlach/src/voltus/voltuspy/marimo-app-server/.venv/lib/python3.11/site-packages/marimo",
  "OS": "Darwin",
  "OS Version": "24.6.0",
  "Processor": "arm",
  "Python Version": "3.11.13",
  "Locale": "C/en_US",
  "Binaries": {
    "Browser": "145.0.7632.110",
    "Node": "v20.19.5",
    "uv": "0.9.14 (Homebrew 2025-12-01)"
  },
  "Dependencies": {
    "click": "8.3.1",
    "docutils": "0.22.3",
    "itsdangerous": "2.2.0",
    "jedi": "0.19.2",
    "markdown": "3.10",
    "narwhals": "2.13.0",
    "packaging": "25.0",
    "psutil": "7.1.3",
    "pygments": "2.19.2",
    "pymdown-extensions": "10.18",
    "pyyaml": "6.0.3",
    "starlette": "0.50.0",
    "tomlkit": "0.13.3",
    "typing-extensions": "4.15.0",
    "uvicorn": "0.38.0",
    "websockets": "15.0.1"
  },
  "Optional Dependencies": {
    "altair": "6.0.0",
    "anywidget": "0.9.21",
    "duckdb": "1.4.3",
    "loro": "1.10.3",
    "nbformat": "5.10.4",
    "openai": "2.9.0",
    "pandas": "1.5.3",
    "polars": "1.36.0",
    "pyarrow": "22.0.0",
    "pytest": "9.0.2",
    "python-lsp-ruff": "2.3.0",
    "python-lsp-server": "1.14.0",
    "ruff": "0.14.8",
    "sqlglot": "28.6.0",
    "watchdog": "5.0.3"
  },
  "Experimental Flags": {}
}

Code to reproduce

#!/usr/bin/env python3
# /// script
# requires-python = ">=3.11"
# dependencies = [
#     "fastapi",
#     "marimo==0.20.2",
#     "uvicorn",
# ]
# ///
"""Reproduction: BaseHTTPMiddleware sets scope["user"] for HTTP but NOT WebSocket.

marimo 0.20+ reads scope["user"] during WebSocket session init
(HTTPRequest.from_request(websocket)), so the dict set by HTTP middleware
is never seen. Instead marimo's own AuthBackend returns SimpleUser("user"),
which is not subscriptable — causing:

    TypeError: 'SimpleUser' object is not subscriptable

Then open http://localhost:8000/ — the cell that accesses
mo.app_meta().request.user["username"] will blow up.
"""

import textwrap, tempfile, os, pathlib

import marimo
import uvicorn
from fastapi import FastAPI
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request


# ── 1. Write a tiny marimo notebook to a temp file ──────────────────────────
notebook_code = textwrap.dedent('''\
    import marimo

    __generated_with = "0.20.2"
    app = marimo.App(width="medium")

    @app.cell
    def _():
        import marimo as mo
        return (mo,)

    @app.cell
    def _(mo):
        req = mo.app_meta().request
        user = req.user if req else None

        mo.md(f"""
        ## User info from `mo.app_meta().request.user`

        - **type**: `{type(user).__name__}`
        - **value**: `{user}`
        - **subscript test**: `{user['username'] if isinstance(user, dict) else 'N/A — not a dict!'}`
        """)
        return

    if __name__ == "__main__":
        app.run()
''')

tmp_dir = tempfile.mkdtemp(prefix="marimo_repro_")
notebook_path = os.path.join(tmp_dir, "repro.py")
pathlib.Path(notebook_path).write_text(notebook_code)
print(f"Notebook written to {notebook_path}")


# ── 2. Build the ASGI app exactly like the marimo docs suggest ──────────────
app = FastAPI(title="Repro: scope['user'] not visible on WebSocket")


# This is the canonical middleware from marimo's docs.
# It ONLY runs for HTTP requests — NOT for WebSocket upgrades.
class AuthMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        request.scope["user"] = {
            "is_authenticated": True,
            "username": "example_user",
        }
        request.scope["meta"] = {"some_key": "some_value"}
        response = await call_next(request)
        return response


app.add_middleware(AuthMiddleware)

# Mount marimo
marimo_app = (
    marimo.create_asgi_app(include_code=True)
    .with_app(path="/", root=notebook_path)
    .build()
)
app.mount("/", marimo_app)


# ── 3. Run ──────────────────────────────────────────────────────────────────
if __name__ == "__main__":
    print("\n→  Open http://localhost:8000/  and look at the cell output.\n")
    print("Expected: user is a dict with 'username'")
    print("Actual (marimo ≥0.20): SimpleUser object — TypeError on subscript\n")
    uvicorn.run(app, host="127.0.0.1", port=8000)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions