Introduction
In the last couple of posts in TDD Auth with FastAPI we’ve been sustainably moved towards a web service that can let users register with the service. Now what?
We have already done the easy part. The next part is to look at the authorization. This involves letting the user log in. And only give access to what they are authorized for. Let us look at the login part first.
Before talking into login, I’d like to tell you that I have always found authentication and authorization challenging. There are so many terminologies involved. A few to name are OAuth, Basic Authentication, JWT, Cookies, SAML, and whatnot. I can’t explain all of them in this single post, but I’ll try to cover some basic building blocks to get you started.
Series Index
We have already seen:
- Project Setup and FastAPI introduction
- Database Setup and User Registration
- Fix the failing test with mocking and dependency injection
- Authentication Premier and Login Endpoint (you are here)
Authentication Premier
What is Authentication?
Authentication in most basic terms is the process of validating an identity to ascertain that they are who they claim to be. By the way, authentication can be achieved using passwords, OTPs, biometrics, authentication apps, access tokens, certificates, and more. We’ll be authenticating using passwords and tokens.
Following is a sequence diagram that roughly describes what happens in a successful login process.
Once you are authenticated, the server provides a token, this token then needs to be sent to the server in each request by the client. you are authorized to do certain activities on the application.
Different kinds of authentication (and scenario)
There are mainly two kinds of application authenticating.
User to application authentication. When I say user to application authentication, I refer to a scenario where a user (using a client, like a web browser) is logging into an application.
Application to application authentication. App to app authentication is when you use one service (say Google) to sign in to another application (say appX). The “Signin with
” button represents an example of app-to-app authentication.
Here you can argue that at the end of the day user is going to be authenticated so this is a user to app authentication. But my point is, that we are being authenticated to an application, using the data from another application.
We are not talking about an app to app authentication in this post. For this post, I’ll only be exploring simple authentication where a user uses their username and password. And when username and password match, an authentication token is sent to the frontend. Their token is required to be identified as logged in and to be able to access restricted resources.
In this section we talk about some terminologies and how are they related to each other.
HTTP and the Authorization Header
You must be knowing about different request and response headers when dealing with HTTP in general. Some of the common ones which are on top of my mind are:
- Request header:
User-Agent
,Referer
,Origin
,Host
,Content-Type
,Content-Length
,Accept
. - Response header:
Status
,Location
,Content-Type
,Cache-Control
.
Among those request headers, there is one more header called Authorization. If you are dealing with auth, the first thing you must do is learn more about this header. I will cover some of the basic info here and give you some links at the end of this section for further reading.
An Authorization
header looks like this:
Authorization: <scheme> <credentials>
<scheme>
is something we’ll see in the next section. <credentials>
are a piece of string, the value of which will depend on what the scheme is.
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization
Authentication Schemes
There are over half a dozen of authentication schemes, but these ones are common.
- Basic
- Bearer
- Digest
Basic
Basic authentication scheme deals with username and password. In basic HTTP authentication, a request contains a header field in the form of:
Authorization: Basic username:password
where credentials are base64 encoded so the actual header will look like this:
Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
Some final points:
- The security of the Basic scheme is only dependent on the transport layer i.e. HTTPS.
- HTTP does not provide a method for a web server to instruct the client to “log out” the user.
- You should not be using the Basic scheme. There is a similar approach taken in the Digest scheme which you might consider.
Bearer
Originally taken from OAuth, Bearer
deals with tokens. The name “Bearer authentication” can be understood as “give access to the bearer of this token”. In terms of the header, the Bearer is not much different from Basic. It looks like
Authorization: Bearer <credentials>
But instead of passing bare username:password in credentials, they are signed and encrypted. They are more secure than Basic because these tokens can have an expiry time set to them. When the server reads the token passed from a client and finds that it has expired, it marks the request as unauthorized. In that case, the client again has to generate the token.
Final points:
- Although Bearer is safer than Basic, it is highly advised to use them over HTTPS.
- We are going to discuss Bearer tokens only in this post. We’ll use JSON Web Token. If you are feeling crazy, read the spec of JWT to know about it in-depth.
- Bearer scheme in my opinion is the most widely used scheme on the internet.
https://stackoverflow.com/questions/34013299/web-api-authentication-basic-vs-bearer
Digest
Simply speaking, Digest is a complex and secure version of the Basic scheme. I have not used Digest at the moment and thus I’m not going to talk about it in this post. But if you want to read about it, please follow https://en.wikipedia.org/wiki/Digest_access_authentication.
Login Endpoint
Define requirement for authentication (/users/auth) endpoint
We have already seen in the above diagram that username and password go from the frontend application to the backend service endpoint. The client sending this password could be a web browser like Firefox, a CLI tool like curl, or even an API testing tool like Postman.
Now let us describe the functional requirement of our /users/auth
endpoint. Thinks about what our endpoints will look like in a finished product. Here is what I can think of:
The consumer hits
/users/auth
with the GET method and should get Method Not Allowed.The API consumer hits
/users/auth
with a POST method:- Without body = returns unauthorized
- With body:
- If user not found OR username exists but the password is wrong, returns unauthorized
- Should check in the database if the user exists, if so, creates and returns the JWT token
Functional requirements turned into test
Below are the test cases for the spec. This is a slightly modified version of the registration cases.
tests/test_users.py
|
|
And when you run these tests, they’re obviously gonna throw 404.
$ pytest --tb=line
============================== test session starts ==============================
platform linux -- Python 3.8.10, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /efs/repos/fastauth
plugins: anyio-3.4.0
collected 9 items
tests/test_main.py . [ 11%]
tests/test_users.py ....FFFF [100%]
==================================== FAILURES ===================================
/efs/repos/fastauth/tests/test_users.py:65: assert 404 == 405
/efs/repos/fastauth/tests/test_users.py:70: assert 404 == 422
/efs/repos/fastauth/tests/test_users.py:78: assert 404 == 422
/efs/repos/fastauth/tests/test_users.py:85: assert 404 == 201
Our tests are red. We’ll have to do something to turn them green.
Write code to fix failing test
Add new models for credential data and token
User has to send their username and password to be able to log in to the system and get a token. Here are the changes:
fastauth/schemas.py
|
|
UserAuthenticate
indicates a structure of JSON that we are supposed to receive in a request from a user.
Token
denotes a structure that the server will return.
Define /users/auth route
Let’s have a route as per our spec to let a user log in. This route, after receiving proper credentials, returns a JWT token
fastauth/main.py
|
|
Let us start from line 33 where we define the route /users/auth
. response_model
says that this endpoint in good terms returns a schemas.Token
. tags
, which is also on 25 is to group endpoint on Swagger UI.
On line 34, we see that authenticate_user
accepts user
which comes from a request, and db
which is a DB session.
On lines 35-37, we check if the passed user exists or not, if not, HTTP 403 is returned. If that is not the case, the function continues to run.
If the user exists, we check if the password matches with the help of a helper function check_username_password
. If it does not, we again throw a 403.
The only case that remained now is to return a token. On lines 43-47, we create a token with an expiry set to ACCESS_TOKEN_EXPIRE_MINUTES
minutes. After this much time, the token expires. We are again using a helper function called encode_jwt_token
.
Auth helper function
Now let us see what helper function we have for aiding in the authentication.
Before writing this file, please install PyJWT
.
pip install PyJWT
fastauth/auth.py
|
|
In check_username_password
, we are checking the received password with the password stored in the database. Of course, the database has the hashed version of the password store. This is one of the best practices we must incorporate. Please don’t store passwords in plain text.
In encode_jwt_token
, we use the PyJWT module to create a JWT token with 15 minutes of expiry. The code is pretty much self-explanatory.
Fixing create_user
I just noticed that I need to decode the output of bcrypt.hashpw
before saving it into the database.
|
|
Confirm if tests are passing
Automated tests
Let us run our test suite
$ pytest
==================== test session starts =====================
platform linux -- Python 3.8.10, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /efs/repos/fastauth
plugins: anyio-3.4.0
collected 9 items
tests/test_main.py . [ 11%]
tests/test_users.py ........ [100%]
===================== 9 passed in 2.17s ======================
Automated tests are in a happy state.
Test manually using SwaggerUI
Let’s test our API manually also.
With this, let’s end this post here. In the next post, we shall implement a protected route and shall see how we can use our so-called JWT token to access those routes. Until then, have a nice day.
Conclusion
In this post, we saw some of the very basic concepts to get you started with a login system. We saw how HTTP and Authorization header is involved in this. We also saw a different types of logins. We talked about Authentication Schemes.
At last, we came up with a login endpoint spec and turned it into tests, next we implemented the test. Kudos to you!
Please join my subscriber list to get updates on my blog. Please enter your email below.