close icon
FastAPI

Build and Secure a FastAPI Server with Auth0

Learn the basics of FastAPI, how to quickly set up a server and secure endpoints with Auth0.

Last Updated On: October 18, 2021

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 handles GET 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:

FastAPI documentation page showing the public endpoint

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 the fastapi module, that's FastAPI dependency injection system;
  • Then, you'll also need to import the HTTPBearer class from the fastapi.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 the Bearer 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:

  1. First, you have the __init__() method:
    1. This method is responsible for specifying the token parameter the VerifyToken class needs;
    2. It also runs the set_up() function to build the configuration the class will need;
    3. And finally, it sets the path for the JWKS file by using the PyJWKClient from the PyJWT 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.
  2. Second, you have the verify() method:
    1. 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;
    2. 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.

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:

  1. Update the imports section to add the import clause for the VerifyToken, then head to your endpoint; you will also need to import the Response class and the status object from fastapi so you can give a detailed response in case of an error;
  2. Then, you'll need to adjust the endpoint by passing the token to the VerifyToken class and checking whether the result of the verify() 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.

  • Twitter icon
  • LinkedIn icon
  • Faceboook icon