Skip to main content

fastapi-startkit-auth

Passport-style OAuth2 + JWT authentication for FastAPI.

fastapi-startkit-auth brings the ergonomics of Laravel Passport to FastAPI: a config-driven guard/provider/passwords model layered on top of OAuth2 grants and signed JWT access tokens (per the FastAPI security tutorial).

Features

Area What you get
Config layer AuthConfig (default / guards / providers / passwords) + AuthProvider that registers into the app
Password grant OAuth2 password grant → signed JWT access tokens with expiry (/oauth/token, /token)
Refresh tokens Opaque refresh tokens with rotation and configurable TTL
Personal access tokens Named, long-lived tokens with scopes/abilities
Client credentials Machine-to-machine grant
Authorization code + PKCE Browser/SPA flow with S256 & plain PKCE
Clients Register / list / delete confidential & public clients
Revocation & introspection RFC 7009 revoke + RFC 7662 introspect
Password reset password_reset_tokens flow with configurable expire + throttle
Guards & deps current_user, optional_user, require_scopes FastAPI dependencies
Pluggable providers In-memory, generic ORM/masoniteorm model, or any custom UserProvider

Installation

Requires Python 3.10+.

pip install fastapi-startkit-auth
# or
uv add fastapi-startkit-auth

Optional extras:

Extra Installs Use when
startkit fastapi-startkit[database]>=0.60,<1.0 (Python 3.12+) Registering AuthServiceProvider, publishing its config stub and migrations via provider:publish -p auth, and the "orm" stores
masoniteorm masonite-orm Using the masoniteorm user provider driver
pip install "fastapi-startkit-auth[startkit]"
pip install "fastapi-startkit-auth[masoniteorm]"

The startkit and masoniteorm extras are mutually exclusive: masonite-orm pins cleo<2 while fastapi-startkit requires cleo>=2.1.

Uses bcrypt directly (not passlib, which imports the crypt stdlib module removed in Python 3.13+), so it runs on modern Python.

Quickstart

from fastapi import Depends
from fastapi_startkit_auth import Application, AuthProvider, AuthConfig, current_user, require_scopes
from myapp.models import User  # any active-record-style model


class Config(AuthConfig):
    key = "change-me-to-a-long-random-secret"   # JWT signing key

    default = {"guard": "api", "passwords": "users"}
    guards = {"api": {"driver": "passport", "provider": "users"}}
    providers = {"users": {"driver": "masoniteorm", "model": User}}
    passwords = {
        "users": {"provider": "users", "table": "password_reset_tokens",
                  "expire": 60, "throttle": 60},
    }


app = Application([(AuthProvider, Config)])
api = app.api  # the underlying FastAPI instance


@api.get("/me")
def me(user=Depends(current_user)):
    return user


@api.get("/reports")
def reports(ctx=Depends(require_scopes("reports:read"))):
    return {"ok": True}

Serve it:

uvicorn myapp:app        # Application is ASGI-callable
uvicorn myapp:app.api    # or serve the FastAPI instance directly

Configuration

AuthConfig mirrors Laravel's config/auth.php and adds JWT/token knobs:

class AuthConfig:
    default   = {"guard": "api", "passwords": "users"}
    guards    = {"api": {"driver": "passport", "provider": "users"}}
    providers = {"users": {"driver": "masoniteorm", "model": User}}
    passwords = {"users": {"provider": "users", "table": "password_reset_tokens",
                            "expire": 60, "throttle": 60}}

    # token settings (all optional, sensible defaults shown)
    key                        = None          # JWT secret (required in production)
    algorithm                  = "HS256"
    access_token_ttl           = 3600          # seconds
    refresh_token_ttl          = 60 * 60 * 24 * 14
    personal_access_token_ttl  = 60 * 60 * 24 * 365
    authorization_code_ttl     = 600
    bcrypt_rounds              = 12

Provider drivers

