How to define a custom security with multiple api keys in fastapi?

Viewed 1490

This is my plan, but it can't be generated to the OpenAPI doc's Authentication section.

class HMACModel(BaseModel):
    api_key: APIKey = APIKey(**{"in": APIKeyIn.header}, name='Api-Key')
    signature: APIKey = APIKey(**{"in": APIKeyIn.header}, name='Signature')


class HMACAuth(APIKeyBase):
    model = HMACModel()
    scheme_name = 'HMACAuth'

    async def __call__(self, request: Request):
        api_key = request.headers.get('Api-Key')
        signature = request.headers.get('Signature')
        print(f's:{signature}, k:{api_key}')
        do_some_check()
        return api_key

@app.get('/')
async def test_api(req: ReqModel, api_key=Depends(HMACAuth())):
        pass

It seems that custom model will be ignore when the OpenAPI object init with analysis output OpenAPI(**output).(There are security shema in output, but missd in OpenAPI object).

(Refer code :https://github.com/tiangolo/fastapi/blob/d60dd1b60e0acd0afcd5688e5759f450b6e7340c/fastapi/openapi/utils.py#L372)

2 Answers

At the moment, getting OpenAPI and multi-api-key dependencies to play nicely seems to be an outstanding problem as seen in this issue.

There are a few ways to handle it, the most straightforward of which is creating two APIKeyHeader dependencies and using them both in each request. You don't actually need to do any inheriting from the APIKeyBase class, as the APIKeyHeader class that FastAPI provides should do the trick on its own.

I would also probably define an additional dependency that uses the other two as sub-dependencies like so:

from typing import Optional

from fastapi import FastAPI, Depends, Security, HTTPException, status
from fastapi.security.api_key import APIKeyHeader


app = FastAPI()


class HMACAuth:
    async def __call__(
        self,
        x_hmac_api_key: str = Depends(APIKeyHeader(name="x-hmac-api-key", scheme_name="HMACApiKey")),
        x_hmac_signature: str = Depends(APIKeyHeader(name="x-hmac-signature", scheme_name="HMACSignature")),
    ) -> Optional[str]:
        if x_hmac_api_key and x_hmac_signature:
            # do whatever check is needed here
            if x_hmac_api_key in ["valid-api-key"] and x_hmac_signature in ["valid-signature"]:
                return x_hmac_api_key

        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Not authenticated",
        )


@app.get(
    "/", 
    response_model=str, 
    name="app:test-api",
    dependencies=[Security(HMACAuth())],
)
async def test_api() -> str:
     return "success"

You would then need to override your app's openapi method as explained in the extending OpenAPI section of the docs.

The reason being that any route with this dependency would have different OpenAPI security schema generated for each sub-dependency. Meaning users would be able to enter in a value for either on in the OpenAPI docs and be "authenticated".

To remedy that, you could do something like this:

from fastapi.openapi.utils import get_openapi


# ...previous code


def merge_hmac_securities(list_of_securities: List[Dict[str, List]]) -> List[dict]:
    """
    Find HMACApiKey and HMACSignature securities and merge them
    """
    original_securities = []
    hmac_security = {}
    for security in list_of_securities:
        if "HMACApiKey" in security or "HMACSignature" in security:
            hmac_security.update(security)
        else:
            original_securities.append(security)

    return original_securities + ([hmac_security] if hmac_security else [])


def custom_openapi():
    if app.openapi_schema:
        return app.openapi_schema

    openapi_schema = get_openapi(
        title="Custom title",
        version="3.0.0",
        description="This is a very custom OpenAPI schema",
        routes=app.routes,
    )

    for path in openapi_schema.get("paths"):
        for http_method in openapi_schema["paths"][path]:
            list_of_securities = openapi_schema["paths"][path][http_method].get("security")
            if list_of_securities:
                merged_list_of_securities = merge_hmac_securities(list_of_securities)
                openapi_schema["paths"][path][http_method]["security"] = merged_list_of_securities

    app.openapi_schema = openapi_schema

    return app.openapi_schema


app.openapi = custom_openapi

Doing that should solve both of your problems.

The key is to specify different scheme_name for each ApiKey header, otherwise they will collide and probably one will overwrite the other in OpenAPI schema

from fastapi.security import APIKeyHeader

api_key = APIKeyHeader(name='Api-Key', scheme_name='api-key')
signature = APIKeyHeader(name='Signature', scheme_name='signature')
Related