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, Caspian uses the centralized
default_signin_redirect
from auth_config.py.
Keep redirect selection out of the sign-in page.
from casp.auth import auth from src.lib.prisma import prisma from casp.rpc import rpc from casp.validate import Rule, Validate from werkzeug.security import check_password_hash from casp.component_decorator import html @rpc() async def do_login(email: str, password: str): clean_email = Validate.email(email) password_check = Validate.with_rules(password, [Rule.REQUIRED, Rule.min(8)]) if clean_email is None or password_check is not True: return "error": "Invalid credentials." # 1. Fetch user asynchronously through the generated ORM user = await prisma.user.find_unique( where="email": clean_email, include="userRole": True ) if not user: return "error": "Invalid credentials." # 2. Verify password stored_password = user.password if not stored_password or not check_password_hash(stored_password, password): return "error": "Invalid credentials." # 3. Build a safe payload without sensitive fields user_data = user.to_dict(omit="password": True) # 4. Create the session and use the centralized default redirect return auth.sign_in(data=user_data, redirect_to=True) def page(): return html(r""" <!-- The form and its PulsePoint script live inline here. --> """)
<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 data = Object.fromEntries( new FormData(e.currentTarget).entries() ); try const result = await pp.rpc("do_login", data); if (result?.error) setMessage(result.error); 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(...)
Role And Guest Gates
require_role gates a page by role;
guest_only keeps signed-in users off
sign-in and sign-up pages.
from casp.auth import guest_only, require_role @require_role("admin", "owner") def page(): # else redirect to /unauthorized return html(r"""<main>Admin</main>""")
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 html(r"""<main>Private account</main>""")
Social Login (Google & GitHub)
OAuth is shipped, not something you assemble. The application bootstrap
registers both providers with
Auth.set_providers(...),
and the auth middleware already serves the sign-in and callback routes under
your
api_auth_prefix.
Link a button at those paths and set credentials in
.env —
do not hand-roll the flow.
# main.py — already generated for you from casp.auth import Auth, GithubProvider, GoogleProvider Auth.set_providers(GithubProvider(), GoogleProvider())
Then point the browser at the provider route:
<a href="/api/auth/signin/google">Continue with Google</a> <a href="/api/auth/signin/github">Continue with GitHub</a>
Environment variables
Google reads
GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET,
and
GOOGLE_REDIRECT_URI.
GitHub reads
GITHUB_CLIENT_ID
and
GITHUB_CLIENT_SECRET.
A provider with no client id is skipped, so the route falls through
instead of failing.
CSRF-protected callback
Each sign-in issues a single-use
state value that
the callback validates and consumes, so a forged callback is rejected.
Session cookies stay HttpOnly, and
max_age
(default "30d")
controls how long the provider session lasts.
auth_config.py.
Protected-route redirects, auth-route redirects, and
default_signin_redirect
are centralized there — a sign-in page should not re-implement
next handling. For
form posts that need one, get_csrf_token()
returns the session CSRF token.