Skip to content
OVEX TECH
Education & E-Learning

Secure Your FastAPI App: Protect Routes & Verify Users

Secure Your FastAPI App: Protect Routes & Verify Users

How to Secure Your FastAPI Application: Protect Routes and Verify Current User

In this tutorial, you’ll learn how to implement robust authorization in your Python FastAPI application. We’ll build upon the authentication system established in the previous tutorial to protect your API routes, ensuring that only authenticated and authorized users can access or modify specific resources. This is the crucial step where your authentication efforts translate into actual security for your application.

What You’ll Learn:

  • Create a reusable dependency to fetch the currently authenticated user.
  • Modify your Pydantic schemas to remove unnecessary user input for resource creation.
  • Protect API routes, ensuring only logged-in users can access them.
  • Implement ownership checks to allow users to edit or delete only their own content.
  • Refactor existing endpoints, like the /me endpoint, to leverage the new dependency.
  • Update your frontend to send authentication tokens and handle authorization responses.
  • Create a user account page for profile management.

Prerequisites:

  • A working FastAPI application with a functional registration and login system.
  • Understanding of JSON Web Tokens (JWT) and how they are generated and stored.
  • Basic knowledge of Pydantic schemas and SQLAlchemy models.
  • Familiarity with frontend JavaScript for handling authentication state and making API requests.

Step 1: Update Pydantic Schemas

Currently, your PostCreate schema likely includes a user_id field that is sent in the request body. This allows any user to claim to be another user by simply changing the user_id in their request. To fix this, we will remove the user_id from the schema, as the user’s identity will be determined from their authentication token.

  1. Open your schemas.py file.
  2. Locate the PostCreate schema.
  3. Remove the user_id field. Your PostCreate schema should now inherit from a base schema (e.g., PostBase) that only contains fields like title and content.

Expert Tip:

By removing the user_id from the client-sent data, you prevent users from impersonating others. The server will reliably determine the user from the trusted JWT.

Step 2: Create a Reusable get_current_user Dependency

This is the core of our authorization logic. We’ll create a dependency function that verifies the JWT, extracts the user ID, and fetches the full user object from the database. This dependency can then be used by any route that requires an authenticated user.

  1. Open your auth.py file.
  2. Add the necessary imports: annotated from typing, Depends, HTTPException, status from fastapi, select from sqlalchemy, AsyncSession from sqlalchemy.ext.asyncio, your database models (e.g., models.User), and get_db from your database utility file.
  3. Define the get_current_user function. This function will:
    • Accept the database session (AsyncSession) and the token (extracted using your existing OAuth2PasswordBearer scheme) as arguments.
    • Call your existing verify_access_token function to get the user ID. If verification fails, raise an HTTPException with status code 401.
    • Convert the user ID to an integer. If this fails, raise a 401 exception.
    • Query the database using SQLAlchemy’s select to retrieve the user object based on the user ID.
    • If the user is not found in the database, raise an HTTPException with status code 401.
    • If successful, return the user object.
  4. Create a type alias for convenience: CurrentUser = Annotated[models.User, Depends(get_current_user)]. This alias makes your route signatures cleaner.

Expert Note:

The /me endpoint you built previously is for direct client interaction. The get_current_user dependency is designed for internal use by other API routes to enforce authorization.

Step 3: Protect API Routes

Now, let’s integrate the CurrentUser dependency into your API routes to protect them.

Protecting the create_post Route:

  1. Open your routers/posts.py file.
  2. Import CurrentUser from your auth.py file.
  3. In the create_post function signature, add current_user: CurrentUser as a parameter. This automatically protects the route.
  4. Remove the block of code that manually verifies if the user exists.
  5. Update the line where you assign the user ID to the post: change post.user_id = user_id to post.user_id = current_user.id.

Protecting update_post and delete_post Routes (with Ownership Checks):

  1. In routers/posts.py, for the update_post_full, update_post_partial, and delete_post functions:
    • Add current_user: CurrentUser to their function signatures.
    • Remove the manual user existence verification block.
    • After verifying that the post exists, add an ownership check: if post.user_id != current_user.id: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized to update/delete this post").
    • For update routes, remove the line that updates the post’s user_id.

Understanding HTTP Status Codes:

  • 401 Unauthorized: Indicates that the request lacks valid authentication credentials (e.g., missing or invalid token). The client should authenticate.
  • 403 Forbidden: Indicates that the client is authenticated but does not have permission to perform the requested action. The client is identified but denied access.

Refactoring the /me Endpoint:

  1. Open your routers/users.py file.
  2. Locate the /me endpoint function.
  3. Remove all the manual code for token extraction, verification, user ID conversion, and database lookup.
  4. The only dependency needed is now current_user: CurrentUser.
  5. Simply return the current_user object. This drastically simplifies the endpoint.

