Skip to main content

A production-grade, pluggable authentication library for FastAPI — JWT, OAuth2 (8 providers), MFA, RBAC, and full flow flexibility (email/SMS, link/OTP, multi-identifier login).

Project description

authwarden

Production-grade FastAPI authentication and authorization library. Wraps proven cryptographic libraries — never rolls its own.

Python FastAPI License: MIT PyPI version


Features

  • JWT authentication — access + refresh tokens, rotation, per-token revocation via jti
  • Password hashing — argon2 (default) or bcrypt via pwdlib, auto-rehash on login
  • Full auth flows — register, verify email, login, logout, forgot/reset/change password
  • MFA — TOTP setup/confirm/disable (pyotp), hashed backup codes
  • OAuth 2.0 / Social login — Google, GitHub, Facebook, Apple, Twitter/X, Microsoft, LinkedIn, Discord
  • RBAC — role hierarchy, scope guards, Depends() factories
  • Session management — pluggable backends (in-memory, Redis)
  • Email — SMTP + console backends, HTML + plain-text templates for every flow
  • Plug-and-play router — mount all endpoints with one line
  • Storage-agnostic — implement AbstractUserStore for any ORM or database

Installation

pip install authwarden

With Redis session support:

pip install authwarden[redis]

Quickstart

from fastapi import FastAPI, Depends
from authwarden import AuthWarden, WardenConfig
from authwarden.storage.memory import MemoryUserStore

app = FastAPI()

warden = AuthWarden(
    config=WardenConfig(secret_key="your-secret-key"),
    user_store=MemoryUserStore(),
)

# Mount all auth endpoints under /auth
app.include_router(warden.router, prefix="/auth", tags=["auth"])

# Protect your own routes
@app.get("/profile")
async def profile(user=Depends(warden.current_user)):
    return user

@app.delete("/admin/users/{user_id}")
async def delete_user(user=Depends(warden.require_roles("admin"))):
    ...

This mounts:

POST /auth/register
POST /auth/verify-email
POST /auth/resend-verification
POST /auth/login
POST /auth/logout
POST /auth/refresh
POST /auth/forgot-password
POST /auth/reset-password
POST /auth/change-password
POST /auth/mfa/setup
POST /auth/mfa/confirm
POST /auth/mfa/disable
GET  /auth/oauth/{provider}/authorize
POST /auth/oauth/{provider}/callback
POST /auth/oauth/{provider}/connect
DEL  /auth/oauth/{provider}/disconnect
GET  /auth/oauth/accounts
POST /auth/set-password

Configuration

from authwarden import WardenConfig

config = WardenConfig(
    # Required
    secret_key="a-long-random-secret",

    # JWT
    algorithm="HS256",
    access_token_ttl=900,        # 15 minutes
    refresh_token_ttl=604800,    # 7 days
    enable_refresh_rotation=True,

    # Passwords
    password_hasher="argon2",    # or "bcrypt"
    min_password_length=8,
    require_password_uppercase=False,
    require_password_digit=False,
    require_password_special=False,

    # Email
    email_backend="smtp",        # or "console" for dev
    smtp_host="smtp.example.com",
    smtp_port=587,
    smtp_username="user@example.com",
    smtp_password="...",
    emails_from_address="noreply@example.com",

    # Registration
    require_email_verification=True,
    allow_registration=True,

    # MFA
    enable_mfa=True,
    mfa_issuer_name="MyApp",

    # Session (optional)
    session_backend="redis",
    redis_url="redis://localhost:6379",

    # Frontend URLs (for email links)
    frontend_base_url="https://myapp.com",
    verify_email_path="/auth/verify-email",
    reset_password_path="/auth/reset-password",
)

All settings can also be loaded from environment variables (via pydantic-settings):

SECRET_KEY=your-secret-key
EMAIL_BACKEND=smtp
SMTP_HOST=smtp.example.com

Custom User Store

Implement AbstractUserStore to connect any database:

from authwarden.storage.base import AbstractUserStore
from authwarden.models.user import UserInDB

class SQLAlchemyUserStore(AbstractUserStore):
    async def get_by_id(self, user_id: str) -> UserInDB | None:
        ...

    async def get_by_email(self, email: str) -> UserInDB | None:
        ...

    async def create(self, user: UserInDB) -> UserInDB:
        ...

    async def update(self, user: UserInDB) -> UserInDB:
        ...

    async def delete(self, user_id: str) -> None:
        ...

OAuth / Social Login

from authwarden.authentication.oauth import OAuthProviderConfig

warden = AuthWarden(
    config=WardenConfig(
        secret_key="...",
        oauth_providers={
            "google": OAuthProviderConfig(
                client_id="...",
                client_secret="...",
                redirect_uri="https://myapp.com/auth/oauth/google/callback",
            ),
            "github": OAuthProviderConfig(
                client_id="...",
                client_secret="...",
                redirect_uri="https://myapp.com/auth/oauth/github/callback",
            ),
        }
    ),
    user_store=MyUserStore(),
)

Apple Sign In requires additional fields:

config = WardenConfig(
    ...
    apple_team_id="TEAM123",
    apple_key_id="KEY123",
    apple_private_key_pem="-----BEGIN PRIVATE KEY-----\n...",
    oauth_providers={
        "apple": OAuthProviderConfig(
            client_id="com.myapp.service",
            client_secret="",   # auto-generated from private key
            redirect_uri="https://myapp.com/auth/oauth/apple/callback",
        )
    }
)

MFA (TOTP)

