Full user lifecycle for HawkAPI. Register, login, email verification, password reset — all on top of three existing plugins:
hawkapi-auth— argon2id passwords, JWT token issuerhawkapi-sqlalchemy— async SQLAlchemy sessionshawkapi-mail— transactional email backends + templates
No fastapi-users dependency; pure stdlib + the three plugins above.
pip install hawkapi-usersfrom hawkapi import HawkAPI
from hawkapi_sqlalchemy import Base, init_database
from hawkapi_mail import init_mail, SMTPBackend, SMTPConfig
from hawkapi_users import (
SQLAlchemyBaseUserTable,
UserManager, UserTokens, UserTokenConfig,
init_users, send_verification_email, send_password_reset_email,
)
class User(SQLAlchemyBaseUserTable):
__tablename__ = "users"
app = HawkAPI()
init_database(app, url="postgresql+asyncpg://...")
mail = init_mail(
app,
backend=SMTPBackend(SMTPConfig(host="smtp.example.com", port=587, start_tls=True)),
default_sender="noreply@example.com",
)
tokens = UserTokens(UserTokenConfig(secret="…stable secret, ≥32 chars…"))
manager = UserManager(model=User, tokens=tokens)
async def on_request_verify(user, token):
await send_verification_email(
mail, to=user.email, token=token,
verify_url_template="https://app.example.com/verify/{token}",
)
async def on_forgot_password(user, token):
await send_password_reset_email(
mail, to=user.email, token=token,
reset_url_template="https://app.example.com/password-reset/{token}",
)
manager.on_after_request_verify = on_request_verify
manager.on_after_forgot_password = on_forgot_password
init_users(app, manager=manager)The plugin mounts under /users (configurable):
| Route | Description |
|---|---|
POST /users/register |
Create account, returns public user record |
POST /users/login |
Verify credentials, returns public user |
POST /users/verify/request |
Send a verification email (always 202) |
POST /users/verify/{token} |
Apply a verification token |
POST /users/password-reset/request |
Send a reset email (always 202) |
POST /users/password-reset/{token} |
Apply a new password |
- Argon2id password hashing (via
hawkapi-auth's pinned parameters). - Token type binding — verify tokens cannot be replayed as reset tokens (the
typeclaim is enforced). - password_version binding — each token carries the user's current
password_version. A successful reset bumps the counter, invalidating every outstanding verify/reset token in one DB write. - Account enumeration prevention —
/verify/requestand/password-reset/requestalways return 202 regardless of whether the email exists. - Timing-safe lookup —
authenticate()runs argon2id against a dummy hash when the user does not exist, equalizing wall-clock time across the hit/miss paths. - Email normalization —
emailis lower-cased and stripped server-side; uniqueness is enforced after normalization.
These are real tradeoffs the plugin does NOT mitigate. Operators must layer additional protection where it matters.
- Email enumeration via
/register(409 response) — registering with an already-used email returns409 Conflict. An attacker can confirm whether any email has an account. Mitigation: rate-limit/registerper source IP and require CAPTCHA for unauthenticated traffic. The anti-enumeration guarantee applies only to/verify/requestand/password-reset/request(always 202). - Inactive-account 403 confirms credentials —
/loginreturns401for bad credentials and403for a disabled account. A403confirms the email+password combination is correct. Treat403as a credential-leak event in audit pipelines. - No rate limiting —
/login,/register,/verify/request,/password-reset/requesthave no built-in throttling. Pair withhawkapi-ratelimitplus per-account lockout. - No session issued by
/login— the route validates credentials and returns the public user record. Issuing a JWT, setting a cookie, or starting a server-side session is the operator's job (manager.on_after_login). - Token-in-URL leakage — verify/reset tokens travel in URL paths and may appear in Referer headers, browser history, and email-server access logs. Use HTTPS + short TTLs (default 1 hour) and prefer SPA flows that strip the token from the URL after consumption.
manager.on_after_register # async (user) -> None
manager.on_after_login # async (user) -> None
manager.on_after_request_verify # async (user, token) -> None
manager.on_after_verify # async (user) -> None
manager.on_after_forgot_password # async (user, token) -> None
manager.on_after_reset_password # async (user) -> NoneUse these to mint a session cookie, write an audit log, send the email, etc.
from hawkapi_mail import TemplateRenderer
from hawkapi_users import send_verification_email
renderer = TemplateRenderer(directory="my/email/templates")
await send_verification_email(
mail,
to=user.email,
token=token,
template="my_verify.html",
text_template="my_verify.txt",
renderer=renderer,
)The built-in templates live in hawkapi_users/templates/ (verify.html/.txt, password_reset.html/.txt) and are used by default.
git clone https://github.com/Hawk-API/hawkapi-users.git
cd hawkapi-users
uv sync --extra dev
uv run pytest -q
uv run ruff check . && uv run ruff format --check .
uv run pyright src/MIT.