Continuing with our Python FastAPI learning series, we will implement proper JSON Web Token (JWT) authentication as discussed in the official tutorial, with a few minor tweaks of our own.

🐍 Index of the Complete Series.

121-feature-image.png
Python FastAPI: Implementing JSON Web Token

The code requires Python 3.12.4. Please refer to the following discussion on how to upgrade to Python 3.12.4.

🚀 Please note, complete code for this post can be downloaded from GitHub with:

git clone -b v0.9.0 https://github.com/behai-nguyen/fastapi_learning.git

❶ Please note 🙏 that the code presented in this post is not my own. It is taken from the following official tutorial page, with a few tweaks: OAuth2 with Password (and hashing), Bearer with JWT tokens.

I am working towards implementing some new functionalities that would make sense with JSON Web Token (JWT) authentication. It would be easier to read if we break them down into individual shorter posts. Hence, this article.

Please also note that the JWT authentication implemented by the official example is valid for a fixed duration. After this duration, the JWT expires (i.e., becomes invalid), and users will need to authenticate again to use the application.

I have previously implemented JWT whose expiry is calculated based on the last access time. It proved complicated. In this post, we will stick with the implementation from the official tutorial.

❷ The full updated structure of the project is outlined below.

-- Please note, those marked with are updated, and those marked with are new.

/home/behai/fastapi_learning/
.
├── cert
│   ├── cert.pem
│   └── key.pem
├── .env ★
├── logger_config.yaml
├── main.py
├── pyproject.toml ★
├── pytest.ini ★
├── README.md ★
├── src
│   └── fastapi_learning
│       ├── businesses
│       │   ├── app_business.py
│       │   ├── base_business.py
│       │   ├── base_validation.py
│       │   ├── employees_mgr.py
│       │   └── employees_validation.py
│       ├── common
│       │   ├── consts.py ★
│       │   ├── jwt_utils.py ☆
│       │   └── queue_logging.py
│       ├── controllers
│       │   ├── admin.py ★
│       │   ├── auth.py ★
│       │   └── __init__.py
│       ├── __init__.py
│       ├── models
│       │   └── employees.py
│       ├── static
│       │   └── styles.css
│       └── templates
│           ├── admin
│           │   └── me.html
│           ├── auth
│           │   ├── home.html
│           │   └── login.html
│           ├── base.html
│           └── templates
│               ├── admin
│               │   └── me.html
│               ├── auth
│               │   ├── home.html
│               │   └── login.html
│               └── base.html
└── tests
    ├── business
    │   └── test_employees_mgr.py
    ├── conftest.py
    ├── __init__.py
    ├── integration
    │   ├── test_admin_itgt.py ★
    │   ├── test_api_itgt.py ★
    │   ├── test_auth_itgt.py ★
    │   └── test_expired_jwt.py ☆
    ├── README.md
    └── unit
        └── test_employees.py

❸ In this section, we will discuss the code changes.

⓵ The tutorial example defines three constants SECRET_KEY, ALGORITHM, and ACCESS_TOKEN_EXPIRE_MINUTES. We move them into the environment .env file as follows:

16
17
18
19
20
21
# to get a string like this run:
# openssl rand -hex 32
SECRET_KEY = "61973d7ebb87638191435feaed4789a0c0ba173bd102f2c1f940344d9745a8be"
ALGORITHM = "HS256"
# 30 * 60 = 30 minutes.
ACCESS_TOKEN_EXPIRE_SECONDS = 1800

The unit of ACCESS_TOKEN_EXPIRE_SECONDS is seconds, which provides better control over the JWT lifetime, especially in tests, as we shall see later.

⓶ The pyproject.toml file now includes the required package pyjwt, as per the aforementioned tutorial.

⓷ In the new module common/jwt_utils.py, we house the two methods create_access_token(...) and decode_access_token(...) taken from the official tutorial.

We made a change to the method create_access_token(...). If the value for the expires_delta parameter is None, we use the value of ACCESS_TOKEN_EXPIRE_SECONDS from the environment file.

⓸ In the /controllers area, we made the changes discussed below:

● In the module auth.py, the login(...) method now calls the create_access_token(...) to create the JWT access token.

● In the module admin.py, the get_current_user(...) method calls decode_access_token(...) to validate incoming requests.

⓹ In the existing tests/integration area, all tests remain in place with some minor updates to work with the new JWT implementation.

⓺ 💥 We added a new integration test module test_expired_jwt.py. These tests ascertain that when a request comes in with an already expired JWT token, the request is denied as expected. As mentioned earlier, we take advantage of setting the value of the environment variable ACCESS_TOKEN_EXPIRE_SECONDS to only a couple of seconds to simulate expiry in these tests. For example:

        ...
        # Login access token expires in 2 seconds.
        os.environ['ACCESS_TOKEN_EXPIRE_SECONDS'] = '2'

        # Login.
        login_response = login('behai_nguyen@hotmail.com', 'password', test_client)

        # Set session (Id) for next request.
        session_cookie = login_response.cookies.get('session')
        test_client.cookies = {'session': session_cookie}

        # Waits out for the access token to expire.
        time.sleep(3)

        response = test_client.get('/admin/me')
        ...

Thank you for reading. I hope you find the information in this post useful. Stay safe, as always.

✿✿✿

Feature image source:

🐍 Index of the Complete Series.