driver meaning
masoniteorm / orm / model wrap a model exposing find(id) and where(field, value).first()
async_model same, for async ORMs where find, first() and save() are coroutines
memory in-memory dict store (users=[...]) — great for tests/demos
instance pass a ready UserProvider via {"instance": ...}
factory pass a zero-arg callable returning a UserProvider

Any object implementing the UserProvider protocol (retrieve_by_id, retrieve_by_credentials, validate_credentials, get_identifier, update_password) is a valid provider. Any of its methods may be async def; one async method is enough for AuthManager to pick the async guards, grants and broker. The sync classes refuse an awaitable result with AsyncMisconfiguration instead of treating it as truthy.

Model-backed providers also accept password_key (the credentials key holding the plaintext password, default: password_field) and is_active (a boolean attribute name or a callable(user) -> bool; inactive users cannot log in, authenticate, refresh or exchange a code, and introspect as inactive). The async_model driver also takes an async def hook; the sync drivers reject one at construction:

providers = {
    "users": {
        "driver": "async_model",
        "model": User,
        "password_field": "hashed_password",
        "password_key": "password",
        "is_active": "is_active",
    }
}

Multiple providers

With more than one provider, register each OAuth client with the provider whose users it serves (Laravel Passport's provider column). The password grant then authenticates against that provider, and refresh, code exchange and introspection re-check the token owner against it. A client without a provider uses the default guard's provider, and /oauth/authorize rejects a client bound to a different provider with unauthorized_client:

client, secret = manager.client_repository.register(name="admin-panel", provider="admins")

ORM stores and migrations

With an async provider or store, AuthManager builds the async guards, grants, token service and password broker automatically (use the AsyncAuth facade for session login/logout). Sessions, API tokens and OAuth tokens persist through the fastapi-startkit ORM (pip install "fastapi-startkit-auth[startkit]", which pulls in fastapi-startkit[database]). The package ships its own models (fastapi_startkit_auth.orm) and uses no raw SQL:

session = {"store": "orm"}
api_tokens = {"store": "orm"}
tokens = {"store": "orm", "connection": "auth"}  # optional ORM connection name

connection names an entry of your database config; omit it to use the default connection. Publish and run the migrations once per app (they are reversible and create the sessions, personal_api_tokens, oauth_access_tokens, oauth_refresh_tokens and oauth_auth_codes tables with their indexes):

python artisan provider:publish -p auth   # copies them to databases/migrations/
python artisan migrate
python artisan migrate:rollback           # drops them again

Single-use guarantees rely on conditional UPDATE/DELETE statements issued through the ORM query builder: refresh-token rotation and authorization-code redemption each have exactly one winner under concurrency.

The former sql and async_sql stores were removed; configuring them raises a ValueError pointing at "orm". Use "memory" or an "instance" store for sync setups.

In mixed setups (async stores with a sync provider or a sync session store), the sync calls — lookups, bcrypt, is_active hooks, a sync session store in SessionMiddleware — run in the threadpool, never on the event loop. An is_active attribute name or async def hook runs inline, since neither blocks.

AuthProvider warms the providers up when the app starts (await manager.warm_up()), so AsyncModelUserProvider computes its dummy hash off the event loop before the first request. A login for an unknown user then costs one bcrypt verify, the same as a wrong password.

HTTP endpoints

Method & path Purpose
POST /oauth/token Unified token endpoint: password, refresh_token, client_credentials, authorization_code
POST /token Simple password grant (FastAPI-tutorial style)
POST /oauth/authorize Approve an auth-code request (requires an authenticated user) → returns code
POST /oauth/introspect RFC 7662 token introspection (requires client authentication)
POST /oauth/revoke RFC 7009 access/refresh token revocation (requires client authentication)
POST/GET /oauth/clients, DELETE /oauth/clients/{id} Client registration & management
POST/GET /oauth/personal-access-tokens, DELETE .../{jti} Personal access tokens
POST /password/email Trigger a password-reset token (delivered out-of-band; see below)
POST /password/reset Reset the password with a token

Password resets

POST /password/email always returns the same generic response whether or not the account exists (no user enumeration) and never puts the token in the response body. Configure how the token reaches the user with a notifier:

class AuthConfig(BaseAuthConfig):
    password_reset_notifier = staticmethod(lambda email, token: send_email(email, token))
    # debug_expose_reset_token = True   # DEV ONLY: echo the token in the response

The code_challenge for public clients is mandatory — a public (secretless) client cannot obtain an authorization code without PKCE, and codes are verified against the code_verifier at exchange.

Example: password grant

curl -X POST localhost:8000/oauth/token \
  -d grant_type=password -d username=ada@example.com -d password=secret -d scope="read write"
# → { "access_token": "...", "token_type": "Bearer", "expires_in": 3600,
#     "refresh_token": "...", "scope": "read write" }

Example: authorization code + PKCE

  1. POST /oauth/authorize with a bearer token and code_challenge → returns a single-use code.
  2. POST /oauth/token with grant_type=authorization_code, the code, and the code_verifier.

Scopes / abilities

Tokens carry scopes; enforce them with the require_scopes dependency:

require_scopes("posts:write")               # must have this scope
require_scopes("a", "b")                     # must have all
require_scopes("a", "b", mode="any")         # must have at least one

* is a wildcard scope that satisfies any check. Inside a handler you can also inspect the AuthContext (ctx.can(...), ctx.can_any(...), ctx.scopes).

Testing

uv sync --group dev
uv run pytest
uv run ruff check .

The async store and flow tests run on aiosqlite; set TEST_ASYNCPG_DSN to a disposable Postgres database to also run them on asyncpg (CI does).

Or with pip:

pip install -e ".[test]"
pytest

Releasing

Releases are cut from main with the release script (maintainers only):

./bin/release.sh          # patch bump
./bin/release.sh minor    # or: major

The script bumps the version (pyproject.toml, __version__, uv.lock), builds sdist + wheel, validates them with twine check, uploads them to PyPI with twine upload, then commits, tags vX.Y.Z, pushes, and creates a GitHub release. Move the Unreleased notes in CHANGELOG.md under the new version before running it.

It requires uv, an authenticated gh, and PyPI credentials for twine: either a [pypi] entry in ~/.pypirc (username = __token__, password = pypi-...) or TWINE_USERNAME=__token__ and TWINE_PASSWORD=pypi-... in the environment.

License

MIT

Metadata

Release files for fastapi-startkit-auth 0.6.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for fastapi-startkit-auth 0.6.2
File Size Uploaded
fastapi_startkit_auth-0.6.2.tar.gz 85.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for fastapi-startkit-auth 0.6.2
File Interpreter ABI Platform
fastapi_startkit_auth-0.6.2-py3-none-any.whl Python 3 none any Details

Total release size: 161.6 kB

Release files / fastapi_startkit_auth-0.6.2.tar.gz

Download URL fastapi_startkit_auth-0.6.2.tar.gz
Size 85.1 kB
Tags Source
SHA-256 checksum
How to use checksums
b034296f5d2b89ef4882bd5b6d6dfe39a82b9fdcfaaf8117027e91123aeda8b7
BLAKE2b-256 checksum
How to use checksums
185028b8cdd594d45dd1ab591fefd0469490d2883ddb1bd847cfc31259af2807
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.7

Release files / fastapi_startkit_auth-0.6.2-py3-none-any.whl

Download URL fastapi_startkit_auth-0.6.2-py3-none-any.whl
Size 76.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
85d4562ed022c70a1ec13a159bd5e6f329dea1e8300fbef8f1bc58ceb95ee470
BLAKE2b-256 checksum
How to use checksums
40d62299cfba87cb2a10db00137a010cf0215b019a38ce9b90c03664963f1399
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.7

Release history Release notifications | RSS feed

0.6.3

2 release files

This release

0.6.2 This release

2 release files

0.6.1

2 release files

0.3.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page