Python FastAPI: Integrating OAuth2 Security with the Application’s Own Authentication Process

In the first post, we explore some aspects of OAuth2 authentication, focusing on the /token path as illustrated in an example from the Simple OAuth2 with Password and Bearer section of the Tutorial – User Guide Security. In this subsequent post, we implement our own custom preliminary login process, leveraging the /token path. This means that both the Swagger UI Authorize button and our application’s login button utilise the same server code.

🐍 Index of the Complete Series.

πŸš€ Please note, complete code for this post can be downloaded from GitHub with:

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

During my research on β€œFastAPI application custom login process”, I’ve encountered implementations where there are two endpoints for handling authentication: the /token endpoint, as discussed in the Tutorial – User Guide Security section, and the application’s own login endpoint.

πŸ’₯ This approach doesn’t seem right to me. In my view, the /token endpoint should serve as the application’s login endpoint as well. In this post, we introduce a preliminary custom login process with the endpoint being the /token endpoint.

The code developed in this post maintains the one-module application structure from the original example. However, we’ve added a login HTML page and organised the project directory structure to prepare for further code changes as we progress. The updated project layout is listed below.

— Please note, those marked with β˜… are updated, and those marked with β˜† are new.

/home/behai/fastapi_learning/
β”œβ”€β”€ main.py β˜…
β”œβ”€β”€ pyproject.toml
β”œβ”€β”€ README.md β˜…
└── src β˜†
    └── fastapi_learning
        β”œβ”€β”€ static
        β”‚ └── styles.css
        └── templates
            β”œβ”€β”€ auth
            β”‚ └── login.html
            └── base.html

Changes to main.py include the following:

β“΅ Lines 21 to 23: Added required imports to support HTML output.

β“Ά Lines 25 to 45: Completely refactored the fake_users_db database to slowly match the test employees MySQL database by Oracle Corporation, as utilised in the SQLAlchemy database wrapper component FastAPI example, and Update the employees table, adding new fields email and password .

β“· Lines 49 to 50: Added initialisation code to prepare support for HTML template processing.

β“Έ Lines 66 to 72: Refactored models to align with the changes in fake_users_db.

β“Ή Lines 95 to 100: Refactored the get_current_active_user(...) method to cease checking the disabled attribute of the User model, as this attribute has been removed from the model.

β“Ί Lines 120 to 123: Implemented the new /login endpoint, which simply returns the login HTML page.

πŸš€ Note that the endpoint code for the /token path, specifically the method async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):, remains unchanged.

πŸ’₯ Regarding the HTML login page, please take note of the following points:

...
    <form method="POST" action="/token" id="loginForm">
        <h1 class="h3 mb-3 fw-normal">Please login</h1>

        {% if message is defined %}
            <h2>{{ message }}</h2>
        {% endif %}

        <div>
            <label for="username">Email address:</label>
            <input type="email" class="form-control" id="username" name="username" placeholder="name@example.com" required value="">
        </div>

        <div>
            <label for="password">Password:</label>
            <input type="password" class="form-control" id="password" name="password" placeholder="Password" required value="">
        </div>
        <button type="submit">Login</button>
    </form>
...	

β“΅ The action of the login form is directed to the /token path. In other words, when the login form is submitted, it sends a POST login request to the same endpoint used by the Authorize button on the Swagger UI page.

β“Ά The names of the two login fields are username and password. This requirement is specified in the tutorial in the section titled Simple OAuth2 with Password and Bearer:

OAuth2 specifies that when using the “password flow” (that we are using) the client/user must send a username and password fields as form data.

πŸš€ Our application’s login process now shares the same server code as the Swagger UI login process. We have two separate β€œclients”:

  1. http://0.0.0.0:port/docs: The Swagger UI client page.
  2. http://0.0.0.0:port/login: Our own custom login page.

On Ubuntu 22.10, run the application with the command:

(venv) ...:~/fastapi_learning$ venv/bin/uvicorn main:app --host 0.0.0.0 --port 5000

When accessing the Swagger UI page on Windows 10 at http://192.168.0.16:5000/docs, we encounter a familiar page:

