FastAPI Authentication by Example
Published on December 5, 2024In this guide, you'll learn how to integrate Auth0 by Okta with FastAPI to implement the following security features:
- Adding user login, sign-up, and logout to a FastAPI application.
- Retrieving and using user profile information to personalize FastAPI pages.
- Protecting FastAPI routes from unauthorized access.
- Making API calls from FastAPI to request data from an external protected API.
This guide uses the Authlib library, which provides developers with a high-level API to handle OAuth and OpenID Connect servers from a low level specification implementation to integration with the major Python frameworks.
How Does Auth0 Work?
With the help of Auth0 by Okta, you don't need to be an expert on identity protocols such as OAuth 2.0 or OpenID Connect, to understand how to secure your web application stack.
First you integrate your FastAPI application with Auth0. Your application will then redirect users to an Auth0 customizable login page where they can log in using their preferred method. Once your users log in successfully, Auth0 redirects them back to your FastAPI app, returning JSON Web Tokens (JWTs) with their authentication and user information.
Get the Starter Application
We have created a starter project to help you learn FastAPI security concepts through hands-on practice. You can focus on building FastAPI routes and services to secure your application.
Start by cloning the web-app_fastapi_python_hello-world
repository on its starter
branch:
git clone -b starter [email protected]:oktadev/auth0-web-app_fastapi_python_hello-world.git
Once you have the cloned repo, make web-app_fastapi_python_hello-world
your current directory:
cd web-app_fastapi_python_hello-world
Next, to work with the project you will create a Python virtual environment:
python3 -m venv .env
Then activate your environment
And finally upgrade pip
and install the project dependencies like so:
pip install -U pip && pip install -r requirements.txt
Now is time to run the project. This starter FastAPI app is a functional application with routes and services that hydrates the user interface. It is important to notice that the message service implemented in the starter project simulates the external API by hardcoding the responses. Later on this guide, you'll integrate this FastAPI application with a real API server using a backend technology of your choice.
Run the following command to get your FastAPI application running on port 4040
:
uvicorn main:app --reload --port 4040
You'll see the output on your terminal like so:
INFO: Will watch for changes in these directories: ['~/web-app_fastapi_python_hello-world']INFO: Uvicorn running on http://127.0.0.1:4040 (Press CTRL+C to quit)INFO: Started reloader process [6212] using WatchFilesINFO: Started server process [6214]INFO: Waiting for application startup.INFO: Application startup complete.
Now you should visit http://127.0.0.1:4040/
on your browser to access the running application and you should see somehting like this:
Configure FastAPI with Auth0
Follow these steps to get started with the Auth0 Identity Platform quickly:
Sign up and create an Auth0 Application
A free account also offers you:
- Auth0 Universal Login for Web, iOS & Android.
- Up to 2 social identity providers like Google, GitHub, and Twitter.
- Up to 3 Actions to customize and extend Auth0's capabilities.
During the sign-up process, you'll create something called an Auth0 Tenant, representing the product or service to which you are adding authentication.
Once you sign in, Auth0 takes you to the Dashboard. In the left sidebar menu, click on "Applications".
Then, click the "Create Application" button. A modal opens up with a form where you can provide a name for the application and choose its type. Use the following values:
Auth0 FastAPI Web Application
Click the "Create" button to complete the process. Your Auth0 application page loads up.
In the next step, you'll learn how to help FastAPI and Auth0 communicate.
What's the relationship between Auth0 Tenants and Auth0 Applications?
Let's say that you have a photo-sharing FastAPI app called "FastAPIgram". You then would create an Auth0 tenant called fastapigram
. From a customer perspective, FastAPIgram is that customer's product or service.
Now, say that FastAPIgram is available on three platforms: web as a web application and Android and iOS as a native mobile application. If each platform needs authentication, you need to create three Auth0 applications to provide the product with everything it needs to authenticate users through that platform.
FastAPIgram users belong to the Auth0 FastAPIgram tenant, which shares them across its Auth0 applications.
Create a communication bridge between FastAPI and Auth0
When using the Auth0 Identity Platform, you don't have to build login and signup forms. Auth0 offers a Universal Login Page to reduce the overhead of adding and managing authentication.
How does Universal Login work?
Your FastAPI application will redirect users to Auth0 whenever they trigger an authentication request. Auth0 will present them with a login page. Once they log in, Auth0 will redirect them back to your FastAPI application. For that redirecting to happen securely, you must specify in your Auth0 Application Settings the URLs to which Auth0 can redirect users once it authenticates them.
As such, click on the "Settings" tab of your Auth0 Application page, locate the "Application URIs" section, and fill in the following values:
http://127.0.0.1:4040/callback
The above value is the URL that Auth0 can use to redirect your users after they successfully log in.
http://127.0.0.1:4040
The above value is the URL that Auth0 can use to redirect your users after they log out.
http://127.0.0.1:4040
Click the "Save Changes" button at the bottom of the page.
Do not close this page yet. You'll need some of its information in the next section.
Add the Auth0 configuration variables to FastAPI
From the application Settings page on the Auth0 Dashboard, you need the Auth0 Domain and Client ID values to allow your FastAPI application to use the communication bridge you created.
What exactly is an Auth0 Domain, an Auth0 Client ID and a Auth0 Client Secret?
Domain
When you created a new Auth0 account, Auth0 asked you to pick a name for your tenant. This name, concatenated with .auth0.com
, is your Auth0 Domain. It's the base URL that you will use to access the Auth0 APIs and the URL where you'll redirect users to log in.
You can also use custom domains to allow Auth0 to do the authentication heavy lifting for you without compromising your branding experience.
Client ID
Each application is assigned a Client ID upon creation, which is an alphanumeric string, and it's the unique identifier for your application (such as q8fij2iug0CmgeLfTfG1tSGdSQyGaTUA
). You cannot modify the Client ID. You will use the Client ID to identify the Auth0 Application to which the FastAPI application needs to connect.
Warning: Another critical piece of information present in the "Settings" is the Client Secret. This secret protects your resources by only granting tokens to requestors if they're authorized. Think of it as your application's password, which must be kept confidential at all times. If anyone gains access to your Client Secret, they can impersonate your application and access protected resources.
Head back to your Auth0 application page and click on the "Settings" tab.
Locate the "Basic Information" section and follow these steps to get the Auth0 Domain, Auth0 Client ID, and Auth0 Client Secret values:
When you enter a value in the input fields present on this page, any code snippet that uses such value updates to reflect it. Using the input fields makes it easy to copy and paste code as you follow along.
As such, enter the "Domain", "Client ID", and "Client Secret" values in the following fields to set up your web application in the next section:
These variables let your FastAPI application identify as an authorized party to interact with the Auth0 authentication server.
Now, create a new .config
file under the FastAPI project directory as follows:
[AUTH0]CLIENT_ID=AUTH0-CLIENT-IDCLIENT_SECRET=AUTH0-CLIENT-SECRETDOMAIN=AUTH0-DOMAIN[WEBAPP]SESSION_SECRET=
.example.config
to .config
and replace the content of the file with the details above.Now you need to fill the missing information for the SESSION_SECRET
. To do so, generate a string using openssl
using the following command:
openssl rand -hex 32
The openssl
CLI tool will generate a random string that you can use as session secret. This string is used to encode the session data.
Load variables to your environment
For these variables to be available to your application you'll need to load them. Go to the config.py
file in the root of the project and use the pre-defined function to do so:
# ... existing codedef load_config():"""Loads configuration from .config file"""config = configparser.ConfigParser()config.read(".config")return configconfig = load_config() # 👈 new code# ... existing code
Keep in mind that this application is using the Python configparser
module to load variables from .config
.
Now you are all set to use all of these variables in the next steps.
Define the Session Middleware
To be able to access the session data you'll need to use the Starlette SessionMiddleware
. This allows you to use access the data using the request.session
dictionary that you'll see in coming steps when logging in a given user.
Define the session middleware like so:
# ... existing codefrom fastapi.templating import Jinja2Templatesfrom starlette.middleware.sessions import SessionMiddleware # 👈 new code# ... existing codedef create_app():app = FastAPI()app.mount("/static", StaticFiles(directory="static"), name="static")# 👇 new codeapp.add_middleware(SessionMiddleware,secret_key=config['WEBAPP']['SESSION_SECRET'])# 👆 new codereturn app# ... existing code
Now your app
has a session you can access.
Set up the OAuth and OpenID Connect with Authlib
To integrate your FastAPI application with Auth0 you'll use the Authlib library, which will handle all the OAuth and OpenID Connect for you.
Initialize Auth0 configuration
So far your application registers one API Router that is responsible for the application routes like /
and /profile
.
To set up Auth0 you'll register a new API router that will take care of all auth
routes and logic.
Start by creating a new directory at the root of your project named auth
:
mkdir auth
And add the following empty files inside of the auth/
directory:
touch auth/__init__.py auth/config.py auth/routes.py
Now you can start by setting up authlib
and register a new Auth0 OAuth provider by adding the following code to the auth/config.py
file:
from authlib.integrations.starlette_client import OAuthfrom config import config"""Storing the configuration into the `auth0_config` variable for later usage"""auth0_config = config['AUTH0']oauth = OAuth()oauth.register("auth0",client_id=auth0_config['CLIENT_ID'],client_secret=auth0_config['CLIENT_SECRET'],client_kwargs={"scope": "openid profile email",},server_metadata_url=f'https://{auth0_config["DOMAIN"]}/.well-known/openid-configuration')
Once you provide authlib
with the Auth0 server details such as the client_id
, client_secrent
, scope
, and server_metadata_url
, the library is fully set up and you can start creating routes to handle the authentication requests with the Auth0 Authentication Server.
What is the server_metadata_url
?
server_metadata_url
?When setting up the OAuth client to use Auth0 in your FastAPI application, you'll need to pass along the URL that holds the OpenID configuration for your tenant.
The OpenID configuration URL provides the Authlib OAuth()
client with all the references to communicate with Auth0 and validate the tokens that it will receive.
The Authlib library bundles together OAuth and OpenID Connect (OIDC) to make it easier for you while setting up OAuth and OIDC. If you pass "openid"
as part of your "scope"
you need to pass the configuration URL in the server_metadatada_url
.
Read "Obtaining OpenID Provider Configuration Information" from the OpenID Connect spec to learn more about OpenID Connect configuration information and discovery. To understand more about how Authlib handles OpenID in FastAPI read FastAPI OpenID Connect Client.
What about using scopes?
You'll notice you are passing scope
as part of the OAuth client arguments. Here you're providing the OpenID Connect Scopes: openid profile email
.
-
openid
: This scope informs the Auth0 Authorization Server that the Client is making an OpenID Connect (OIDC) request to verify the user's identity. OpenID Connect is an authentication protocol. -
profile
: This scope value requests access to the user's default profile information, such asname
,nickname
, andpicture
. -
email
: This scope value requests access to theemail
andemail_verified
information.
The details of the OpenID Connect Scopes go into the ID Token. However, you can define custom API scopes to implement access control. You'll identify those custom scopes in the calls that your client applications make to that API. Auth0 includes API scopes in the access token as the scope
claim value.
The concepts about API scopes or permissions are better covered in an Auth0 API tutorial such as "Use TypeScript to Create a Secure API with Node.js and Express: Role-Based Access Control".
Create the auth API Router
Now populate the auth/routes.py
with the following content:
from fastapi import APIRouter, Requestauth_router = APIRouter()@auth_router.get("/login")async def login(request: Request):"""Redirects the user to the Auth0 Universal Login (https://auth0.com/docs/authenticate/login/auth0-universal-login)"""return {"message": "Login"}@auth_router.get("/signup")async def signup(request: Request):"""Redirects the user to the Auth0 Universal Login (https://auth0.com/docs/authenticate/login/auth0-universal-login)"""return {"message": "Sign Up"}@auth_router.get("/logout")def logout(request: Request):"""Redirects the user to the Auth0 Universal Login (https://auth0.com/docs/authenticate/login/auth0-universal-login)"""return {"message": "Logout"}@auth_router.get("/callback")async def callback(request: Request):"""Callback redirect from Auth0"""return {"message": "Callback"}
These routes will hold the logic that makes your application interact with Auth0. You will update and understand each one of them shortly.
Register the auth API Router
Now to register the auth
router with the main application update the main.py
file in the root of your project like so:
from config import appfrom auth.routes import auth_router # 👈 new codefrom webapp.routes import webapp_routerapp.include_router(auth_router) # 👈 new codeapp.include_router(webapp_router)
This allow your application to have access to the new auth
endpoints you just created.
Update the Auth0 callback route
With the Auth0 configuration implemented and the auth
API Router registered, first you'll need to update the route that handles the authentication callback.
from fastapi import APIRouter, Request # ... existing codefrom fastapi.responses import RedirectResponse # 👈 new codefrom auth.config import oauth # 👈 new code# ... existing code@auth_router.get("/callback")async def callback(request: Request):"""Callback redirect from Auth0"""# 👇 new codetoken = await oauth.auth0.authorize_access_token(request)# Store `id_token`, and `userinfo` in sessionrequest.session['id_token'] = token['id_token']request.session['userinfo'] = token['userinfo']# 👆 new codereturn RedirectResponse(url=request.url_for("profile")) # 👈 updated code
After the user attempts to log in, either if it was successful or not, Auth0 will redirect them to the /callback
endpoint.
In the callback
route you authenticate the user coming from the request
, store the ID (user) token in session
and redirect the user to the profile
endpoint. When there's an error, the application will respond with a 400
error status code.
In the following steps you'll update the login, logout, and sign up routes as well as the templates to use them.
Create the Login Button
You have created the callback
route to handle a login flow in your app. Now you need to create a Login Button that will trigger the authentication flow.
Create a new template that will handle the authentication buttons in the navigation bar:
touch templates/navigation/desktop/nav_bar_buttons.html
Next, add the following content:
<div class="nav-bar__buttons"><a href="{{ url_for('login') }}" class="button__login" >Log In</a></div>
You'll create an additional file for the mobile template:
touch templates/navigation/mobile/mobile_nav_bar_buttons.html
Now, and add the following content:
<div class="mobile-nav-bar__buttons"><a href="{{ url_for('login') }}" class="button__login" >Log In</a></div>
In both templates you are creating a button labeled "Log In". When the users click on this button they will make a GET
request to /login
endpoint.
Right now this endpoint will show a JSON object with a temporary message {"message": "Login"}
. You'll adjust this logic in the next section.
But before you do that, add the new button to the navigation bar.
For the Desktop template, make the following changes:
<div class="nav-bar__container"><nav class="nav-bar">{% include 'navigation/desktop/nav_bar_brand.html' %}{% include 'navigation/desktop/nav_bar_tabs.html' %}{% include 'navigation/desktop/nav_bar_buttons.html' %} <!-- 👈 new code --></nav></div>
And similarly for mobile:
<div class="mobile-nav-bar__container"><nav id="mobile-nav-bar" class="mobile-nav-bar">{% include 'navigation/mobile/mobile_nav_bar_brand.html' %}{% include 'navigation/mobile/menu_button.html' %}<div id="mobile-menu" class="mobile-nav-bar__menu mobile-nav-bar__menu--closed">{% include 'navigation/mobile/mobile_nav_bar_tabs.html' %}{% include 'navigation/mobile/mobile_nav_bar_buttons.html' %} <!-- 👈 new code --></div></nav></div>
Update the login
route
For the user to be able to authenticate you need to update the /login
logic as follows:
from fastapi import APIRouter, Requestfrom fastapi.responses import RedirectResponsefrom auth.config import auth0_config, oauth # 👈 updated code# ... existing code@auth_router.get("/login")async def login(request: Request):"""Redirects the user to the Auth0 Universal Login (https://auth0.com/docs/authenticate/login/auth0-universal-login)"""# 👇 new codeif 'id_token' not in request.session:return await oauth.auth0.authorize_redirect(request,redirect_uri=request.url_for("callback"))# 👆 new codereturn RedirectResponse(url=request.url_for("profile")) # 👈 updated code# ... existing code
If you run your server and look at the main page http://127.0.0.1:4040
, now you should see the Log In button on the navigation bar like so:
When the user presses the "Log In" button from the UI, the login
route will redirect the user to the Auth0 Universal Login Page, where the user will be able to authenticate. After completion Auth0 will redirect the user to the callback
endpoint completing the authentication process.
Create the Sign-Up Button
Users are able to log in, but they also need to be able to Sign Up.
Register the signup
route
Similarly to when you updated the login
route, you need to adjust the signup
route. The process only differs in that you will pass an additional parameter to Auth0, hinting the service to start the process on the Sign Up page as opposed to the Log In page.
# ... existing code@auth_router.get("/signup")async def signup(request: Request):"""Redirects the user to the Auth0 Universal Login (https://auth0.com/docs/authenticate/login/auth0-universal-login)"""# 👇 new codeif 'id_token' not in request.session: # it could be userinfo instead of id_tokenreturn await oauth.auth0.authorize_redirect(request,redirect_uri=request.url_for("callback"),screen_hint="signup")# 👆 new codereturn RedirectResponse(url=request.url_for("profile")) # 👈 updated code# ... existing code
When the user presses the "Sign Up" button, they will be redirected to a sign-up page instead of a login page.
Add the sign up button to the template
Open the templates/navigation/desktop/nav_bar_buttons.html
and add a new button:
<div class="nav-bar__buttons"><a href="{{ url_for('signup') }}" class="button__sign-up">Sign Up</a> <!-- 👈 new code --><a href="{{ url_for('login') }}" class="button__login" >Log In</a></div>
The same button needs to be added to the corresponding mobile template.
<div class="nav-bar__buttons"><a href="{{ url_for('signup') }}" class="button__sign-up">Sign Up</a> <!-- 👈 new code --><a href="{{ url_for('login') }}" class="button__login" >Log In</a></div>
And you should be able to see the Sign Up button on your navigation bar alongside the Log In button.
Create the Log Out Button
It's time to let your users log out and exit their sessions.
Update the logout
route
Similarly to the previous steps, you need to update the logout
route.
from urllib.parse import quote_plus, urlencode # 👈 new codefrom fastapi import APIRouter, Request# ... existing code@auth_router.get("/logout")def logout(request: Request):"""Redirects the user to the Auth0 Universal Login (https://auth0.com/docs/authenticate/login/auth0-universal-login)"""# 👇 new coderesponse = RedirectResponse(url="https://" + auth0_config['DOMAIN']+ "/v2/logout?"+ urlencode({"returnTo": request.url_for("home"),"client_id": auth0_config['CLIENT_ID'],},quote_via=quote_plus,))request.session.clear()# 👆 new codereturn response # 👈 updated code# ... existing code
The logout
route code is somewhat different from login
and signup
as it doesn't depend on authlib
, but rather it cleans the session to log out the user from the FastAPI application and it performs a redirect to Auth0 to log out the user from the authorization server side as well.
If you don't perform the Auth0 log out, and the user presses the "Log In" button again, it won't be necessary to provide any credentials, and the authentication flow will continue automatically with the Auth0 current authenticated user.
Add the logout button to the template
Open the templates/navigation/desktop/nav_bar_buttons.html
and add a new button:
<div class="nav-bar__buttons"><a href="{{ url_for('logout') }}" class="button__logout">Log Out</a> <!-- 👈 new code --><a href="{{ url_for('signup') }}" class="button__sign-up">Sign Up</a><a href="{{ url_for('login') }}" class="button__login" >Log In</a></div>
The same button needs to be added to the mobile template.
<div class="nav-bar__buttons"><a href="{{ url_for('logout') }}" class="button__logout">Log Out</a> <!-- 👈 new code --><a href="{{ url_for('signup') }}" class="button__sign-up">Sign Up</a><a href="{{ url_for('login') }}" class="button__login" >Log In</a></div>
With the current changes your application should look something like the image below no matter the authentication status of the user.
Now you will focus on providing a better experience for your users by conditionally hiding some of the navigation bar buttons depending on the user's authentication status.
Render Content Based on Authentication
Because you are storing the user's ID token in the session, you'll use this value to decide whether a user is authenticated or not.
Render authentication buttons conditionally
Update the desktop template to conditionally render the buttons depending on the authentication status.
<div class="nav-bar__buttons">{% if request.session %} <!-- 👈 new code --><a href="{{ url_for('logout') }}" class="button__logout">Log Out</a>{% else %} <!-- 👈 new code --><a href="{{ url_for('login') }}" class="button__login" >Log In</a><a href="{{ url_for('signup') }}" class="button__sign-up" >Sign Up</a>{% endif %} <!-- 👈 new code --></div>
You should apply these changes to the corresponding mobile template as well.
<div class="nav-bar__buttons">{% if request.session %} <!-- 👈 new code --><a href="{{ url_for('logout') }}" class="button__logout">Log Out</a>{% else %} <!-- 👈 new code --><a href="{{ url_for('login') }}" class="button__login" >Log In</a><a href="{{ url_for('signup') }}" class="button__sign-up" >Sign Up</a>{% endif %} <!-- 👈 new code --></div>
Render navigation tabs conditionally
Similarly, only authenticated users are allowed to access the Protected
and Admin
routes, so you should hide the tabs from the navigation bar appropriately.
{% macro tab(label='', path='') %}<ahref="{{ path }}"class="nav-bar__tab {% if request.url == path %}nav-bar__tab--active{% endif %}">{{ label }}</a>{% endmacro %}<div class="nav-bar__tabs">{{ tab(label="Profile", path=url_for('profile')) }}{{ tab(label="Public", path=url_for('public')) }}{% if request.session %} <!-- 👈 new code -->{{ tab(label="Protected", path=url_for('protected')) }}{{ tab(label="Admin", path=url_for('admin')) }}{% endif %} <!-- 👈 new code --></div>
You should also apply these changes to the corresponding mobile template.
{% macro tab(label='', path='') %}<ahref="{{ path }}"class="nav-bar__tab {% if request.url == path %}nav-bar__tab--active{% endif %}">{{ label }}</a>{% endmacro %}<div class="nav-bar__tabs">{{ tab(label="Profile", path=url_for('profile')) }}{{ tab(label="Public", path=url_for('public')) }}{% if request.session %} <!-- 👈 new code -->{{ tab(label="Protected", path=url_for('protected')) }}{{ tab(label="Admin", path=url_for('admin')) }}{% endif %} <!-- 👈 new code --></div>
Then your application should look like this when the user is not authenticated yet:
Similarly, an authenticated user would see this main page:
Secure your FastAPI routes
Now that the navigation tabs and buttons will render conditionally, you'll need to make sure that only authenticated users have access to certain routes.
Follow the steps in the next sections to the protect the routes from unwanted access.
Create the protection dependency
Create the dependencies.py
file inside the auth/
directory like so:
touch auth/dependencies.py
Next update the auth/dependencies.py
file to contain the following:
from fastapi import Request, HTTPException, statusdef protected_endpoint(request: Request):"""This Dependency protects an endpoint and it can only be accessed if the user has an active session"""if 'id_token' not in request.session: # it could be userinfo instead of id_token# this will redirect people to the login after if they are not logged inraise HTTPException(status_code=status.HTTP_307_TEMPORARY_REDIRECT,detail="Not authorized",headers={"Location": "/login"})
This protection guard is responsible for redirecting non-authenticated users to the login page if they try to access protected pages directly.
Secure the profile, protected and admin routes
To protect your endpoints, you'll use the dependency that validates the user session, and if no session is found it will redirect the users automatically to the login
route. If, on the contrary, there's an existing session you'll continue with the default method.
Now, simply adjusting a few lines of code you'll require a user to be authenticated to access a given protected route.
from fastapi import APIRouter, Depends, Request # 👈 updated codefrom auth.dependencies import protected_endpoint # 👈 new codefrom config import templatesfrom services.message_service import MessageServicewebapp_router = APIRouter()# ... existing code@webapp_router.get("/profile", dependencies=[Depends(protected_endpoint)]) # 👈 updated codedef profile(request: Request):# ... existing code@webapp_router.get("/protected", dependencies=[Depends(protected_endpoint)]) # 👈 updated codedef protected(request: Request):# ... existing code@webapp_router.get("/admin", dependencies=[Depends(protected_endpoint)]) # 👈 updated codedef admin(request: Request):# ... existing code
To see the new behavior in action remember to logout and then try to access a protected page such as /protected
. You should be redirected to the login page instead.
Retrieve User Profile Information
After a user successfully logs in, Auth0 sends an ID token to your FastAPI web application. Authentication systems, such as Auth0, use ID Tokens in token-based authentication to cache user profile information and provide it to a client application. The caching of ID tokens can improve the performance and responsiveness of your FastAPI application.
Because you are storing the tokens and information processed by authlib
during the authentication routine, you have direct access to the user information from Auth0 in the request.session
object.
You can now process that information in the profile route to build the template that shows the user information on the screen with actual user data instead of the mock data present on starter app.
# ... existing code@webapp_router.get("/profile", dependencies=[Depends(ProtectedEndpoint)])def profile(request: Request):"""Profile endpoint, should only be accessible after login"""# Remove mock datareturn templates.TemplateResponse("profile.html",{"request": request,"userinfo": request.session['userinfo'] # 👈 updated code})# ... existing code
And now your user should see something like this on their profile page after logging in:
Integrate Your FastAPI Application with a Protected API Server
This section focuses on showing you how to get an access token in your FastAPI Web App and how to use it to make API calls to protected API endpoints.
When you use Auth0, you delegate the authentication process to a centralized service. Auth0 provides you with functionality to log in and log out users from your FastAPI application. However, your application may need to access protected resources from another API.
You can also protect an API with Auth0. There are multiple API quickstarts to help you integrate Auth0 with your backend platform.
When you use Auth0 to protect your API, you also delegate the authorization process to a centralized service that ensures only approved client applications can access protected resources on behalf of a user.
Retrieve an Access Token from Auth0
Thanks to authlib
there isn't a lot you need to do as you are already storing the information processed by the library, which not only contains user information, the ID Token, but also the access token needed by the application.
To retrieve the access token when you are storing it in a session you can simply do:
access_token = request.session['access_token']
How can you make secure API calls from a FastAPI Web App?
Instead of creating an API from scratch to test the authentication and authorization flow between the client and the server, you can pair this FastAPI web client application with an API server that matches the technology stack you use at work. The FastAPI "Hello World" sample application that you have been building up can interact with any of the "Hello World" API server samples from the Auth0 Developer Center.
Pick an API code sample in your preferred backend framework and language from the list below and follow the instructions on the code sample page to set it up. Once you complete the sample API server setup, please return to this page to learn how to integrate that API server with your FastAPI application.
Call a protected API from you FastAPI Web App
Add external API Variables
Now that you setup your API, you should have an Auth0 Audience. Store the audience in the field bellow:
Then adjust your .config
file in your FastAPI web application accordingly:
[AUTH0]# ... existing code# 👇 new code[API]AUTH0_AUDIENCE=AUTH0-AUDIENCEAPI_SERVER_URL=http://localhost:6060# 👆 new code[WEBAPP]# ... existing code
.config
change so the new variables can be properly loaded in.You are using AUTH0_AUDIENCE
to add the value of your Auth0 API Audience so that your FastAPI web application can request resources from the API represented by that value.
What AUTH0_AUDIENCE
and API_SERVER_URL
represent?
AUTH0_AUDIENCE
and API_SERVER_URL
represent?The API_SERVER_URL
is the URL where your sample API server listens for requests. In production, you'll change this value to the URL of your live server.
Your FastAPI application must pass an access token when it calls a target API to access protected resources. You can request an access token in a format that the API can verify by passing the audience
during the authentication request in your login
and signup
routes.
The value of the Auth0 Audience must be the same for both the FastAPI web client application and the API server you decided to set up.
Why is the Auth0 Audience value the same for both apps? Auth0 uses the value of the audience property to determine which resource server (API) the user is authorizing your FastAPI application to access. It's like a phone number. You want to ensure that your FastAPI application "texts the right API".
Now, add the audience
login. You'll need to make two updates, first to the login
endpoint, this will make Auth0 return the access token:
# ... existing codefrom auth.config import auth0_config, oauthfrom config import config # 👈 new code# ... existing code@auth_router.get("/login")async def login(request: Request):# ... existing codeif 'id_token' not in request.session:return await oauth.auth0.authorize_redirect(request,redirect_uri=request.url_for("callback"), # 👈 updated codeaudience=config['API']['AUTH0_AUDIENCE'] # 👈 new code)return RedirectResponse(url=request.url_for("profile"))# ... existing code
Now make the change also to the callback
function so you can store the received access token in the session like so:
# ... existing code@auth_router.get("/callback")async def callback(request: Request):"""Callback redirect from Auth0"""token = await oauth.auth0.authorize_access_token(request)# Store `access_token`, `id_token`, and `userinfo` in session # 👈 updated coderequest.session['access_token'] = token['access_token'] # 👈 new coderequest.session['id_token'] = token['id_token']request.session['userinfo'] = token['userinfo']return RedirectResponse(url=request.url_for("profile"))
This change will show a new consent page to the users once they login again as the user now need to give your web app permissions to make requests in the users name.
Update the Message Service to call an external API
Until now, the message service had hardcoed values to emulate the API response for public, protected, and admin pages.
Now you'll update the service to communicate to external API. Update the services/message_service.py
as follows:
import httpx # 👈 new codefrom typing import TypedDictfrom config import config # 👈 new codeclass MessageServiceType(TypedDict):text: str# 👇 new codedef make_request(path, access_token=None):url = f"{config['API']['API_SERVER_URL']}{path}"if access_token is None:headers = {}else:headers = {'authorization': 'Bearer {}'.format(access_token)}r = httpx.get(url, headers=headers)return r.json()# 👆 new codeclass MessageService:def public_message(self) -> MessageServiceType:return {'text': 'This is a public message.'}# 👇 updated codedef protected_message(self, access_token):return make_request('/api/messages/protected', access_token)def admin_message(self, access_token):return make_request('/api/messages/admin', access_token)# 👆 updated code
Since your changes now affect the signature of the functions admin_message
and protected_message
to expect a parameter access_token
you need to provide it during their usage. Update it as follows:
# ... existing code@webapp_router.get("/protected", dependencies=[Depends(protected_endpoint)])def protected(request: Request):"""Protected endpoint, should only be accessible after login"""access_token = request.session['access_token'] # 👈 new codereturn templates.TemplateResponse("protected.html",{"request": request,"message": MessageService().protected_message(access_token) # 👈 updated code})@webapp_router.get("/admin", dependencies=[Depends(protected_endpoint)])def admin(request: Request):"""Admin endpoint, should only be accessible after login if the user has the admin permissions"""access_token = request.session['access_token'] # 👈 new codereturn templates.TemplateResponse("admin.html",{"request": request,"message": MessageService().admin_message(access_token) # 👈 new code})
You are getting the access token from the session
, which you previously stored after the user authenticated in the callback
method. When the MessageService
pass a None
as the value for the access_token
to the API
and you'll get a { message: Permission Denied }
error from the External API.
Conclusion
You learned to implemented user authentication in FastAPI to identify your users, get user profile information, and control the content that your users can access by protecting routes and API resources.
This guide covered the most common authentication use case for a FastAPI Web application: simple login and logout. However, Auth0 is an extensible and flexible identity platform that can help you achieve even more. If you have a more complex use case, check out the Auth0 Architecture Scenarios to learn more about the typical architecture scenarios we have identified when working with customers on implementing Auth0.
We'll cover advanced authentication patterns and tooling in future guides, such as using a pop-up instead of redirecting users to log in, adding permissions to ID tokens, using metadata to enhance user profiles, and much more.
Click the GitHub button below to see the complete code you just built.