Skip to content

Repository files navigation

AuthFort

AuthFort

Complete authentication and authorization system for Python applications. Drop-in auth for FastAPI — JWT, OAuth, roles, sessions, and a TypeScript client SDK.

PyPI PyPI npm CI codecov License: MIT Python TypeScript Docs


Packages

AuthFort consists of three packages that work together:

Package Language Install Description
authfort Python pip install authfort Full auth server — user management, JWT, OAuth, roles, sessions, JWKS
authfort-service Python pip install authfort-service Lightweight JWT verifier for microservices — JWKS + introspection
authfort-client TypeScript npm install authfort-client Client SDK — token lifecycle, refresh dedup, React/Vue/Svelte hooks

Features

  • Email/Password Auth — Signup, login, argon2 password hashing, email format validation
  • JWT RS256 — Stateless access tokens with automatic key management
  • Refresh Token Rotation — Secure rotation with theft detection
  • OAuth 2.1 + PKCE — Built-in providers + generic OAuth/OIDC for any provider
  • Email Verification — Token-based email confirmation flow
  • Magic Links — Passwordless login via emailed link
  • Email OTP — Passwordless login via 6-digit code
  • Role-Based Access Control — Add/remove roles, require_role dependency
  • Password Reset — Programmatic token generation (you control delivery — email, SMS, etc.)
  • Change Password — Old password verification, automatic token invalidation
  • Session Management — List, revoke individual, revoke all (with exclude for "sign out other devices")
  • Ban/Unban — Instant invalidation (bumps token version, revokes all tokens)
  • Rate Limiting — Per-endpoint IP + email based, in-memory sliding window, pluggable for Redis
  • Admin User Management — List, search, get, delete users with pagination and filtering
  • Event Hooks — 24 event types (user_created, login, user_deleted, rate_limit_exceeded, etc.)
  • JWKS Endpoint/.well-known/jwks.json with automatic key rotation
  • Token Introspection — RFC 7662 for microservice architectures
  • Multi-Database — PostgreSQL (primary), SQLite, MySQL via SQLAlchemy
  • Cookie & Bearer Modes — HttpOnly cookies or Authorization header
  • Client SDK — TypeScript with React, Vue, and Svelte integrations
  • Microservice Verifier — Lightweight JWT verification without database access
  • Fully Typedpy.typed marker for both Python packages, TypeScript client with full type definitions

Quick Start

Auth Server (FastAPI)

pip install authfort[fastapi]
from authfort import AuthFort, CookieConfig
from fastapi import FastAPI, Depends

auth = AuthFort(
    database_url="postgresql+asyncpg://user:pass@localhost/mydb",
    cookie=CookieConfig(),
)

app = FastAPI()
app.include_router(auth.fastapi_router(), prefix="/auth")
app.include_router(auth.jwks_router())

@app.get("/api/profile")
async def profile(user=Depends(auth.current_user)):
    return {"email": user.email, "roles": user.roles}

This gives you these endpoints out of the box:

Method Endpoint Description
POST /auth/signup Create a new user
POST /auth/login Authenticate and get tokens
POST /auth/refresh Refresh access token
POST /auth/logout Revoke refresh token
GET /auth/me Get current user info
POST /auth/magic-link Request magic link
POST /auth/magic-link/verify Verify magic link token
POST /auth/otp Request email OTP
POST /auth/otp/verify Verify email OTP
POST /auth/verify-email Verify email address
GET /auth/oauth/{provider}/authorize Start OAuth flow
GET /auth/oauth/{provider}/callback OAuth callback
GET /.well-known/jwks.json Public keys (JWKS)
POST /introspect Token introspection (RFC 7662)

Programmatic API

For operations beyond the REST endpoints:

# User management
user = await auth.create_user("admin@example.com", "password", name="Admin", email_verified=True)
await auth.add_role(user.id, "admin")
await auth.ban_user(user.id)

# Password reset (you handle delivery)
token = await auth.create_password_reset_token("user@example.com")
if token:
    await send_reset_email(email, token)
await auth.reset_password(token, "new_password")

# Change password (authenticated)
await auth.change_password(user.id, "old_password", "new_password")

# Session management
sessions = await auth.get_sessions(user.id, active_only=True)
await auth.revoke_session(session_id)
await auth.revoke_all_sessions(user.id, exclude=user.session_id)  # keep current

