Skip to content
OVEX TECH
Education & E-Learning

Implement Secure Password Reset in FastAPI

Implement Secure Password Reset in FastAPI

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.

  1. Open your terminal in your project directory.
  2. If you use `pip`, run: pip install aiosmtplib
  3. 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.

  1. Open your `config.py` file.
  2. 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. Use SecretStr to keep it secure.
    • mail_from: The email address that will appear in the ‘From’ field.
    • mail_use_tls: Set to True to 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.

  1. Go to Mailtrap.io and create a free account.
  2. After logging in, choose ‘Email Sandbox’ and create a new inbox for your project (e.g., ‘FastAPI Blog’).
  3. Navigate to your new sandbox and find the ‘Show Credentials’ or similar option.
  4. Copy the SMTP host, port, username, and password provided by Mailtrap.
  5. Open your .env file 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.

  1. Create a new file named email_utils.py in your project’s root directory.
  2. Add necessary imports: EmailMessage (from `email.message`), aiohttp (from `aiosmtplib`), and Jinja2 (for templates). Also import your Settings.
  3. Set up the Jinja2 template environment. Ensure it points to your `templates/email` subdirectory.
  4. Create a send_email function that takes recipient, subject, plain text body, and HTML content as arguments. This function will use aiohttp to connect to your SMTP server and send the email. It includes a plain text fallback for email clients that don’t support HTML.
  5. Create a send_password_reset_email function. This function will:
    • Build the password reset URL using your frontend_url and 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_email function to send the prepared email.

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.

  1. Inside your main `templates` directory, create a new subdirectory called `email`.
  2. Inside the `email` directory, create a file named password_reset.html.
  3. Use basic HTML structure. Employ table-based layouts for consistency.
  4. Add inline CSS for styling (e.g., font, background color). Many email clients strip out “ tags.
  5. Include placeholders for the username and the reset URL using Jinja2 syntax (e.g., {{ username }}, {{ reset_url }}).
  6. Add a clear call-to-action button or link for the reset URL.
  7. Include a notice about the token’s expiration time (e.g., ‘This link will expire in 1 hour’).
  8. 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.

  1. Open your `models.py` file.
  2. 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: A DateTime field indicating when the token expires.
  3. Add a relationship in your User model to link back to PasswordResetToken. 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.

  1. Open your `utils.py` file (or wherever you store utility functions like password hashing).
  2. Add imports for `hashlib` and `secrets`.
  3. Implement a generate_reset_token() function using `secrets.token_urlsafe(32)`. This creates a secure, URL-safe random string.
  4. 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.

  1. Open your `schemas.py` file.
  2. Add the following new schemas:
    • ForgotPasswordRequest: Requires only an email (string). Used when a user first requests a password reset.
    • ResetPasswordRequest: Requires the token (string from the URL) and the new_password (string). Used when submitting the new password after clicking the reset link.
    • ChangePasswordRequest: Requires the current_password and the new_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.

  1. Open your `users/router.py` file.
  2. Add necessary imports: BackgroundTasks from `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.
  3. Add a new POST endpoint, e.g., /request-password-reset.
  4. This endpoint should accept:
    • The ForgotPasswordRequest schema.
    • BackgroundTasks to run email sending in the background.
    • A database session.
  5. 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 PasswordResetToken record 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 Accepted status 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.

  1. In the same `users/router.py` file, add a new POST endpoint, e.g., /reset-password.
  2. This endpoint should accept:
    • The ResetPasswordRequest schema.
    • A database session.
  3. Inside the endpoint logic:
    • Hash the token provided in the request using `hash_reset_token()`.
    • Query the database for a PasswordResetToken matching 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 PasswordResetToken records 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.

  1. In `users/router.py`, add a POST endpoint, e.g., /change-password.
  2. This endpoint should be protected (require authentication) and accept:
    • The ChangePasswordRequest schema.
    • The currently logged-in user object (obtained via dependency injection).
    • A database session.
  3. Inside the endpoint logic:
    • Verify the user’s current_password against 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.

  1. If you are using SQLite, delete the existing database file (e.g., `blog.db`).
  2. If you are using a migration tool like Alembic (which you’ll learn later), run the appropriate migration commands.
  3. 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)

Leave a Reply

Your email address will not be published. Required fields are marked *

Written by

John Digweed

2,381 articles

Life-long learner.