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
.env
file then read it and on your Authorization.py put something like:if os.environ.get("ENVIRONMENT") == "development":
. – Crystallography