Feature: Add async fastapi support#9
Open
joschrag wants to merge 37 commits into
Open
Conversation
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
75e3c34 to
7303a35
Compare
The loop's last iteration (pct=100) already sets task-bar to 100, and the is_shutdown early-return path skips the post-loop line too, so it was dead.
…, redirect, coerce_response).
…ware with body replay).
…API server.state).
…oauth state branch.
…ession lookup under -O. secure_session was silently dropped on FastAPI; route it through Backend.setup_session so it maps to SessionMiddleware https_only, matching the Flask/Quart SESSION_COOKIE_SECURE behaviour. Replace the FastAPI session lookup's reliance on Starlette's bare `assert "session" in scope` (stripped under python -O, which would surface a KeyError) with an explicit scope check that raises RuntimeError consistently.
…/ba005. ba002/ba003 collided with the groups test ids; renumber the auth_func login and misconfiguration tests across the Flask, Quart and FastAPI variants so the short-id prefixes stay unique and parallel across backends.
…ples. Keep scripts/, .coverage and .vscode/ out of the tracked tree so ruff (which respects .gitignore) stays green, and add an examples/** per-file-ignore for the D/DOC rule families. Example apps are runnable demos, not shipped library code, so forced docstring coverage on every demo page was the only thing keeping `ruff check .` red on this branch.
…ic_routes. get_public_routes/get_public_callbacks run inside _authorize on every request yet rebuilt a Backend and re-ran the isinstance chain each time. Route all four public_routes sites through get_active_backend() — the process-global Auth.__init__ already caches for exactly this "no instance in scope" case — dropping per-request allocation churn. Construct the Flask fallback lazily inside get_active_backend() instead of as an eager module-level _DEFAULT_BACKEND, so Flask is no longer cemented as the default at import. Behaviour is identical under the one-backend-per-process model: store/read_config take the server explicitly, so the resolved backend only supplies the storage strategy, which already matches the running server.
…m and merge the view triplets. OIDCAuth branched on isinstance(backend, FastAPIBackend/QuartBackend) to pick a sync, _async, or _fastapi view set, and the _fastapi triplet differed from _async only in passing the Starlette request to authlib and coercing responses by hand. Push those differences into the backend: add Backend.is_async, Backend.get_oauth (symmetric with make_oauth so retrieval knows where storage put the registry), and Backend.oauth_authorize_redirect/oauth_authorize_access_token (the FastAPI overrides inject the ContextVar-resolved request the Starlette client needs). OIDCAuth now selects views on backend.is_async, the Quart and FastAPI paths share one _async view set, and every async view funnels its return through backend.coerce_response — the single coercion boundary, with a bare str now rendered as HTML to match Flask/Quart and absorb the old logout special case. get_oauth delegates to the backend, dropping the StarletteRequest re-export leak from oidc_auth. Verified by the full OIDC integration suite (Flask/Quart/FastAPI) and the wiring tests.
The FastAPI auth middleware caches and replays the callback body so Dash's inner middleware can re-parse it, but the replacement receive returned the same http.request event on every call. An ASGI app that polls receive() after the body to detect a client disconnect would loop on the same body event. Track whether the body was delivered and return http.disconnect on subsequent reads, per the ASGI contract. Dash's inner middleware doesn't poll, so this was latent, not breaking.
…state validation. Pin the negative paths the new code most needs locked down: the auth middleware's unparseable-body branch (malformed JSON reaches decide as None) and its short-circuit-after-body-parse branch (decide blocks when needs_body is True), plus the replayed receive emitting http.disconnect once the body is consumed. Add a Flask integration test that drives the real authlib state path end to end — /login stores the state, and /callback with a tampered or missing state is rejected 401 — so the anti-CSRF invariant the browser tests mock past stays covered against authlib changes.
…t Auth construction. Calling app._setup_server() inside enable_ws_auth ran Dash's server setup at Auth(...) construction time -- before later module-level @callback registrations and outside Dash's normal lifecycle. Move the GLOBAL_CALLBACK_MAP migration into _authorize_ws_message, which already runs (and resolves the owning app) just before Dash validates the request against callback_map. It is now lazy and idempotent: fires on the first real WS callback_request, after every @callback is registered, and is a single flag check thereafter. No construction-time double initialization.
…tup. The multi-app hook resolution test broke when the hook moved to get_active_backend().ws_identity(ws) (it left the active backend unset, so the FlaskBackend fallback's ws_identity raised and the hook failed closed to 4401). Set a QuartBackend as active to exercise the Quart identity path the test monkeypatches, give the Auth double an app stub for the lazy _setup_server() migration, and assert only the owning app's map is set up.
… the global fallback. add_public_routes, get_public_routes, and get_public_callbacks called get_active_backend() -- the process-global that falls back to FlaskBackend when no Auth has set it -- then store_config/read_config on app.server. On a FastAPI app the fallback writes to the nonexistent server.config (AttributeError); detect_backend(app.server) routes through server.state instead. Brings the three siblings in line with public_callback, which was already fixed this way. Adds a regression test that exercises the helpers on a FastAPI app with the active backend left at its Flask fallback.
7303a35 to
fa38ff8
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add FastAPI backend support for authenticated Dash apps
Adds a FastAPIBackend so dash-auth-async can protect Dash 4.x apps served on
FastAPI/Starlette — covering BasicAuth, OIDC, group protection, and authenticated
WebSocket callbacks — alongside the existing Flask and Quart backends.
Added
url_for/redirect, response coercion, and detect_backend wiring
http.disconnect once the body is consumed)
through, hardened session lookup under -O
make_oauth/get_oauth state branch, annotated view params so Starlette injects the request
callback_map migration in the message hook, fails closed
global fallback
counter/clock page + private progress-task page with design/plan docs and README
integration (mocked IDP) + wiring, OIDC state/CSRF, WebSocket security +
protected-callback streaming, and unit tests for the backend, group protection, and public routes
Changed
backends stay polymorphic instead of isinstance-dispatched
duplicated view triplets
*_fastapi), re-ided auth_func login tests off ba002/ba003, graceful threaded-server
shutdown in conftest.py
scoped docstring rules off examples; ignore local scratch dirs
Fixed
process-global fallback