I have recently been exploring how to use FastAPI to build web apps. Check out this recent blog post How to create a FastAPI Web App with MongoDB and Beanie. One area I have found difficult is configuring authentication for web apps in FastAPI. The FastAPI security docs are really good, however they focus only on securing API endpoints, and not how to implement security for a web app. This blog post will demonstrate how to stand up a fully functional FastAPI web app with authentication.
TL/DR
All of the code can be found on GitHub: https://github.com/SamEdwardes/personal-blog/tree/main/blog/2022-04-14-fastapi-webpage-with-auth. You can download the code as a zip file using think link from DownGit. If you you want to see the complete code jump right to the Full code overview section of this blog post.
Motivation
Setting up authentication is an important step in taking your app from a proof of concept to something you can actually deploy and use. In this blog post I want to implement authentication in a FastAPI app that works for web apps.
The site I will build is very simple, but hopefully it is complex enough to demonstrate how you could expand on these concepts and build out a full functional web app with authentication. There are two primary ways I want to use authentication:
- Define endpoints that are only accessible to users who have authenticated, and
- Have content in pages that will dynamically render only if a user is authenticated.
Expand this section to see a screenshot and brief description of each page.
Home page
The home page has two different possible views. A view for users who are NOT logged in:
And a view for users who are logged in:
Login page
A simple login page:
Private page
A “private” page that can only be viewed by users who are logged in:
If you try to view the page before logging in you will get this response:
Project setup
To get started first create a new directory for all of the project files and then create/activate a python virtual environment:
Next, set up the file structure for the project:
After running the commands your file structure should look like this:
Next, set up your requirements.txt file:
Install all of the requirements, and you are ready to go!
If you want to see the app running in action copy and paste the code from the Full code overview section into the appropriate file. Then run the following to launch the app:
Full code overview
Your project structure will look like this:
Below is the complete code for each file 🎉!
Code walk through
In the following section I will walk through the key parts of code and describe what they are doing.
Step 1: Models and data
In order to keep our example simple our app will not use an external database. Instead, we will just use pydantic models to define some dummy data.
One key item it point out is the use of passlib. When a we create our user we hash the password. Imagine that you were storing data about your users in a database. It is important to hash the password so that if data was ever exposed to the public the passwords would be hashed, and not readable to the public.
We have also created a function called get_user
. This function will be used in many places throughout to access users from our “database”.
Here is a quick example of what passlib hashed passwords look like:
Step 2: Setup FastAPI
Nothing too fancy is happening here. As with any FastAPI app we initiate our FastAPI()
app object. I will point out a few areas of interest:
settings
: we create a settings object to store some settings information that will be accessed by different parts of our app. You do not need to do this using a class, but I chose to use a class as I think it is a clean way to organize the code.templates
: To make a web app we need some way to build out a user interface. FastAPI comes with built in support for using Jinja. The linetemplates = Jinja2Templates(directory="templates")
tells FastAPI where our template files are located.
Step 3: Authentication logic
To me, this is the most complex part of the app. Expand the code snippet below to see all of the details. In this section I will walk through some of the key parts in more detail below.
Expand to see the code snippets related to authentication 🔐
OAuth2PasswordBearer
FastAPI comes with a class out of the box named OAuth2PasswordBearer
(see FastAPI docs: Security - First Steps). This is useful for authenticating APIs. However, I could not figure out how to use it in the context of a web app.
In order to use a similar approach that works with web apps I made a small modification to FastAPIs implementation of OAuth2PasswordBearer
. Instead of getting the authorization
from the request
header, we get it from the request
cookie.
After defining our custom OAuth2PasswordBearerWithCookie
we create an instance.
Access token
The access token is essentially how we will allow users to access protected web pages after authenticating. Just like in the FastAPI docs, I will use JWT tokens. Read more about them here: https://jwt.io. Our function create_access_token
will be used to create a new JWT after a user has authenticated. The token will then be stored in the browser as a cookie (in a following code snippet).
Authenticating a user
We need a function that performs the actual user authentication when someone attempts to login. Our authenticate_user
function will do two things:
- First it will check to see if the username exists in the database.
- If the username does exist, it will check to see that the passwords match. Since we have saved the hashed password in our database we need to hash the provided password using the same algorithm.
Decoding the token
The above function authenticate_user
is only called when a user first logs in. Our next function decode_token
will be called to grant an already logged in user access to an endpoint. This function will decode the JWT so that we can get the required information from it. In our use cases, we need to decode the token so that we can figure out who the user is. There are a few key things to point out:
- We the line
token = token.removeprefix("Bearer").strip()
removes the string “Bearer” from the start of token string. This will be necessary when we are calldecode_token()
from within theget_current_user_from_cookie()
function.
Below is a little example that demonstrates how JWT encodes your information. The username is actually stored in your cookie. But, since it is encrypted it is not possible to know the actual username unless your key the secret key to decrypt the token.
You can see the cookies that are currently set in your browser using your browser’s built in developer tools.
Getting the user from JWT
In order to not ask the user for a username and password on every request we need a way to securely remember that they have already been validated. In section Login for access token we will save our JWT token as a cookie. When decoded, that cookie contains the users username. If the token has a valid username, we will provide the user with access to the endpoint. To get a token, you need a username and password. So we can trust that if they have a token they have already authenticated.
The two functions below cover two different scenarios:
get_current_user_from_token
: This function will be used when we want to protect an entire endpoint. For example see section Private page.get_current_user_from_cookie
: This function will be used for endpoints that will be available to uses who have authenticated and those who have not. However, the behavior will be different based on if they have authenticated or not. For example see section Home page
Login for access token
OK, this is finally the last function related to authentication logic 🔒! In this function (which is also an API endpoint) we will save the JWT token in the users browser as a cookie. Note that this is the bit of code where we actually call authenticate_user
to validate that the password is correct. This endpoint will be called when the user submits their username and password using the login page.
Step 4: Views and HTML templates
If you made it this far, you are in the home stretch. All that is left is to define our views and HTML templates 🎉!
Home page
Our home page is actually pretty interesting. The content of the home page will be dynamic. If the user is logged in they will see one thing. If they are not logged in they will see something elses.
We achieve this in our API endpoint by attempting to get the current user from the cookie we set. If this function fails, it means the user is not logged in so we assign None
to user
.
Then on the templating side we render different content based on if user
is None
or not:
Private page
Our private page is only accessible to users who have logged in. Unlike our home page, we do not need any conditional logic here.
How do we achieve this? They key is our use of Depends
in the function definition. If we use user: User = Depends(get_current_user_from_token))
on any endpoint, that endpoint will only be accessible to users who have logged in.
Login page - GET
Our login page is very simple. It is accessible to everyone, and there is no conditional logic. When you hit the “submit” button of the login form it will send a post request to /auth/login
.
Login page - POST
The “post” side of logging in is a bit more complicated. There are a few things happening here:
- We create a class
LoginForm
to handle the logic of validating the form data. In the event that the submitted form data is not valid we will re-render the “templates/login.html” with the same form data and an error message. - In the event that the form is valid, we will attempt to validate the users credentials. The line
login_for_access_token(response=response, form_data=form)
will attempt to log the user in, and if successful set a cookie with the users data encoded. - Notice that we we validate the form our response is
response = RedirectResponse("/", status.HTTP_302_FOUND)
. This will redirect our user to the home page. It is important that you includestatus.HTTP_302_FOUND
. If you do not, FastAPI will send a post request against"/"
. We have not defined a post request endpoint for"/"
. By usingstatus.HTTP_302_FOUND
FastAPI knows to send a get request (this part was tricky and took me a lot of googling to figure out 🕵️).
Logout endpoint
The final feature our web app is the ability to logout. There is no logout view, instead we only have a logout endpoint. In the site map we include a link to call the logout endpoint. The logic of logging out is pretty simple:
- Delete the cookie containing the encoded user information.
- Redirect the user to the home page.
Wrap up
Congratulations 🎉! Your web app is complete and ready to go. Check out the complete source code on GitHub: https://github.com/SamEdwardes/personal-blog/tree/main/blog/2022-04-14-fastapi-webapp-with-auth>. If you have any questions or feedback find me on twitter @TheReaLSamlam, or submit an issue on GitHub: https://github.com/SamEdwardes/personal-blog/issues/new/choose.
Reference
Here are some of the resources I found useful while researching this blog post:
- https://fastapi.tiangolo.com/tutorial/security/
- https://www.fastapitutorial.com/blog/authentication-in-fastapi/
- https://github.com/talkpython/web-applications-with-fastapi-course
- https://stackoverflow.com/questions/62119138/how-to-do-a-post-redirect-get-prg-in-fastapi
- https://github.com/MushroomMaula/fastapi_login
- https://github.com/mjhea0/awesome-fastapi
- https://kernelpanic.io/demystifying-authentication-with-fastapi-and-a-frontend
- https://github.com/nofoobar/JobBoard-Fastapi/tree/0e1bdf676a66e3f341d00c590b3fd07b269488f4