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 27, 2023

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.

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 .venv

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 .venv/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[standard]' pydantic-settings '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:

!https://images.ctfassets.net/23aumh6u8s0i/3fE3ngkaleHJw78ZNzQway/24719642235593cbf2016b4b86f38c54/Screen_Shot_2021-10-04_at_09.55.16.png

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 a 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 an Auth0 API

Before you begin protecting endpoints in your API you’ll need to create an API on the Auth0 Dashboard. If you haven't an Auth0 account, you can sign up for a free one. Then, go to the APIs section and click on Create API.

This will open a new window for configuring the API. Set the following fields in that window:

  • Name, a friendly name or description for the API. Enter Fast API Example for this sample.
  • Identifier, which is an identifier that the client application uses to request access tokens for the API. Enter the string https://fastapiexample.com. This identifier is also known as audience.
  • Signing Algorithm, leave the default setting, RS256.

After entering those values, click the Create button.

Settings and Environment Variables

Now that you created your Auth0 API, you need to get back to the code. To effectively protect our endpoints, we need to verify that the token available in the Authorization header is valid, corresponds to our application, and was signed by the right party.

To do that, you’ll need to access on your application some Auth0 configuration values. We’ll use FastAPI settings to retrieve this values either from a .env file or from environment variables.

For our local purposes, let’s start by storing the configuration values on a .env file like the following. Remember to update the values accordingly and to place the .env file in the root folder of your project:

# .env

AUTH0_DOMAIN = your.domain.auth0.com
AUTH0_API_AUDIENCE = https://your.api.audience
AUTH0_ISSUER = https://your.domain.auth0.com/
AUTH0_ALGORITHMS = RS256

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 .env file as an entry:

# .gitignore
.env

Next, let’s create a module to retrieve the application settings. Start by creating a new file application/config.py with the following contents:

from functools import lru_cache

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    auth0_domain: str
    auth0_api_audience: str
    auth0_issuer: str
    auth0_algorithms: str

    class Config:
        env_file = ".env"

@lru_cache()
def get_settings():
    return Settings()

The defined Settings class uses Pydantic settings module to retrieve the application’s settings directly from the either the .env file or from environment variables. It is important that the define properties match the configs ignoring capitalization.

Add JSON Web Token (JWT) Validation

The next piece of the puzzle is where the magic happens. You'll create a VerifyToken class to handle JWT token validation. Performing the right validations is critical to the security of your application, so we’ll delegate the hard tasks to the library PyJWT.

Create a new file application/utils.py to host our validation code and add the following helper code:

from fastapi import HTTPException, status

class UnauthorizedException(HTTPException):
    def __init__(self, detail: str, **kwargs):
        """Returns HTTP 403"""
        super().__init__(status.HTTP_403_FORBIDDEN, detail=detail)

class UnauthenticatedException(HTTPException):
    def __init__(self):
        super().__init__(
            status_code=status.HTTP_401_UNAUTHORIZED, detail="Requires authentication"
        )

So far we only defined two new exceptions, one for when no JWT token is given (unauthenticated), and the other when the validation of the JWT fails (unauthorized).

Let’s now start with the verification code by creating a VerifyToken class that will initialize the application settings and will retrieve the KWKS that are needed to validate the incoming token’s signatures.

Update the application/utils.py files as follows:

from typing import Optional # 👈 new imports

import jwt # 👈 new imports
from fastapi import Depends, HTTPException, status # 👈 new imports
from fastapi.security import SecurityScopes, HTTPAuthorizationCredentials, HTTPBearer # 👈 new imports

from application.config import get_settings # 👈 new imports

class UnauthorizedException(HTTPException):
    def __init__(self, detail: str, **kwargs):
"""Returns HTTP 403"""
super().__init__(status.HTTP_403_FORBIDDEN, detail=detail)

class UnauthenticatedException(HTTPException):
    def __init__(self):
        super().__init__(
            status_code=status.HTTP_401_UNAUTHORIZED, detail="Requires authentication"
        )

# 👇 new code
class VerifyToken:
"""Does all the token verification using PyJWT"""

def __init__(self):
        self.config = get_settings()

        # 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.auth0_domain}/.well-known/jwks.json'
        self.jwks_client = jwt.PyJWKClient(jwks_url)
# 👆 new code

Finally, we need a method that retrieves the token from the request and performs all validations. Here is the revised VerifyToken class with the new verify method:

class VerifyToken:
    """Does all the token verification using PyJWT"""

    def __init__(self):
        self.config = get_settings()

        # 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.auth0_domain}/.well-known/jwks.json'
        self.jwks_client = jwt.PyJWKClient(jwks_url)

        # 👇 new code
    async def verify(self,
                     security_scopes: SecurityScopes,
                     token: Optional[HTTPAuthorizationCredentials] = Depends(HTTPBearer())
                     ):
        if token is None:
            raise UnauthenticatedException

        # This gets the 'kid' from the passed token
        try:
            signing_key = self.jwks_client.get_signing_key_from_jwt(
                token.credentials
            ).key
        except jwt.exceptions.PyJWKClientError as error:
            raise UnauthorizedException(str(error))
        except jwt.exceptions.DecodeError as error:
            raise UnauthorizedException(str(error))

        try:
            payload = jwt.decode(
                token.credentials,
                signing_key,
                algorithms=self.config.auth0_algorithms,
                audience=self.config.auth0_api_audience,
                issuer=self.config.auth0_issuer,
            )
        except Exception as error:
            raise UnauthorizedException(str(error))
    
        return payload
        # 👆 new code

The verify method consists of three steps to validate the integrity of the token:

  1. It grabs the token from the Authorization header.
  2. 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.
  3. 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.

All done! You are ready now to start securing your endpoints.

Protect Your Endpoints

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.

Since we abstracted all of the logic to the VerifyToken class, validating endpoints is now easy.

Here's what your main.py file should look like with all the changes above:

"""Python FastAPI Auth0 integration example
"""

from fastapi import FastAPI, Security
from .utils import VerifyToken # 👈 Import the new class

# Creates app instance
app = FastAPI()
auth = VerifyToken() # 👈 Get a new instance

@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(auth_result: str = Security(auth.verify)): # 👈 Use Security and the verify method to protect your endpoints
    """A valid access token is required to access this route"""
    return auth_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 .env 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": 1691399881,
    "exp": 1691486281,
    "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.

Thanks for reading!

  • Twitter icon
  • LinkedIn icon
  • Faceboook icon