Python FastAPI Tutorial (Part 14): Password Reset - Email, Tokens, and Background Tasks
Based on Corey Schafer's video on YouTube. If you like this content, support the original creators by watching, liking and subscribing to their content.
Reset tokens must be unguessable, short-lived (default 60 minutes), and single-use, enforced by deleting tokens after use and when new tokens are requested.
Briefing
A complete password reset system for a FastAPI blog app comes together by combining three security requirements—unguessable, expiring, single-use reset tokens—with non-blocking email delivery. The flow is designed so attackers can’t learn which email addresses exist in the system, and it avoids tying reset validity to JWTs that can’t be cleanly invalidated without extra infrastructure.
The implementation starts with async email sending. Because FastAPI runs on an async event loop, the tutorial installs AIO SMTP lib (instead of Python’s built-in synchronous SMTP) and wires it to configuration stored in Pydantic settings. Email credentials and SMTP details live in an uncommitted .env file, with TLS enabled and a dedicated front-end base URL used to build reset links. For development, Mail Trap provides an email sandbox so test messages are captured safely rather than sent to real inboxes.
Reset tokens are handled as database-backed secrets. A new SQLAlchemy model, password reset tokens, stores token_hash (a SHA-256 hash) rather than the raw token, along with expires_at and created_at. When a user requests a reset, any existing tokens for that user are deleted, then a new random token is generated using secrets.token_urlsafe(32), hashed, stored with an expiration time (default 60 minutes), and emailed. The raw token is what goes into the reset URL; the database only ever sees the hash.
Email delivery is pushed into FastAPI BackgroundTasks so the API responds immediately with a 202 Accepted message. That endpoint deliberately returns the same generic response whether or not the email exists, preventing email enumeration attacks. If the user clicks the reset link, the reset password endpoint hashes the submitted token, looks up the matching token hash in the database, checks expires_at (including a SQLAlchemy time zone workaround for SQLite), and rejects invalid or expired tokens with a generic error. On success, it updates the user’s password hash, deletes all outstanding reset tokens for that user (invalidating any other links), and does not auto-login—forcing a normal login afterward.
For logged-in users, a change-password endpoint verifies the current password before allowing an update, then deletes any outstanding reset tokens to keep the account consistent. To make the system usable in a browser, the tutorial adds front-end routes and templates for forgot password and reset password pages. The reset page extracts the token from the query string, submits the new password to the reset endpoint, and includes a referer-suppression measure (referrerPolicy set to no-referrer) to reduce token leakage via browser headers.
End-to-end testing confirms the backend endpoints and the UI layer work together: requesting a reset yields a Mail Trap email with a working tokenized link, resetting via the API updates credentials, and changing passwords from the account page succeeds. The tutorial closes by noting the next step—moving from SQLite to Postgres and using Olympic for migrations—so the same token logic can run in a production-ready database setup.
Cornell Notes
The password reset design uses random, single-use tokens stored as SHA-256 hashes in the database, with a short expiration window (default 60 minutes). A “forgot password” request always returns the same 202 Accepted message to avoid email enumeration, while email sending runs asynchronously via FastAPI BackgroundTasks. When a reset link is used, the API hashes the submitted token, verifies it exists and hasn’t expired, updates the user’s password hash, and deletes all reset tokens for that user to invalidate any other links. Logged-in users can change passwords only after providing the correct current password, and that action also clears outstanding reset tokens. Front-end templates provide a browser-friendly flow that calls the same API endpoints.
Why store a hash of the reset token in the database instead of the token itself?
How does the system prevent attackers from learning which emails are registered?
What makes the reset token “single use” in this design?
Why use BackgroundTasks for password reset emails, and what limitation comes with it?
How does the reset endpoint handle token expiration with SQLite time zone quirks?
What security measure is added to the reset-password front-end page regarding the token in the URL?
Review Questions
- Walk through the exact steps taken from a forgot password request to a successful password reset, including which values are hashed and when tokens are deleted.
- What responses does the forgot password endpoint return, and how do those choices mitigate email enumeration attacks?
- How would you modify the design if you needed stronger guarantees than BackgroundTasks provides when the server restarts?
Key Points
- 1
Reset tokens must be unguessable, short-lived (default 60 minutes), and single-use, enforced by deleting tokens after use and when new tokens are requested.
- 2
Store only token hashes (SHA-256) in the database; send the raw token only inside the email link.
- 3
Return a uniform 202 Accepted message for forgot-password requests to prevent email enumeration.
- 4
Send reset emails asynchronously using FastAPI BackgroundTasks so API responses aren’t blocked by SMTP round trips.
- 5
Validate reset tokens by hashing the submitted token, looking up token_hash, and checking expires_at (with a SQLite time zone workaround).
- 6
After a successful reset, update the user password hash and delete all outstanding reset tokens for that user; do not auto-login.
- 7
Provide browser-friendly templates that call the same API endpoints, including referrer suppression to reduce token leakage.