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
pip install fastapi-startkit-auth
# optional ORM provider driver
pip install "fastapi-startkit-auth[masoniteorm]"
Uses
bcryptdirectly (notpasslib, which imports thecryptstdlib 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() |
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.
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
POST /oauth/authorizewith a bearer token andcode_challenge→ returns a single-usecode.POST /oauth/tokenwithgrant_type=authorization_code, thecode, and thecode_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
pip install -e ".[test]"
pytest
Or with uv:
uv sync --group dev
uv run 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, builds sdist + wheel, validates them with
twine check, uploads to PyPI, then commits, tags vX.Y.Z, and creates a
GitHub release. It requires uv, gh (authenticated), and PyPI credentials
for twine (e.g. a pypi-* API token via TWINE_USERNAME=__token__ /
TWINE_PASSWORD or ~/.pypirc).
License
MIT
Metadata
Release files for fastapi-startkit-auth 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| fastapi_startkit_auth-0.3.0.tar.gz | 39.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| fastapi_startkit_auth-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 101.4 kB
Release files / fastapi_startkit_auth-0.3.0.tar.gz
| Download URL | fastapi_startkit_auth-0.3.0.tar.gz |
|---|---|
| Size | 39.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6019971c9301c934d3a7d6c6c8ed8b8f27b4fd2a6d4398cc035ad0511a3d67b1
|
|
BLAKE2b-256 checksum How to use checksums |
ba073e9170d048b1e4a12e513444e05010f3ff053d9c7ef6d02994350cb1632b
|
| 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.3.0-py3-none-any.whl
| Download URL | fastapi_startkit_auth-0.3.0-py3-none-any.whl |
|---|---|
| Size | 62.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
87ce25c4253c045d400b2260567a7a48b6cbe685a254b57675db74e104cb2c1e
|
|
BLAKE2b-256 checksum How to use checksums |
fa3ac8667489f5221e9c0a80e7cf55201e74bd484e2d742700f184cb4b7cdd07
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.7
|