Introduction
In the previous article, I introduced middleware and how to add it to FastAPI. Also, that brought the process of adding environment variables into the FastAPI app.
A common environment variable used in server-side programming is API keys. These keys can authorize access to the server endpoints or are required for calling third-party APIs. Furthermore, API keys often are secret. So, it's best practice not to hardcode the value or commit the value to a repository.
In this article, let's pass an API key into our app and use it to protect a route.
Environment Variables
To recap, we pass in environment variables when we start the server. Later, even these values (passed in on the command-line) will be variables pulled in from the environment. Again, this to protect those values from ending up in server logs or histories.
For now, let's throw in some arbitrary key values to see how this works. Start the app by passing in an API_KEY
variable.
$ SANITY_API_KEY=123 API_KEY=123 ENV=development uvicorn main:app --reload
We'll be pulling this value off of the settings
object that we initialize before creating the app.
# this has to be done after we declare the class Settings
settings = Settings()
app = FastAPI(
# ...
)
Import Typing Objects
At the top of the file, import two built-in types to use for the key.
from fastapi.security.api_key import APIKeyHeader, APIKey
#...
You could also pull the API key from the cookies of the request, but we're going to use an Authorization header.
Import Helper Functions
FastAPI provides two functions to help us manage security:
Depends()
Security()
Add the import for Depends and Security to the fastapi
import statement.
from fastapi import FastAPI, Depends, Security
# ...
Protect Route
The Depends function lets us tell the app that the endpoint needs a parameter; it depends on it. If the parameter is not there, it can't execute.
# ... /article/related
async def get_related_articles(
id: str = Query(..., title="The ID of the article to find related articles for."),
api_key: APIKey = Depends(get_api_key),
):
# ... function code
In the code above, we add a parameter to this function called api_key
. This parameter is defined in the handler function as the route /article/related
.
Notice that we pass a function in. This function get_api_key
needs to return the API key value. However, this function is not defined.
Before the first route (index route), add the get_api_key
function.
# ...
async def get_api_key(
api_key_header: str = Security(api_key_header),
):
if api_key_header == API_KEY:
return api_key_header
else:
raise HTTPException(status_code=403)
# ... routes
The Security function helps retrieve the api_key_function
from the request.
This is read as, if the API key pulled from the Authorization header is equal to the API key defined as an environment variable, return the value. Else, return an unsuccessful status code.
You may have noticed that we're working through this backward. I think that's a better way of explaining it. The final piece of the puzzle is declaring the api_key_header
variable, API_KEY,
and the header's name. Put this code above the get_api_key
function.
# ...
API_KEY = settings.API_KEY
API_KEY_NAME = "Authorization"
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=True)
# ... get_api_key function
Now, the route is protected by the API key header! We can quickly check the authorization by visiting the /docs
URL on port 8000. We see an Authorize button and a lock next to the protected route.
Clicking on the lock icon renders the authorization options popup.
We can view the route and "Try it Out" after entering the API key specified when we started the app.
Conclusion
This is not quite an ideal authorization workflow, and FastAPI has other options for securing APIs and routes.
However, this is well on its way to being a viable option. The biggest problem is the API key should use some encryption. Then, it should be decrypted in the get_api_key
function.
In the final article, we'll talk about deploying to Heroku. Thanks for reading!