Skip to content

Authentication

Zeython ships session-based authentication: a signed cookie (Starlette's SessionMiddleware, keyed off APP_SECRET_KEY) holding the logged-in user's ID, CSRF protection that comes with it automatically (cookie auth without it is forgeable from any other site the user's browser has open), password hashing with no C-extension dependency, and a small set of functions to check/require a logged-in user in a handler.

This is deliberately scoped to cookie sessions for a server-rendered or same-origin app. Token-based auth for a separate frontend is a reasonable thing to add later, but is out of scope here — don't force it into this code path.

Setup

# app/Models/user.py
from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column
from zeython import Authenticatable, Model, required, email

class User(Model, Authenticatable):
    __tablename__ = "users"
    __hidden__ = ("password_hash",)          # never serialize this
    __rules__ = {"name": [required()], "email": [required(), email()]}

    name: Mapped[str] = mapped_column(String(255))
    email: Mapped[str] = mapped_column(String(255), unique=True)
    password_hash: Mapped[str] = mapped_column(String(255))
# main.py
from zeython import Application, AuthServiceProvider, DatabaseServiceProvider, RouteServiceProvider
from app.Models.user import User

app = Application()
app.register(DatabaseServiceProvider)
app.register(AuthServiceProvider(app, user_model=User))
app.register(RouteServiceProvider(app, modules=("routes.web",)))

APP_SECRET_KEY must be set in .envAuthServiceProvider raises on boot if it isn't (zeython new generates one for you automatically).

Registration and login

# app/Controllers/auth_controller.py
from starlette.responses import JSONResponse
from zeython import AuthManager, Controller, UnauthorizedException, ValidationException
from zeython.auth import current_user, login as auth_login, logout as auth_logout
from app.Models.user import User

class AuthController(Controller):
    async def register(self, request):
        data = await request.json()
        password = data.pop("password", None)
        if not password:
            raise ValidationException({"password": ["Password is required."]})

        user = User(**data)
        user.set_password(password)      # from Authenticatable
        await user.save()                 # runs User.__rules__ too
        auth_login(request, user)
        return JSONResponse(user.to_dict(), status_code=201)

    async def login(self, request):
        data = await request.json()
        manager: AuthManager = request.app.state.container.make(AuthManager)
        user = await manager.attempt(data.get("email", ""), data.get("password", ""))
        if user is None:
            raise UnauthorizedException("Invalid email or password.")
        auth_login(request, user)
        return JSONResponse(user.to_dict())

    async def logout(self, request):
        auth_logout(request)
        return JSONResponse({"message": "Logged out."})

Note the import aliases (login as auth_login): inside a method also named login, the bare name login still resolves to the module-level function (Python scoping doesn't let a method's own name shadow it), but aliasing avoids the confusion of reading login(...) inside def login(...).

Protecting a route

from zeython.auth import require_auth

async def show(self, request):
    user = await require_auth(request)   # raises UnauthorizedException (401) if not logged in
    ...

Use current_user(request) instead when the route should work either way (returns None rather than raising):

from zeython.auth import current_user

async def home(self, request):
    user = await current_user(request)
    return render(request, "home.html", {"user": user})

Configuration

.env key Default Meaning
APP_SECRET_KEY (required) Signs the session cookie. Generated automatically by zeython new.
SESSION_COOKIE_NAME zeython_session Cookie name.
SESSION_MAX_AGE 1209600 (14 days) Cookie lifetime in seconds.
SESSION_HTTPS_ONLY false Set true once you're serving over HTTPS.
CSRF_ENABLED true See CSRF Protection.
CSRF_COOKIE_NAME csrf_token
CSRF_HEADER_NAME X-CSRF-Token

Password hashing

zeython.hash_password/zeython.verify_password use PBKDF2-HMAC-SHA256 at 600,000 iterations (OWASP's current minimum recommendation), with stdlib hashlib only — no bcrypt/argon2 C-extension dependency. Authenticatable wraps these as set_password()/check_password().