The GET /login path should simply return the login HTML page, while the remaining paths should function as discussed in the first post.

When accessing the application’s login page on Windows 10 at http://192.168.0.16:5000/login, we are presented with our custom login page:

Upon logging in using one of the following credentials: behai_nguyen@hotmail.com/password or pranav.furedi.10198@gmail.com/password, we should receive a successful JSON response, as depicted in the screenshot below:

When attempting to log in using an invalid credential, we should receive an HTTP 400 response, which indeed occurs, as seen in the screenshots below:

In the current implementation, the login process is incomplete, but it serves as an appropriate preliminary step nonetheless. We will conclude this post here as I don’t want to make it too long… In the next post or so, we will implement stateful sessions and extend OAuth2PasswordBearer to maintain authenticated sessions. This means that after a successful login, users can access protected application routes until they choose to log out.

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.

Python FastAPI: Some Further Studies on OAuth2 Security

FastAPI provides excellent tutorials that thoroughly introduce the framework. Two sections on security, namely Tutorial – User Guide Security and Advanced User Guide Security, have sparked further questions, which we are discussing in this post. Hopefully, this discussion will lead to a better understanding of how FastAPI security works.

🐍 Index of the Complete Series.

I intend to conduct further studies on the FastAPI framework and document any necessary issues I encounter.

This post begins with an example from the Simple OAuth2 with Password and Bearer section of the Tutorial – User Guide Security. The discussions in this post will also be applicable to later OAuth2 examples in the official tutorial, which include JSON Web Token and scopes.

❢ My first question concerns OAuth2PasswordBearer: how does Swagger UI remember the access_token?

When running this example and accessing Swagger UI, we encounter a page like the one shown in the screenshot below:

Following the tutorial, logging in via the Authorize button with the credentials johndoe and secret, and accessing the /users/me path, we receive a successful response, as shown in the screenshot below:

We can observe that the token johndoe was sent via the Authorization header, as indicated in the tutorial. We understand that it is the client’s responsibility to remember the access_token. Initially, I assumed that Swagger UI might store it as a cookie. However, as seen in the screenshot below, this was not the case:

If we refresh the Swagger UI page now and access the /users/me path again, we find ourselves no longer authenticated. This can be verified by attempting to Authorize again, then refreshing the browser; accessing /users/me would return as not authenticated.

πŸš€ It appears that the Swagger UI client merely remembers the access_token in memory. While this point may not be significantly important, I would still like to understand it.

❷ The second question is whether the /token path within the Swagger UI functions equivalently to the Authorize button.

This question arises logically because the /token path displays the same login screen as the Authorize button, as depicted in the screenshot below:

Additionally, the response from the /token path is identical to that of the Authorize button, as shown in the screenshot below:

However, despite this similarity, accessing /users/me returns as not authenticated, as seen below:

πŸš€ It seems that within the Swagger UI, the /token path does NOT work the same as the Authorize button.

I feel the need to emphasize this point because I experienced confusion when I based my code on this example. Absent-mindedly, I clicked on the /token path, and while the login was successful, the protected paths β€œsuddenly failed” within Swagger UI! This left me confused for a little while until I realised what I had done.

❸ Building upon the two previous questions, the final question is: if I use Postman to access the path /users/me with the header Authorization set to Bearer johndoe and Bearer alice, would I receive successful responses? My anticipation was that I would, and indeed, that was the case, as shown in the screenshots below:

πŸš€ It’s quite logical. Everything would fall apart if this didn’t work πŸ˜‚

We conclude this post here. While it may not cover anything significant, these are the questions that enable me to understand FastAPI better. In a future post, we’ll delve into building our own login UI and how the /token path, OAuth2PasswordBearer, and OAuth2PasswordRequestForm come together in our custom login process.

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.

Python: A SQLAlchemy Wrapper Component That Works With Both Flask and FastAPI Frameworks

In late 2022, I developed a database wrapper component for SQLAlchemy. Initially designed for use with the Flask framework, it was discovered that this component also seamlessly integrates with the FastAPI framework. In this post, I will describe this component and provide examples of how it is used within both frameworks.

