DEV Community

whchi
whchi

Posted on

Protect your FastAPI document with HTTP basic authN

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
if app_env == 'staging':
    app = FastAPI(docs_url=None, openapi_url=None, redoc_url=None)
Enter fullscreen mode Exit fullscreen mode
  1. create a middleware for http basic authN
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)
Enter fullscreen mode Exit fullscreen mode

replace YOUR_USERNAME and YOUR_PASSWORD

  1. enable document paths and document html
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')
Enter fullscreen mode Exit fullscreen mode

That is, you will see the login modal when entering doc path

Image description

Put it all together

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')
Enter fullscreen mode Exit fullscreen mode

Top comments (0)