Skip to main content

OAuth2/OIDC authentication and authorization for FastAPI APIs

Project description

axioms-fastapi PyPI Pepy Total Downloads

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.

GitHub Release GitHub Actions Test Workflow Status PyPI - Version Python Wheels Python Versions GitHub last commit PyPI - Status License PyPI Downloads CodeFactor codecov

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.

Where to use Axioms package

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 (iss claim) to prevent token substitution attacks
  • Authentication classes for standard DRF integration
  • Permission classes for claim-based authorization: scopes, roles, and permissions
  • 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 audience
  • AXIOMS_ISS_URL (recommended): Full issuer URL for validating the iss claim
  • AXIOMS_JWKS_URL (optional): Full URL to your JWKS endpoint - if not provided, constructed from AXIOMS_ISS_URL
  • AXIOMS_DOMAIN (deprecated): Use AXIOMS_ISS_URL instead. If provided, constructs issuer and JWKS URLs

Configuration Hierarchy:

  1. AXIOMS_JWKS_URL (if explicitly set) OR
  2. AXIOMS_ISS_URL + /.well-known/jwks.json (if AXIOMS_ISS_URL is set) OR
  3. https://{AXIOMS_DOMAIN} + /.well-known/jwks.json (if AXIOMS_DOMAIN is 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
    • False if token is invalid (expired, wrong audience, etc.)
    • None if no Authorization header present
  • missing_auth_header (bool): True if Authorization header is missing
  • invalid_bearer_token (bool): True if 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")
    }

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

axioms_fastapi-0.0.11rc31763181816.tar.gz (74.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

axioms_fastapi-0.0.11rc31763181816-py3-none-any.whl (21.1 kB view details)

Uploaded Python 3

File details

Details for the file axioms_fastapi-0.0.11rc31763181816.tar.gz.

File metadata

File hashes

Hashes for axioms_fastapi-0.0.11rc31763181816.tar.gz
Algorithm Hash digest
SHA256 633e56205a24aea684a49415a197406037919e532a25b903db0ce9d78e1cb95a
MD5 2ded1b1903eb6ddc43554e086633183a
BLAKE2b-256 a744823772d2e764d39590015757358534f7b39389e07f4ffd8061f70cbbbb93

See more details on using hashes here.

Provenance

The following attestation bundles were made for axioms_fastapi-0.0.11rc31763181816.tar.gz:

Publisher: release.yml on abhishektiwari/axioms-fastapi

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file axioms_fastapi-0.0.11rc31763181816-py3-none-any.whl.

File metadata

File hashes

Hashes for axioms_fastapi-0.0.11rc31763181816-py3-none-any.whl
Algorithm Hash digest
SHA256 339e78fc83413e95b1e10641f1e9637e8b13a3d9c3071cd199b5decdd8cbdb3c
MD5 88804802078bf19849d6085fe6576d7e
BLAKE2b-256 a4892b73f7d6caa5574fa3d92099a5b1d8c1ff02ba5379c668a521c871d65991

See more details on using hashes here.

Provenance

The following attestation bundles were made for axioms_fastapi-0.0.11rc31763181816-py3-none-any.whl:

Publisher: release.yml on abhishektiwari/axioms-fastapi

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page