avatarAnders Wiklund

Summary

This web content is part 7 of a tutorial series on building a FastAPI application with MongoDB, focusing on implementing HTTP Basic Authentication for item endpoints.

Abstract

The article is the seventh installment in a series dedicated to creating a RESTful API using FastAPI and MongoDB. In this part, the author guides readers through the process of adding HTTP Basic Authentication to the item endpoints of the API for enhanced security. The article outlines the necessary code changes, including updates to configuration files, the creation of a security module, and the implementation of authentication checks in the API routes. It also provides a new directory structure, emphasizing the importance of securing sensitive endpoints and the use of environment variables and secrets for storing credentials. A simple test script is included to verify the authentication functionality. The author stresses that HTTP Basic Authentication is suitable for non-public APIs and recommends OAuth2 for public-facing APIs, providing resources for further learning on more advanced security measures.

Opinions

  • The author believes that HTTP Basic Authentication is adequate for APIs behind a firewall but suggests OAuth2 for public APIs due to its higher security standards.
  • They recommend using environment variables and secrets files to manage sensitive information like usernames and passwords securely.
  • The article implies that adding authentication to an API can be straightforward when using FastAPI's built-in security features.
  • The author values the ease of implementation and the clarity provided by FastAPI's documentation and error handling, as evidenced by the inclusion of error constants and detailed code examples.
  • They encourage readers to engage with additional learning resources, such as a book on microservice APIs and related YouTube videos, to deepen their understanding of API security.
  • The author appreciates community engagement, suggesting that readers clap for the article if they find it helpful, indicating a desire for feedback and interaction with the audience.

Extensive FastAPI with MongoDB example — part7

From a similar article by Dinesh Kumar K B

Introduction

This is part7 in the series. This part adds HTTP Basic Authentication to the Item endpoints. The health endpoint is left unprotected on purpose.

Here’s a brief outline of the article parts in the series:

  1. A basic RESTful API
  2. Extending validation and documentation in the code
  3. How to handle configuration data
  4. Normalized log handling
  5. How to use MongoDB with FastAPI
  6. A health status endpoint
  7. HTTP Basic Authentication (this article)
  8. FastAPI Mock Testing
  9. Docker container handling

HTTP Basic Authentication should only be used in a non-public API that is not exposed to Internet. If your API is going to be public you need to use one of the OAuth2 variants that is relevant for you.

A good explanation of this, and what type to use where is described in the excellent book Microservice APIs. Code examples for this book can be found in appendix C (open source available). There are also a number of YouTube videos about security from the author that might be interesting to watch if that is your cup of tea.

Go to the path where you previously created the fastapi_mongo directory and create the structure below. The easiest way is to copy the part6 directory (and rename it) so you only have to create the new part of the structure. The new files can be created empty. We will fill them later during our walkthrough.

📂 fastapi_mongo
|_📃 logging_config_dev.json
|_📂 part7
  |_📃 run.py
  |_📃 verify.py
  |_📂 src
    |_📃 custom_logging.py
    |_📃 db.py
    |_📃 health_manager.py
    |_📃 main.py
    |_📃 schemas.py
    |_📃 security.py
    |_📃 __init__.py
    |_📂 api
      |_📃 crud_items.py
      |_📃 item_routes.py
    |_📂 apidocs
      |_📃 description.md
      |_📃 fastapi_mongo.png
      |_📃 openapi_documentation.py
    |_📂 config
      |_📃 .env
      |_📃 setup.py
      |_📃 __init__.py
|

The Code

By adding HTTP Basic Authentication to the example, some changes have been made compared to part6. Here’s a summary of the changes:

  • version is bumped in config/.env file and a new variable, SERVICE_USER is added.
  • Content of setup.py has two new authentication attributes, one being a secret.
  • The security.py file is new.
  • Authentication validation is added to the Item endpoints in file item_routes.py.
  • a small async test script, verify.py is added.

config/.env

Replace the previous content of the file with this:

# Service information
VERSION="0.7.0"
NAME="FastAPI-MongoDB-example"

# Authentication data.
SERVICE_USER="service_user"

The version is bumped and the SERVICE_USER variable is added.

config/setup.py

Replace the previous content of the file with this:

# BUILTIN modules
import site

# Third party modules
from dotenv import load_dotenv
from pydantic import BaseSettings

# Constants
MISSING_SECRET = '>>> missing SECRETS file <<<'
""" Error message for missing secrets file. """
MISSING_ENV = '>>> missing ENV value <<<'
""" Error message for missing values in the .env file. """


# ---------------------------------------------------------
#
class Configuration(BaseSettings):
    """ Configuration parameters. """

    # project
    name: str = MISSING_ENV
    version: str = MISSING_ENV

    # database URL
    mongo_url: str = MISSING_SECRET

    # authentication
    service_user: str = MISSING_ENV
    service_pwd: str = MISSING_SECRET

    class Config:
        secrets_dir = f'{site.USER_BASE}/secrets'


# ---------------------------------------------------------

# Note that the ".env" file is always implicitly loaded.
load_dotenv()

config = Configuration()
""" Configuration parameters instance. """

I have added a new default error constant for missing environment values and assigned them to where they belong. Beside the benefits during troubleshooting, it also makes it clear where the value should come from.

The service_user and attribute is new, and it relates to the value in the .env file

