Authentication¶
Require Any Certificate¶
Use @require_certificate to require authentication:
from xitzin.auth import require_certificate
@app.gemini("/private")
@require_certificate
def private_page(request: Request):
return "# Private Content"
If no certificate is provided, the client receives status 60 (Certificate Required).
Get User Identity¶
Access certificate information with get_identity():
from xitzin.auth import require_certificate, get_identity
@app.gemini("/whoami")
@require_certificate
def whoami(request: Request):
identity = get_identity(request)
return f"""# Your Identity
Fingerprint: {identity.fingerprint}
Short ID: {identity.short_id}
"""
The CertificateIdentity provides:
fingerprint: Full SHA-256 hash (64 chars)short_id: First 16 characters for displaycert: Raw certificate object
Restrict to Specific Certificates¶
Use @require_fingerprint() for whitelist-based access:
from xitzin.auth import require_fingerprint
ADMIN_CERTS = [
"a1b2c3d4e5f6...", # Alice
"f6e5d4c3b2a1...", # Bob
]
@app.gemini("/admin")
@require_fingerprint(*ADMIN_CERTS)
def admin_panel(request: Request):
return "# Admin Panel"
Returns:
- Status 60 if no certificate
- Status 61 if certificate not in list
Optional Authentication¶
Use @optional_certificate to personalize without requiring auth:
from xitzin.auth import optional_certificate
@app.gemini("/")
@optional_certificate
def home(request: Request):
identity = request.state.identity
if identity:
return f"# Welcome back, {identity.short_id}!"
return "# Welcome, visitor!"
Check Certificate Directly¶
Access certificate via the request:
@app.gemini("/check")
def check_cert(request: Request):
if request.client_cert_fingerprint:
return f"Certificate: {request.client_cert_fingerprint[:16]}..."
return "No certificate provided"
Properties:
request.client_cert: The certificate object (or None)request.client_cert_fingerprint: SHA-256 fingerprint (or None)
Build a User System¶
# Simple user store
users = {}
@app.gemini("/register")
@require_certificate
def register(request: Request):
identity = get_identity(request)
if identity.fingerprint in users:
return f"# Already registered as {users[identity.fingerprint]['name']}"
return "=> /register/name Choose a username"
@app.input("/register/name", prompt="Choose a username:")
@require_certificate
def register_name(request: Request, query: str):
identity = get_identity(request)
users[identity.fingerprint] = {
"name": query,
"registered": datetime.now(),
}
return f"# Welcome, {query}!"
@app.gemini("/profile")
@require_certificate
def profile(request: Request):
identity = get_identity(request)
user = users.get(identity.fingerprint)
if not user:
return "=> /register Please register first"
return f"""# {user['name']}'s Profile
Registered: {user['registered']}
Certificate: {identity.short_id}
"""
Persistent Users with SQLModel¶
For production applications, store users in a database using SQLModel.
The get_or_create pattern automatically creates new users on first visit.
SQLModel Setup
See the SQLModel reference for installation and middleware setup.
User Model with get_or_create¶
from datetime import datetime
from sqlmodel import Field, Session, SQLModel, select
class User(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
fingerprint: str = Field(unique=True, index=True)
username: str | None = None
created_at: datetime = Field(default_factory=datetime.utcnow)
@classmethod
def get_or_create(cls, session: Session, fingerprint: str) -> "User":
"""Get existing user or create new one from certificate fingerprint."""
user = session.exec(
select(cls).where(cls.fingerprint == fingerprint)
).first()
if not user:
user = cls(fingerprint=fingerprint)
session.add(user)
session.commit()
session.refresh(user)
return user
@property
def display_name(self) -> str:
"""Username or anonymous identifier."""
return self.username or f"Anon-{self.fingerprint[:8]}"
Using in Routes¶
from xitzin import Xitzin, Request, Redirect
from xitzin.auth import require_certificate
from xitzin.sqlmodel import get_session, init_db, SessionMiddleware
from sqlmodel import create_engine
app = Xitzin()
engine = create_engine("sqlite:///app.db")
init_db(app, engine)
app.middleware(SessionMiddleware(engine))
@app.gemini("/profile")
@require_certificate
def profile(request: Request):
session = get_session(request)
user = User.get_or_create(session, request.client_cert_fingerprint)
return f"""# {user.display_name}
Member since: {user.created_at.strftime('%B %Y')}
=> /profile/edit Edit profile
"""
@app.input("/profile/edit/username", prompt="New username (3-20 chars):")
@require_certificate
def edit_username(request: Request, query: str):
if not 3 <= len(query) <= 20:
return "# Error\nUsername must be 3-20 characters."
session = get_session(request)
user = User.get_or_create(session, request.client_cert_fingerprint)
user.username = query
session.add(user)
session.commit()
return Redirect("/profile")
User Loader Middleware¶
For apps where every authenticated route needs the user, auto-load with middleware:
@app.middleware
async def load_user(request: Request, call_next):
if request.client_cert_fingerprint:
session = get_session(request)
request.state.user = User.get_or_create(
session,
request.client_cert_fingerprint
)
else:
request.state.user = None
return await call_next(request)
# All handlers can now access request.state.user
@app.gemini("/dashboard")
@require_certificate
def dashboard(request: Request):
return f"# Welcome, {request.state.user.display_name}"
Registration Flow¶
For apps requiring explicit registration before creating an account:
@app.gemini("/register")
@require_certificate
def register_check(request: Request):
session = get_session(request)
existing = session.exec(
select(User).where(
User.fingerprint == request.client_cert_fingerprint
)
).first()
if existing:
return Redirect("/profile")
return """# Welcome!
You haven't registered yet.
=> /register/username Choose a username
"""
@app.input("/register/username", prompt="Choose a username (3-20 chars):")
@require_certificate
def register_username(request: Request, query: str):
if not 3 <= len(query) <= 20:
return "# Error\nUsername must be 3-20 characters."
session = get_session(request)
# Check uniqueness
existing = session.exec(
select(User).where(User.username == query)
).first()
if existing:
return "# Error\nUsername already taken."
# Create user
user = User(
fingerprint=request.client_cert_fingerprint,
username=query
)
session.add(user)
session.commit()
return Redirect("/profile")
Decorator Order¶
When combining decorators, @require_certificate should be closest to the function:
# Correct order
@app.gemini("/admin")
@require_certificate
def admin(request: Request):
...
# Also correct
@app.input("/private", prompt="Enter data:")
@require_certificate
def private_input(request: Request, query: str):
...
Caching User Lookups¶
For applications with frequent requests from the same users (games, social apps), cache user lookups to reduce database queries.
Using UserSessionMiddleware¶
The built-in UserSessionMiddleware caches user lookups automatically.
It supports both sync and async loaders - sync loaders run in a thread pool
to avoid blocking the event loop.
from xitzin.middleware import UserSessionMiddleware
from sqlmodel import Session, select
def load_user(fingerprint: str) -> User | None:
with Session(engine) as session:
return session.exec(
select(User).where(User.fingerprint == fingerprint)
).first()
# Create middleware (keep reference for cache management)
user_middleware = UserSessionMiddleware(load_user, cache_size=100)
@app.middleware
async def user_session(request, call_next):
return await user_middleware(request, call_next)
@app.gemini("/profile")
@require_certificate
def profile(request: Request):
user = request.state.user # Loaded by middleware
if not user:
return "=> /register Please register first"
return f"# Hello, {user.username}"
With an async database driver:
async def load_user(fingerprint: str) -> User | None:
async with async_session() as session:
result = await session.execute(
select(User).where(User.fingerprint == fingerprint)
)
return result.scalar_one_or_none()
user_middleware = UserSessionMiddleware(load_user)
Cache Invalidation¶
Clear the cache when user data changes:
def update_user(user: User):
with Session(engine) as session:
session.add(user)
session.commit()
user_middleware.clear_cache() # Invalidate all cached users
Using functools.lru_cache¶
For simpler cases without middleware, use Python's @lru_cache:
from functools import lru_cache
@lru_cache(maxsize=100)
def get_user(fingerprint: str) -> User | None:
with Session(engine) as session:
return session.exec(
select(User).where(User.fingerprint == fingerprint)
).first()
@app.gemini("/profile")
@require_certificate
def profile(request: Request):
user = get_user(request.client_cert_fingerprint)
if not user:
return "=> /register Please register first"
return f"# Hello, {user.username}"
# Clear cache when user data changes:
def update_user(user: User):
with Session(engine) as session:
session.add(user)
session.commit()
get_user.cache_clear()