Protect your FastAPI document with HTTP basic authN
Tags: fastapi, webdev
Published: 2023-08-09 02:32:03

When working in enterprise with frontends as a backend engineer, it common to deploy a staging(test) environment to expedite development. 

One of the advantages of using FastAPI is it built-in API documentation. However, we don't want to expose it in the staging environment to the public, unless you can control the networking aspect.

In this article, I'll demonstrate how to secure it using HTTP basic authentication.


## Code
1. disable doc url by env
```py
if app_env == 'staging':
    app = FastAPI(docs_url=None, openapi_url=None, redoc_url=None)
```

2. create a middleware for http basic authN
```py
import base64
import secrets

from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
from starlette.requests import Request
from starlette.responses import Response


class ApidocBasicAuthMiddleware(BaseHTTPMiddleware):

    async def dispatch(  # type: ignore
            self, request: Request, call_next: RequestResponseEndpoint):
        if request.url.path in ['/docs', '/openapi.json', '/redoc']:
            auth_header = request.headers.get('Authorization')
            if auth_header:
                try:
                    scheme, credentials = auth_header.split()
                    if scheme.lower() == 'basic':
                        decoded = base64.b64decode(credentials).decode('ascii')
                        username, password = decoded.split(':')
                        correct_username = secrets.compare_digest(
                            username, 'YOUR_USERNAME')
                        correct_password = secrets.compare_digest(
                            password, 'YOUR_PASSWORD')
                        if correct_username and correct_password:
                            return await call_next(request)
                except Exception:
                    ...
            response = Response(content='Unauthorized', status_code=401)
            response.headers['WWW-Authenticate'] = 'Basic'
            return response
        return await call_next(request)

app.add_middleware(ApidocBasicAuthMiddleware)
```
replace **YOUR_USERNAME** and **YOUR_PASSWORD**

3. enable document paths and document html
```py
from typing import Any, Dict

from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
from fastapi.openapi.utils import get_openapi

@app.get(
    '/docs',
    tags=['documentation'],
    include_in_schema=False,
)
async def get_swagger_documentation() -> HTMLResponse:
    return get_swagger_ui_html(openapi_url='/openapi.json', title='docs')


@app.get(
    '/openapi.json',
    tags=['documentation'],
    include_in_schema=False,
)
async def openapi() -> Dict[str, Any]:
    return get_openapi(title='FastAPI', version='0.1.0', routes=app.routes)


@app.get(
    '/redoc',
    tags=['documentation'],
    include_in_schema=False,
)
async def get_redoc() -> HTMLResponse:
    return get_redoc_html(openapi_url='/openapi.json', title='docs')
```
That is, you will see the login modal when entering doc path

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7hspuantobkn6qn82fvm.png)
## Put it all together
```py
import base64
import secrets

from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
from starlette.requests import Request
from starlette.responses import Response
from typing import Any, Dict
from fastapi import FastAPI
from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
from fastapi.openapi.utils import get_openapi

app = FastAPI(docs_url=None, openapi_url=None, redoc_url=None)
app.add_middleware(ApidocBasicAuthMiddleware)

@app.get(
    '/docs',
    tags=['documentation'],
    include_in_schema=False,
)
async def get_swagger_documentation() -> HTMLResponse:
    return get_swagger_ui_html(openapi_url='/openapi.json', title='docs')


@app.get(
    '/openapi.json',
    tags=['documentation'],
    include_in_schema=False,
)
async def openapi() -> Dict[str, Any]:
    return get_openapi(title='FastAPI', version='0.1.0', routes=app.routes)


@app.get(
    '/redoc',
    tags=['documentation'],
    include_in_schema=False,
)
async def get_redoc() -> HTMLResponse:
    return get_redoc_html(openapi_url='/openapi.json', title='docs')
```