Production-grade authentication framework for FastAPI. Async-first, provider-based, plugin-extensible, and secure by default.
- Async-first: Built on FastAPI, SQLAlchemy async, and async Redis. Every storage operation is
async. - Provider-based auth: Password, Generic OAuth, Generic OIDC, Magic Link, Email OTP, and custom providers behind a uniform
AuthProviderprotocol. - Plugin extensibility: 2FA, Organizations, API Keys, and more behind an
AllSafePluginprotocol with per-plugin routers, schemas, hooks, and table metadata. - Storage abstraction: Core services depend on
typing.Protocolinterfaces — the core never imports SQLAlchemy. Swap Postgres for memory or any backend without touching auth logic. - Fast path: JWT verification in
auth.user()decodes and validates the token in memory with no database and no Redis queries on the hot request path. - Secure by default:
- Argon2id password hashing (
time_cost=3,memory_cost=65536 KiB,parallelism=4). - Refresh-token rotation with reuse detection (token-theft defense).
- Tokens (refresh, verification, OTP, magic-link) stored only as SHA-256 hashes.
HttpOnly,Secure,SameSitecookies with safe defaults.- Account-enumeration protection on forgot-password.
- OAuth
stateparameter + PKCE enforced. - CSRF header validation for cookie-based auth.
- Argon2id password hashing (
pip install allsafe-fastfrom fastapi import FastAPI
from allsafe_fast import Auth
app = FastAPI()
auth = Auth() # reads ALLSAFE_* env vars
app.include_router(auth.router, prefix="/auth")
@app.get("/me")
async def me(user = auth.user()):
return {"id": str(user.id), "email": user.email}
@app.get("/admin")
async def admin(user = auth.user(role="admin")):
return {"ok": True}All configuration is read from environment variables prefixed with ALLSAFE_.
| Variable | Default | Description |
|---|---|---|
ALLSAFE_DATABASE_URL |
— | SQLAlchemy async URL, e.g. postgresql+asyncpg://user:pass@host/db |
ALLSAFE_REDIS_URL |
— | Redis URL, e.g. redis://localhost:6379/0 |
ALLSAFE_SECRET_KEY |
— | JWT signing key (min 32 chars; required in production) |
ALLSAFE_ACCESS_TOKEN_EXPIRE |
900 |
Access token lifetime in seconds (15 min) |
ALLSAFE_REFRESH_TOKEN_EXPIRE |
2592000 |
Refresh token lifetime in seconds (30 days) |
ALLSAFE_COOKIE_SECURE |
true |
Set Secure on auth cookies |
ALLSAFE_COOKIE_HTTP_ONLY |
true |
Set HttpOnly on auth cookies |
ALLSAFE_COOKIE_SAME_SITE |
lax |
Cookie SameSite policy |
ALLSAFE_RATE_LIMIT |
5 |
Requests per window per identifier |
ALLSAFE_RATE_LIMIT_WINDOW |
900 |
Rate-limit window in seconds |
ALLSAFE_ISSUER |
allsafe_fast |
JWT iss claim |
ALLSAFE_AUDIENCE |
application |
JWT aud claim |
ALLSAFE_ENV |
development |
development or production |
Copy .env.example to .env and fill in values, or run:
allsafe initsrc/allsafe_fast/
├── auth.py # Auth facade + user() dependency factory
├── config.py # AuthConfig + pydantic-settings
├── exceptions.py # Exception hierarchy
├── types.py # UserPrincipal, AuthResult, TokenPair, ...
├── core/ # IdentityService, AccountService, SessionManager,
│ # AuthenticationEngine, AuthorizationService,
│ # VerificationService, HookRegistry
├── models/ # Plain dataclasses: User, Account, Session, Verification
├── storage/ # Protocols + memory + sqlalchemy adapters
├── security/ # passwords, jwt, tokens, hashing, cookies, secrets
├── providers/ # password, oauth, oidc, magic_link, email_otp
├── plugins/ # plugin protocol + registry
├── api/ # router, dependencies, context, schemas
├── rate_limit/ # protocols + redis + memory limiters
├── redis/ # client, key builders, state manager
└── services/ # email service + hooks re-export
auth.user() returns a FastAPI dependency that:
- Reads the JWT from the
Authorization: Bearer …header (orallsafe_sessioncookie). - Verifies signature,
exp,iss,aud, andsubin memory. - Constructs a
UserPrincipaldirectly from the claims.
No database query. No Redis query. That is the hot path for authenticated requests.
Core services (core/*) depend only on UserStoreProtocol, AccountStoreProtocol, SessionStoreProtocol, and VerificationStoreProtocol from storage/protocols.py. The SQLAlchemy adapter implements them; an in-memory adapter is provided for tests. You can write your own adapter for any backend.
When a refresh token is used:
- The stored SHA-256 hash is looked up.
- If found and not revoked, the old session is revoked and a brand-new access/refresh pair is issued.
- If a revoked refresh token is presented again, this is treated as token theft — all of the user's sessions are revoked immediately.
AllSafe never auto-merges accounts on email match alone. A new OAuth identity with an email that matches an existing user creates a pending account that must be explicitly linked by the authenticated user, preventing account takeover via a rogue OAuth provider.
allsafe init # write .env with a generated secret
allsafe secret # print a fresh secret key
allsafe db generate # print SQL DDL for all tables
allsafe db migrate # run alembic migrations
allsafe doctor # check config, DB, and Redis connectivityMIT