This guide walks you through creating a complete FastAPI application with login and protected routes using AuthX.
Complete example
Here’s a minimal but fully functional authentication system:
from fastapi import FastAPI, Depends, HTTPException
from pydantic import BaseModel
from authx import AuthX, AuthXConfig
app = FastAPI()
# Configure AuthX
config = AuthXConfig(
JWT_SECRET_KEY = "your-secret-key" , # Change this!
JWT_TOKEN_LOCATION = [ "headers" ],
)
auth = AuthX( config = config)
auth.handle_errors(app)
# User model
class User ( BaseModel ):
username: str
password: str
# Sample user database
USERS = {
"user1" : { "password" : "password1" , "email" : "user1@example.com" },
"user2" : { "password" : "password2" , "email" : "user2@example.com" },
}
@app.post ( "/login" )
def login ( user : User):
if user.username in USERS and USERS [user.username][ "password" ] == user.password:
access_token = auth.create_access_token(user.username)
return { "access_token" : access_token, "token_type" : "bearer" }
raise HTTPException( status_code = 401 , detail = "Invalid credentials" )
@app.get ( "/protected" , dependencies = [Depends(auth.access_token_required)])
def protected ():
return { "message" : "You have access to this protected resource" }
Never hardcode secrets in production! Use environment variables to store your JWT_SECRET_KEY.
Step-by-step guide
Let’s build this authentication system step by step.
Create your FastAPI app
Start by creating a basic FastAPI application: from fastapi import FastAPI
app = FastAPI( title = "My Authenticated API" )
Configure AuthX
Create an AuthXConfig instance with your settings: from authx import AuthXConfig
config = AuthXConfig(
JWT_SECRET_KEY = "your-secret-key" , # Required for signing tokens
JWT_ALGORITHM = "HS256" , # Default algorithm
JWT_TOKEN_LOCATION = [ "headers" ], # Accept tokens in headers
)
In production, load secrets from environment variables: import os
config = AuthXConfig(
JWT_SECRET_KEY = os.getenv( "JWT_SECRET_KEY" ),
)
Initialize AuthX
Create an AuthX instance and register error handlers: from authx import AuthX
auth = AuthX( config = config)
auth.handle_errors(app)
The handle_errors() call ensures authentication errors return proper HTTP responses instead of 500 errors.
Create a login endpoint
Create an endpoint that validates credentials and returns a token: from fastapi import HTTPException
from pydantic import BaseModel
class User ( BaseModel ):
username: str
password: str
USERS = {
"user1" : { "password" : "password1" , "email" : "user1@example.com" },
}
@app.post ( "/login" )
def login ( user : User):
if user.username in USERS and USERS [user.username][ "password" ] == user.password:
access_token = auth.create_access_token(user.username)
return { "access_token" : access_token, "token_type" : "bearer" }
raise HTTPException( status_code = 401 , detail = "Invalid credentials" )
In a real application, use proper password hashing (bcrypt, argon2) and fetch users from a database.
Protect your routes
Use auth.access_token_required as a dependency to protect routes: from fastapi import Depends
@app.get ( "/protected" , dependencies = [Depends(auth.access_token_required)])
def protected ():
return { "message" : "You have access to this protected resource" }
What happens:
No token → 401 Missing JWT in request
Invalid token → 401 Invalid Token
Valid token → Route executes normally
Run the application
Save your code to main.py and run it:
uvicorn main:app --reload
Your API is now running at http://localhost:8000.
Test the API
Let’s test the authentication flow:
Try accessing the protected route (should fail)
curl http://localhost:8000/protected
Response: {
"detail" : "Missing JWT in request"
}
Login to get a token
curl -X POST "http://localhost:8000/login" \
-H "Content-Type: application/json" \
-d '{"username": "user1", "password": "password1"}'
Response: {
"access_token" : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." ,
"token_type" : "bearer"
}
Access the protected route with the token
curl http://localhost:8000/protected \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
Response: {
"message" : "You have access to this protected resource"
}
Get the current user
To access token information in your protected routes:
from fastapi import Depends
from authx import TokenPayload
@app.get ( "/me" )
def get_current_user ( payload : TokenPayload = Depends(auth.access_token_required)):
return {
"user_id" : payload.sub,
"email" : USERS .get(payload.sub, {}).get( "email" )
}
The TokenPayload contains:
sub - Subject (the user ID you passed to create_access_token)
exp - Expiration time
iat - Issued at time
type - Token type (“access” or “refresh”)
fresh - Whether the token is fresh
scopes - List of scopes (if provided)
Add refresh tokens
For longer sessions, use refresh tokens to issue new access tokens:
from fastapi import Request
@app.post ( "/login" )
def login ( user : User):
if user.username in USERS and USERS [user.username][ "password" ] == user.password:
access_token = auth.create_access_token(user.username)
refresh_token = auth.create_refresh_token(user.username)
return {
"access_token" : access_token,
"refresh_token" : refresh_token,
"token_type" : "bearer"
}
raise HTTPException( status_code = 401 , detail = "Invalid credentials" )
@app.post ( "/refresh" )
async def refresh ( request : Request):
refresh_token = await auth.get_refresh_token_from_request(request)
payload = auth.verify_token(refresh_token, verify_type = True )
# Create new access token
access_token = auth.create_access_token(payload.sub)
return { "access_token" : access_token, "token_type" : "bearer" }
Access tokens typically expire in 15 minutes, while refresh tokens can last for days or weeks.
Next steps
Now that you have basic authentication working, explore more features:
Token locations Learn how to accept tokens from cookies, query parameters, or JSON body
Scopes Implement fine-grained permissions with scope-based authorization
Token freshness Require re-authentication for sensitive operations
Configuration Explore all configuration options for customizing AuthX