Protect FastAPI Docs behind Firebase Authentication
FastAPI is a python backend framework that automatically generates API documentation. By default, access to this documentation is unsecured, allowing anyone to view the documentation and understand how the API processes data. If you want to go too fast, you may deploy the API while forgetting a very big security flaw. To remedy this, one can use Firebase Authentication to secure the documentation.
😎 Let’s do this
First of all, if you don’t know how to run a FastAPI server I direct you to this article Install FastAPI and run your first FastAPI server on Windows and if you have already done it, you can skip this step.
The contents of the main.py file are as follows:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"messag":"FastAPI documentation secured by Firebase Authentication."}
Once the server is launched, the documentation is accessible on these three links:
Interactive API docs (http://127.0.0.1:8000/docs)[http://127.0.0.1:8000/docs]
Alternative API docs (http://127.0.0.1:8000/redoc)[http://127.0.0.1:8000/redoc]
OpenAPI Schema (http://127.0.0.1:8000/openapi.json)[http://127.0.0.1:8000/openapi.json]
FastAPI offers 2 links to access the application documentation and a schema generated with all our APIs using the OpenAPI standard for API definition. We therefore start by deactivating these links in the parameters of the FastAPI class by setting docs_url, redoc_url, openapi_url to None.
app = FastAPI(docs_url=None,
redoc_url=None,
openapi_url=None)
After this access to the doc returns the following message: {“detail”:”Not Found”}
So let’s create the routes to the documentation with get methods and HTMLResponse for swagger and redoc , and Dict for apenapi. These three functions allow us to do this: get_redoc_html, get_swagger_ui_html, get_openapi.
@app.get("/docs", include_in_schema=False)
async def get_swagger_documentation():
return get_swagger_ui_html(openapi_url="/openapi.json", title="docs")
@app.get("/redoc", include_in_schema=False)
async def get_redoc_documentation():
return get_redoc_html(openapi_url="/openapi.json", title="docs")
@app.get("/openapi.json", include_in_schema=False)
async def openapi():
return get_openapi(title=app.title, version=app.version, routes=app.routes)
In this way we can pass a dependency that checks admin credentials (sush like email/password or username/password) before showing the documentation. To get user credentials, you can use HTTP Basic Auth provided by FastAPI. So we will use username as email and password to sign in admin using Firebase Authentication.
This is how user can enter username and password
To initialize our firebase instance we will use the pyrebase package and import the configuration from the .env file
The .env file looks like this with the real values:
FIREBASE_API_KEY=---------api-key-----------
FIREBASE_AUTH_DOMAIN=--------auth-domain-------------
FIREBASE_DATABASE_URL=--------database-url-----------
FIREBASE_PROJECT_ID=------project-id---------------
FIREBASE_STORAGE_BUCKET=-----------storage-bucket-----------------
FIREBASE_MESSAGING_SENDER_ID=---------messaging-sender-id-------------
FIREBASE_APP_ID=------------app-id---------------
FIREBASE_MEASUREMENT_ID=-------measurement-id--------------
The firebase initialization code is:
import pyrebase
import os
from dotenv import load_dotenv
load_dotenv() # take environment variables from .env.
FIREBASE_API_KEY = os.environ.get("FIREBASE_API_KEY")
FIREBASE_AUTH_DOMAIN = os.environ.get("FIREBASE_AUTH_DOMAIN")
FIREBASE_DATABASE_URL = os.environ.get("FIREBASE_DATABASE_URL")
FIREBASE_PROJECT_ID = os.environ.get("FIREBASE_PROJECT_ID")
FIREBASE_STORAGE_BUCKET = os.environ.get("FIREBASE_STORAGE_BUCKET")
FIREBASE_MESSAGING_SENDER_ID = os.environ.get("FIREBASE_MESSAGING_SENDER_ID")
FIREBASE_APP_ID = os.environ.get("FIREBASE_APP_ID")
FIREBASE_MEASUREMENT_ID = os.environ.get("FIREBASE_MEASUREMENT_ID")
fireBaseConfig = {
'apiKey': FIREBASE_API_KEY,
'authDomain': FIREBASE_AUTH_DOMAIN,
'databaseURL': FIREBASE_DATABASE_URL,
'projectId': FIREBASE_PROJECT_ID,
'storageBucket': FIREBASE_STORAGE_BUCKET,
'messagingSenderId': FIREBASE_MESSAGING_SENDER_ID,
'appId': FIREBASE_APP_ID,
'measurementId': FIREBASE_MEASUREMENT_ID
}
firebase = pyrebase.initialize_app(fireBaseConfig)
After firebase initialization, dependency function will use username as email and password to login admin to firebase authentication. If the connection succeeds, this dependency will display the documentation, otherwise it will display a 401 error code. The function of the dependency is:
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from .firebase_initialize import firebase
security = HTTPBasic()
def firebaseAdminAuth(credentials: HTTPBasicCredentials = Depends(security)):
"""_summary_
Args:
credentials (HTTPBasicCredentials, optional): _description_. Defaults to Depends(security).
Raises:
HTTPException: _description_
Returns:
_type_: _description_
This is a function that authorize only admin user to access documentation.
===> Admin is created manually on Firebase authentication with email: admin@gmail.com & password:123456789
"""
try:
email = credentials.username # use username field as email to sign in
password = credentials.password
response = firebase.auth().sign_in_with_email_and_password(email, password)
return response
except Exception as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
headers={"WWW-Authenticate": "Basic"},
)
Then we inject the dependency into the methods:
@app.get("/docs", include_in_schema=False)
async def get_swagger_documentation(response: str = Depends(firebaseAdminAuth)):
return get_swagger_ui_html(openapi_url="/openapi.json", title="docs")
@app.get("/redoc", include_in_schema=False)
async def get_redoc_documentation(response: str = Depends(firebaseAdminAuth)):
return get_redoc_html(openapi_url="/openapi.json", title="docs")
@app.get("/openapi.json", include_in_schema=False)
async def openapi(response: str = Depends(firebaseAdminAuth)):
return get_openapi(title=app.title, version=app.version, routes=app.routes)
The documentation is now protected. The server displays the documentation, only if the email and password match a firebase authentication account.
References
[2]: https://fastapi.tiangolo.com/tutorial/first-steps
[3]: https://fastapi.tiangolo.com/tutorial/security/
[4]: https://fastapi.tiangolo.com/advanced/security/http-basic-auth/