This component is based on a concept I previously implemented using Delphi and later PHP. Essentially, it consists of base classes representing database tables, equipped with the ability to interact with databases and implement generic functionalities for CRUD operations.

While learning the Flask framework, I found the conventional approach of accessing the database layer through the Flask application instance uncomfortable. In my previous projects, the database layer has always remained independent of other layers. The business layer ensures data validity and then delegates CRUD operations to the database layer. The UI layer, whether a desktop application or web client, communicates solely with the business layer, never directly accessing the database layer.

In SQLAlchemy, models representing database tables typically subclass sqlalchemy.orm.DeclarativeBase (this class supersedes the sqlalchemy.orm.declarative_base function). Accordingly, the abstract base class in this database wrapper component is a sqlalchemy.orm.DeclarativeBase subclass, accompanied by another custom base class providing additional dunder methods.

Further subclasses of this abstract base class implement additional functionalities. Application models inherit from either the ReadOnlyTable or the WriteCapableTable base classes.

Application models are required to implement their own specific database reading methods. For example, selecting all customers with the surname Nguyα»…n.

The Database class is responsible for establishing connections to the target database. Once the database connection is established, application models can interact with the target database.

πŸš€ The full documentation can be found at https://bh-database.readthedocs.io/en/latest/.

Next, we will explore some examples. πŸ’₯ The first two are simple, single-module web server applications where the web layer directly accesses the database layer. Although not ideal, it simplifies usage illustration.

The latter two examples include complete business layers, where submitted data is validated before being passed to the database layer for CRUD operations.

❢ example.py: A Simple Single-Module Flask Application.

● Windows 10: F:\bh_database\examples\flaskr\example.py
● Ubuntu 22.10: /home/behai/bh_database/examples/flaskr/example.py

from sqlalchemy import (
    Column,
    Integer,
    Date,
    String,
)

import flask

from bh_database.core import Database
from bh_database.base_table import WriteCapableTable

from bh_apistatus.result_status import ResultStatus

SQLALCHEMY_DATABASE_SCHEMA = 'employees'
SQLALCHEMY_DATABASE_URI = 'mysql+mysqlconnector://root:pcb.2176310315865259@localhost:3306/employees'
# Enable this for PostgreSQL.
# SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg2://postgres:pcb.2176310315865259@localhost/employees'

class Employees(WriteCapableTable):
    __tablename__ = 'employees'

    emp_no = Column(Integer, primary_key=True)
    birth_date = Column(Date, nullable=False)
    first_name = Column(String(14), nullable=False)
    last_name = Column(String(16), nullable=False)
    gender = Column(String(1), nullable=False)
    hire_date = Column(Date, nullable=False)

    def select_by_partial_last_name_and_first_name(self, 
            last_name: str, first_name: str) -> ResultStatus:
        
        return self.run_stored_proc('get_employees', [last_name, first_name], True)

def create_app(config=None):
    """Construct the core application."""

    app = flask.Flask(__name__, instance_relative_config=False)

    init_extensions(app)
    
    init_app_database(app)

    return app
    
def init_extensions(app):
    app.url_map.strict_slashes = False

def init_app_database(app):    
    Database.disconnect()
    Database.connect(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_DATABASE_SCHEMA)

app = create_app()

@app.get('/employees/search/<last_name>/<first_name>')
def search_employees(last_name: str, first_name: str) -> dict:
    """ last_name and first_name are partial using %.

    An example of a valid route: http://localhost:5000/employees/search/%nas%/%An
    """

    return Employees() \
        .select_by_partial_last_name_and_first_name(last_name, first_name) \
        .as_dict()

if __name__ == '__main__':  
   app.run()

To execute the example.py application:

▢️Windows 10: (venv) F:\bh_database\examples\flaskr>venv\Scripts\flask.exe --app example run --host 0.0.0.0 --port 5000
▢️Ubuntu 22.10: (venv) behai@hp-pavilion-15:~/bh_database/examples/flaskr$ venv/bin/flask --app example run --host 0.0.0.0 --port 5000

Accessing the example.py application running locally from Windows 10:

http://localhost:5000/employees/search/%nas%/%An

Accessing the example.py application running on Ubuntu 22.10 from Windows 10:

http://192.168.0.16:5000/employees/search/%nas%/%An

❷ example.py: A Simple Single-Module FastAPI Application.

● Windows 10: F:\bh_database\examples\fastapir\example.py
● Ubuntu 22.10: /home/behai/bh_database/examples/fastapir/example.py

from sqlalchemy import (
    Column,
    Integer,
    Date,
    String,
)

from fastapi import FastAPI
from fastapi.responses import JSONResponse

from bh_database.core import Database
from bh_database.base_table import WriteCapableTable

from bh_apistatus.result_status import ResultStatus

SQLALCHEMY_DATABASE_SCHEMA = 'employees'
SQLALCHEMY_DATABASE_URI = 'mysql+mysqlconnector://root:pcb.2176310315865259@localhost:3306/employees'
# Enable this for PostgreSQL.
# SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg2://postgres:pcb.2176310315865259@localhost/employees'

class Employees(WriteCapableTable):
    __tablename__ = 'employees'

    emp_no = Column(Integer, primary_key=True)
    birth_date = Column(Date, nullable=False)
    first_name = Column(String(14), nullable=False)
    last_name = Column(String(16), nullable=False)
    gender = Column(String(1), nullable=False)
    hire_date = Column(Date, nullable=False)

    def select_by_partial_last_name_and_first_name(self, 
            last_name: str, first_name: str) -> ResultStatus:
        
        return self.run_stored_proc('get_employees', [last_name, first_name], True)

app = FastAPI()

Database.disconnect()
Database.connect(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_DATABASE_SCHEMA)

@app.get("/employees/search/{last_name}/{first_name}", response_class=JSONResponse)
async def search_employees(last_name: str, first_name: str):
    """ last_name and first_name are partial using %.

    An example of a valid route: http://localhost:5000/employees/search/%nas%/%An
    """

    return Employees() \
        .select_by_partial_last_name_and_first_name(last_name, first_name) \
        .as_dict()

To execute the example.py application:

▢️Windows 10: (venv) F:\bh_database\examples\fastapir>venv\Scripts\uvicorn.exe example:app --host 0.0.0.0 --port 5000
▢️Ubuntu 22.10: (venv) behai@hp-pavilion-15:~/bh_database/examples/fastapir$ venv/bin/uvicorn example:app --host 0.0.0.0 --port 5000

Accessing the example.py application running locally from Windows 10:

http://localhost:5000/employees/search/%nas%/%An

Accessing the example.py application running on Ubuntu 22.10 from Windows 10:

http://192.168.0.16:5000/employees/search/%nas%/%An

❸ A more comprehensive Flask application: a fully documented web server example with CRUD operations.

Please refer to https://github.com/behai-nguyen/bh_database/tree/main/examples/flaskr for the full source code, instructions on setting up the environment, installing packages, running tests, and finally running the application.

The layout of the example project is as follows:

/home/behai/bh_database/examples/flaskr
β”œβ”€β”€ app.py
β”œβ”€β”€ .env
β”œβ”€β”€ pyproject.toml
β”œβ”€β”€ pytest.ini
β”œβ”€β”€ README.md
β”œβ”€β”€ src
β”‚ └── flaskr
β”‚     β”œβ”€β”€ business
β”‚     β”‚ β”œβ”€β”€ app_business.py
β”‚     β”‚ β”œβ”€β”€ base_business.py
β”‚     β”‚ β”œβ”€β”€ base_validation.py
β”‚     β”‚ β”œβ”€β”€ employees_mgr.py
β”‚     β”‚ └── employees_validation.py
β”‚     β”œβ”€β”€ config.py
β”‚     β”œβ”€β”€ controllers
β”‚     β”‚ └── employees_admin.py
β”‚     β”œβ”€β”€ __init__.py
β”‚     β”œβ”€β”€ models
β”‚     β”‚ └── employees.py
β”‚     β”œβ”€β”€ static
β”‚     β”‚ └── styles.css
β”‚     └── templates
β”‚         β”œβ”€β”€ admin
β”‚         β”‚ β”œβ”€β”€ emp_edit.html
β”‚         β”‚ β”œβ”€β”€ emp_search.html
β”‚         β”‚ └── emp_search_result.html
β”‚         └── base.html
└── tests
    β”œβ”€β”€ business
    β”‚ └── test_employees_mgr.py
    β”œβ”€β”€ conftest.py
    β”œβ”€β”€ __init__.py
    β”œβ”€β”€ integration
    β”‚ └── test_employees_itgt.py
    └── unit
        └── test_employees.py