# Enable globally
config = WardenConfig(secret_key="...", enable_mfa=True)

# Endpoints are automatically mounted:
# POST /auth/mfa/setup    → returns { secret, qr_uri, backup_codes }
# POST /auth/mfa/confirm  → activates MFA after verifying first code
# POST /auth/mfa/disable  → requires password + TOTP or backup code

Login with MFA:

POST /auth/login
{
  "email": "user@example.com",
  "password": "hunter2",
  "totp_code": "123456"
}

RBAC

from fastapi import Depends

# Require a single role
@app.get("/admin")
async def admin(user=Depends(warden.require_roles("admin"))):
    ...

# Require any of multiple roles
@app.get("/reports")
async def reports(user=Depends(warden.require_roles("admin", "analyst"))):
    ...

# Require a scope
@app.post("/items")
async def create_item(user=Depends(warden.require_scopes("items:write"))):
    ...

Email Templates

Override any template by subclassing EmailTemplates:

from authwarden.email.templates import EmailTemplates

class MyTemplates(EmailTemplates):
    def verify_email(self, user, link: str) -> tuple[str, str, str]:
        # returns (subject, plain_text, html)
        return (
            "Verify your account",
            f"Click here: {link}",
            f"<a href='{link}'>Verify your account</a>",
        )

warden = AuthWarden(config=..., user_store=..., email_templates=MyTemplates())

Auth Flows Reference

Flow Endpoint Auth required
Register POST /auth/register No
Verify email POST /auth/verify-email No
Resend verification POST /auth/resend-verification No
Login POST /auth/login No
Logout POST /auth/logout Bearer token
Refresh token POST /auth/refresh No
Forgot password POST /auth/forgot-password No
Reset password POST /auth/reset-password No
Change password POST /auth/change-password Bearer token
MFA setup POST /auth/mfa/setup Bearer token
MFA confirm POST /auth/mfa/confirm Bearer token
MFA disable POST /auth/mfa/disable Bearer token
OAuth authorize GET /auth/oauth/{provider}/authorize No
OAuth callback POST /auth/oauth/{provider}/callback No
Connect provider POST /auth/oauth/{provider}/connect Bearer token
Disconnect provider DELETE /auth/oauth/{provider}/disconnect Bearer token
List linked accounts GET /auth/oauth/accounts Bearer token
Set password (OAuth) POST /auth/set-password Bearer token

Security Notes

  • Passwords hashed with argon2 by default (bcrypt available)
  • JWT tokens include a jti (UUID) claim — enables per-token revocation
  • Password reset and email verification tokens stored as hashes only
  • Forgot password and resend verification always return 200 (anti-enumeration)
  • Constant-time comparison used for all token lookups (hmac.compare_digest)
  • PKCE (S256) used for all OAuth flows
  • Refresh token rotation enabled by default — old jti blacklisted on use
  • MFA backup codes stored as argon2 hashes, single-use
  • OAuth provider tokens encrypted at rest (Fernet)

Development

git clone https://github.com/yourusername/authwarden.git
cd authwarden
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest

Running Tests

# All tests
pytest

# With coverage
pytest --cov=authwarden --cov-report=term-missing

# Specific suite
pytest tests/test_oauth.py -v

Project Structure

authwarden/
├── core/              # WardenConfig, AuthWarden facade, request context
├── authentication/    # JWT, password hashing, OAuth provider base
├── flows/             # One module per auth flow
├── mfa/               # TOTP + backup codes
├── permissions/       # Roles + scope guards
├── session/           # Memory + Redis session backends
├── dependencies/      # FastAPI Depends() factories
├── routers/           # Plug-and-play FastAPI routers
├── email/             # SMTP, console backends + templates
├── models/            # Pydantic v2 models
└── storage/           # AbstractUserStore + MemoryUserStore

License

MIT — see LICENSE.

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

authwarden-0.7.1.tar.gz (81.8 kB view details)

Uploaded Source

Built Distribution

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

authwarden-0.7.1-py3-none-any.whl (84.2 kB view details)

Uploaded Python 3

File details

Details for the file authwarden-0.7.1.tar.gz.

File metadata

  • Download URL: authwarden-0.7.1.tar.gz
  • Upload date:
  • Size: 81.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for authwarden-0.7.1.tar.gz
Algorithm Hash digest
SHA256 b9e3d28bffc54846c7d86098555f34e7fde12ee1c7ca93c168fe38a40f5c480e
MD5 bbb45a54ee13d173b6c6118434ce41bb
BLAKE2b-256 bb5ec2d7828bd222939fba97b9f28d82db38cbc0c1fa86d385c65448d4482c2b

See more details on using hashes here.

Provenance

The following attestation bundles were made for authwarden-0.7.1.tar.gz:

Publisher: publish.yml on timihack/authwarden

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

File details

Details for the file authwarden-0.7.1-py3-none-any.whl.

File metadata

  • Download URL: authwarden-0.7.1-py3-none-any.whl
  • Upload date:
  • Size: 84.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for authwarden-0.7.1-py3-none-any.whl
Algorithm Hash digest
SHA256 d3ac743eec2e81f7d99625138515f042969499caa1b8ad876db1811dcc8afe09
MD5 3ce8c58072bc6bb22a62fe021a71929f
BLAKE2b-256 716543d5959d223ad0452147a388e9e4375f56e45d4bec2605b477261ff9dcac

See more details on using hashes here.

Provenance

The following attestation bundles were made for authwarden-0.7.1-py3-none-any.whl:

Publisher: publish.yml on timihack/authwarden

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