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 now uses a dedicated centralized auth configuration file for all auth-related behavior.
Configuration
Auth settings are now centralized in
src/lib/auth/auth_config.py.
This file is the single place where you define token behavior, route
visibility, auth routes, redirect paths, API prefixes, and optional
role-based access rules.
This keeps authentication configuration clean, reusable, and separate from
your application bootstrapping logic. Secrets such as
AUTH_SECRET
and
AUTH_COOKIE_NAME
should stay in your
.env,
while framework-level auth behavior is configured in
build_auth_settings().
Centralized Auth Rules
Define all authentication behavior in one place: token validity, route access mode, auth pages, redirect destinations, and role-based route restrictions.
from __future__ import annotations from casp.auth import AuthSettings def build_auth_settings() -> AuthSettings: return AuthSettings( # Token settings default_token_validity="1h", token_auto_refresh=False, # Route protection is_all_routes_private=False, public_routes=["/"], auth_routes=["/signin", "/signup"], private_routes=[], # Role-based access is_role_based=False, role_identifier="role", role_based_routes={}, # Redirects / prefixes default_signin_redirect="/dashboard", default_signout_redirect="/signin", api_auth_prefix="/api/auth", )
default_token_validity
Controls how long the auth token remains valid, for example
"1h",
"7d", or
other supported duration strings.
token_auto_refresh
Enables or disables automatic token refresh behavior for active sessions.
is_all_routes_private
When set to
True,
every route is private by default unless explicitly listed in
public_routes.
public_routes / auth_routes / private_routes
Define which paths are public, which belong to auth pages, and which require authentication when global-private mode is not enabled.
default_signin_redirect
The fallback redirect used after a successful sign-in when no custom destination is provided.
default_signout_redirect
The default location users are sent to after signing out.
is_role_based / role_identifier
Enables role-aware authorization and defines which payload field is used as the user role key.
role_based_routes
Maps paths to a list of allowed roles. The expected format is
PATH -> [ROLES].
Role-Based Routes
When role-based access is enabled, Caspian checks the configured
role_identifier
inside the auth payload and matches the current path against
role_based_routes.
Example RBAC Configuration
def build_auth_settings() -> AuthSettings: return AuthSettings( is_role_based=True, role_identifier="role", role_based_routes={ "/report": ["admin"], "/admin": ["admin", "superadmin"], }, )
The Auth Object
The global auth object
manages the session lifecycle. It abstracts FastAPI's response and cookie
logic, and uses your centralized configuration automatically.
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 submits the form via
RPC. After authentication, the redirect falls back to the centralized
auth config when no
next
value is provided.
from casp.auth import auth from casp.prisma.db import prisma from casp.rpc import rpc from werkzeug.security import check_password_hash from casp.page import render_page @rpc() async def do_login(email: str, password: str, next: str | None = None): # 1. Fetch user asynchronously 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. Build a safe payload without sensitive fields user_data = user.to_dict(omit={"password": True}) # 4. Resolve redirect from query or centralized config redirect_url = next or auth.settings.default_signin_redirect # 5. Create session + redirect response return auth.sign_in(data=user_data, redirect_to=redirect_url) def page(): 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 { const payload = { ...data, next: searchParams.get("next") }; 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, protect entire pages, or define path-level rules centrally in
auth_config.py.
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__)