Migration Guide¶
Argon2-only default password helper¶
The library default password-helper policy is now Argon2-only. PasswordHelper.from_defaults(),
bare PasswordHelper(), BaseUserManager(..., password_helper=None), and
LitestarAuthConfig.resolve_password_helper() all use that default.
Unsupported stored password hashes now fail closed under that policy: verification returns False
and verify_and_update() does not emit a replacement hash for an unsupported stored value.
Before upgrading a deployment that still depends on unsupported stored password hashes:
- Re-hash or reset those credentials out of band while the previous release is still serving traffic.
- Confirm the persisted hashes now match your intended Argon2 policy.
- Deploy the new release only after those credentials no longer depend on unsupported formats.
Self-service password rotation endpoint¶
Self-service profile updates and password rotation are now separate contracts. UserUpdate
no longer includes password, and requests that try to set password on the self-service
profile update path are rejected with REQUEST_BODY_INVALID.
Update clients that let authenticated users change their own password to call
POST /users/me/change-password with ChangePasswordRequest:
Wrong current passwords return the login-shaped LOGIN_BAD_CREDENTIALS contract. Invalid
replacement passwords return UPDATE_USER_INVALID_PASSWORD. Admin-initiated password rotation
continues through AdminUserUpdate on the privileged users update path.
Superuser boolean to role membership¶
Superuser status is now derived from role membership. The public
is_superuser guard still exists, but it checks whether user.roles contains
the configured superuser_role_name (default "superuser") instead of reading
user.is_superuser.
Before upgrading a database that still has an is_superuser column, preserve
the data by backfilling role membership for every true row:
- Ensure the role catalog contains the configured superuser role.
- Insert missing
user_roleassociation rows for users whereuser.is_superuser = true. - Deploy code that no longer reads, writes, or serializes
user.is_superuser. - Drop the old
is_superusercolumn after verifying those users authenticate with the expected role membership.
Example SQL shape for the default role name:
INSERT INTO role (name, description)
VALUES ('superuser', 'Superuser access')
ON CONFLICT (name) DO NOTHING;
INSERT INTO user_role (user_id, role_name)
SELECT id, 'superuser'
FROM "user"
WHERE is_superuser = true
ON CONFLICT DO NOTHING;
Adjust table names, quoting, and conflict handling for your database dialect
and custom model family. Applications using a custom
LitestarAuthConfig.superuser_role_name should backfill that normalized role
name instead of "superuser".
Code changes to make at the same time:
- Remove
is_superuserfrom custom SQLAlchemy user models and DTOs. - Stop passing
is_superusertoBaseUserManager.create(...),BaseUserManager.update(...),/auth/register, and/users/*payloads. The generated register and users request schemas now reject undeclared keys during request decoding withErrorCode.REQUEST_BODY_INVALID, so stale clients surface immediately instead of being silently accepted. - Grant or revoke superuser access by mutating the normalized
rolescollection through an admin path, seed script, migration, or the role-admin CLI/controller.
Custom password-hash column mapping¶
Custom SQLAlchemy user models should keep hashed_password as the runtime
attribute consumed by managers, stores, and token fingerprinting.
When the only customization is the SQL column name, set
UserModelMixin.auth_hashed_password_column_name = "password_hash" on the
app-owned user model. Existing app models that already declare
hashed_password = mapped_column(...) directly remain valid when the
application intentionally owns that mapped attribute shape; no auth-layer
behavior change is required either way.
Typing: UP bound narrowing and direct config construction¶
The typing-only API was tightened so downstream annotations describe the same
runtime contracts the library already expects. Runtime behavior is unchanged,
but type checkers may now surface code that relied on broad Any-based bounds,
helper-based config construction, manual generic parameters, or plain str
dependency keys.
LitestarAuthConfig.create() to direct construction¶
Construct LitestarAuthConfig directly. The dataclass now owns the full public
configuration surface without separate wrapper helpers.
Before:
from uuid import UUID
from litestar_auth import LitestarAuthConfig
config = LitestarAuthConfig.create(
user_model=User,
user_manager_class=UserManager,
session_maker=session_maker,
)
After:
from uuid import UUID
from litestar_auth import LitestarAuthConfig
config = LitestarAuthConfig[User, UUID](
user_model=User,
user_manager_class=UserManager,
session_maker=session_maker,
)
UP bound=UserProtocol[Any] consumer code¶
The library's public UP type variable is now bounded to UserProtocol instead
of UserProtocol[Any]. Code that mirrors the old broad bound can usually drop
the Any parameter, or can bind the user and ID together with Python 3.12
generic parameter syntax when the ID type matters.
Before:
from typing import Any, TypeVar
from litestar_auth.types import UserProtocol
UP = TypeVar("UP", bound=UserProtocol[Any])
def user_id(user: UP) -> object:
return user.id
After:
from typing import TypeVar
from litestar_auth.types import UserProtocol
UP = TypeVar("UP", bound=UserProtocol)
def user_id[ID](user: UserProtocol[ID]) -> ID:
return user.id
Use UserProtocol as the broad runtime-checkable user bound. Use
UserProtocol[ID] when the function or class needs to preserve the concrete ID
type through its return values or collaborators.
TOTP user-model validation moves to startup¶
Apps with totp_config enabled must use a user_model that exposes the
TotpUserProtocol fields: email and totp_secret. The plugin now checks that
contract during startup, so a misconfigured app fails before routes are mounted.
Previously, the same misconfiguration could surface only after a login reached
the pending-2FA branch.
DbSessionDependencyKey adoption¶
Annotate custom DB-session dependency keys with DbSessionDependencyKey instead
of plain str. This keeps application code aligned with
LitestarAuthConfig.db_session_dependency_key and documents the Python
identifier constraint at the call site.
Before:
from litestar_auth import LitestarAuthConfig
db_session_dependency_key: str = "db_session"
config = LitestarAuthConfig[User, UUID](
user_model=User,
user_manager_class=UserManager,
session_maker=session_maker,
db_session_dependency_key=db_session_dependency_key,
)
After:
from litestar_auth import LitestarAuthConfig
from litestar_auth.types import DbSessionDependencyKey
db_session_dependency_key: DbSessionDependencyKey = "db_session"
config = LitestarAuthConfig[User, UUID](
user_model=User,
user_manager_class=UserManager,
session_maker=session_maker,
db_session_dependency_key=db_session_dependency_key,
)
String rate-limit slot keys to AuthRateLimitSlot¶
These snippets use InMemoryRateLimiter only to show the slot-key migration in a
small single-process/dev/test setup. For production multi-worker deployments,
use RedisRateLimiter or RedisAuthPreset and declare the topology with
LitestarAuthConfig.deployment_worker_count.
Before:
from litestar_auth.ratelimit import AuthRateLimitConfig, EndpointRateLimit, InMemoryRateLimiter
config = AuthRateLimitConfig.from_shared_backend(
backend=InMemoryRateLimiter(max_attempts=5, window_seconds=60),
endpoint_overrides={
"totp_verify": EndpointRateLimit(
backend=InMemoryRateLimiter(max_attempts=3, window_seconds=60),
scope="ip",
namespace="totp-verify",
),
},
)
After:
from litestar_auth.ratelimit import (
AuthRateLimitConfig,
AuthRateLimitSlot,
EndpointRateLimit,
InMemoryRateLimiter,
)
config = AuthRateLimitConfig.from_shared_backend(
backend=InMemoryRateLimiter(max_attempts=5, window_seconds=60),
endpoint_overrides={
AuthRateLimitSlot.TOTP_VERIFY: EndpointRateLimit(
backend=InMemoryRateLimiter(max_attempts=3, window_seconds=60),
scope="ip",
namespace="totp-verify",
),
},
)
AuthRateLimitSlot keeps override mappings typed, IDE-friendly, and aligned
with the preferred public surface.