Changelog
All notable changes to AuthFort are documented here. The format is based on Keep a Changelog.
v0.0.28
Section titled “v0.0.28”authClient.getUser()now updates_stateto'authenticated'— previously, onlysignIn/initialize()and the verify methods drove the state machine;getUser()set_userdirectly and left_statestuck at the constructor default'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 treated it as a logout. A successful/meis now positive proof of authentication and is reflected in state.
Upgrade note
Section titled “Upgrade note”No breaking changes. Apps using the React/Vue/Svelte adapters with no pre-mount auth calls were unaffected. Apps using route guards that called getUser() before the adapter’s effect fired benefit from this fix; any local authClient.initialize().catch(() => {}) workaround in your app entry can now be removed.
v0.0.27
Section titled “v0.0.27”AuthFort(...)now acceptsmfa_issuerandmfa_backup_code_count— both fields existed onAuthFortConfigsince v0.0.22 but were never plumbed through the constructor, so they couldn’t be set by SDK users.mfa_issuersilently fell back tojwt_issuerfor TOTP enrollment regardless of what callers passed.AUTHFORT_TABLESregistry now includes the MFA and password-history tables —authfort_user_mfa,authfort_mfa_backup_codes(added v0.0.22), andauthfort_password_history(added v0.0.25) were missing from the registry. Apps usingregister_foreign_tables/alembic_filtersfor table-prefix isolation could have these tables incorrectly filtered out of AuthFort’s migration scope.
Upgrade note
Section titled “Upgrade note”No breaking changes. Apps that previously hit a TypeError trying to pass mfa_issuer will now work as the docs describe. Apps using alembic_filters should re-run alembic revision --autogenerate to confirm no spurious drops are detected for the previously-missing tables.
v0.0.26
Section titled “v0.0.26”- One-line FastAPI integration —
auth.install_fastapi(app, prefix="/auth")mounts both routers and registers a globalAuthErrorexception handler so errors always surface as clean 4xx with a structured body, never leaking through to the downstream app’s 500 handler. authfort.integrations.fastapi.authfort_exception_handlerexported for manual wiring viaapp.add_exception_handler(...).
email_deliverability_check=Truenow gates magic-link, OTP, login, and forgot-password — not just signup.k@k.ksubmitted to/auth/magic-linkwithallow_passwordless_signup=Truepreviously slipped through; now correctly rejected with 400invalid_email./auth/magic-linkand/auth/otpendpoints now return 400 on invalid email input instead of letting the exception escape as 500.
Upgrade note
Section titled “Upgrade note”Existing app.include_router(auth.fastapi_router(), prefix="/auth") keeps working. Recommended: switch to auth.install_fastapi(app, prefix="/auth") for automatic error handling.
v0.0.25
Section titled “v0.0.25”- HIBP breach check — password-setting endpoints reject passwords found in the Have I Been Pwned corpus via k-anonymity. Enabled by default; fail-open so HIBP outages don’t block signups. Disable with
check_pwned_passwords=Falseif you need to. - Refresh token cross-check — cookie-mode
/auth/refreshnow verifies the access token’ssubandsidclaims match the stored refresh token. Defends against cookie-swap attempts. Returns 401refresh_token_mismatchand revokes the refresh token on failure. - Password history (opt-in) — set
password_history_count=Nto prevent reuse of the last N passwords. Common values: 4 (PCI-DSS), 12 (SOC 2), 24 (FedRAMP). Adds theauthfort_password_historytable. - Optional email deliverability check — set
email_deliverability_check=Trueto require MX records at signup. Default off; the canonical deliverability gate remains email verification. - No-op password change rejected — setting a new password equal to the current one returns 400
password_unchanged. - Defensive email validation —
validate_user_emailnow returns 400 on any malformed input (previously could surface 500 on pathological inputs).
Events
Section titled “Events”PasswordPwnedRejected(email stored as SHA-256 hash)RefreshTokenMismatchPasswordReuseRejected
Migration
Section titled “Migration”- New Alembic migration
004_add_password_history.py— runalembic upgrade head. Empty table, no data migration.
Client SDK
Section titled “Client SDK”- Distinguishable
console.warnwhen/refreshfails withrefresh_token_mismatch, so apps can tell cookie-swap defense triggers apart from ordinary session expiry. No behavior change — still clears auth.
Upgrade notes
Section titled “Upgrade notes”HIBP check is on by default. If your app flows rely on specific weak passwords (e.g., test data, demo accounts), disable via check_pwned_passwords=False before upgrading.
v0.0.24
Section titled “v0.0.24”- client: TypeScript build error —
signInWithProvidermethod signature now matches theAuthClientinterface
v0.0.23
Section titled “v0.0.23”- MFA enforced on OAuth login — if a user has TOTP MFA enabled, logging in via Google, GitHub, or any OAuth provider now triggers the same MFA challenge as password login
- Client
initialize()detects?mfa_token=in the URL after an OAuth redirect and transitions tomfa_pendingautomatically — no extra code needed in your app - Client Popup OAuth flow (
signInWithProviderwithmode: 'popup') now resolves with{ status: 'mfa_required' }when the account has MFA enabled
Breaking Changes
Section titled “Breaking Changes”- Client
signInWithProviderpopup mode return type changed fromPromise<AuthUser>toPromise<SignInResult>— update any popup mode callers to checkresult.status
v0.0.22
Section titled “v0.0.22”- TOTP MFA — Google Authenticator, Authy, and any RFC 6238-compatible app
POST /auth/mfa/init— generate secret + QR URIPOST /auth/mfa/confirm— enable MFA, receive backup codesPOST /auth/mfa/verify— complete two-step login with TOTP or backup codePOST /auth/mfa/disable— disable MFA (requires current TOTP or backup code)POST /auth/mfa/backup-codes/regenerate— new set of backup codesGET /auth/mfa/status— current MFA status and remaining backup code count
mfa_enabledJWT claim — all access tokens carry this flag; downstream services can check posture with zero latency- MFA challenge token —
POST /loginreturns{ mfa_required: true, mfa_token }when MFA is enabled; submit to/mfa/verifyto receive full tokens - Replay protection — same TOTP code blocked within the same 30s window
- Admin
admin_disable_mfa(user_id)— forcibly disable MFA on any account - Config
mfa_issuer— authenticator app display name (defaults tojwt_issuer) - Config
mfa_backup_code_count— number of backup codes per user (default 10) - Service
TokenPayload.mfa_enabled— parsed from themfa_enabledJWT claim - Client
signIn()now returnsSignInResult—{ status: 'authenticated', user }or{ status: 'mfa_required' } - Client
verifyMFA(code)— complete a pending MFA login - Client
isMFAPendingin React hook, Vue composable, and Svelte store - Client
AuthUser.mfaEnabled— account-level MFA status - Events
MFAEnabled,MFADisabled,MFALogin,MFAFailed,BackupCodeUsed,BackupCodesRegenerated
Breaking Changes
Section titled “Breaking Changes”- Client
signIn()return type changed fromPromise<AuthUser>toPromise<SignInResult>— update callers to checkresult.status
Dependencies
Section titled “Dependencies”- server:
pyotp >= 2.9.0— runuv add pyotpinserver/
v0.0.21
Section titled “v0.0.21”- Default
pool_recyclelowered from 3600s to 300s — preventsConnectionDoesNotExistErrorbehind PgBouncer
- New
pool_recycleconfig option onAuthFort()— tune connection recycling interval (default 300s)
v0.0.20
Section titled “v0.0.20”- Input validation and sanitization for all user-facing fields (VAPT fix)
- Email validation using
email-validator— rejects SQL injection, XSS, header injection, XXE payloads - Name and phone sanitization using
nh3— strips all HTML tags (prevents stored XSS) - Avatar URL validation — only
http://andhttps://URLs accepted - Minimum password length enforcement (
min_password_lengthconfig, default 8) - New dependencies:
email-validator(>=2.3.0),nh3(>=0.3.4)
v0.0.19
Section titled “v0.0.19”set_password(user_id, new_password)— passwordless users (magic link, OTP, OAuth) can set an initial passwordPOST /auth/set-passwordREST endpoint (authenticated)PasswordSetevent fired when a passwordless user sets their initial passwordcreate_password_reset_token()now works for all users — passwordless/OAuth users can use forgot-password to set a password
- Passwordless users no longer get misleading “social login” error — new
no_passworderror code guides them correctly change_password()distinguishes OAuth (oauth_account) from passwordless (no_password) users- Banned check in
login()moved after password verification (security: prevents banned-account probing)
v0.0.18
Section titled “v0.0.18”create_user(email_verified=True)— mark email as verified at creation time (admin-created accounts)update_user(user_id, email_verified=True)— admin can manually verify or unverify a user’s emailEmailVerifiedevent fires automatically on verification viacreate_user()orupdate_user()(no duplicate if already verified)
v0.0.17
Section titled “v0.0.17”trust_proxyconfig — trustX-Forwarded-For/X-Real-IPfrom any sourcetrusted_proxiesconfig — only trust proxy headers from listed IPs/CIDRs (recommended for production)- Centralized IP extraction across all auth and OAuth endpoints
- Stable
session_idacross refresh token rotation — JWTsidclaim no longer changes on refresh - Migration
002_add_session_id— addssession_idcolumn (runauthfort migrateto apply) get_sessions()deduplicates bysession_id— one entry per logical sessionrevoke_session()andrevoke_all_sessions(exclude=...)operate on stablesession_id
- client: Cookie-mode refresh deduplication — concurrent 401s share a single
/refreshcall
v0.0.16
Section titled “v0.0.16”change_password()returns 400 (not 401) for wrong old password — prevents client SDK 401 retry loop- Login on OAuth-only account returns 400 (not 401) with
oauth_accountcode — wrong auth method is a bad request, not an auth failure
v0.0.15
Section titled “v0.0.15”authfort migrateCLI command — run migrations without a bootstrap script (uvx authfort migrate --database-url "...")register_foreign_tables(metadata)— register AuthFort table stubs for FK resolution in consumer modelsalembic_filters()— returns bothinclude_nameandinclude_objectfilters forcontext.configure(**alembic_filters())
Removed
Section titled “Removed”alembic_exclude()— replaced byalembic_filters()
v0.0.14
Section titled “v0.0.14”- Boolean column defaults in migration use
falseinstead of0— fixes PostgreSQL table creation failure
v0.0.13
Section titled “v0.0.13”AuthUserandAuthUserRoleexports — SQLAlchemy models for ORM JOINs against consumer tables
v0.0.12
Section titled “v0.0.12”bannedfield onUserResponse— visible in all user responses (login, signup, get_user, list_users, current_user)
v0.0.11
Section titled “v0.0.11”RateLimitConfig— per-endpoint rate limits ("5/min"format) with in-memory sliding window- IP-based rate limiting on all 8 auth endpoints
- Email-based rate limiting on login, signup, magic-link, otp, otp/verify (catches distributed attacks)
- 429 +
Retry-Afterheader on rate limit exceeded RateLimitExceededevent with endpoint, IP, email, limit, key_typeRateLimitStoreprotocol — pluggable for Redis or other backendsauth.list_users()— paginated listing with query/banned/role filters, sort_by/sort_orderauth.get_user(user_id)— single user lookup with rolesauth.delete_user(user_id)— cascade delete (roles → tokens → accounts → verification tokens → user)auth.get_user_count()— count with same filtersListUsersResponseschema,UserDeletedeventondelete="CASCADE"on all user foreign keysRateLimitConfig,RateLimitExceeded,ListUsersResponse,UserDeletedexported from top-level
v0.0.10
Section titled “v0.0.10”- Email verification flow —
create_email_verification_token()/verify_email() - Magic link passwordless login —
create_magic_link_token()/verify_magic_link() - Email OTP passwordless login —
create_email_otp()/verify_email_otp() GenericOAuthProvider— connect any OAuth 2.0 provider with custom endpointsGenericOIDCProvider— connect any OIDC provider via discovery URLallow_passwordless_signupconfig — auto-create users for unknown emails via magic link/OTPemail_verify_ttl,magic_link_ttl,email_otp_ttlconfig params- 5 new endpoints:
/magic-link,/magic-link/verify,/otp,/otp/verify,/verify-email - 6 new events:
email_verification_requested,email_verified,magic_link_requested,magic_link_login,email_otp_requested,email_otp_login - client:
requestMagicLink(),verifyMagicLink(),requestOTP(),verifyOTP(),verifyEmail()methods - client:
OAuthProvidertype accepts any string for generic providers
v0.0.9
Section titled “v0.0.9”auth.has_role(user_id, role)— single-role convenience checkauth.get_jwks()— JWKS dict for non-FastAPI frameworksauth.cleanup_expired_sessions()— delete expired/revoked sessionsauth.update_user(user_id, *, name, avatar_url, phone)— update profile fieldsauth.get_provider_tokens(user_id, provider)— retrieve stored OAuth tokensphonefield on User model,create_user(), and/auth/signuprsa_key_sizeconfig — configurable RSA key size (default 2048)frontend_urlconfig — cross-origin OAuth redirect support- OAuth
redirect_toquery param — redirect after callback - OAuth
mode=popup— popup flow withpostMessage - OAuth
extra_scopes— request additional provider API scopes - OAuth provider token storage — saves
access_tokenandrefresh_tokenfrom providers UserUpdatedevent — fired on profile updateAuthTokensadded to public exports- All 16 event classes exported from top-level
- client:
OAuthProvidertype,OAuthSignInOptions, popup mode,avatarUrl/phoneinsignUp() - client: Auto-initialize in React, Vue, and Svelte integrations
Changed
Section titled “Changed”- OAuth providers use
extra_scopesinstead ofscopes— required scopes always included jwt_algorithmremoved — RS256 is hardcoded (usersa_key_sizefor key strength)
- OAuth provider
refresh_tokenwas never saved — now stored on callback
v0.0.8
Section titled “v0.0.8”Breaking
Section titled “Breaking”- server: Replaced
sqlmodeldependency withsqlalchemy[asyncio]>=2.0 - server: Bundled migrations reset to single
001_initial_schema.py— existing dev databases need a freshauth.migrate()(drop old DB first)
Changed
Section titled “Changed”- All models use SQLAlchemy
DeclarativeBase+mapped_column()instead of SQLModel - All repositories use
session.execute().scalars()instead ofsession.exec() models/__init__.pyexportsBasefor Alembic and test usage
Removed
Section titled “Removed”sqlmodeldependency- Bundled migration
002_composite_index.py(merged into 001)
- Eliminated 85 false SQLModel deprecation warnings in pytest
v0.0.7
Section titled “v0.0.7”- OAuth ban check — banned users can no longer login via OAuth
- OAuth email normalization — provider emails are lowercased before lookup
- OAuth concurrent signup —
IntegrityErroron duplicate email is caught gracefully - Atomic
bump_token_version(),ban_user(),revoke_all_user_refresh_tokens(), signing key deactivation - Introspection checks session validity via
sidclaim - Password change and reset now revoke all refresh tokens
- OAuth
login_failedevents fired on all error paths auth.cleanup_expired_tokens()for verification token cleanup- Migration
002_composite_indexfor query performance get_refresh_token_by_id()repository function
v0.0.6
Section titled “v0.0.6”Breaking
Section titled “Breaking”- All database tables renamed with
authfort_prefix (requires fresh DB) SQLModel.metadata.create_all()replaced byawait auth.migrate()
auth.migrate()— bundled Alembic migrationsalembic_exclude()— filter AuthFort tables from your Alembic autogenerateCookieConfig(domain=".example.com")— subdomain cookie sharingServiceAuth(cookie_name="access_token")— cookie fallback
v0.0.5
Section titled “v0.0.5”RefreshRequest.refresh_tokennow optional — fixes 422 when client sends empty body with cookie
Changed
Section titled “Changed”- client: Bearer mode sends refresh token in request body
- client: Bearer mode
fetch()no longer sendscredentials: 'include'
- client:
TokenStorageinterface — pluggable storage for bearer mode
v0.0.4
Section titled “v0.0.4”create_password_reset_token(email)— generate reset tokenreset_password(token, new_password)— one-time use resetchange_password(user_id, old_password, new_password)— authenticated changerevoke_all_sessions(user_id, *, exclude=session_id)— keep current sessionsession_idonUserResponse(fromsidJWT claim)password_reset_ttlconfig param- 3 new events:
password_reset_requested,password_reset,password_changed
v0.0.3
Section titled “v0.0.3”- ESM imports in client SDK (added
.jsextensions) - JWKS rate limiting (
_last_fetch_attemptinitialized to-inf) dependenciesplacement inpyproject.toml
- Exported
UserResponseandAuthResponsefrom top-level "files": ["dist"]in clientpackage.json- CI/CD pipeline (
ci.yml+release.yml)
v0.0.1
Section titled “v0.0.1”Initial release with core authentication, JWT RS256, refresh token rotation, OAuth 2.1 + PKCE (Google, GitHub), RBAC, session management, ban/unban, 15 event types, JWKS, token introspection, multi-database support, FastAPI integration, microservice verifier, and TypeScript client SDK with React/Vue/Svelte integrations.