User Auth in FastAPI with JWT Tokens

UPDATED ON:

undraw svg category

Welcome to Part 7 of Up and Running with FastAPI. If you missed part 6, you can find it here .

This series is focused on building a full-stack application with the FastAPI framework. The app allows users to post requests to have their residence cleaned, and other users can select a cleaning project for a given hourly rate.

Up And Running With FastAPI

In the previous post, we implemented a naive registration system that lets users sign up. We didn’t provide a login mechanism yet, and for good reason. At the moment, new users’ passwords are being stored in plain text and we’re hard-coding in a salt instead of generating one ourselves. In fact, we’re not doing any kind of authentication or authorization, and that’s exactly what we’re going to handle in this post. Brew a fresh cup of coffee, as this is going to be a long one.

Security and New Packages

Before we get going, we’ll need to install a few new packages and rebuild our docker container. Let’s get that out of the way now.

requirements.txt

We’re installing two new packages here:

Go ahead and build the docker container with the new packages.

While we wait for our container to build, let’s chat about security.

So why do we need to hash passwords in the first place? Well, it’s probably easier to think about the problems with the reverse case. If we stored plaint text passwords in our database, anyone who works on our application would be able to see each user’s password whenever they want.

Even if we could completely trust everyone who has access to our database, we can’t trust the rest of the Internet. If our application were to be hacked, every user’s password would be available to that hacker without any additional effort. Given that most people reuse passwords for multiple applications, that situation would be a security nightmare.

Instead, what we’ll do is generate a unique hash for every user’s password upon registration, and store the hashed password in the database instead. So when the user attempts to login, we simply hash the submitted password using the same algorithm and check the output against what we have stored in the database for that user. Using this approach obfuscates the user’s actual password, and still gives us a way to securely authenticate users.

However, there’s a small problem with this case as well. Hackers can create what are known as Rainbow Tables - precomputed hash tables - where they could simply search for the hash and find the corresponding plain-text password. To combat this problem, we’ll add a unique salt to our hashing algorithm.

Any time a user signs up, a new salt is randomly generated for their password and stored in the database. When a user authenticates, the salt and the password are concatenated and processed with our cryptographic hash function. This method mitigates the batch advantage of multi-target attacks by forcing hackers to only crack one password at a time. The process would then be slow and arduous, eliminating the advantage of precomputed hash tables.

Now when a user attempts to login, we have a tried and true method for authentication. We’ll look up the user by username or email address, grab the salt we’ve generated for them, and hash the combination of submitted password and salt. If the output matches what we’ve stored in our database, we consider that user logged in.

So how do we keep users logged in? That’s where JWTs come in. JSON Web Tokens are an open, industry standard method for representing claims securely between two parties.

DISCLAIMER: As with any security system, there are problems with JWTs - but that’s a discussion for another post. For our application, they fit quite nicely.

Users that authenticate successfully will be sent an encoded, stateless token holding just enough information to uniquely identify the user. Any future requests must include that token in the request headers, and that will be how we know who the request is coming from. Tokens expire after a set period of time, so the user won’t be authenticated forever. The developer can determine exactly what time window works best for their own application.

Combining all of those pieces together gives us the system that what we’ll implement here and passlib with pyjwt will be a central part of orchestrating this authentication flow.

Configuring Our Auth Service

As the FastAPI docs state, security is often a complex and “difficult” issue. Readers who are unfamiliar with standard authentication practices are highly encouraged to read the FastAPI docs on security, as they cover a large amount of jargon that will be useful in understanding the parts to come.

Let’s quickly explore this diagram to get a feel for the registration flow we expect users to go through.

So the process can be broken into steps:

  1. Users send their username, email, and password to our server
  2. We check for email and username uniqueness. If all is clear, we generate a salt and use it to hash the user’s password. We store the user and their hashed password in our database.
  3. Taking the user’s username or email address, we encode it into a JSON Web Token and send that token back to the user.
  4. Any time the user needs to make a request to our API, they include the token we’ve sent them and we identify them by decoding its contents.

We’re going to start by creating an AuthService class that will handle any authentication-related logic. Admittedly, simple functions would probably accomplish the same task. But since we’ll be adding quite a bit of logic over the next couple posts, we’ll stick with a class and refactor as needed.

And add the following to the new authentication.py file:

services/authentication.py

Though relatively small, this utility class provides us with exactly the methods we need to salt our passwords and convert plaintext passwords into hashed ones.

