Skip to content

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 display
  • cert: 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()