Authentication
PyView works with Starlette’s session and authentication systems—you’re not locked into any particular auth pattern. This guide covers accessing session data, protecting routes, and implementing custom authentication.
Sessions
Sessions let you persist data across requests for a user. The session dict is passed to your mount() method.
Enabling Sessions
Add Starlette’s SessionMiddleware to your application:
from starlette.middleware.sessions import SessionMiddlewarefrom pyview import PyView
app = PyView()app.add_middleware(SessionMiddleware, secret_key="your-secret-key")Important: Use a strong, random secret key in production. Generate one with
openssl rand -base64 32.
Accessing Session Data
The session is available as the second parameter to mount():
from pyview import LiveView, LiveViewSocketfrom typing import TypedDict, Optional
class DashboardContext(TypedDict): user_id: Optional[str] username: Optional[str] notifications: list
class DashboardLiveView(LiveView[DashboardContext]): async def mount(self, socket: LiveViewSocket[DashboardContext], session): # Read from session user_id = session.get("user_id") username = session.get("username")
socket.context = { "user_id": user_id, "username": username, }
if user_id: # Load user-specific data socket.context["notifications"] = await load_notifications(user_id)Session Persistence
Here’s the key pattern: sessions are read-only within LiveViews. Your LiveView reads session data, but to modify it (login, logout, updating preferences), use regular Starlette routes.
A clean way to organize this is to put auth routes in their own module:
from starlette.applications import Starlettefrom starlette.routing import Routefrom starlette.responses import RedirectResponse
async def login(request): form = await request.form() user = await authenticate(form["username"], form["password"])
if user: request.session["user_id"] = user.id request.session["username"] = user.username return RedirectResponse("/dashboard", status_code=303)
return RedirectResponse("/login?error=invalid", status_code=303)
async def logout(request): request.session.clear() return RedirectResponse("/", status_code=303)
routes = [ Route("/login", login, methods=["POST"]), Route("/logout", logout, methods=["POST"]),]
auth_app = Starlette(routes=routes)Then mount it in your main app:
from auth import auth_app
app = PyView()app.mount("/auth", auth_app)Now your auth endpoints are at /auth/login and /auth/logout, and your main app stays clean.
Authentication with @requires
The @requires decorator protects LiveViews, requiring users to have specific authentication scopes.
Basic Usage
from pyview import LiveViewfrom pyview.auth import requires
@requires("authenticated")class SettingsLiveView(LiveView[SettingsContext]): async def mount(self, socket, session): # Only authenticated users reach this code socket.context = {"user_id": session["user_id"]}If the user isn’t authenticated, they’ll receive a 403 Forbidden response.
Redirect Unauthenticated Users
Redirect to a login page instead of showing an error:
@requires("authenticated", redirect="/login")class DashboardLiveView(LiveView[DashboardContext]): async def mount(self, socket, session): socket.context = {"user": await load_user(session["user_id"])}Multiple Scopes
Require multiple scopes (user must have all of them):
@requires(["authenticated", "admin"])class AdminLiveView(LiveView[AdminContext]): async def mount(self, socket, session): socket.context = {"users": await load_all_users()}Custom Status Code
@requires("authenticated", status_code=401)class ProtectedLiveView(LiveView): passSetting Up Authentication
The @requires decorator works with Starlette’s authentication system. You need an AuthenticationMiddleware that sets the user’s scopes.
Basic Session-Based Auth
from starlette.authentication import ( AuthCredentials, AuthenticationBackend, SimpleUser,)from starlette.middleware.authentication import AuthenticationMiddleware
class SessionAuthBackend(AuthenticationBackend): async def authenticate(self, conn): user_id = conn.session.get("user_id")
if user_id is None: # Not logged in - no scopes return AuthCredentials([]), None
# Load user and their permissions user = await load_user(user_id)
scopes = ["authenticated"] if user.is_admin: scopes.append("admin")
return AuthCredentials(scopes), SimpleUser(user.username)
app = PyView()app.add_middleware(SessionMiddleware, secret_key="...")app.add_middleware(AuthenticationMiddleware, backend=SessionAuthBackend())OAuth Example
For OAuth providers (Google, GitHub, etc.), see the pyview-example-auth repository which demonstrates integration with authlib.
Custom Auth Providers
For advanced cases, implement the AuthProvider protocol:
from starlette.websockets import WebSocketfrom starlette.responses import Responsefrom pyview.auth import AuthProvider, AuthProviderFactoryfrom pyview import LiveView
class APIKeyAuthProvider(AuthProvider): def __init__(self, required_key: str): self.required_key = required_key
async def has_required_auth(self, websocket: WebSocket) -> bool: """Called when WebSocket connects to verify auth.""" # Check header, query param, or session api_key = websocket.query_params.get("api_key") return api_key == self.required_key
def wrap(self, func): """Wrap the HTTP endpoint for initial page load.""" async def wrapped(request): api_key = request.query_params.get("api_key") if api_key != self.required_key: return Response("Unauthorized", status_code=401) return await func(request) return wrapped
# Apply to a LiveViewclass APIProtectedView(LiveView): pass
AuthProviderFactory.set(APIProtectedView, APIKeyAuthProvider("secret-key"))app.add_live_view("/api/dashboard", APIProtectedView)Secret Key Configuration
PyView uses a secret key for session serialization. Set it via environment variable:
export PYVIEW_SECRET="your-secret-key-here"If not set, PyView generates a random key on startup—fine for development, but sessions won’t persist across restarts.
Generate a production key:
openssl rand -base64 32Complete Example
Here’s a full example with login, logout, and protected routes, organized into separate modules:
from starlette.applications import Starlettefrom starlette.authentication import AuthCredentials, AuthenticationBackend, SimpleUserfrom starlette.routing import Routefrom starlette.responses import RedirectResponse, HTMLResponse
class SessionAuthBackend(AuthenticationBackend): async def authenticate(self, conn): user_id = conn.session.get("user_id") if not user_id: return AuthCredentials([]), None return AuthCredentials(["authenticated"]), SimpleUser(conn.session.get("username", ""))
async def login_page(request): error = request.query_params.get("error", "") return HTMLResponse(f""" <form method="post" action="/auth/login"> <input name="username" placeholder="Username" required> <input name="password" type="password" placeholder="Password" required> <button type="submit">Login</button> {"<p style='color:red'>Invalid credentials</p>" if error else ""} </form> """)
async def login(request): form = await request.form() # In production, verify against database if form["username"] == "demo" and form["password"] == "demo": request.session["user_id"] = "1" request.session["username"] = form["username"] return RedirectResponse("/dashboard", status_code=303) return RedirectResponse("/auth/login?error=1", status_code=303)
async def logout(request): request.session.clear() return RedirectResponse("/auth/login", status_code=303)
routes = [ Route("/login", login_page, methods=["GET"]), Route("/login", login, methods=["POST"]), Route("/logout", logout, methods=["POST"]),]
auth_app = Starlette(routes=routes)from pyview import LiveView, LiveViewSocketfrom pyview.auth import requiresfrom typing import TypedDict
class DashboardContext(TypedDict): username: str
@requires("authenticated", redirect="/auth/login")class DashboardLiveView(LiveView[DashboardContext]): async def mount(self, socket: LiveViewSocket[DashboardContext], session): socket.context = {"username": session.get("username", "User")}from starlette.middleware.sessions import SessionMiddlewarefrom starlette.middleware.authentication import AuthenticationMiddlewarefrom pyview import PyViewfrom auth import auth_app, SessionAuthBackendfrom views.dashboard import DashboardLiveView
app = PyView()app.add_middleware(SessionMiddleware, secret_key="change-me-in-production")app.add_middleware(AuthenticationMiddleware, backend=SessionAuthBackend())
app.mount("/auth", auth_app)app.add_live_view("/dashboard", DashboardLiveView)Related
- LiveView Lifecycle — The
mount()method where you access sessions - Routing — Adding regular routes alongside LiveViews