We’ll instantiate a single instance in the services/__init__.py file and use it throughout our application.

services/__init__.py

Before we move on to the next part, let’s try out our new service so we get a feel for what’s going on. Let’s enter our docker container and get a python repl going.

Once inside the repl, import the service and test the methods on our AuthService class.

Some interesting things going on here.

Every salt and hashed password generated by our auth_service starts with "$2b$12$". The $2b is the ident and it specifies the version of the bcrypt algorithm used when creating a new hash. The $12$ identifies how many rounds are used to hash the password (technically it’s log rounds, so it actually uses $2^{12}$ here). Feel free to play around with this bcrypt calculator to see how the bcrypt hashing algorithm works right in the browser.

Observant readers may also have identified something rather concerning - we called the auth_service.hash_password twice with the same arguments and got a different output!

What happened?

Anyone familiar with passlib and bcrypt (or readers who’ve read the FAQ for in the passlib docs ) will know that bcrypt automatically generates a salt each time a password is hashed, and stores it in the hash table. That means we don’t even need to explicitly create our own salt or store it in the database with our user. All of that is done for us.

It is nice to understand what’s happening underneath the hood, so we’ll keep our current methodology for now. It won’t affect our auth system, so don’t worry. It’ll just require a bit of extra overhead.

Refactoring User Registration

We’re about ready to bring our password hashing algorithm into our user registration flow, but let’s add a new test first.

tests/test_users.py

We start by creating a new user as before, and also checking that the user exists in the database after the POST request. Afterwards we ensure that the salt we’re storing in the database exists and is not the "123" we currently have hardcoded in. We also check that the password we have stored in the database is not the one the user originally sent to the server. Finally, we assert that the verify_password function - which we haven’t written yet - tells us that the hashed version of the submitted password matches the one we have stored.

Run the test and watch it fail. We can fix this easily by removing the “123” we’ve hardcoded in, and we will. We’ll also use our new auth_service to handle password hashing for us. Head into the UsersRepository and update it with the following:

db/repositories/users.py

We’re attaching the auth_service to our UsersRepository and applying the create_salt_and_hashed_password method to the user’s plaintext password. Once we get the UserPasswordUpdate that’s returned from our method, we use it to update the new_user before exporting the model to a dictionary and passing it to our REGISTER_NEW_USER_QUERY.

At this point, our test is almost passing. Run it again and it should inform us that the AuthService object has no verify_password method. Let’s go ahead and take care of that now.

services/authentication.py

The verify_password method uses our previously defined password context to verify the password/salt combo with our hashed password stored in the database.

Run the tests again and watch them pass.

At this point, we have the foundation of a legit authentication system set up. However, we still have no way of keeping the user logged in, so we’ll have handle that part next.

JSON Web Tokens

Readers who unfamiliar with token-based authentication are highly encouraged to read JWT.io’s introductory article found here . For the initiated, the basics are as follows:

A JWT is simply an encoded string containing three parts separated by periods. The example provided in the previously mentioned article states that they follow this general structure: xxxxxx.yyyyyy.zzzzz. These three parts are called the header , payload , and signature , respectively.

Users’ usernames (or email addresses) will be encoded in the payload and will be used to identify them in every request across our API. The encoding algorithm will be stored in the header, and the signature will be a string composed of the other two parts, base64 encoded, and then signed using a secure secret.

Readers who feel that the previous paragraphs are way over their heads should check out JWT.io’s debugger tool found here and get a feel for how JWTs are created.

Fortunately, we won’t have to worry too much about how to encode the JWT, as we have the pyjwt library to handle that for us. It is important for us to know what they’re made of, however, as we’ll be composing them in the next couple sections.

Token Models and Updating the Config

At this point, we’re ready to start defining some Pydantic models that we’ll use to validate the shape of our auth tokens. Create a new token.py file in our models directory like so:

And add the following code:

models/token.py

Ok, let’s break down what’s going on here.

We’re using multiple models to compose the payload that we’ll encode into our JWTs. The JWTMeta has most of the attributes our payload will need:

  1. iss - the issuer of the token (that’s us)
  2. aud - who this token is intended for
  3. iat - when this token was issued at
  4. exp - when this token expires and is no longer valid proof that the requesting user is logged in.

We’re setting default values for all of them, but we can also customize any attribute as needed. Our JWTCreds model will store the fields that we’ll use to identify the user - in this case the email and username. As we build our JWT, we’ll combine these two models and encode the results to serve as our token payload.

