FastAPI is a relatively new Python framework that enables you to create applications very quickly. This framework allows you to read API request data seamlessly with built-in modules and is a lightweight alternative to Flask.
In this article, we will go over the features of FastAPI
, set up a basic API, protect an endpoint using Auth0, and you'll learn how simple it is to get started.
Before we get started, you can also check out the contents of this blog post in video format by playing the video below: π
Prerequisites
Before you start building with FastAPI, you need to have Python 3.8.2
and a free Auth0 account; you can sign up here.
If you got that Python version installed and your Auth0 account, you can create a new FastAPI
application. To begin, create a new directory to develop within. For this example, you will make a directory called fastapi-example
and a subfolder called application
; this subfolder is where your code will live.
In the fastapi-example
folder, create a virtual environment using the following command:
python3 -m venv .env
This creates a virtual environment, and it separates the dependencies from the rest of your computer libraries. In other words, you don't pollute the global namespace with libraries and dependencies, which might impact other Python projects.
After creating the virtual environment, you need to activate it. For Unix-based operating systems, here's the command:
source .env/bin/activate
If you are in another operating system, you can find a list of how you can activate an environment on this documentation page. After activating your virtual environment, you can install the packages you are going to use: FastAPI
, uvicorn server, pyjwt, and update pip
:
pip install -U pip
pip install fastapi uvicorn 'pyjwt[crypto]'
Get Started with FastAPI
Now that all the libraries are installed, you can create a main.py
file inside the application
folder; that's where your API code will live. The contents of the main.py
will look like this:
"""main.py
Python FastAPI Auth0 integration example
"""
from fastapi import FastAPI
# Creates app instance
app = FastAPI()
@app.get("/api/public")
def public():
"""No access token required to access this route"""
result = {
"status": "success",
"msg": ("Hello from a public endpoint! You don't need to be "
"authenticated to see this.")
}
return result
Let's break this down:
- To start, you are importing the
FastAPI
library - Then creating your app by instantiating a
FastAPI()
object - After that, you use
@app.get
to define a route that handlesGET
requests Finally, you have the path operation function called
public()
, which is a function that will run each time that route is called, and it returns a dictionary with the welcome message.Now that you've got your first endpoint code, to get the server up and running, run the following command on the root directory of the project:
uvicorn application.main:app --reload
With your server is running, you can go either to http://127.0.0.1:8000/docs
to see the automatically generated documentation for the first endpoint like shown in the image below:
Or you can make your first request in a new terminal window by using cURL
. Keep in mind that if you are a Windows user on an older version of the operating system, you will have to install curl before running the following command:
curl -X 'GET' \
--url http://127.0.0.1:8000/api/public
And you should see a JSON as a result of the request you just did similar to this:
{
"status": "success",
"msg": "Hello from a public endpoint! You don't need to be authenticated to see this."
}
For simplicity's sake, you are going to use the cURL
for the rest of this post.
Create a Private Endpoint
Now that a base API server is set up, you will add one more endpoint to your main.py
file. In this application, you will have a GET /api/public
route available for everyone and a GET /api/private
route that only you can access with the access token you'll get from Auth0.
Now you need to update the main.py
file. Here's what you'll need to change to the imports section:
- First, you need to import
Depends
from thefastapi
module, that's FastAPI dependency injection system; - Then, you'll also need to import the
HTTPBearer
class from thefastapi.security
module, a built-in security scheme for authorization headers with bearer tokens; - You will need to create the authorization scheme based on the
HTTPBearer
. This will be used to guarantee the presence of the authorization header with theBearer
token in each request made to the private endpoint.
The token informs the API that the bearer of the token has been authorized to access the API and perform specific actions specified by the scope that was granted during authorization.
Other than updating the imports, you need to implement the private endpoint. The /api/private
endpoint will also accept GET
requests, and here is what the code main.py
looks like for now:
"""main.py
Python FastAPI Auth0 integration example
"""
from fastapi import Depends, FastAPI # π new imports
from fastapi.security import HTTPBearer # π new imports
# Scheme for the Authorization header
token_auth_scheme = HTTPBearer() # π new code
# Creates app instance
app = FastAPI()
@app.get("/api/public")
def public():
"""No access token required to access this route"""
result = {
"status": "success",
"msg": ("Hello from a public endpoint! You don't need to be "
"authenticated to see this.")
}
return result
# new code π
@app.get("/api/private")
def private(token: str = Depends(token_auth_scheme)):
"""A valid access token is required to access this route"""
result = token.credentials
return result
The Depends
class is responsible for evaluating each request that a given endpoint receives against a function, class, or instance. In this case, it will evaluate the requests against the HTTPBearer
scheme that will check the request for an authorization header with a bearer token.
You can find more details on how
FastAPI
dependency injection works on its documentation.
Now your private endpoint returns the received token. If no token is provided, it will return a 403 Forbidden
status code with the detail saying you are "Not authenticated"
. Because you used the --reload
flag while running your server, you don't need to re-run the command; uvicorn
will pick up the changes and update the server every time you save your files. Now make a request to the GET /api/private
endpoint to check its behavior. First, let's make a request without passing an authorization header:
curl -X 'GET' \
--url 'http://127.0.0.1:8000/api/private'
# {"detail": "Not authenticated"}
And now, if you make a request with the authorization header, but with a random string as token value, you should see the same random value as a result:
curl -X 'GET' \
--url 'http://127.0.0.1:8000/api/private' \
--header 'Authorization: Bearer FastAPI is awesome'
# "FastAPI is awesome"
As you can see, your endpoint isn't protected since it accepts any string as the value for the authorization header. It is not enough to receive an authorization header; you must also verify the value of the bearer token to let somebody access the endpoint. Let's fix that behavior.
Set up Auth0 an API
Before you get to the point where you are ready to validate tokens in your endpoints, you need to set up an API in Auth0. When this API is set up, you get access to a few pieces of information that Auth0 requires - an audience, client ID, and client secret.
You also need to have access to that information from within the server; that's where a configuration file comes into play. You will need to create a configuration file called .config
at the root of the project. This is what the .config
file should look like below. Remember to update the values accordingly:
# .config
[AUTH0]
DOMAIN = your.domain.auth0.com
API_AUDIENCE = your.api.audience
ALGORITHMS = RS256
ISSUER = https://your.domain.auth0.com/
This configuration is the first piece of the puzzle of checking for the Auth0 configuration settings in the token validation stage. Another good rule to follow is to never commit your configuration files with environment variables to source code. To prevent this from occurring, you should create a .gitignore
file in the project's root and add the .config
file as an entry:
# .gitignore
.config
Add JSON Web Token (JWT) Validation
Your FastAPI
server now has a GET /api/private
route, but it is not protected yet. It only checks if you have an authorization header in the request, which means you are missing a step in the process: you need to validate the access token. To do that, you need to create an object that does all the steps for validating a JWT because Auth0's access tokens are JWTs.
To separate responsibilities from the routing definition, you should create a new file called utils.py
inside the application
folder to hold all the utility code, like validating the access token and reading the configuration information.
Start by importing the Python os
library, as well as the PyJWT
and configparser
libraries. The OS library gives you access to the environment variables. The JWT library gives you functions to check and validate a JWT. The ConfigParser
class from the namesake library provides a way for Python to read the configuration settings found in the .config
file you created earlier. And the first thing you have after the imports is a function called set_up()
, which you can see below:
"""utils.py
"""
import os
import jwt
from configparser import ConfigParser
def set_up():
"""Sets up configuration for the app"""
env = os.getenv("ENV", ".config")
if env == ".config":
config = ConfigParser()
config.read(".config")
config = config["AUTH0"]
else:
config = {
"DOMAIN": os.getenv("DOMAIN", "your.domain.com"),
"API_AUDIENCE": os.getenv("API_AUDIENCE", "your.audience.com"),
"ISSUER": os.getenv("ISSUER", "https://your.domain.com/"),
"ALGORITHMS": os.getenv("ALGORITHMS", "RS256"),
}
return config
The set_up()
function is responsible for reading the .config
file and creating a configuration object that works like a dictionary. Because this sample code is prepared to also run on environment variables, the set_up()
function by default will try to read the .config
file. You can change this behavior by setting the ENV
environment variable to any other value, in which case a dictionary will be created by reading all the environment variables you can see under the else
clause above.
The next piece of the puzzle is where the magic happens. You'll create a VerifyToken
class to handle JWT token validation:
# paste the code π after the set_up() function in the utils.py file
class VerifyToken():
"""Does all the token verification using PyJWT"""
def __init__(self, token):
self.token = token
self.config = set_up()
# This gets the JWKS from a given URL and does processing so you can
# use any of the keys available
jwks_url = f'https://{self.config["DOMAIN"]}/.well-known/jwks.json'
self.jwks_client = jwt.PyJWKClient(jwks_url)
def verify(self):
# This gets the 'kid' from the passed token
try:
self.signing_key = self.jwks_client.get_signing_key_from_jwt(
self.token
).key
except jwt.exceptions.PyJWKClientError as error:
return {"status": "error", "msg": error.__str__()}
except jwt.exceptions.DecodeError as error:
return {"status": "error", "msg": error.__str__()}
try:
payload = jwt.decode(
self.token,
self.signing_key,
algorithms=self.config["ALGORITHMS"],
audience=self.config["API_AUDIENCE"],
issuer=self.config["ISSUER"],
)
except Exception as e:
return {"status": "error", "message": str(e)}
return payload
Let's break down this class to understand the steps here:
- First, you have the
__init__()
method:- This method is responsible for specifying the
token
parameter theVerifyToken
class needs; - It also runs the
set_up()
function to build the configuration the class will need; - And finally, it sets the path for the
JWKS
file by using thePyJWKClient
from thePyJWT
package. A JSON Web Key Set, or JWKS for short contains the information necessary to validate a token signature and ensure that it is a valid token. Because Auth0 implements OAuth 2.0, it has a "well-known" endpoint which you can call and get the extra metadata used to validate the token and its properties.
- This method is responsible for specifying the
- Second, you have the
verify()
method:- This method uses the key ID (
kid
claim present in the token header) to grab the key used from the JWKS to verify the token signature. If this step fails by any of the possible errors, the error message is returned; - Then, the method tries to decode the JWT by using the information gathered so far. In case of errors, it returns the error message. When successful, the token payload is returned.
- This method uses the key ID (
Let's see how to use this piece of code in our private endpoint in the section ahead.
Validate an Auth0 Access Token
The final puzzle piece is to import the class you just created in the utils.py
file and use it in the GET /api/private
endpoint. Here's what you need to change:
- Update the imports section to add the import clause for the
VerifyToken
, then head to your endpoint; you will also need to import theResponse
class and thestatus
object fromfastapi
so you can give a detailed response in case of an error; - Then, you'll need to adjust the endpoint by passing the token to the
VerifyToken
class and checking whether the result of theverify()
method is an error.
Here's what your main.py
file should look like with all the changes above:
"""main.py
Python FastAPI Auth0 integration example
"""
from fastapi import Depends, FastAPI, Response, status # π new imports
from fastapi.security import HTTPBearer
from .utils import VerifyToken # π new import
# Scheme for the Authorization header
token_auth_scheme = HTTPBearer()
# Creates app instance
app = FastAPI()
@app.get("/api/public")
def public():
"""No access token required to access this route"""
result = {
"status": "success",
"msg": ("Hello from a public endpoint! You don't need to be "
"authenticated to see this.")
}
return result
@app.get("/api/private")
def private(response: Response, token: str = Depends(token_auth_scheme)): # π updated code
"""A valid access token is required to access this route"""
result = VerifyToken(token.credentials).verify() # π updated code
# π new code
if result.get("status"):
response.status_code = status.HTTP_400_BAD_REQUEST
return result
# π new code
return result
With this update, you are properly setting up your protected endpoint and doing all the verification steps for the access tokens you need. π
Even though you started your server with the --reload
flag because you need to make sure the configuration is loaded, it is a good time to terminate the uvicorn
process and then restart the server. That will guarantee the proper functionality of your API with the configuration parameters from the .config
file or environment variables.
Before you can make requests to the protected endpoint in the FastAPI
server, you need the access token from Auth0. You can get it by copying it from the Auth0 dashboard in the Test
tab of your API.
You can also use a curl POST
request to Auth0's oauth/token
endpoint to get the access token, and you can copy this request from the Test
tab of your API in the Auth0 dashboard. The curl request will look like this; remember to fill the values as necessary:
curl -X 'POST' \
--url 'https://<YOUR DOMAIN HERE>/oauth/token' \
--header 'content-type: application/x-www-form-urlencoded' \
--data grant_type=client_credentials \
--data 'client_id=<YOUR CLIENT ID HERE>' \
--data client_secret=<YOUR CLIENT SECRET HERE> \
--data audience=<YOUR AUDIENCE HERE>
In the command line, you should see a response containing your bearer token, like this one:ββ
{
"access_token": "<YOUR_BEARER_TOKEN>",
"expires_in": 86400,
"token_type": "Bearer"
}
Now you can use this access token to access the private endpoint:
curl -X 'GET' \
--url 'http://127.0.0.1:8000/api/private' \
--header 'Authorization: Bearer <YOUR_BEARER_TOKEN>'
If the request succeeds, the server will send back the payload of the access token:
{
"iss": "https://<YOUR_DOMAIN>/",
"sub": "iojadoijawdioWDasdijasoid@clients",
"aud": "http://<YOUR_AUDIENCE>",
"iat": 1630341660,
"exp": 1630428060,
"azp": "ADKASDawdopjaodjwopdAWDdsd",
"gty": "client-credentials"
}
Keep in mind that if the validation fails, you should see the details of what went wrong.
And thatβs it β you have finished protecting the private endpoint and testing its protection.
Recap
You learned quite a few things in this blog post. To start, you learned the basics of FastAPI
by implementing two endpoints β one public, one private. You saw how simple it is to make requests to both of these endpoints. You created a verification class and saw how PyJWT helps you validate an Auth0 access token, and you learned what JWKS is.
You went through the process of creating your API in the Auth0 dashboard. You also learned how to secure one of your endpoints by leveraging the dependency injection system FastAPI provides to help you implement integrations. And you did all of this very quickly.
In short, youβve learned how easy it is to get up and running with FastAPI
, as well as how to use Auth0 for protecting your endpoints.
In this GitHub repo, youβll find the full code for the sample application you built today. If you have any questions, ask them in the community forum thread for this blog post.