Authentication
Caspian provides a robust, session-based authentication system built on FastAPI's security utilities. It is secure by default (HttpOnly cookies), supports Role-Based Access Control (RBAC), and integrates seamlessly with the async RPC layer.
Configuration
Auth settings are now centralized in
main.py
within the
setup_auth() function. This
allows you to configure global route protection, define roles, and set
redirect paths right at the application entry point.
Global Protection & Rules
You can make your application private by default using
is_all_routes_private=True, or whitelist specific public routes.
def setup_auth(): configure_auth(AuthSettings( # Token validity duration default_token_validity="7d", # Route protection mode # False = only explicitly private routes require auth is_all_routes_private=False, # Define public/private paths public_routes=["/"], auth_routes=["/signin", "/signup"], # Where to go after login/logout default_signin_redirect="/dashboard", default_signout_redirect="/signin", # Role-based access control is_role_based=True, role_based_routes={ "/admin": ["admin", "superadmin"], }, ))
The Auth Object
The global auth object
manages the session lifecycle. It abstracts FastAPI's response and cookie
logic.
auth.sign_in(data, redirect_to?)
Creates a session.
data is a
dict stored in the secure session cookie. Returns a response object
handling the cookie set.
auth.sign_out(redirect_to?)
Destroys the session and clears HttpOnly cookies.
auth.is_authenticated()
Returns
True if
the current session is valid.
auth.get_payload()
Retrieves the user data stored during sign-in.
Implementation Example
A complete async sign-in flow. The backend handles verification using Prisma (Async), while the frontend handles the form submission via RPC.
from casp.auth import auth from casp.prisma.db import prisma from casp.rpc import rpc from werkzeug.security import check_password_hash @rpc() async def do_login(email: str, password: str, next: str | None = None): # 1. Fetch User (Async Await) user = await prisma.user.find_unique( where={'email': email}, include={'userRole': True} ) if not user: raise ValueError("Invalid credentials") # 2. Verify Password stored_password = user.password if not stored_password or not check_password_hash(stored_password, password): raise ValueError("Invalid credentials") # 3. Create Session Payload (exclude sensitive data) user_data = user.to_dict(omit={'password': True}) # 4. Sign In & Redirect # Use global settings for default redirect if 'next' is missing redirect_url = next or auth.settings.default_signin_redirect return auth.sign_in(data=user_data, redirect_to=redirect_url) def page(): # Renders the HTML template return render_page(__file__)
<form onsubmit="handleSubmit(event)" class="space-y-5"> <!-- Error Message --> <p class="text-red-600 text-sm font-medium">{message}</p> <div class="space-y-2"> <label class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"> Email </label> <input name="email" type="email" required class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:ring-4 focus-visible:outline-1 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm focus-visible:ring-ring/20 dark:focus-visible:ring-ring/40 focus-visible:border-ring" /> </div> <div class="space-y-2"> <label class="text-sm font-medium leading-none"> Password </label> <input name="password" type="password" required class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] placeholder:text-muted-foreground focus-visible:ring-4 focus-visible:outline-1 focus-visible:ring-ring/20 dark:focus-visible:ring-ring/40 focus-visible:border-ring md:text-sm" /> </div> <button type="submit" class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 bg-primary text-primary-foreground shadow-xs hover:bg-primary/90 h-9 px-4 py-2 w-full" > Sign in </button> </form> <script> const [message, setMessage] = pp.state(""); async function handleSubmit(e) { e.preventDefault(); const formData = new FormData(e.target); const data = Object.fromEntries(formData); try { // Pass 'next' param if available const payload = { ...data, next: searchParams.get("next") }; // Server returns redirect command automatically handled by client await pp.rpc("do_login", payload); } catch (error) { setMessage("Invalid email or password"); } } </script>
Protecting Routes
You can protect individual actions using the
@rpc
decorator, or protect entire pages.
Action Level (RPC)
Best for securing specific buttons or form submissions. The client receives a 401/403 error.
@rpc(require_auth=True) async def delete_account(): # Only runs if authenticated await prisma.user.delete(...)
Page Level
Best for securing entire views. Redirects unauthenticated users to the sign-in page.
from casp.auth import require_auth @require_auth def page(): return render_page(__file__)