How to authenticate API requests with Clerk and FastAPI
Use built-in FastAPI HTTPBearer utility to validate bearer tokens and get the active user session in your Python API applications
Need to authenticate calls to your Python API but can’t find a tutorial or SDK to use? I got you covered with these few simple steps!
While building a SaaS, I realised Clerk doesn’t offer a Python SDK (yet maybe?), so I decided to dig deeper and see if there is a way to use the built-in FastAPI security utilities to read and validate the authentication bearer token that is sent from my frontend application.
Follow the implementation steps below to get started quickly.
A. Preparation Steps
Skip these if you already have a FastAPI application.
1. Install dependencies
Add these packages to your Python project using pip
:
# Create a virtual environment
python -m venv venv
# Activate the virtual environment
source venv/bin/activate
# Install dependencies
pip install fastapi uvicorn
2. Listen to HTTP requests
In a file server.py,
create a server listening on port 8000:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello World"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
I like to use this approach with uvicorn
so that I don’t have to install the uvicorn
CLI and can instead simply run python server.py
. To try it out:
python server.py
INFO: Started server process [54863]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
B. Validate Bearer Tokens
1. Parse the bearer token
We will use the FastAPI HTTPBearer utility to read the token from the Authorization
HTTP header.
Of course we could simply parse the token from the header directly with string manipulation, but the utility makes the code easier to read. Bonus points for neat code!
In server.py
, add these lines (commented 1 to 6):
from fastapi import FastAPI, Depends # <- (1)
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer # <- (2)
from typing import Annotated # <- (3)
app = FastAPI()
security = HTTPBearer() # <- (4)
@app.get("/")
async def root(
credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)] # <- (5)
):
print(f"Got token: {credentials.credentials[:10]}...") # <- (6)
return {"message": "Hello World"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
Let’s start the server and do some tests:
python server.py
INFO: Started server process [55315]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
From another terminal:
# Send a request without authentication token
curl -i localhost:8000
HTTP/1.1 403 Forbidden
date: Sat, 17 Feb 2024 09:51:19 GMT
server: uvicorn
content-length: 30
content-type: application/json
{"detail":"Not authenticated"}
# Now we send an (arbitrary) authentication token
curl -i localhost:8000 -H 'Authorization: Bearer supersecret'
HTTP/1.1 200 OK
date: Sat, 17 Feb 2024 09:52:41 GMT
server: uvicorn
content-length: 25
content-type: application/json
{"message":"Hello World"}
# ... and I can see my server printing the following:
Got token: supersecre...
INFO: 127.0.0.1:50722 - "GET / HTTP/1.1" 200 OK
2. Validate the token
With networkless validation, we can use our JWT signing key to verify the token validity:
Install the PyJWT library with the cryptography
package:
pip install "pyjwt[crypto]"
In server.py
, add or update with these lines (1 to 5):
import requests # <- (1)
from fastapi import FastAPI, Depends, Response, status # <- (2)
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from typing import Annotated
CLERK_PEM_PUBLIC_KEY = """
-----BEGIN PUBLIC KEY-----
<YOUR JWT PUBLIC KEY>
-----END PUBLIC KEY-----
""" # <- (3)
app = FastAPI()
security = HTTPBearer()
@app.get("/")
async def root(
credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)],
response: Response # <- (4)
):
print(f"Got token: {credentials.credentials[:10]}...")
# (5) add all this section below
try:
jwt.decode(token, key=CLERK_PEM_PUBLIC_KEY, algorithms=['RS256'])
return {"message": "Hello World"}
except jwt.exceptions.PyJWTError:
response.status_code = status.HTTP_400_BAD_REQUEST
return {"message": "Invalid token"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
To test it, we can fetch the token used by our frontend application when logging in with Clerk. We can find it by running await Clerk.session.getToken()
in the console:
From a terminal, run the following:
# Without a valid token
curl -i localhost:8000 -H 'Authorization: Bearer invalidtoken'
HTTP/1.1 400 Bad Request
date: Sun, 18 Feb 2024 13:21:01 GMT
server: uvicorn
content-length: 27
content-type: application/json
{"message":"Invalid token"}
# With a valid token
curl -i localhost:8000 -H 'Authorization: Bearer eyJhbGc...'
HTTP/1.1 200 OK
date: Sun, 18 Feb 2024 13:08:58 GMT
server: uvicorn
content-length: 25
content-type: application/json
{"message":"Hello World"}
And it works 🎉! The application does check that the token is valid and not yet expired.
Resources
Want to Connect? I am on these social platforms:
Twitter: https://twitter.com/redouaneoachour