The service_pwd attribute is new, and it relates to a value in a secrets file. This means that have to create a new secrets file.

If you are on Windows, create the following file:

C:\\Users\\**********\\AppData\\Roaming\\Python\\secrets\\service_pwd

If you are on Linux or MacOS, create the following file:

/home/**********/.local/secrets/service_pwd

REPLACE THE ASTERIXES ABOVE WITH YOUR LOGGED IN USERNAME!

Generate a new UUID at a python prompt like this:

>> import uuid
>> uuid.uuid4()
UUID('706936a8-65d4-4446-9be2-081bd15c0e8a')

Note that your result will be different than this. So copy the content of the string part into the service_pwd file you created earlier.

security.py

Insert this content into the file:

# BUILTIN modules
import secrets

# Third party modules
from starlette.status import HTTP_401_UNAUTHORIZED

from fastapi import Depends, HTTPException
from fastapi.security import HTTPBasic, HTTPBasicCredentials

# local modules
from .config.setup import config

# Constants
SECURITY = HTTPBasic()
""" HTTP basic authentication object. """


# ---------------------------------------------------------
#
def validate_authentication(credentials: HTTPBasicCredentials = Depends(SECURITY)):
    """ Validate authentication.

    Security is based on HTTP Basic Auth.

    This should only be used for the simplest cases. For example when you are
    running in a network placed behind a firewall and there's no access from
    Internet to the internal network.

    :param credentials: Authentication credentials.
    :raise HTTPException: 401 => When incorrect username or password is supplied.
    """

    correct_password = secrets.compare_digest(credentials.password, config.service_pwd)
    correct_username = secrets.compare_digest(credentials.username, config.service_user)

    if not (correct_username and correct_password):
        raise HTTPException(
            status_code=HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Basic"})

The validate_authentication method compares the given values, from a GUI popup windows with what’s stored in the our configuration. When they differ the appropriate HTTP exception is raised.

api/item_routes.py

Replace the beginning of the file with this:

# BUILTIN modules
from typing import List

# Third party modules
from bson import errors
from pymongo import errors
from pydantic import UUID4
from fastapi.responses import Response
from fastapi import HTTPException, APIRouter, status, Query, Path, Depends

# Local modules
from . import crud_items as crud
from ..security import validate_authentication
from ..schemas import (Category, ItemSchema, QueryArguments,
                       ItemArgumentResponse, AlreadyExistError,
                       NotFoundError, NoArgumentsError, DbOperationFailedError)

# Constants
ROUTER = APIRouter(prefix="/v1/items", tags=["items"],
                   dependencies=[Depends(validate_authentication)])

What has changed is the security import and the new dependencies parameter for the APIRouter instantiation. It’s really simple to implement authentication, isn’t it.

verify.py

Insert this content into the file:

# BUILTIN modules
import asyncio
from pprint import pprint

# Third party modules
from httpx import ConnectError, ReadTimeout, BasicAuth, AsyncClient

# Local program modules
from src.config.setup import config

# Constants
AUTH = BasicAuth(username=config.service_user, password=config.service_pwd)


# ---------------------------------------------------------
#
async def process(use_auth: bool = False):

    async with AsyncClient() as client:
        response = await client.get(url='http://localhost:8000/v1/items',
                                    headers={'Content-Type': 'application/json'},
                                    timeout=(9.05, 60), auth=(AUTH if use_auth else None))

    if response.status_code == 200:
        print(f'\nTRUE =>')
        pprint(response.json())

    else:
        print(f'\nFALSE => \nstatus: {response.status_code}, error: {response.json()}')


# ---------------------------------------------------------
#
async def test():
    try:
        await process()
        await process(use_auth=True)

    # You will end up here if you have not started the API server program (run.py).
    except ConnectError as why:
        print(f'ERROR => {why}')

    # You can test this by changing the read timeout value from 60 seconds to 0.01.
    except ReadTimeout as why:
        print(f'TIMEOUT => {why}')


# ---------------------------------------------------------

if __name__ == "__main__":

    asyncio.run(test())

This is a small asynchronous test script that can be used to verify that the authentication works. It also gives you an insight into how to access the API with from python code.

It will also work to call from synchronous code, for example with the requests package. The response time between the two process calls increases from around 250 milliseconds to around two seconds…

Exploring the APPI in the browser

It is time to see what the new functionality looks like and what it does.

Enable the virtual environment in a command window and run the run.py program under part7. Remember you have the -t option if you want to activate the automatic reload functionality.

If everything works the output should look something like this:

Let’s start the web page and see what’s changed:

Beside the version, can you spot the change?

There are visual padlocks on all item endpoints on the right side, and an authorize key below the image.

When you first press the GET button to view all items, the “Try it out” button and the Execute button you get this:

The browser automatically prompts you for the required credentials. When you enter them out correctly everything is fine and you will get the desired result in the GUI.

Conclusion

The example code for this part is available in my GitHub repository.

In this article we have explored how to add HTTP Basic Authentication to some of the endpoints. If you need higher authentication, like OAuth2, take a look at some or these YouTube videos, and source code. They will aid you with that implementation.

I hope you enjoyed this article and got inspired to test these techniques yourself. Remember, if you like this article don’t hesitate to clap (I mean digitally 😊).

Happy coding in Gotham.

/Anders

Fastapi
Python Web Developer
Python3
Python Programming
Python
Recommended from ReadMedium