# Admin user management
users = await auth.list_users(query="john", role="admin", limit=20)
user = await auth.get_user(user_id)
await auth.delete_user(user_id)
count = await auth.get_user_count(banned=True)

# Event hooks
@auth.on("user_created")
async def on_signup(event):
    await send_welcome_email(event.email)

@auth.on("password_reset")
async def on_reset(event):
    log.info(f"Password reset for user {event.user_id}")

Microservice Verifier

For downstream services that need to verify JWTs without database access:

pip install authfort-service[fastapi]
from authfort_service import ServiceAuth

service = ServiceAuth(
    jwks_url="https://auth.example.com/.well-known/jwks.json",
    issuer="authfort",
)

@app.get("/api/data")
async def protected(user=Depends(service.current_user)):
    return {"user": user.sub, "roles": user.roles}

Client SDK

npm install authfort-client
import { createAuthClient } from 'authfort-client';

const auth = createAuthClient({
  baseUrl: '/auth',
  tokenMode: 'cookie',
});

await auth.initialize();
await auth.signUp({ email: 'user@example.com', password: 'secret' });
await auth.signIn({ email: 'user@example.com', password: 'secret' });

// auth.fetch() is a drop-in replacement for fetch — handles auth automatically
const res = await auth.fetch('/api/profile');

React

import { AuthProvider, useAuth } from 'authfort-client/react';

function App() {
  return (
    <AuthProvider client={auth}>
      <Profile />
    </AuthProvider>
  );
}

function Profile() {
  const { user, isAuthenticated, isLoading, client } = useAuth();

  if (isLoading) return <p>Loading...</p>;
  if (!isAuthenticated) return <p>Not signed in</p>;

  return <p>Hello {user.email}</p>;
}
Vue
import { provideAuth, useAuth } from 'authfort-client/vue';

// Root component
setup() {
  provideAuth(auth);
}

// Any child component
const { user, isAuthenticated } = useAuth();
Svelte
import { createAuthStore } from 'authfort-client/svelte';

const { state, user, isAuthenticated } = createAuthStore(auth);

