OAuth2/OIDC authentication and authorization for FastAPI APIs
Project description
axioms-fastapi

OAuth2/OIDC authentication and authorization for FastAPI APIs. Supports authentication and claim-based fine-grained authorization (scopes, roles, permissions) using JWT tokens. Works with access tokens issued by various authorization servers including AWS Cognito, Auth0, Okta, Microsoft Entra, etc.
Using Flask or Django REST Framework? This package is specifically for FastAPI. For Flask applications, use axioms-flask-py. For DRF applications, use axioms-drf-py.
When to use axioms-fastapi?
Use axioms-fastapi in your Django REST Framework backend to securely validate JWT access tokens issued by OAuth2/OIDC authorization servers like AWS Cognito, Auth0, Okta, Microsoft Entra, Keyclock etc. Clients - such as single-page applications (React, Vue), mobile apps, or AI agents—obtain access tokens from the authorization server and send them to your backend. In response, axioms-fastapi fetches JSON Web Key Set (JWKS) from the issuer, validates token signatures, enforces audience/issuer claims, and provides scope, role, and permission-based authorization for your API endpoints.
How it is different?
Unlike other DRF plugins, axioms-fastapi focuses exclusively on protecting resource servers, by letting authorization servers do what they do best. This separation of concerns raises the security bar by:
- Delegates authorization to battle-tested OAuth2/OIDC providers
- Works seamlessly with any OAuth2/OIDC ID with simple configuration
- Enterprise-ready defaults using current JWT and OAuth 2.1 best practices
Features
- JWT token validation with automatic public key retrieval from JWKS endpoints
- Algorithm validation to prevent algorithm confusion attacks (only secure asymmetric algorithms allowed)
- Issuer validation (
issclaim) to prevent token substitution attacks - Authentication classes for standard DRF integration
- Permission classes for claim-based authorization:
scopes,roles, andpermissions - Object-level permission classes for resource ownership verification
- Support for both OR and AND logic in authorization checks
- Middleware for automatic token extraction and validation
- Flexible configuration with support for custom JWKS and issuer URLs
- Simple integration with Django REST Framework Resource Server or API backends
- Support for custom claim and/or namespaced claims names to support different authorization servers
Installation
pip install axioms-fastapi
Quick Start
1. Configure your FastAPI application
from fastapi import FastAPI, Depends
from axioms_fastapi import init_axioms, require_auth, require_scopes, register_axioms_exception_handler
app = FastAPI()
# Initialize Axioms with your configuration
init_axioms(
app,
AXIOMS_AUDIENCE="your-api-audience",
AXIOMS_ISS_URL="https://your-auth.domain.com",
AXIOMS_JWKS_URL="https://your-auth.domain.com/.well-known/jwks.json"
)
# Register exception handler for authentication/authorization errors
register_axioms_exception_handler(app)
2. Protect your routes
from axioms_fastapi import require_auth, require_permissions
@app.get("/api/protected")
async def protected_route(payload=Depends(require_auth)):
"""Route protected by JWT authentication."""
user_id = payload.sub
return {"user_id": user_id, "message": "Authenticated"}
@app.get("/api/admin")
async def admin_route(
payload=Depends(require_auth),
_=Depends(require_permissions(["admin:write"]))
):
"""Route requiring admin:write permission."""
return {"message": "Admin access granted"}
Configuration
The SDK supports the following configuration options:
AXIOMS_AUDIENCE(required): Your resource identifier or API audienceAXIOMS_ISS_URL(recommended): Full issuer URL for validating theissclaimAXIOMS_JWKS_URL(optional): Full URL to your JWKS endpoint - if not provided, constructed fromAXIOMS_ISS_URLAXIOMS_DOMAIN(deprecated): UseAXIOMS_ISS_URLinstead. If provided, constructs issuer and JWKS URLs
Configuration Hierarchy:
AXIOMS_JWKS_URL(if explicitly set) ORAXIOMS_ISS_URL+/.well-known/jwks.json(ifAXIOMS_ISS_URLis set) ORhttps://{AXIOMS_DOMAIN}+/.well-known/jwks.json(ifAXIOMS_DOMAINis set)
Environment Variables
Create a .env file:
AXIOMS_AUDIENCE=your-api-audience
AXIOMS_ISS_URL=https://your-auth.domain.com
# Optional - if JWKS endpoint is non-standard:
# AXIOMS_JWKS_URL=https://your-auth.domain.com/.well-known/jwks.json
# Deprecated - use AXIOMS_ISS_URL instead:
# AXIOMS_DOMAIN=your-auth.domain.com
Middleware (Optional)
You can use middleware to automatically extract and validate JWT tokens for all incoming requests. The middleware sets attributes on request.state that you can access in your route handlers.
Adding Middleware
from fastapi import FastAPI, Request
from axioms_fastapi import init_axioms, register_axioms_exception_handler
from axioms_fastapi.middleware import AccessTokenMiddleware
app = FastAPI()
# Initialize Axioms configuration
init_axioms(
app,
AXIOMS_AUDIENCE="api.example.com",
AXIOMS_ISS_URL="https://auth.example.com"
)
# Add middleware to automatically process tokens
app.add_middleware(AccessTokenMiddleware)
# Register exception handler
register_axioms_exception_handler(app)
@app.get("/profile")
async def get_profile(request: Request):
# Access token payload from request.state.auth_jwt
if request.state.auth_jwt:
return {
"user_id": request.state.auth_jwt.sub,
"email": request.state.auth_jwt.get("email")
}
elif request.state.auth_jwt is False:
return {"error": "Invalid token"}, 401
else:
return {"error": "No token provided"}, 401
Request State Attributes
The middleware sets the following attributes on request.state:
auth_jwt(Box|False|None):- Box object with token payload if valid
Falseif token is invalid (expired, wrong audience, etc.)Noneif no Authorization header present
missing_auth_header(bool):Trueif Authorization header is missinginvalid_bearer_token(bool):Trueif Bearer format is invalid
Usage Examples
Basic Authentication
from fastapi import FastAPI, Depends
from axioms_fastapi import init_axioms, require_auth, require_scopes, register_axioms_exception_handler
app = FastAPI()
init_axioms(
app,
AXIOMS_AUDIENCE="api.example.com",
AXIOMS_ISS_URL="https://auth.example.com",
AXIOMS_JWKS_URL="https://auth.example.com/.well-known/jwks.json"
)
register_axioms_exception_handler(app)
@app.get("/profile")
async def get_profile(payload=Depends(require_auth)):
return {
"user_id": payload.sub,
"email": payload.get("email"),
"name": payload.get("name")
}
Safe Methods (Skip Authentication for Specific HTTP Methods)
By default, OPTIONS requests skip authentication to support CORS preflight requests. You can customize which HTTP methods skip authentication:
from functools import partial
from axioms_fastapi import require_auth
# Allow GET and OPTIONS without authentication
require_auth_safe = partial(require_auth, safe_methods=["GET", "OPTIONS"])
@app.get("/public-data")
async def public_data(payload=Depends(require_auth_safe)):
# GET requests don't require authentication
# payload will be an empty Box for safe methods
if not payload:
return {"data": "public content"}
return {"data": "personalized content", "user": payload.sub}
# Disable safe methods (require auth for all methods including OPTIONS)
require_auth_strict = partial(require_auth, safe_methods=[])
@app.options("/strict")
async def strict_options(payload=Depends(require_auth_strict)):
# Even OPTIONS requires authentication
return {"allowed_methods": ["GET", "POST"]}
Default behavior:
OPTIONSrequests skip authentication (for CORS preflight)- All other methods require authentication
Common use cases:
- CORS preflight:
safe_methods=["OPTIONS"](default) - Public read, authenticated write:
safe_methods=["GET", "HEAD", "OPTIONS"] - Strict mode:
safe_methods=[](all methods require auth)
Scope-Based Authorization (OR Logic)
from axioms_fastapi import require_auth, require_scopes
@app.get("/api/resource")
async def resource_route(
payload=Depends(require_auth),
_=Depends(require_scopes(["read:resource", "write:resource"]))
):
# User needs EITHER 'read:resource' OR 'write:resource' scope
return {"data": "success"}
Role-Based Authorization
from axioms_fastapi import require_auth, require_roles
@app.get("/admin/users")
async def admin_route(
payload=Depends(require_auth),
_=Depends(require_roles(["admin", "superuser"]))
):
# User needs EITHER 'admin' OR 'superuser' role
return {"users": []}
Permission-Based Authorization
from axioms_fastapi import require_auth, require_permissions
@app.post("/api/resource")
async def create_resource(
payload=Depends(require_auth),
_=Depends(require_permissions(["resource:create"]))
):
return {"message": "Resource created"}
AND Logic (Chaining Dependencies)
@app.get("/api/strict")
async def strict_route(
payload=Depends(require_auth),
_=Depends(require_scopes(["read:resource"])),
__=Depends(require_scopes(["write:resource"]))
):
# User needs BOTH 'read:resource' AND 'write:resource' scopes
return {"data": "requires both scopes"}
Mixed Authorization
@app.get("/api/advanced")
async def advanced_route(
payload=Depends(require_auth),
_=Depends(require_scopes(["openid", "profile"])), # openid OR profile
__=Depends(require_roles(["editor"])), # AND editor role
___=Depends(require_permissions(["resource:read", "resource:write"])) # AND read OR write
):
# User needs: (openid OR profile) AND (editor) AND (read OR write)
return {"data": "complex authorization"}
Object-Level Permissions (Row-Level Security)
Protect individual resources based on ownership using check_object_ownership:
from fastapi import FastAPI, Depends, HTTPException
from sqlmodel import Field, Session, SQLModel
from axioms_fastapi import init_axioms, check_object_ownership, register_axioms_exception_handler
app = FastAPI()
init_axioms(
app,
AXIOMS_AUDIENCE="api.example.com",
AXIOMS_ISS_URL="https://auth.example.com",
AXIOMS_JWKS_URL="https://auth.example.com/.well-known/jwks.json"
)
register_axioms_exception_handler(app)
# SQLModel with ownership field
class Article(SQLModel, table=True):
id: int = Field(primary_key=True)
title: str
content: str
user: str = Field(index=True) # Owner field - matches JWT 'sub' claim
def get_session():
# Your database session logic
pass
def get_article(article_id: int, session: Session = Depends(get_session)):
article = session.get(Article, article_id)
if not article:
raise HTTPException(status_code=404, detail="Article not found")
return article
# Only article owner can read their article
@app.get("/articles/{article_id}")
async def read_article(
article: Article = Depends(check_object_ownership(get_article))
):
# check_object_ownership verifies article.user == JWT 'sub' claim
return {"id": article.id, "title": article.title, "user": article.user}
# Only article owner can update their article
@app.patch("/articles/{article_id}")
async def update_article(
title: str,
article: Article = Depends(check_object_ownership(get_article)),
session: Session = Depends(get_session)
):
article.title = title
session.add(article)
session.commit()
return {"id": article.id, "title": article.title}
Custom Owner Field
Use a different field name for ownership:
class Comment(SQLModel, table=True):
id: int = Field(primary_key=True)
text: str
created_by: str = Field(index=True) # Custom owner field name
def get_comment(comment_id: int, session: Session = Depends(get_session)):
comment = session.get(Comment, comment_id)
if not comment:
raise HTTPException(status_code=404, detail="Comment not found")
return comment
@app.patch("/comments/{comment_id}")
async def update_comment(
text: str,
# Specify custom owner_field parameter
comment: Comment = Depends(check_object_ownership(get_comment, owner_field="created_by")),
session: Session = Depends(get_session)
):
comment.text = text
session.commit()
return {"id": comment.id, "text": comment.text}
Custom Claim Field
Match ownership using a different JWT claim (e.g., email):
class Project(SQLModel, table=True):
id: int = Field(primary_key=True)
name: str
owner_email: str = Field(index=True) # Matches JWT 'email' claim
def get_project(project_id: int, session: Session = Depends(get_session)):
project = session.get(Project, project_id)
if not project:
raise HTTPException(status_code=404, detail="Project not found")
return project
@app.get("/projects/{project_id}")
async def read_project(
# Match project.owner_email with JWT 'email' claim
project: Project = Depends(
check_object_ownership(
get_project,
owner_field="owner_email",
claim_field="email"
)
)
):
return {"id": project.id, "name": project.name, "owner_email": project.owner_email}
Custom Claim Names
Support for different authorization servers with custom claim names:
init_axioms(
app,
AXIOMS_AUDIENCE="api.example.com",
AXIOMS_ISS_URL="https://auth.example.com",
AXIOMS_JWKS_URL="https://auth.example.com/.well-known/jwks.json",
AXIOMS_ROLES_CLAIMS=["cognito:groups", "roles"],
AXIOMS_PERMISSIONS_CLAIMS=["permissions", "cognito:roles"],
AXIOMS_SCOPE_CLAIMS=["scope", "scp"]
)
Error Handling
The SDK raises AxiomsHTTPException for authentication and authorization errors. Register the exception handler to return proper error responses with WWW-Authenticate headers:
from fastapi import FastAPI
from axioms_fastapi import init_axioms, register_axioms_exception_handler
app = FastAPI()
init_axioms(
app,
AXIOMS_AUDIENCE="api.example.com",
AXIOMS_ISS_URL="https://auth.example.com",
AXIOMS_JWKS_URL="https://auth.example.com/.well-known/jwks.json"
)
# Register exception handler for Axioms errors
register_axioms_exception_handler(app)
This will automatically handle both authentication (401) and authorization (403) errors with proper WWW-Authenticate headers.
Complete Example
For a complete working example, check out the example folder in this repository or checkout our docs.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file axioms_fastapi-0.0.11rc31763183621.tar.gz.
File metadata
- Download URL: axioms_fastapi-0.0.11rc31763183621.tar.gz
- Upload date:
- Size: 75.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1cf5c88819a8f4ae78425cb938e62af7a5738df449b0692e832469823d2d00c3
|
|
| MD5 |
10ee9b94e4f3cb9661a01663236d3674
|
|
| BLAKE2b-256 |
4de223a0eecd57f709603fe7b617d3e4f57aa94651a2cc023a3ee73c3cbabc27
|
Provenance
The following attestation bundles were made for axioms_fastapi-0.0.11rc31763183621.tar.gz:
Publisher:
release.yml on abhishektiwari/axioms-fastapi
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
axioms_fastapi-0.0.11rc31763183621.tar.gz -
Subject digest:
1cf5c88819a8f4ae78425cb938e62af7a5738df449b0692e832469823d2d00c3 - Sigstore transparency entry: 701751720
- Sigstore integration time:
-
Permalink:
abhishektiwari/axioms-fastapi@344a25e3d9b461359985070726085578e9f8a972 -
Branch / Tag:
refs/pull/3/merge - Owner: https://github.com/abhishektiwari
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@344a25e3d9b461359985070726085578e9f8a972 -
Trigger Event:
pull_request
-
Statement type:
File details
Details for the file axioms_fastapi-0.0.11rc31763183621-py3-none-any.whl.
File metadata
- Download URL: axioms_fastapi-0.0.11rc31763183621-py3-none-any.whl
- Upload date:
- Size: 21.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6bb57590b86bb71fefd9f72fd26074eda0004af798d7b2c21c0851a1c9ec3bb4
|
|
| MD5 |
361ea4f9138101278a6f725be0e50c68
|
|
| BLAKE2b-256 |
a77f2c319de6c6e0a0f311c62c8ce7d314419cb108f51daeeb29891290351ded
|
Provenance
The following attestation bundles were made for axioms_fastapi-0.0.11rc31763183621-py3-none-any.whl:
Publisher:
release.yml on abhishektiwari/axioms-fastapi
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
axioms_fastapi-0.0.11rc31763183621-py3-none-any.whl -
Subject digest:
6bb57590b86bb71fefd9f72fd26074eda0004af798d7b2c21c0851a1c9ec3bb4 - Sigstore transparency entry: 701751722
- Sigstore integration time:
-
Permalink:
abhishektiwari/axioms-fastapi@344a25e3d9b461359985070726085578e9f8a972 -
Branch / Tag:
refs/pull/3/merge - Owner: https://github.com/abhishektiwari
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@344a25e3d9b461359985070726085578e9f8a972 -
Trigger Event:
pull_request
-
Statement type: