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
/meendpoint, 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.
- Open your
schemas.pyfile. - Locate the
PostCreateschema. - Remove the
user_idfield. YourPostCreateschema should now inherit from a base schema (e.g.,PostBase) that only contains fields liketitleandcontent.
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.
- Open your
auth.pyfile. - Add the necessary imports:
annotatedfromtyping,Depends,HTTPException,statusfromfastapi,selectfromsqlalchemy,AsyncSessionfromsqlalchemy.ext.asyncio, your database models (e.g.,models.User), andget_dbfrom your database utility file. - Define the
get_current_userfunction. This function will:- Accept the database session (
AsyncSession) and the token (extracted using your existingOAuth2PasswordBearerscheme) as arguments. - Call your existing
verify_access_tokenfunction to get the user ID. If verification fails, raise anHTTPExceptionwith status code 401. - Convert the user ID to an integer. If this fails, raise a 401 exception.
- Query the database using SQLAlchemy’s
selectto retrieve the user object based on the user ID. - If the user is not found in the database, raise an
HTTPExceptionwith status code 401. - If successful, return the user object.
- Accept the database session (
- 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:
- Open your
routers/posts.pyfile. - Import
CurrentUserfrom yourauth.pyfile. - In the
create_postfunction signature, addcurrent_user: CurrentUseras a parameter. This automatically protects the route. - Remove the block of code that manually verifies if the user exists.
- Update the line where you assign the user ID to the post: change
post.user_id = user_idtopost.user_id = current_user.id.
Protecting update_post and delete_post Routes (with Ownership Checks):
- In
routers/posts.py, for theupdate_post_full,update_post_partial, anddelete_postfunctions:- Add
current_user: CurrentUserto 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.
- Add
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:
- Open your
routers/users.pyfile. - Locate the
/meendpoint function. - Remove all the manual code for token extraction, verification, user ID conversion, and database lookup.
- The only dependency needed is now
current_user: CurrentUser. - Simply return the
current_userobject. This drastically simplifies the endpoint.
Protecting User Profile Routes (update_user, delete_user):
- In
routers/users.py:- For the
update_userfunction, addcurrent_user: CurrentUserto 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_userfunction, addcurrent_user: CurrentUserto 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").
- For the
Step 4: Update Frontend for Authorization
The frontend needs to send the JWT with requests and handle authorization errors gracefully.
Layout and Navbar Updates:
- Open your
layout.htmlfile. - 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.
- Replace the existing JavaScript block in
layout.htmlwith 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:
- Open your
static/js/posts.js(or equivalent file for post creation). - Import the
get_tokenfunction from yourauth.jsfile. - Before making the fetch request, add a check: if no token exists, redirect to the login page.
- Remove the hard-coded user ID (e.g.,
user_id: 1) from the data being sent in the fetch request. - In the fetch request headers, add the
Authorization: Bearer {token}header. - After receiving the response, check if the status code is 401. If it is, redirect to the login page.
Post Detail Page Updates:
- Open your
templates/post.htmlfile. - Modify the HTML structure for post actions (edit/delete buttons). Add an ID (e.g.,
post_actions) and a Bootstrap class (e.g.,d-noneordisplay-none) to hide these actions by default. - Replace the entire script block at the bottom of
post.htmlwith the new script. This script will:- Import
get_current_userandget_token. - Store the post owner’s ID.
- Implement an
check_ownershipfunction 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_ownershipwhen the page loads.
- Import
Step 5: Implement the Account Page
Create a dedicated page for users to view and manage their profile information.
Backend Route for Account Page:
- Open your
main.pyfile (or your main application router file). - Add a new route for
/account. This route should render anaccount.htmltemplate. - 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:
- Create a new file:
templates/account.html. - 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.
- In the script block of
account.html:- Implement a
load_user_datafunction that fetches the current user from the/meAPI endpoint. If no user is found, redirect to login (client-side guard). - Implement an
update_form_handlerthat sends a PATCH request to update user details, including theAuthorizationheader. Clear the user cache after a successful update. - Implement a
logoutfunction that clears the token and redirects to the homepage. - Implement a
delete_account_handlerthat sends a DELETE request to the API, clears the token, and redirects to the homepage upon success.
- Implement a
Step 6: Testing Your Implementation
Thorough testing is essential to ensure your authorization logic works correctly.
- Delete your current database file (e.g.,
blog.db) to start with a clean slate. - Restart your FastAPI server.
- Test Unauthorized Access: Try to create a post without logging in. You should receive a 401 Unauthorized error.
- Test Creating a Post: Register a new user, log in, and create a post. Verify that the post is created successfully and the
user_idis correctly associated with the logged-in user. - 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.
- Test Account Page: Navigate to the account page. Verify that your user information is displayed correctly. Test updating your profile information.
- Test Logout: Log out and verify that you are redirected appropriately and cannot access protected resources.
- 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)