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
library;FastAPI
- Then creating your app by instantiating a
object;FastAPI()
- After that, you use
to define a route that handles@app.get
requests;GET
- Finally, you have the path operation function called
, which is a function that will run each time that route is called and it returns a dictionary with the welcome message.public()
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 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
from theDepends
module, that's FastAPI dependency injection system;fastapi
- Then you'll also need to import the
class from theHTTPBearer
module, a built-in security scheme for authorization headers with bearer tokens;fastapi.security
- You will need to create the authorization scheme based on the
. This will be used to guarantee the presence of the authorization header with theHTTPBearer
token in each request made to the private endpoint.Bearer
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
. This identifier is also known as audience.https://fastapiexample.com
- 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:- It grabs the token from the
header.Authorization
- This method uses the key ID (
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.kid
- 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!