Protecting User Profile Routes (update_user, delete_user):

  1. In routers/users.py:
    • For the update_user function, add current_user: CurrentUser to the signature. At the beginning of the function, add an ownership check: if user.id != current_user.id: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized to update this user").
    • For the delete_user function, add current_user: CurrentUser to the signature. At the beginning of the function, add the ownership check: if user.id != current_user.id: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized to delete this user").

Step 4: Update Frontend for Authorization

The frontend needs to send the JWT with requests and handle authorization errors gracefully.

Layout and Navbar Updates:

  1. Open your layout.html file.
  2. Update the logged-in navbar. Replace the username display and logout button with a single “Account” link. The logout functionality will be moved to the account page.
  3. Replace the existing JavaScript block in layout.html with the updated script. Key changes include updating the “Account” button text with the username and removing the direct logout handler.

Create Post Form Handler Updates:

  1. Open your static/js/posts.js (or equivalent file for post creation).
  2. Import the get_token function from your auth.js file.
  3. Before making the fetch request, add a check: if no token exists, redirect to the login page.
  4. Remove the hard-coded user ID (e.g., user_id: 1) from the data being sent in the fetch request.
  5. In the fetch request headers, add the Authorization: Bearer {token} header.
  6. After receiving the response, check if the status code is 401. If it is, redirect to the login page.

Post Detail Page Updates:

  1. Open your templates/post.html file.
  2. Modify the HTML structure for post actions (edit/delete buttons). Add an ID (e.g., post_actions) and a Bootstrap class (e.g., d-none or display-none) to hide these actions by default.
  3. Replace the entire script block at the bottom of post.html with the new script. This script will:
    • Import get_current_user and get_token.
    • Store the post owner’s ID.
    • Implement an check_ownership function to compare the post owner ID with the currently logged-in user’s ID. If they match, it reveals the edit/delete buttons.
    • Update the edit and delete form handlers to include token checks, authorization headers, and specific error handling for 401 (redirect to login) and 403 (show an “not authorized” modal).
    • Call check_ownership when the page loads.

Step 5: Implement the Account Page

Create a dedicated page for users to view and manage their profile information.

Backend Route for Account Page:

  1. Open your main.py file (or your main application router file).
  2. Add a new route for /account. This route should render an account.html template.
  3. Note: This route is not protected on the server-side in the same way API endpoints are. The protection is handled client-side by JavaScript, as JWTs are not automatically sent with standard browser requests.

Frontend Account Template:

  1. Create a new file: templates/account.html.
  2. Add the HTML structure for the account page, including sections for:
    • Displaying user profile information (username, email).
    • A placeholder for profile picture updates (to be implemented later).
    • A “Update Profile” form.
    • A “Logout” button.
    • A “Danger Zone” section with a “Delete Account” button.
  3. In the script block of account.html:
    • Implement a load_user_data function that fetches the current user from the /me API endpoint. If no user is found, redirect to login (client-side guard).
    • Implement an update_form_handler that sends a PATCH request to update user details, including the Authorization header. Clear the user cache after a successful update.
    • Implement a logout function that clears the token and redirects to the homepage.
    • Implement a delete_account_handler that sends a DELETE request to the API, clears the token, and redirects to the homepage upon success.

Step 6: Testing Your Implementation

Thorough testing is essential to ensure your authorization logic works correctly.

  1. Delete your current database file (e.g., blog.db) to start with a clean slate.
  2. Restart your FastAPI server.
  3. Test Unauthorized Access: Try to create a post without logging in. You should receive a 401 Unauthorized error.
  4. Test Creating a Post: Register a new user, log in, and create a post. Verify that the post is created successfully and the user_id is correctly associated with the logged-in user.
  5. Test Ownership Restrictions:
    • Log in as User A and create a post.
    • Log in as User B.
    • Attempt to edit or delete User A’s post. You should receive a 403 Forbidden error.
    • Attempt to edit or delete User B’s own post. This should succeed.
  6. Test Account Page: Navigate to the account page. Verify that your user information is displayed correctly. Test updating your profile information.
  7. Test Logout: Log out and verify that you are redirected appropriately and cannot access protected resources.
  8. Test Delete Account: On the account page, use the “Delete Account” functionality. Verify that the user and their associated posts are deleted, and you are logged out.

By following these steps, you have successfully implemented a robust authorization layer for your FastAPI application, ensuring that only authenticated and authorized users can access and modify data according to their permissions and ownership.


Source: Python FastAPI Tutorial (Part 11): Authorization – Protecting Routes and Verifying Current User (YouTube)

Leave a Reply

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

Written by

John Digweed

1,282 articles

Life-long learner.