Extensive FastAPI with MongoDB example — part7
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:
- A basic RESTful API
- Extending validation and documentation in the code
- How to handle configuration data
- Normalized log handling
- How to use MongoDB with FastAPI
- A health status endpoint
- HTTP Basic Authentication (this article)
- FastAPI Mock Testing
- 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