❹ A more comprehensive FastAPI application: a fully documented web server example with CRUD operations.

Please refer to https://github.com/behai-nguyen/bh_database/tree/main/examples/fastapir for the full source code, instructions on setting up the environment, installing packages, running tests, and finally running the application.

The layout of the example project is as follows:

/home/behai/bh_database/examples/fastapir
β”œβ”€β”€ .env
β”œβ”€β”€ main.py
β”œβ”€β”€ pyproject.toml
β”œβ”€β”€ pytest.ini
β”œβ”€β”€ README.md
β”œβ”€β”€ src
β”‚ └── fastapir
β”‚     β”œβ”€β”€ business
β”‚     β”‚ β”œβ”€β”€ app_business.py
β”‚     β”‚ β”œβ”€β”€ base_business.py
β”‚     β”‚ β”œβ”€β”€ base_validation.py
β”‚     β”‚ β”œβ”€β”€ employees_mgr.py
β”‚     β”‚ └── employees_validation.py
β”‚     β”œβ”€β”€ config.py
β”‚     β”œβ”€β”€ controllers
β”‚     β”‚ β”œβ”€β”€ employees_admin.py
β”‚     β”‚ └── __init__.py
β”‚     β”œβ”€β”€ __init__.py
β”‚     β”œβ”€β”€ models
β”‚     β”‚ └── employees.py
β”‚     β”œβ”€β”€ static
β”‚     β”‚ └── styles.css
β”‚     └── templates
β”‚         β”œβ”€β”€ admin
β”‚         β”‚ β”œβ”€β”€ emp_edit.html
β”‚         β”‚ β”œβ”€β”€ emp_search.html
β”‚         β”‚ └── emp_search_result.html
β”‚         └── base.html
└── tests
    β”œβ”€β”€ business
    β”‚ └── test_employees_mgr.py
    β”œβ”€β”€ conftest.py
    β”œβ”€β”€ __init__.py
    β”œβ”€β”€ integration
    β”‚ └── test_employees_itgt.py
    └── unit
        └── test_employees.py

πŸ’₯ Except for the framework-specific layer code, the remaining code in these two examples is very similar.

Let’s briefly discuss their similarities:

  • /models and /business code are identical. They could be shared across both examples, but I prefer to keep each example self-contained.
  • /tests/unit and /tests/business code are identical.

And there are differences in the following areas:

  • /controllers: This is the web layer, which is framework-specific, so understandably they are different.
  • /tests/integration: The sole difference is framework-specific: how the HTTP response value is extracted:
    • Flask: response.get_data(as_text=True)
    • FastAPI: response.text
  • /tests/conftest.py: This file is framework-dependent. Both modules return the same fixtures, but the code has nothing in common.
  • /templates/base.html: There is one difference:
    • Flask: <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
    • FastAPI: <link rel="stylesheet" href="{{ url_for('static', path='/styles.css') }}">

    That is, Flask uses filename, while FastAPI uses path.

The /controllers layer is thin in the sense that the code is fairly short; it simply takes the client-submitted data and passes it to the business layer to handle the work. The business layer then forwards the validated data to the database layer, and so on. The differences between the two implementations are minor.

It has been an interesting exercise developing this wrapper component. The fact that it seamlessly integrates with the FastAPI framework is just a bonus for me; I didn’t plan for it since I hadn’t learned FastAPI at the time. I hope you find this post useful. Thank you for reading, and stay safe as always.

✿✿✿

Feature image source:

Design a site like this with WordPress.com
Get started