Search code examples
pythonauthorizationfastapiswagger-uiopenapi

How to disable Authentication in FastAPI based on environment?


I have a FastAPI application for which I enable Authentication by injecting a dependency function.

controller.py

router = APIRouter(
prefix="/v2/test",
tags=["helloWorld"],
dependencies=[Depends(api_key)],
responses={404: {"description": "Not found"}},

)

Authorzation.py

async def api_key(api_key_header: str = Security(api_key_header_auth)):
if api_key_header != API_KEY:
    raise HTTPException(
        status_code=401,
        detail="Invalid API Key",
    )

This works fine. However, I would like to disable the authentication based on environment. For instance, I would want to keep entering the authentication key in localhost environment.


Solution

  • You could create a subclass of APIKeyHeader class and override the __call__() method to perform a check whether the request comes from a "safe" client, such as localhost or 127.0.0.1, using request.client.host, as explained here. If so, you could set the api_key to application's API_KEY value, which would be returned and used by the check_api_key() dependency function to validate the api_key. In case there were multiple API keys, one could perform a check on the client's hostname/IP in both the __call__() and the check_api_key() functions, and raise an exception only if the client's IP is not in the safe_clients list.

    Example

    from fastapi import FastAPI, Request, Depends, HTTPException
    from starlette.status import HTTP_403_FORBIDDEN
    from fastapi.security.api_key import APIKeyHeader
    from fastapi import Security
    from typing import Optional
    
    API_KEY = 'some-api-key'
    API_KEY_NAME = 'X-API-KEY'
    safe_clients = ['127.0.0.1']
    
    
    class MyAPIKeyHeader(APIKeyHeader):
        async def __call__(self, request: Request) -> Optional[str]:
            if request.client.host in safe_clients:
                api_key = API_KEY
            else:
                api_key = request.headers.get(self.model.name)
                if not api_key:
                    if self.auto_error:
                        raise HTTPException(
                            status_code=HTTP_403_FORBIDDEN, detail='Not authenticated'
                        )
                    else:
                        return None
    
            return api_key
    
    
    api_key_header_auth = MyAPIKeyHeader(name=API_KEY_NAME)
    
    
    async def check_api_key(request: Request, api_key: str = Security(api_key_header_auth)):
        if api_key != API_KEY:
            raise HTTPException(status_code=401, detail='Invalid API Key')
    
     
    app = FastAPI(dependencies=[Depends(check_api_key)])
    
    
    @app.get('/')
    def main(request: Request):
        return request.client.host
    

    Example (UPDATED)

    The previous example could be simplified to the one below, which does not require overriding the APIKeyHeader class. You could instead set the auto_error flag to False, which would prevent APIKeyHeader from raising the pre-defined error in case the api_key is missing from the request, but rather let you handle it on your own in the check_api_key() function.

    from fastapi import FastAPI, Request, Security, Depends, HTTPException
    from fastapi.security.api_key import APIKeyHeader
    
    
    # List of valid API keys
    API_KEYS = [
        'z77xQYZWROmI4fY4',
        'FXhO4i3bLA1WIsvR'
    ]
    API_KEY_NAME = 'X-API-KEY'
    safe_clients = ['127.0.0.1']
    api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
    
    
    async def check_api_key(request: Request, api_key: str = Security(api_key_header)):
        if api_key not in API_KEYS and request.client.host not in safe_clients:
            raise HTTPException(status_code=401, detail='Invalid or missing API Key')
    
     
    app = FastAPI(dependencies=[Depends(check_api_key)])
    
    
    @app.get('/')
    def main(request: Request):
        return request.client.host
    

    How to remove/hide the Authorize button from Swagger UI

    The example provided above will work as expected, that is, users whose their IP address is included in the safe_clients list won't be asked to provide an API key in order to issue requests to the API, regardless of the Authorize button being present in Swagger UI page when accessing the autodocs at /docs. If you still, however, would like to remove the Authorize button from the UI for safe_clients, you could have a custom middleware, as demonstrated here, in order to remove the securitySchemes component from the OpenAPI schema (in /openapi.json)—Swagger UI is actually based on OpenAPI Specification. This approach was inspired by the link mentioned earlier, as well as here and here. Please make sure to add the middleware after initialising your app in the example above (i.e., after app = FastAPI(dependencies=...))

    from fastapi import Response
    
    # ... rest of the code is the same as above
    
    app = FastAPI(dependencies=[Depends(check_api_key)])
    
    
    @app.middleware("http")
    async def remove_auth_btn(request: Request, call_next):
        response = await call_next(request)
        if request.url.path == '/openapi.json' and request.client.host in safe_clients:
            response_body = [section async for section in response.body_iterator]
            resp_str = response_body[0].decode()  # convert "response_body" bytes into string
            resp_dict = json.loads(resp_str)  # convert "resp_str" into dict
            del resp_dict['components']['securitySchemes']  # remove securitySchemes
            resp_str = json.dumps(resp_dict)  # convert "resp_dict" back to str
            return Response(content=resp_str, status_code=response.status_code, media_type=response.media_type)
        
        return response