Skip to content

Identity contract

An account's shape (which columns exist) is read from your model; IdentityConfig carries the intent a schema can't express, the login resolution order and the recovery factor, validated against the model when CRUDAuth is built so a config that contradicts the model fails at startup rather than splitting into a second source of truth.

  • make_auth_identity(identifiers=, recovery=, oauth=) builds the user-column mixin for a shape; AuthUserMixin is its default output (email + username login, email recovery).
  • IdentityConfig(login=, recovery=) declares the intent; pass it as CRUDAuth(identity=...).

See the account-shape recipes for end-to-end examples.

crudauth.identity.IdentityConfig dataclass

IdentityConfig(
    login: list[str] = (lambda: ["email", "username"])(),
    recovery: str | None = "email",
)

How an account's identity behaves, validated against the model at construction.

Attributes:

Name Type Description
login list[str]

Logical fields a login identifier is matched against, in order; first match wins. Each must be a unique column on the model (asserted at construction, which is what makes first-match-wins safe).

recovery str | None

The single field verify/reset/change is delivered against, or None to disable recovery (the recovery endpoints aren't mounted). Must be a unique column when set.

Example
# username login, phone recovery
CRUDAuth(..., identity=IdentityConfig(login=["username"], recovery="phone"))

# anonymous: username login, no recovery at all
CRUDAuth(..., identity=IdentityConfig(login=["username"], recovery=None))

crudauth.models.mixin.make_auth_identity

make_auth_identity(
    *,
    identifiers: Iterable[str] = ("email", "username"),
    recovery: str | None = "email",
    oauth: bool = True,
) -> type[Any]

Build a declarative mixin carrying crudauth's user columns for one account shape.

The model is the source of truth for shape: this emits the columns, and CRUDAuth reads them back at construction, so the two can't disagree. username, hashed_password, the status flags, token_version, and the timestamps are always emitted; email and the oauth-linkage columns are conditional.

Parameters:

Name Type Description Default
identifiers Iterable[str]

Logical fields a user may log in with. Each becomes a NOT NULL unique column. email here makes the email column required.

('email', 'username')
recovery str | None

The single field recovery (verify/reset) is delivered against, or None for an account shape with no recovery. "email" (and not an identifier) makes the email column nullable but unique. A non-email factor emits a {factor}_verified bookkeeping flag (e.g. phone_verified); you still declare the factor column itself (e.g. phone) with your own constraints, the same way you would any column.

'email'
oauth bool

Emit the oauth-linkage columns (oauth_provider / google_id / github_id / timestamps). Required for OAuth login.

True

Returns:

Type Description
type[Any]

A declarative mixin class. Inherit it on your model alongside Base.

Example
# the default - email + username login, email recovery (today's shape)
class User(Base, AuthUserMixin):
    __tablename__ = "users"

# username-only login, phone recovery
Identity = make_auth_identity(identifiers=["username"], recovery="phone")
class User(Base, Identity):
    __tablename__ = "users"
    phone: Mapped[str | None] = mapped_column(unique=True, default=None)