Skip to content

Bring Your Own Model

NeoZen's factors and identifiers use generic protocols. If your application has its own User model (e.g., an SQLAlchemy model from Advanced Alchemy), you don't need to inherit from NeoZen's base classes. You just need to satisfy the structural interface.

The Problem

NeoZen's PasswordFactor is generic:

class PasswordFactor[S: PasswordSubjectModel]:
    def __init__(
        self,
        identifier: IdentifierProtocol[S],
        subject_storage: RepositoryProtocol[S],
        hasher: PasswordHasherProtocol,
    ) -> None: ...

If your User class doesn't inherit from PasswordSubjectModel, type checkers complain even though it has all the right fields.

Solution: Factory Functions

NeoZen provides factory functions that accept Any-typed storage, so you don't need type: ignore:

from neozen.core.identifiers.email import create_email_identifier
from neozen.core.factors.password import create_password_factor

# Your model -- no NeoZen inheritance needed
class User:
    id: str
    email: str
    hashed_password: str | None
    is_active: bool

# Your repository adapter
class UserRepoAdapter:
    async def get_one_by(self, **filters) -> User:
        # Wrap your ORM query
        ...
    async def update_obj(self, obj: User, **data) -> User:
        for k, v in data.items():
            setattr(obj, k, v)
        return obj
    # Other methods can raise NotImplementedError if unused

# Wire it up -- no type: ignore needed
adapter = UserRepoAdapter()
identifier = create_email_identifier(adapter)
factor = create_password_factor(identifier, adapter, hasher)

user = await factor.authenticate({
    "identifier": "alice@example.com",
    "password": "secret",
})

Required Fields

Your model must structurally match these fields:

For EmailIdentifier

Field Type Used For
email str Lookup by email
id str Subject identifier

For PasswordFactor

Field Type Used For
hashed_password str \| None Password verification
id str Subject identifier

For TOTPFactor

Field Type Used For
totp_secret str \| None TOTP secret storage
totp_enabled bool Whether TOTP is active
id str Subject identifier

Repository Protocol

You only need to implement the methods that your factor actually calls. NeoZen provides split protocols to make this explicit:

from neozen.core.repository import ReadableRepositoryProtocol, WritableRepositoryProtocol

# PasswordFactor only needs these:
class MinimalAdapter:
    async def get_one_by(self, **filters) -> User:
        """Required: lookup user by email."""
        ...

    async def update_obj(self, obj: User, **data) -> User:
        """Required: update hashed_password after hash upgrade."""
        ...

Which Methods Each Factor Uses

Factor get_one_by update_obj create get_many_by count exists delete
PasswordFactor Yes Yes -- -- -- -- --
TOTPFactor Yes Yes -- -- -- -- --
MagicLinkFactor -- -- Yes -- Yes* -- --

*Via the token repository, not the subject repository.

Dependency Injection with Dishka

If your app uses dishka, NeoZen ships providers instead of factory functions. SQLAlchemyProvider supplies the engine, one session per request and a RepositoryProtocol[Model] for any mapped model. The session never commits on its own: commit what you mean to keep. Anything uncommitted is rolled back when the request scope closes. The identifier providers build on it:

import dishka
from sqlalchemy.ext.asyncio import AsyncSession

from neozen.core.identifiers import EmailIdentifierProvider, IdentifierProtocol
from neozen.storage.sqlalchemy import SQLAlchemyProvider, Subject

container = dishka.make_async_container(
    SQLAlchemyProvider("postgresql+asyncpg://localhost/app"),
    EmailIdentifierProvider(),
)

async with container() as request:
    identifier = await request.get(IdentifierProtocol[Subject])
    subject = await identifier.get_by_identifier("ada@example.com")
    ...
    await (await request.get(AsyncSession)).commit()

The email, username and phone providers all provide IdentifierProtocol[M] for any model, so register only one of them per container (or per dishka component).

For the OAuth authorization endpoint, build it from your configuration with NeoZenFactory.create_authorization_endpoint(...) (see the configuration reference) inside a provider of your own, passing it the repositories from SQLAlchemyProvider.