Authentication¶
litestar-admin provides flexible authentication options to secure your admin panel. This guide covers JWT authentication, custom backends, and OAuth integration.
Overview¶
Authentication in litestar-admin is handled through pluggable backends that implement the AuthBackend protocol. The package includes a built-in JWT backend, and you can create custom backends for other authentication methods.
Quick Setup with JWT¶
The fastest way to add authentication is using the built-in JWT backend:
from litestar_admin import AdminPlugin, AdminConfig
from litestar_admin.auth import JWTAuthBackend, JWTConfig
async def load_user(user_id: str | int):
"""Load user from database by ID or email."""
# Your user loading logic here
return await user_repository.get(user_id)
# Create JWT backend
jwt_config = JWTConfig(
secret_key="your-super-secret-key-change-in-production",
)
auth_backend = JWTAuthBackend(
config=jwt_config,
user_loader=load_user,
)
# Add to admin config
admin_config = AdminConfig(
title="Secure Admin",
auth_backend=auth_backend,
)
JWT Configuration¶
The JWTConfig dataclass provides comprehensive options for JWT authentication.
Required Settings¶
secret_key¶
The secret key used for signing JWT tokens. This must be kept secure.
import os
JWTConfig(
secret_key=os.environ["JWT_SECRET_KEY"], # Load from environment
)
Warning
Never hardcode secret keys in your source code. Always use environment variables or a secrets manager.
Token Settings¶
algorithm¶
JWT signing algorithm. Defaults to "HS256".
JWTConfig(
secret_key="...",
algorithm="HS512", # Use HS512 for extra security
)
token_expiry¶
Access token expiry time in seconds. Defaults to 3600 (1 hour).
JWTConfig(
secret_key="...",
token_expiry=1800, # 30 minutes
)
refresh_token_expiry¶
Refresh token expiry time in seconds. Defaults to 86400 (24 hours).
JWTConfig(
secret_key="...",
refresh_token_expiry=604800, # 7 days
)
Token Location¶
token_location¶
Where to look for the JWT token. Options are "header" (default) or "cookie".
# Header-based (default)
JWTConfig(
secret_key="...",
token_location="header",
)
# Cookie-based
JWTConfig(
secret_key="...",
token_location="cookie",
)
token_header¶
HTTP header name for token extraction. Defaults to "Authorization".
JWTConfig(
secret_key="...",
token_header="X-Admin-Token",
)
token_prefix¶
Prefix for header-based tokens. Defaults to "Bearer".
JWTConfig(
secret_key="...",
token_prefix="Bearer", # Token: "Bearer <token>"
)
Optional Claims¶
issuer¶
Token issuer claim for additional validation.
JWTConfig(
secret_key="...",
issuer="https://myapp.com",
)
audience¶
Token audience claim for additional validation.
JWTConfig(
secret_key="...",
audience="myapp-admin",
)
JWTAuthBackend¶
The JWTAuthBackend class handles authentication using the configured JWT settings.
Constructor Arguments¶
config¶
The JWTConfig instance with JWT settings.
user_loader¶
An async callable that takes a user ID (or email) and returns an AdminUser object or None.
async def load_user(user_id: str | int) -> AdminUser | None:
"""Load user from database."""
user = await db.get_user(user_id)
return user # Must implement AdminUser protocol
password_verifier (optional)¶
An async callable for verifying passwords during login.
async def verify_password(stored_hash: str, password: str) -> bool:
"""Verify password against stored hash."""
return bcrypt.checkpw(password.encode(), stored_hash.encode())
backend = JWTAuthBackend(
config=jwt_config,
user_loader=load_user,
password_verifier=verify_password,
)
The AdminUser Protocol¶
Your user class must implement the AdminUser protocol:
from litestar_admin.auth import AdminUser
class User:
"""Your user class implementing AdminUser protocol."""
@property
def id(self) -> str | int:
"""User's unique identifier."""
return self._id
@property
def email(self) -> str:
"""User's email address."""
return self._email
@property
def roles(self) -> list[str]:
"""User's role names (e.g., ['admin', 'editor'])."""
return self._roles
@property
def permissions(self) -> list[str]:
"""User's permission strings (e.g., ['models:read', 'models:write'])."""
return self._permissions
Example Implementation¶
from dataclasses import dataclass
@dataclass
class AdminUserModel:
"""Admin user implementation."""
id: int
email: str
password_hash: str
_roles: list[str]
_permissions: list[str]
@property
def roles(self) -> list[str]:
return self._roles
@property
def permissions(self) -> list[str]:
return self._permissions
Complete JWT Example¶
Here’s a complete example with database integration:
from __future__ import annotations
import os
from dataclasses import dataclass
import bcrypt
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from litestar import Litestar
from litestar.plugins.sqlalchemy import SQLAlchemyConfig, SQLAlchemyPlugin
from litestar_admin import AdminConfig, AdminPlugin
from litestar_admin.auth import JWTAuthBackend, JWTConfig
# User model (simplified)
@dataclass
class User:
id: int
email: str
password_hash: str
is_admin: bool
@property
def roles(self) -> list[str]:
return ["admin"] if self.is_admin else ["viewer"]
@property
def permissions(self) -> list[str]:
if self.is_admin:
return ["models:read", "models:write", "models:delete"]
return ["models:read"]
# Database session (you'd get this from your app state)
async def get_db_session() -> AsyncSession:
...
# User loader function
async def load_user(identifier: str | int) -> User | None:
"""Load user by ID or email."""
async with get_db_session() as session:
# Try to load by ID first
if isinstance(identifier, int) or identifier.isdigit():
query = select(User).where(User.id == int(identifier))
else:
# Load by email for authentication
query = select(User).where(User.email == identifier)
result = await session.execute(query)
return result.scalar_one_or_none()
# Password verification
async def verify_password(stored_hash: str, password: str) -> bool:
"""Verify password using bcrypt."""
return bcrypt.checkpw(
password.encode("utf-8"),
stored_hash.encode("utf-8"),
)
# JWT configuration
jwt_config = JWTConfig(
secret_key=os.environ["JWT_SECRET_KEY"],
algorithm="HS256",
token_expiry=3600, # 1 hour
refresh_token_expiry=86400 * 7, # 7 days
token_location="header",
)
# Create auth backend
auth_backend = JWTAuthBackend(
config=jwt_config,
user_loader=load_user,
password_verifier=verify_password,
)
# Create admin plugin
admin_plugin = AdminPlugin(
config=AdminConfig(
title="Secure Admin",
auth_backend=auth_backend,
),
)
# Create app
app = Litestar(
plugins=[admin_plugin],
)
Session Authentication (SSO)¶
If your application already uses Litestar’s session middleware (e.g., ServerSideSessionConfig), you can use SessionAuthBackend to give logged-in users seamless access to the admin panel without a second login. Unauthenticated or non-admin users see a 404 — the admin panel’s existence is not leaked.
Quick Setup¶
from litestar_admin import AdminPlugin, AdminConfig
from litestar_admin.auth import SessionAuthBackend
async def retrieve_user_handler(session: dict, connection):
"""Load admin user from the host app's session."""
user_id = session.get("user_id")
if not user_id:
return None
user = await load_user_from_db(user_id)
if user is None or not user.is_admin:
return None
return user # Must implement AdminUserProtocol
backend = SessionAuthBackend(
retrieve_user_handler=retrieve_user_handler,
session_key="user_id", # Key in session dict (default)
)
admin_plugin = AdminPlugin(
config=AdminConfig(
title="My Admin",
auth_backend=backend,
),
)
How It Works¶
SessionAuthBackend follows the same retrieve_user_handler(session, connection) pattern as Litestar’s built-in SessionAuth. The host app configures session middleware independently — the admin backend simply reads from connection.session.
The retrieve_user_handler receives:
session: The session dictionary (same asconnection.session)connection: TheASGIConnectionfor accessing app state, headers, etc.
Constructor Arguments¶
retrieve_user_handler¶
An async callable with signature (dict, ASGIConnection) -> AdminUserProtocol | None. This is the only required argument.
session_key¶
The key in the session dictionary that holds the user identifier. Defaults to "user_id". Used by the backend to short-circuit early when the key is absent.
authenticate_handler (optional)¶
An async callable for email/password login via the admin login form. If not provided, the admin login form is effectively disabled — authentication happens entirely through the host app’s session.
async def authenticate_handler(connection, credentials):
"""Optional: allow password login through the admin form too."""
email = credentials.get("email")
password = credentials.get("password")
user = await verify_and_load(email, password)
if user and user.is_admin:
return user
return None
backend = SessionAuthBackend(
retrieve_user_handler=retrieve_user_handler,
authenticate_handler=authenticate_handler,
)
Complete Example with SQLAlchemy¶
from litestar import Litestar
from litestar.middleware.session.server_side import ServerSideSessionConfig
from litestar.stores.redis import RedisStore
from litestar_admin import AdminConfig, AdminPlugin
from litestar_admin.auth import SessionAuthBackend
# Your existing user model
class AdminUserAdapter:
"""Wrap your app's User model to satisfy AdminUserProtocol."""
def __init__(self, user):
self._user = user
@property
def id(self): return str(self._user.id)
@property
def email(self): return self._user.email
@property
def roles(self): return [r.name for r in self._user.roles]
@property
def permissions(self): return ["admin:access"]
async def retrieve_user_handler(session, connection):
user_id = session.get("user_id")
if not user_id:
return None
user = await load_user(user_id) # Your DB lookup
if user is None or not user.is_admin:
return None
return AdminUserAdapter(user)
# Session auth backend — no JWT, no secrets to configure
backend = SessionAuthBackend(retrieve_user_handler=retrieve_user_handler)
admin_plugin = AdminPlugin(
config=AdminConfig(title="My Admin", auth_backend=backend),
)
# Host app with session middleware
session_config = ServerSideSessionConfig(store="sessions")
app = Litestar(
plugins=[admin_plugin],
middleware=[session_config.middleware],
stores={"sessions": RedisStore.with_client(url="redis://localhost")},
)
Custom Auth Backend¶
You can create custom authentication backends by implementing the AuthBackend protocol:
from litestar_admin.auth import AuthBackend, AdminUser
class CustomAuthBackend:
"""Custom authentication backend."""
async def authenticate(
self,
connection: ASGIConnection,
credentials: dict[str, str],
) -> AdminUser | None:
"""Authenticate user with credentials.
Args:
connection: The ASGI connection.
credentials: Dict with 'email' and 'password'.
Returns:
Authenticated user or None.
"""
email = credentials.get("email")
password = credentials.get("password")
# Your authentication logic
user = await your_auth_service.authenticate(email, password)
return user
async def get_current_user(
self,
connection: ASGIConnection,
) -> AdminUser | None:
"""Get currently authenticated user.
Args:
connection: The ASGI connection.
Returns:
Current user or None.
"""
# Extract session/token from connection
session_id = connection.cookies.get("session_id")
if not session_id:
return None
return await your_session_service.get_user(session_id)
async def login(
self,
connection: ASGIConnection,
user: AdminUser,
) -> dict[str, str]:
"""Create session for user.
Args:
connection: The ASGI connection.
user: The authenticated user.
Returns:
Dict with session tokens.
"""
session_id = await your_session_service.create_session(user)
return {"session_id": session_id}
async def logout(
self,
connection: ASGIConnection,
) -> None:
"""Destroy current session.
Args:
connection: The ASGI connection.
"""
session_id = connection.cookies.get("session_id")
if session_id:
await your_session_service.destroy_session(session_id)
async def refresh(
self,
connection: ASGIConnection,
) -> dict[str, str] | None:
"""Refresh session tokens.
Args:
connection: The ASGI connection.
Returns:
New tokens or None if refresh failed.
"""
# Implement refresh logic
return None # Or new tokens
OAuth Integration¶
For OAuth authentication, install the oauth extra:
pip install "litestar-admin[oauth]"
Note
OAuth integration requires the litestar-oauth package. This feature is planned for a future release.
Auth API Endpoints¶
When authentication is enabled, the following endpoints are available:
POST /admin/api/auth/login¶
Authenticate with credentials.
Request:
{
"email": "admin@example.com",
"password": "secret123"
}
Response:
{
"access_token": "eyJ...",
"refresh_token": "eyJ...",
"token_type": "bearer",
"expires_in": "3600"
}
POST /admin/api/auth/logout¶
Invalidate current session.
Response:
{
"message": "Logged out successfully"
}
POST /admin/api/auth/refresh¶
Refresh access token using refresh token.
Request Header:
X-Refresh-Token: eyJ...
Response:
{
"access_token": "eyJ...",
"token_type": "bearer",
"expires_in": "3600"
}
GET /admin/api/auth/me¶
Get current user information.
Response:
{
"id": 1,
"email": "admin@example.com",
"roles": ["admin"],
"permissions": ["models:read", "models:write"]
}
Security Best Practices¶
Use Strong Secret Keys
import secrets secret_key = secrets.token_urlsafe(32)
Store Secrets Securely
import os JWTConfig(secret_key=os.environ["JWT_SECRET_KEY"])
Use HTTPS in Production
JWTConfig( cookie_secure=True, # Only send over HTTPS )
Set Appropriate Token Expiry
JWTConfig( token_expiry=1800, # 30 minutes for sensitive apps )
Enable HTTP-Only Cookies
JWTConfig( cookie_httponly=True, # Prevent XSS access )
Use SameSite Cookies
JWTConfig( cookie_samesite="strict", # Prevent CSRF )