How to disable Authentication in FastAPI based on environment?
Asked Answered
C

1

5

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.

Camail answered 3/5, 2023 at 0:25 Comment(2)
I think that you can do it playing with the .env file then read it and on your Authorization.py put something like: if os.environ.get("ENVIRONMENT") == "development":.Crystallography
as I have already injected the dependency how will I by pass it?Camail
S
5

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
    
Stricklan answered 3/5, 2023 at 6:21 Comment(1)
This way I will be applying key through code for safe clients. I want to entirely remove the depends on authorization if the request comes from local host. So that we don't see an authorize button at all. Is this possible? to set decency in router based on safe clients?Camail

© 2022 - 2024 — McMap. All rights reserved.