// In template
{#if $isAuthenticated}
  Hello {$user.email}
{/if}

Architecture

┌──────────────┐     ┌──────────────┐     ┌──────────────────┐
│  Frontend    │     │  Auth Server │     │  Microservices   │
│  (React/Vue) │────▶│  (authfort)  │     │  (authfort-      │
│  authfort-   │     │              │◀────│   service)       │
│  client      │     │  PostgreSQL  │     │                  │
└──────────────┘     │  / SQLite    │     │  Verifies JWTs   │
                     │  / MySQL     │     │  via JWKS        │
                     └──────────────┘     └──────────────────┘
                            │
                            ├── /.well-known/jwks.json
                            ├── /auth/signup, /auth/login, ...
                            └── /introspect

Database Support

Database Install Status
PostgreSQL pip install authfort (asyncpg included) Primary, recommended
SQLite pip install authfort[sqlite] Full support
MySQL pip install authfort[mysql] Full support

Requirements

  • Python: 3.11+
  • Node.js: 18+ (for client SDK)
  • Database: PostgreSQL, SQLite, or MySQL
  • ORM: SQLAlchemy (included)

Development

Running Tests

# Server (491 tests)
cd server
uv sync --extra sqlite --extra fastapi
uv run pytest tests/ -v

# Service (28 tests)
cd service
uv sync --extra fastapi
uv run pytest tests/ -v

# Client (92 tests)
cd client
npm ci
npx vitest run

Coverage

# Server — generates terminal report + HTML
cd server
uv run pytest tests/ --cov --cov-report=term-missing --cov-report=html

# Service
cd service
uv run pytest tests/ --cov --cov-report=term-missing --cov-report=html

# Client
cd client
npx vitest run --coverage

Dead Code Detection

# Server
cd server
uv run vulture src/ vulture_whitelist.py

# Service
cd service
uv run vulture src/ vulture_whitelist.py

# Client
cd client
npx knip

Contributing

Contributions are welcome! See CONTRIBUTING.md for the full guide.

Quick Overview

  1. Fork the repository
  2. Create a feature branch (feat/my-feature)
  3. Run tests (see Development above)
  4. Submit a pull request

Reporting Issues

Changelog

See CHANGELOG.md for the full version history.

Latest — v0.0.31

  • TOTP MFA security hardening — closed a TOTP replay window (a used code could be replayed for ~30–60s; now rejected for its full 90s horizon); added MFA brute-force lockout (429 mfa_locked after N failed verifications, DB-backed so it holds across workers, configurable via mfa_max_failed_attempts/mfa_lockout_seconds, new mfa_locked event); the MFA challenge now survives key rotation (kid lookup); enabling/disabling MFA bumps token_version so the mfa_enabled claim can't go stale; codes are whitespace-tolerant ("123 456" works); and the OAuth MFA redirect carries mfa_token in the URL fragment instead of the query string (out of logs/Referer). Run alembic upgrade head.

v0.0.30

  • RedisRateLimitStore — Redis-backed rate limiting shared across workers/replicas. The default in-memory store is per-process, so N workers silently multiply every limit by N; multi-process deployments should pass rate_limit_store=RedisRateLimitStore.from_url("redis://...") (install authfort[redis]). Fails closed on Redis outage. Custom stores can now be injected via the new rate_limit_store kwarg (sync or async).
  • OAuth errors no longer leak provider details — code-exchange / user-info failures return a generic message; the underlying exception is logged server-side instead of echoed in the response.
  • CI hardening — server tests now also run against PostgreSQL 16 on every push/PR; all GitHub Actions bumped off the deprecated Node 20 runner.

v0.0.29

  • delete_user() now anonymizes + soft-deletes by default — keeps the authfort_users row and its id (so external tables that FK authfort_users.id stay valid) while scrubbing PII, killing credentials/MFA, revoking all sessions, freeing the email for re-signup, and flagging is_deleted. The "erase the person, not the row" pattern. Pass delete_user(id, hard=True) for the legacy full row delete. Adds is_deleted/deleted_at columns (run alembic upgrade head) and a deleted=True toggle on list_users() / get_user_count() / get_user(). Deleted accounts are rejected across every auth path; the banned flag is unchanged.

v0.0.28

  • authClient.getUser() now updates auth state — previously, a successful /me set _user but left _state stuck at 'unauthenticated'. Apps using getUser() as a route guard (TanStack Router beforeLoad, etc.) and subscribing to onAuthStateChange for streaming-feature lifecycle silently broke after page refresh: listeners saw a stale 'unauthenticated' and aborted the stream. A successful /me now correctly transitions state to 'authenticated'.

v0.0.27

  • AuthFort(...) now accepts mfa_issuer and mfa_backup_code_count — these config fields existed since v0.0.22 but were never wired through the constructor, so SDK users couldn't actually set them. TOTP enrollment now uses the issuer label you pass instead of silently falling back to jwt_issuer.
  • AUTHFORT_TABLES registry includes MFA + password-history tablesauthfort_user_mfa, authfort_mfa_backup_codes, and authfort_password_history were missing from the helper registry, causing them to be filtered out for apps using alembic_filters / register_foreign_tables for table-prefix isolation.

v0.0.26

  • auth.install_fastapi(app) — one-call FastAPI integration (mounts routers + registers AuthError exception handler), so validation errors always surface as clean 4xx instead of leaking through as 500
  • Deliverability check now applies to magic-link / OTP / login / forgot-password — previously was signup-only, meaning passwordless paths could still accept undeliverable emails

v0.0.25

  • HIBP password breach check — rejects passwords found in public breach corpora on signup / change / reset (k-anonymity, fail-open). On by default; disable with check_pwned_passwords=False.
  • Refresh token cross-check — cookie-mode /auth/refresh verifies the access token's sub and sid match the stored refresh token, closing a cookie-swap attack surface
  • Password history (opt-in) — password_history_count=N prevents reuse of the last N passwords (PCI-DSS, SOC 2, FedRAMP compliance)
  • Optional email deliverability checkemail_deliverability_check=True to require MX records at signup
  • Reject no-op password change (new == current); defensive 500→400 fix on malformed email input

License

This project is licensed under the MIT License.


If you found this useful, give it a

Documentation · Report Bug · Request Feature · Contributing Guide

Made by Bhagyajit Jagdev

About

Comprehensive authentication and authorization library for Python

Resources

Contributing

Security policy

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages