Complete authentication and authorization system for Python applications. Drop-in auth for FastAPI — JWT, OAuth, roles, sessions, and a TypeScript client SDK.
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 |
- 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_roledependency - 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
excludefor "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.jsonwith 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 Typed —
py.typedmarker for both Python packages, TypeScript client with full type definitions
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) |
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}")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}npm install authfort-clientimport { 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');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}┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ 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 | Install | Status |
|---|---|---|
| PostgreSQL | pip install authfort (asyncpg included) |
Primary, recommended |
| SQLite | pip install authfort[sqlite] |
Full support |
| MySQL | pip install authfort[mysql] |
Full support |
- Python: 3.11+
- Node.js: 18+ (for client SDK)
- Database: PostgreSQL, SQLite, or MySQL
- ORM: SQLAlchemy (included)
# 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# 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# Server
cd server
uv run vulture src/ vulture_whitelist.py
# Service
cd service
uv run vulture src/ vulture_whitelist.py
# Client
cd client
npx knipContributions are welcome! See CONTRIBUTING.md for the full guide.
- Fork the repository
- Create a feature branch (
feat/my-feature) - Run tests (see Development above)
- Submit a pull request
- Bug reports — Open an issue
- Feature requests — Open an issue
See CHANGELOG.md for the full version history.
- 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_lockedafter N failed verifications, DB-backed so it holds across workers, configurable viamfa_max_failed_attempts/mfa_lockout_seconds, newmfa_lockedevent); the MFA challenge now survives key rotation (kid lookup); enabling/disabling MFA bumpstoken_versionso themfa_enabledclaim can't go stale; codes are whitespace-tolerant ("123 456" works); and the OAuth MFA redirect carriesmfa_tokenin the URL fragment instead of the query string (out of logs/Referer). Runalembic upgrade head.
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 passrate_limit_store=RedisRateLimitStore.from_url("redis://...")(installauthfort[redis]). Fails closed on Redis outage. Custom stores can now be injected via the newrate_limit_storekwarg (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.
delete_user()now anonymizes + soft-deletes by default — keeps theauthfort_usersrow and itsid(so external tables that FKauthfort_users.idstay valid) while scrubbing PII, killing credentials/MFA, revoking all sessions, freeing the email for re-signup, and flaggingis_deleted. The "erase the person, not the row" pattern. Passdelete_user(id, hard=True)for the legacy full row delete. Addsis_deleted/deleted_atcolumns (runalembic upgrade head) and adeleted=Truetoggle onlist_users()/get_user_count()/get_user(). Deleted accounts are rejected across every auth path; thebannedflag is unchanged.
authClient.getUser()now updates auth state — previously, a successful/meset_userbut left_statestuck at'unauthenticated'. Apps usinggetUser()as a route guard (TanStack RouterbeforeLoad, etc.) and subscribing toonAuthStateChangefor streaming-feature lifecycle silently broke after page refresh: listeners saw a stale'unauthenticated'and aborted the stream. A successful/menow correctly transitions state to'authenticated'.
AuthFort(...)now acceptsmfa_issuerandmfa_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 tojwt_issuer.AUTHFORT_TABLESregistry includes MFA + password-history tables —authfort_user_mfa,authfort_mfa_backup_codes, andauthfort_password_historywere missing from the helper registry, causing them to be filtered out for apps usingalembic_filters/register_foreign_tablesfor table-prefix isolation.
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
- 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/refreshverifies the access token'ssubandsidmatch the stored refresh token, closing a cookie-swap attack surface - Password history (opt-in) —
password_history_count=Nprevents reuse of the last N passwords (PCI-DSS, SOC 2, FedRAMP compliance) - Optional email deliverability check —
email_deliverability_check=Trueto require MX records at signup - Reject no-op password change (new == current); defensive 500→400 fix on malformed email input
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