API reference¶
Manually maintained surface of the public modules. Anything not listed here is internal. Guide-level explanations live in Strategies and Guides; this page is signatures, types, and return values.
Entry points (fastauth, fastauth.core)¶
SessionAuth¶
SessionAuth(
adapter: type[Adapter],
db_session_dependency: Callable[[], AsyncGenerator[Any]],
user_model: type[UserT],
session_model: type[SessionT],
rate_limiter: RateLimiter | None = None,
tags: list[str | Enum] | None = None,
prefix: str = "/auth",
config: FastAuthConfig | None = None,
password_reset_token_model: type[PasswordResetTokenT] | None = None,
)
- Attributes:
.router: APIRouter,.current_user(session-cookie dependency),.config,.ctx: AuthContext,.password_hasher, built schemas (.signup_schema,.login_schema,.user_response_schema,.password_reset_token_schema). - Hook decorators:
.on_before_signup,.on_after_signup,.on_signup_failure,.on_before_login,.on_login_failure,.on_after_login,.on_after_logout,.on_token_reuse_detected(no-op holder on sessions),.on_password_reset_requested,.on_password_changed. strategyis the class attribute"session", not a constructor arg.
JWTAuth¶
JWTAuth(
adapter: type[Adapter],
db_session_dependency: Callable[[], AsyncGenerator[Any]],
user_model: type[UserT],
refresh_model: type[RefreshT],
rate_limiter: RateLimiter | None = None,
tags: list[str | Enum] | None = None,
prefix: str = "/auth",
config: FastAuthConfig | None = None, # must include jwt=JWTConfig(...)
password_reset_token_model: type[PasswordResetTokenT] | None = None,
)
Raises ValueError when config.jwt is missing. Same attributes/hooks as SessionAuth, plus .refresh_model, working on_token_reuse_detected, and:
await auth.purge_expired_refresh_tokens(session: Any) -> int— deletes expired refresh rows via the adapter (flushes; caller commits). Designed for a scheduler job.
OIDCAuth¶
OIDCAuth(
adapter: type[Adapter],
db_session_dependency: Callable[[], AsyncGenerator[Any]],
user_model: type[UserT],
oauth_account_model: type[OAuthAccountT],
strategy: Literal["session", "jwt"],
session_model: type[SessionT] | None = None, # required when strategy="session"
refresh_model: type[RefreshT] | None = None, # required when strategy="jwt"
tags: list[str | Enum] | None = None, # default: [provider names]
prefix: str = "/auth/oidc",
config: FastAuthConfig | None = None, # must include oidc=OIDCConfig(...)
rate_limiter: RateLimiter | None = None,
)
- Attributes:
.router,.current_user(session or JWT flavor perstrategy),.config,.ctx: OIDCContext,.providers: dict[str, OIDCProvider],.state_manager: OAuthStateManager. - Hooks:
.on_after_oidc_login(fn)only — none of the password-flow hooks exist here. - Raises
ValueErrorfor missingconfig.oidc/ missing strategy model,TypeErrorfor non-compliant models. SkipsFastAuth.__init__deliberately (see ARCHITECTURE).
OAuth2Auth¶
Same shape as OIDCAuth with prefix="/auth/oauth2", config.oauth2: OAuth2Config, .providers: dict[str, OAuth2Provider], .ctx: OAuth2Context, and .on_after_oauth2_login(fn).
FastAuth¶
Shared base: config, schemas, router, hook holders, per-request adapter binding. Registers no routes and sets no current_user — not usable directly; use the four classes above.
Config (fastauth.config)¶
All models frozen (ValidationError on mutation). Generate secrets with openssl rand -hex 32.
FastAuthConfig(session=SessionConfig(), cookies=CookieConfig(), password=PasswordConfig(), rate_limit=RateLimitConfig(), oidc: OIDCConfig | None = None, oauth2: OAuth2Config | None = None, jwt: JWTConfig | None = None)SessionConfig(expire_days: int = 7)—gt=0, le=30.CookieConfig(session_cookie_name="fastauth_session", refresh_cookie_name="fastauth_refresh", secure: bool = True, samesite: SameSite = "lax", path="/", domain: str | None = None)— names reject; , space = ";samesite="none"requiressecure=True.PasswordConfig(min_length=8, max_length=128, hash_schemes: list[str] | None = None)—min_length >= 1, lengths consistent;Nonekeeps argon2recommended(), e.g.["bcrypt"]to switch. Lengths flow into signup/login/reset schemas.JWTConfig(secret_key: str, algorithm: JWTAlgorithm = "HS256", access_token_expire_minutes=10, refresh_token_expire_days=7)— secret 32–512 chars, placeholders rejected (change-me,secret,password,test, …); algorithmHS256/HS384/HS512; access1–30min, refresh1–30days.RateLimitConfig(enabled=True, window=60, max_requests=100, storage: Literal["database","memory","redis"] = "memory", trusted_ip_header: str | None = None, custom_rules: dict[str, tuple[int,int]])— defaults:/login (10,3),/signup (60,3),/refresh (60,5),/forgot-password (300,3),/reset-password (300,3),/{provider}/login (60,10),/{provider}/callback (60,5).OIDCConfig(secret_key: str, providers: list[OIDCProviderConfig] = [])— frozen; same placeholder rejection as JWT.OIDCProviderConfig(name, client_id, client_secret, redirect_uri, metadata_url: str, scopes=["openid","email","profile"], extra_authorize_params: dict[str,str] = {}).OAuth2Config(secret_key: str, providers: list[OAuth2ProviderConfig] = [])— frozen, same secret rules.OAuth2ProviderConfig(name, client_id, client_secret, redirect_uri, authorization_url, token_url, userinfo_url: str, scopes: list[str] = [], map_profile_to_user: Callable[[dict], Awaitable[OAuthUserInfo]], extra_authorize_params: dict[str,str] = {}).
Adapters¶
Adapter ABC (fastauth.adapters.adapters)¶
Subclass for a custom ORM. Stores one request-scoped db_session plus app models; flush, never commit (routes commit; only the rate limiter commits inside check()).
# classmethods
get_extra_fields(model: type) -> dict[str, tuple[type, Any]] # fastauth_input columns
get_response_fields(model: type) -> dict[str, tuple[type, Any]] # fastauth_returned columns
# users / credentials
get_user_by_email(email: str) -> UserT | None
get_user_by_id(user_id: Any) -> UserT | None
create_user(data: dict[str, Any]) -> UserT # hash data["password"]
issue_credential(user: UserT) -> SessionT | str # row (session) or token (JWT)
resolve_credential(token: str) -> UserT | None
revoke_credential(token: str) -> str | None # returns user id
# guards (raise ValueError when the model wasn't provided)
require_password_reset_model() -> type[PasswordResetTokenT]
require_oauth_account_model() -> type[OAuthAccountT]
# OAuth identities (EmailAlreadyRegistered on email collision)
get_oidc_account(provider: str, provider_user_id: str) -> OAuthAccountT | None
create_user_from_oidc(user_info: OAuthUserInfo) -> UserT
get_oauth2_account(provider: str, provider_user_id: str) -> OAuthAccountT | None
create_user_from_oauth2(user_info: OAuthUserInfo) -> UserT
# refresh tokens (base defaults raise NotImplementedError)
issue_refresh_token(user: UserT) -> str
consume_refresh_token(token: str) -> UserT | None # single-use burn
revoke_refresh_token(token: str) -> str | None
purge_expired_refresh_tokens() -> int
# password reset
create_password_reset_token(user: UserT) -> str # returns raw token, stores hash
consume_password_reset_token(token: str) -> UserT | None # single-use, 15-min
set_password(user: UserT, new_password: str) -> None
revoke_credentials_on_password_reset(user: UserT) -> None
RateLimiterAdapter ABC: check(key: str, window: int, max_requests: int) -> bool — True if allowed (and counted).
SQLAlchemy (fastauth.adapters.sqlalchemy)¶
SQLAlchemySessionAdapter(db_session: AsyncSession, user_model, session_model, jwt_config=None, refresh_model=None, session_expire_days=7, password_hasher=None, password_reset_token_model=None, oauth_account_model=None)—issue_credentialreturns the session row;resolve_credentialrejects unknown/expired/inactive; session logoutrevoke_credentialdeletes the row; password reset deletes the user's sessions.SQLAlchemyJWTAdapter(...)— same constructor (session_model unused);issue_credential(user) -> str(signed access token),resolve_credentialvalidates type/exp/user,revoke_credentialis a no-op returning the user id for the hook; refresh rotation is single-use with family revocation on reuse (RefreshTokenReusedinternally),purge_expired_refresh_tokens() -> int.- Mixins (
models.py) — inherit plus your own__tablename__(anduser_idFK + unique constraints where noted): FastAuthUserMixin:id: UUID(pk,uuid7),email(unique, indexed),hashed_password: str | None,is_active: bool = True,password_changed_at: datetimeFastAuthSessionMixin:id: UUID(pk, randomuuid4),expires_at,created_at(+ youruser_idFK)FastAuthRefreshTokenMixin:id: UUID(pk, mirrors JWTjti),expires_at,created_at,used_at: datetime | None,revoked_at: datetime | None(+ youruser_idFK)FastAuthRateLimitMixin:key: str(pk),count: int,window_start: datetimeFastAuthPasswordResetTokensMixin:id: UUID(pk),token_hash: str(SHA-256 hex, unique),expires_at,used_at: datetime | None(+ youruser_idFK)FastAuthOAuthAccountMixin:id: UUID(pk),provider: str,provider_user_id: str(+ youruser_idFK and mandatory unique(provider, provider_user_id))
Rate-limit backends (fastauth.adapters.rate_limit)¶
InMemoryRateLimiter()— per-process dict; no constructor args.SQLAlchemyRateLimiter(db_session: AsyncSession, model)—SELECT … FOR UPDATEcounter; commits even when denying (releases the lock); blocked hits don't inflate the count.RedisRateLimiterAdapter(redis_client: redis.asyncio.Redis)— one Lua script does atomicINCR+ conditionalEXPIRE; returnsbool(script_result).
Exceptions (fastauth.adapters.exceptions)¶
EmailAlreadyRegistered(email: str | None)— OAuth email collided with an existing account; routes map it to 400.RefreshTokenReused(user_id: UUID)— consumed refresh token presented again; the refresh route revokes the family and fireson_token_reuse_detected.
Hooks (fastauth.hooks)¶
Handler aliases (all async, all observers return None):
SignupHandler = Callable[[BaseModel, Request], Awaitable[BaseModel]] # before
SignupSuccessHandler = Callable[[BaseModel, Request], Awaitable[None]]
SignupFailureHandler = Callable[[str, Request], Awaitable[None]] # error string
LoginHandler = Callable[[BaseModel, Request], Awaitable[BaseModel]] # before
LoginFailureHandler = Callable[[LoginFailure, Request], Awaitable[None]]
LoginSuccessHandler = Callable[[BaseModel, Request], Awaitable[None]]
LogoutHandler = Callable[[str], Awaitable[None]] # user id
TokenReuseHandler = Callable[[str, Request], Awaitable[None]] # user id
PasswordResetHandler = Callable[[PasswordResetRequested, Request], Awaitable[None]]
PasswordChangedHandler = Callable[[PasswordChanged, Request], Awaitable[None]]
# oauth.py reuses the name LoginSuccessHandler for its own alias:
# OIDC observers receive OAuthUserInfo, OAuth2 observers receive OAuth2LoginResult
LoginSuccessHandler = Callable[[BaseModel, Request], Awaitable[None]] # oauth.py
Holder methods (each XxxHooks class): on_before_signup(fn), on_after_signup(fn), on_signup_failure(fn); on_before_login(fn), on_login_failure(fn), on_after_login(fn); LogoutHooks().add_after_logout(fn) (exposed as auth.on_after_logout); on_token_reuse_detected(fn); on_password_reset_requested(fn), on_password_changed(fn); on_after_oidc_login(fn); on_after_oauth2_login(fn). run_* counterparts execute them (before-runners re-validate and may raise; observer-runners never raise).
Events (fastauth.hooks.models, all frozen): LoginFailure(user_id: str | None, error: str); PasswordResetRequested(user_id: str, email: str, token: str) — your handler sends the email; PasswordChanged(user_id: str). Abort: HookAbort(status_code=400, detail="Rejected") (fastauth.hooks, an HTTPException).
Providers & types (fastauth.oauth, fastauth.types)¶
OIDCProvider(
name: str, client_id: str, client_secret: str, metadata_url: str,
scope: str = "openid email profile",
extra_authorize_params: dict[str, str] | None = None,
)
await provider.get_authorize_url(redirect_uri: str, state: str) -> str
await provider.fetch_user_info(code: str, redirect_uri: str) -> OAuthUserInfo
OAuth2Provider(
name: str, client_id: str, client_secret: str,
authorization_url: str, token_url: str, userinfo_url: str,
map_profile_to_user: Callable[[dict], Awaitable[OAuthUserInfo]],
scope: str = "",
extra_authorize_params: dict[str, str] | None = None,
)
await provider.get_authorize_url(redirect_uri: str, state: str) -> str
await provider.fetch_user_info(code: str, redirect_uri: str) -> OAuth2LoginResult
OAuthUserInfo(provider: str, provider_user_id: str, email: str | None,
email_verified: bool, name: str | None, avatar_url: str | None,
others: dict[str, Any] | None = None)
OAuth2LoginResult(user_info: OAuthUserInfo, access_token: str,
refresh_token: str | None, expires_at: datetime | None)
Schemas (fastauth.schemas)¶
Static: LoginRequest(email: EmailStr, password: SecretStr); UserResponseBase(id: UUID, email, is_active); TokenResponse(access_token: str, token_type="access"); ForgotPasswordRequest(email); ResetPasswordRequest(token: str, new_password: SecretStr). Builders (lengths honor PasswordConfig): build_signup_schema(extra_fields, password_config?), build_login_schema(password_config?), build_user_response_schema(extra_fields), build_password_reset_token_schema(password_config?) — each returns a type[BaseModel].
Security & cookies (fastauth.security, fastauth.cookies)¶
hash_password(password: str | SecretStr, hasher: PasswordHash | None = None) -> str— the only place aSecretStris unwrapped.verify_password(password: str | SecretStr, hashed: str, hasher: PasswordHash | None = None) -> bool— fail-closed on unknown hashes.OAuthStateManager(secret_key: str, max_age: int = 300)—generate(provider: str) -> tuple[raw_state, signed_cookie](43-chartoken_urlsafe(32)state),verify(cookie_value: str | None, returned_state: str | None, provider: str) -> bool(constant-time compare, provider-bound, expiry-checked).- Cookies:
set_session_cookie(response, session_id, *, name?, max_age?, secure=True, samesite="lax", path="/", domain?),set_refresh_cookie(...)(same shape),clear_session_cookie(response, *, ...)/clear_refresh_cookie(...), plussession_cookie_kwargs(cfg, *, max_age),refresh_cookie_kwargs(cfg, *, max_age),clear_cookie_kwargs(cfg). AlwaysHttpOnly;SameSite=Nonerequiressecure=True.
Dependencies (fastauth.dependencies)¶
RateLimiter(rate_limiter_adapter?, db_session_dependency?, rate_limit_model?, rate_limit_config: RateLimitConfig | None = None, redis_client: redis.Redis | None = None)—memoryneeds nothing;databaseraisesValueErrorwithout model/dependency/adapter;redisraises without client/adapter;enabled=Falseneeds nothing and yields no-op dependencies..limit(window: int | None = None, max_requests: int | None = None)and.limit_for(path: str)(looks upcustom_rules, else globals) return FastAPI dependencies raising429on breach. Key isip:path;trusted_ip_headeropts into a client-controlled IP header.current_user.py:session_current_user(ctx) -> dependency(cookie → user, 401 otherwise),jwt_current_user(ctx) -> dependency(bearer → user, 401 +WWW-Authenticate: Bearer). Normally used asDepends(auth.current_user).
Protocols (fastauth.protocols)¶
Structural contracts (attribute names only) checked at startup by ensure_model_compliance(model, protocol, *, name: str) -> None (TypeError listing missing attributes): UserProtocol(id, email, hashed_password, is_active), SessionProtocol(id, user_id, expires_at, created_at), RefreshTokenProtocol(+ used_at, revoked_at), RateLimitProtocol(key, count, window_start), PasswordResetTokenProtocol(id, user_id, token_hash, expires_at, used_at), OAuthAccountProtocol(id, user_id, provider, provider_user_id). Generic TypeVars (UserT, …) are intentionally unbound — SQLAlchemy Mapped invariance defeats static bounds.