How to Implement Secure Password Reset in FastAPI
Forgetting your password happens to everyone. When a user forgets their password in your FastAPI application, you need a reliable way for them to reset it securely. This guide shows you how to build a complete password reset system, from requesting a reset to setting a new password.
What You’ll Learn
In this tutorial, you will learn how to:
- Install necessary packages for sending emails asynchronously.
- Configure email settings for your application.
- Set up a development email service like Mailtrap.
- Create secure, single-use password reset tokens.
- Send emails asynchronously using FastAPI’s background tasks.
- Design and send HTML email templates for password resets.
- Store password reset tokens securely in your database.
- Implement API endpoints for requesting and resetting passwords.
- Add a feature for logged-in users to change their password directly.
Prerequisites
- A working FastAPI application with user authentication.
- Basic understanding of Python, FastAPI, and Pydantic.
- Familiarity with database models (SQLAlchemy) and API endpoints.
Step 1: Install Dependencies
To send emails asynchronously, you need a special library. Python’s built-in `smtplib` is synchronous and can block your application. We’ll use `aiosmtplib`, which is designed for asynchronous operations.
- Open your terminal in your project directory.
- If you use `pip`, run:
pip install aiosmtplib - If you use `uv`, run:
uv add aiosmtplib
Note: The `email-validator` package for Pydantic’s email string type is usually included if you installed FastAPI with standard extras. If not, install it separately.
Step 2: Configure Email Settings
You need to tell your application how to connect to an email server. We’ll add these settings to your existing `config.py` file.
- Open your `config.py` file.
- Add the following settings to your `Settings` class:
reset_token_expiration_minutes: Set how long reset tokens are valid (e.g., 60 minutes).mail_server: The hostname of your SMTP server (e.g., ‘sandbox.smtp.mailtrap.io’).mail_port: The port number for your SMTP server (usually 587 for TLS).mail_username: Your SMTP username.mail_password: Your SMTP password. UseSecretStrto keep it secure.mail_from: The email address that will appear in the ‘From’ field.mail_use_tls: Set toTrueto use TLS encryption.frontend_url: The base URL of your frontend application. This is crucial for building correct reset links.
Expert Tip: Store sensitive information like your email password in a .env file, not directly in your code. Use SecretStr from Pydantic to prevent accidental logging of these secrets.
Step 3: Set Up a Development Email Service (Mailtrap)
During development, you don’t want to send real emails. Mailtrap is a free service that acts as an email sandbox. It catches your test emails, letting you inspect them without sending them to actual users.
- Go to Mailtrap.io and create a free account.
- After logging in, choose ‘Email Sandbox’ and create a new inbox for your project (e.g., ‘FastAPI Blog’).
- Navigate to your new sandbox and find the ‘Show Credentials’ or similar option.
- Copy the SMTP host, port, username, and password provided by Mailtrap.
- Open your
.envfile and add these credentials for your email settings.
Security Note: Never hardcode your email credentials directly in your code. Always use environment variables or a secret management system.
Step 4: Create Email Utility Functions
We’ll create a new file, `email_utils.py`, to handle sending emails and creating email templates.
- Create a new file named
email_utils.pyin your project’s root directory. - Add necessary imports:
EmailMessage(from `email.message`),aiohttp(from `aiosmtplib`), andJinja2(for templates). Also import yourSettings. - Set up the Jinja2 template environment. Ensure it points to your `templates/email` subdirectory.
- Create a
send_emailfunction that takes recipient, subject, plain text body, and HTML content as arguments. This function will useaiohttpto connect to your SMTP server and send the email. It includes a plain text fallback for email clients that don’t support HTML. - Create a
send_password_reset_emailfunction. This function will:- Build the password reset URL using your
frontend_urland a generated token. - Render an HTML email template (which we’ll create next) using Jinja2, passing the reset URL and username.
- Include a plain text version of the email.
- Call the
send_emailfunction to send the prepared email.
- Build the password reset URL using your
Expert Note: Always include a plain text version of your email. Some email clients or users may not be able to view or prefer plain text over HTML.
Step 5: Create the Password Reset Email Template
Emails need their own HTML templates. These should be simple and use inline CSS for best compatibility across different email clients.
- Inside your main `templates` directory, create a new subdirectory called `email`.
- Inside the `email` directory, create a file named
password_reset.html. - Use basic HTML structure. Employ table-based layouts for consistency.
- Add inline CSS for styling (e.g., font, background color). Many email clients strip out “ tags.
- Include placeholders for the username and the reset URL using Jinja2 syntax (e.g.,
{{ username }},{{ reset_url }}). - Add a clear call-to-action button or link for the reset URL.
- Include a notice about the token’s expiration time (e.g., ‘This link will expire in 1 hour’).
- Consider adding a fallback link for email clients that struggle with buttons.
Important: Avoid complex CSS or JavaScript, as most email clients do not support them well.
Step 6: Define Database Models for Reset Tokens
Instead of using JWTs for password resets, it’s best practice to use secure, single-use tokens stored in your database. This allows you to invalidate tokens easily.
- Open your `models.py` file.
- Add a new SQLAlchemy model called
PasswordResetToken. This model should include: id: Primary key.user_id: A foreign key linking to the `users` table.token_hash: A string (e.g., 64 characters) to store the hashed version of the token. Never store the plain token.expires_at: ADateTimefield indicating when the token expires.- Add a relationship in your
Usermodel to link back toPasswordResetToken. This allows you to easily find all reset tokens associated with a user.
Security Best Practice: Store only the *hash* of the token in the database, not the token itself. This way, if your database is compromised, the actual reset tokens remain secret.
Step 7: Create Token Generation and Hashing Utilities
You need functions to create secure tokens and hash them for storage.
- Open your `utils.py` file (or wherever you store utility functions like password hashing).
- Add imports for `hashlib` and `secrets`.
- Implement a
generate_reset_token()function using `secrets.token_urlsafe(32)`. This creates a secure, URL-safe random string. - Implement a
hash_reset_token()function using `hashlib.sha256()`. This function takes the token string, encodes it, and returns its hexadecimal hash digest.
Expert Note: SHA256 is suitable for hashing tokens because tokens are already random and unpredictable. For passwords, which can be weak, a slower, more computationally intensive hash like Argon2 is preferred to prevent brute-force attacks.
Step 8: Define Request Schemas
FastAPI uses Pydantic schemas to define the structure of request data.
- Open your `schemas.py` file.
- Add the following new schemas:
ForgotPasswordRequest: Requires only anemail(string). Used when a user first requests a password reset.ResetPasswordRequest: Requires thetoken(string from the URL) and thenew_password(string). Used when submitting the new password after clicking the reset link.ChangePasswordRequest: Requires thecurrent_passwordand thenew_password. Used by logged-in users to change their password.
Step 9: Implement API Endpoints
Now, let’s add the actual API routes to handle password reset requests.
Step 9.1: The Forgot Password Endpoint
This endpoint is called when a user enters their email to start the reset process.
- Open your `users/router.py` file.
- Add necessary imports:
BackgroundTasksfrom `fastapi`, `delete` from `sqlalchemy` (aliased as `sql_delete`), your token utilities (`generate_reset_token`, `hash_reset_token`), and your email utility (`send_password_reset_email`). Also import your new schemas. - Add a new POST endpoint, e.g.,
/request-password-reset. - This endpoint should accept:
- The
ForgotPasswordRequestschema. BackgroundTasksto run email sending in the background.- A database session.
- Inside the endpoint logic:
- Find the user by email (case-insensitively).
- If the user exists, delete any existing reset tokens for that user.
- Generate a new, unhashed token using `generate_reset_token()`.
- Hash the token using `hash_reset_token()`.
- Calculate the expiration time based on your settings.
- Create a new
PasswordResetTokenrecord in the database with the hashed token and expiration time. - Add a background task to call
send_password_reset_email, passing the user’s email, the unhashed token, and other necessary details. - Return a
202 Acceptedstatus code with a generic message (e.g., ‘If an account with that email exists, a reset link has been sent.’). Do not confirm if the email address is valid to prevent enumeration attacks.
Security Note: Returning a 202 status code and a generic message, regardless of whether the email exists, prevents attackers from discovering valid email addresses in your system.
Step 9.2: The Reset Password Endpoint
This endpoint is called when the user clicks the link in the email and submits their new password.
- In the same `users/router.py` file, add a new POST endpoint, e.g.,
/reset-password. - This endpoint should accept:
- The
ResetPasswordRequestschema. - A database session.
- Inside the endpoint logic:
- Hash the token provided in the request using `hash_reset_token()`.
- Query the database for a
PasswordResetTokenmatching the hashed token. - If no token is found, return an error (e.g., ‘Invalid or expired reset token’).
- Check if the token has expired. If it has, delete the token from the database and return an error. Note: For SQLite, you might need to add timezone info back to the stored datetime for accurate comparison. This is a workaround for SQLite and not needed for PostgreSQL.
- If the token is valid and not expired, find the associated user.
- If the user doesn’t exist, return an error.
- Update the user’s password hash with the new password (using your existing password hashing logic).
- Delete all
PasswordResetTokenrecords associated with this user to invalidate any other pending tokens. - Commit the changes to the database.
- Return a success message (e.g., ‘Password successfully reset.’).
Important: After a successful password reset, always delete all related reset tokens for that user to ensure they are single-use.
Step 9.3: The Change Password Endpoint (for Logged-in Users)
Allow logged-in users to change their password directly from their account page.
- In `users/router.py`, add a POST endpoint, e.g.,
/change-password. - This endpoint should be protected (require authentication) and accept:
- The
ChangePasswordRequestschema. - The currently logged-in user object (obtained via dependency injection).
- A database session.
- Inside the endpoint logic:
- Verify the user’s
current_passwordagainst their stored hash. - If the current password is correct, hash the
new_password. - Update the user’s password hash in the database.
- Commit the changes.
- Return a success message.
- If the current password is incorrect, return an appropriate error.
Step 10: Recreate the Database
Since you’ve added new database models, you need to update your database schema.
- If you are using SQLite, delete the existing database file (e.g., `blog.db`).
- If you are using a migration tool like Alembic (which you’ll learn later), run the appropriate migration commands.
- When your FastAPI application starts up, it will recreate the database with the new table structure.
Production Note: In a production environment, always use a migration tool like Alembic to manage database schema changes safely and efficiently.
Conclusion
You have now successfully implemented a secure password reset flow in your FastAPI application. This includes handling email sending asynchronously, generating and validating secure tokens, and creating robust API endpoints. These features are essential for a complete and user-friendly authentication system.
Source: Python FastAPI Tutorial (Part 14): Password Reset – Email, Tokens, and Background Tasks (YouTube)