That encoded string will be attached to an access token and sent to the user once we’ve successfully authenticated them. The token_type attribute on our AccessToken model gives us the flexibility to modify our authentication system at a future date.

And that’s it! Not too bad, right?

Before we move on, let’s talk about the items we’re importing from config. It’s a good idea to standarize how we handle JWTs across our application, so this is the perfect time to beef up our config.

The first thing we’ll need it a secret key. There are many ways to get one, but the easiest is to run this in the terminal:

That’ll give us a string that looks like dada789f4abcdef14.... Copy that and open up the core/config.py file. Remove the CHANGEME that we defaulted our SECRET_KEY to in our second post. Let’s also add other JWT-specific options we’ll need.

core/config.py

We’ve removed the default from our SECRET_KEY, meaning it’ll throw an error if none is found in our .env file. Go ahead and add the secret key openssl just generated for us to our .env file.

Besides the items we’re using in our token models, we’ve also defined the jwt algorithm we’ll use to encode tokens and the token prefix we’ll expect to see in the authorization header users will send for authenticated requests.

Make sure to stop the currently running docker container and start it up again. Once that’s ready to go, we’re on to some tests!

Test Fixtures for Users and Token Headers

This is going to be a heavily involved section, so buckle up.

We’ll need to handle a few new wrinkles associated with creating and testing users, so we’ll start by updating our conftest.py file with a new fixture:

tests/conftest.py

For the most part, the test_user fixture is standard. It instantiates a UserCreate model with whatever test data we want, checks if a user exists in the database with the same email, and returns that user if a record is found. Otherwise, it creates a new user and then returns the UserInDB model.

So why are we checking for user existence? Remember that our database persists for the duration of the testing session. With that in mind, also recall that both the username and email attributes have unique constraints on them. This is important, since we don’t want more than one user with the same email or with the same username in our system.

Accordingly, if we ran this test_user fixture more than once, pytest would throw an error stemming from the postgres unique constraint we’ve attached to both of these attributes. Curious readers should feel free to remove the existing_user check at a later point to observer this error in action.

Let’s also go ahead and write some tests that use the test_user fixture, beginning with auth token creation.

Update the tests/test_users.py file with the following:

tests/test_users.py

A bunch of fun things happenings here.

First, we’re testing to ensure that we can use our AuthService class to create an access token for a user. We decode the token using the pyjwt package and assert that it contains the username of the user who we created the token for.

Second, we test for the case where no user is encoded into the token, and ensure that we don’t see anything in the payload.

Last, we pass invalid secrets and audience strings to the create_access_token_for_user method and ensure that the pyjwt package raises a PyJWTError.

Run those tests and watch them fail.

We haven’t built our create_access_token_for_user method, so let’s do that now.

services/authentication.py

We start by simply returning None if we’re not passed a user or if the user isn’t an instance of UserInDB. If all is well we craft the token’s meta and creds, then use them to create our token payload. Using the JWT_ALGORITHM we selected in config and our SECRET_KEY cast to a string, we encode the payload into our token and return the string contents.

Run the tests again and they should all pass.

With our token logic in place, we should update our UserPublic model to also store an optional access token. Open up the models/user.py and update it like so:

models/user.py

By accepting an optional access_token attribute, we can now return the user along with their token as soon as they’ve registered.

Let’s do that now.

Head into api/routes/users.py and update our register_new_user route with the following:

api/routes/users.py

At this point, our registration route should return a valid access token. Because we’ve added an additional attribute to the UserPublic model, our successful registration test is no longer passing. That’s a simple fix.

tests/test_users.py

We can now run our tests again and watch them all pass.

Trying Out Our New Access Tokens

With all of that finished, we should now be able to see the token returned when we register a user.

Let’s head to FastAPI’s interactive docs and test it out.

open api docs

We enter in a user email, a unique username, and a password at least 7 characters in length and tada! Our access token is attached the response body - along with the rest of the user. We’re now ready to start implementing a login flow.

But we’ll save that until the next post.

Wrapping Up and Resources

There’s a lot packed into this one post, and we’re only just getting started. In the next post, we’ll dive a little further into security and handle login, along with some authorization dependencies.

Github Repo

All code up to this point can be found here:

Special thanks to Joao Ant for correcting errors in the original code.

Tags:

Previous Post undraw svg category

Designing a Robust User Model in a FastAPI App

Next Post undraw svg category

Auth Dependencies in FastAPI