fastapi-startkit-auth 0.6.2__tar.gz → 0.6.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastapi_startkit_auth-0.6.3/CHANGELOG.md +296 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/PKG-INFO +107 -60
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/README.md +105 -58
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/pyproject.toml +2 -2
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/__init__.py +60 -14
- fastapi_startkit_auth-0.6.3/src/fastapi_startkit_auth/apitokens/facade.py +63 -0
- fastapi_startkit_auth-0.6.3/src/fastapi_startkit_auth/clients/credentials.py +46 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/clients/models.py +12 -0
- fastapi_startkit_auth-0.6.3/src/fastapi_startkit_auth/clients/orm.py +91 -0
- fastapi_startkit_auth-0.6.3/src/fastapi_startkit_auth/clients/repository.py +61 -0
- fastapi_startkit_auth-0.6.3/src/fastapi_startkit_auth/commands/oauth2_client.py +67 -0
- fastapi_startkit_auth-0.6.3/src/fastapi_startkit_auth/config/__init__.py +33 -0
- fastapi_startkit_auth-0.6.3/src/fastapi_startkit_auth/config/auth.py +223 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/dependencies.py +38 -23
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/exceptions.py +28 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/facade.py +51 -34
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/grants/authorization_code.py +77 -11
- fastapi_startkit_auth-0.6.3/src/fastapi_startkit_auth/grants/client_credentials.py +54 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/grants/password.py +32 -3
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/grants/refresh.py +27 -5
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/guards/guard.py +11 -5
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/guards/session.py +30 -13
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/guards/token.py +26 -5
- fastapi_startkit_auth-0.6.3/src/fastapi_startkit_auth/manager.py +571 -0
- fastapi_startkit_auth-0.6.3/src/fastapi_startkit_auth/middleware/context.py +69 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/middleware/csrf.py +39 -7
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/middleware/session.py +9 -1
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/orm.py +19 -0
- fastapi_startkit_auth-0.6.3/src/fastapi_startkit_auth/policy.py +83 -0
- fastapi_startkit_auth-0.6.3/src/fastapi_startkit_auth/provider.py +221 -0
- fastapi_startkit_auth-0.6.3/src/fastapi_startkit_auth/publishable/migrations/2026_10_02_000001_add_resource_to_oauth_tables.py +15 -0
- fastapi_startkit_auth-0.6.3/src/fastapi_startkit_auth/publishable/migrations/2026_10_04_000001_create_oauth_clients_table.py +18 -0
- fastapi_startkit_auth-0.6.3/src/fastapi_startkit_auth/publishable/migrations/2026_10_04_000002_add_family_id_to_oauth_refresh_tokens_table.py +16 -0
- fastapi_startkit_auth-0.6.3/src/fastapi_startkit_auth/publishable/migrations/2026_10_05_000001_add_scopes_to_oauth_clients_table.py +11 -0
- fastapi_startkit_auth-0.6.3/src/fastapi_startkit_auth/py.typed +0 -0
- fastapi_startkit_auth-0.6.3/src/fastapi_startkit_auth/request_context.py +27 -0
- fastapi_startkit_auth-0.6.3/src/fastapi_startkit_auth/routes.py +497 -0
- fastapi_startkit_auth-0.6.3/src/fastapi_startkit_auth/routes_password.py +53 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/routes_spa.py +1 -12
- fastapi_startkit_auth-0.6.3/src/fastapi_startkit_auth/scoped_method.py +20 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/security/jwt.py +19 -3
- fastapi_startkit_auth-0.6.3/src/fastapi_startkit_auth/sessions/facade.py +48 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/sessions/orm.py +6 -12
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/sessions/state.py +3 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/sessions/store.py +21 -11
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/tokens/models.py +9 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/tokens/orm.py +70 -16
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/tokens/repository.py +47 -8
- fastapi_startkit_auth-0.6.3/src/fastapi_startkit_auth/tokens/service.py +469 -0
- fastapi_startkit_auth-0.6.3/tests/conftest.py +303 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/tests/test_api_tokens.py +11 -18
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/tests/test_async_auth.py +32 -27
- fastapi_startkit_auth-0.6.3/tests/test_async_refresh_race.py +77 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/tests/test_async_safety.py +46 -32
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/tests/test_async_stores.py +26 -0
- fastapi_startkit_auth-0.6.3/tests/test_class_config.py +128 -0
- fastapi_startkit_auth-0.6.3/tests/test_client_scopes.py +325 -0
- fastapi_startkit_auth-0.6.3/tests/test_clients.py +58 -0
- fastapi_startkit_auth-0.6.3/tests/test_facade_api.py +174 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/tests/test_grants.py +46 -11
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/tests/test_guard_registry.py +13 -8
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/tests/test_integration.py +12 -14
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/tests/test_jwt.py +22 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/tests/test_manager.py +6 -7
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/tests/test_model_provider.py +3 -3
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/tests/test_multi_provider.py +25 -18
- fastapi_startkit_auth-0.6.3/tests/test_oauth21.py +549 -0
- fastapi_startkit_auth-0.6.3/tests/test_oauth2_client_command.py +146 -0
- fastapi_startkit_auth-0.6.3/tests/test_oauth_hardening.py +406 -0
- fastapi_startkit_auth-0.6.3/tests/test_resource_binding.py +500 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/tests/test_security_fixes.py +8 -9
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/tests/test_session_auth.py +22 -22
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/tests/test_spa_csrf.py +97 -46
- fastapi_startkit_auth-0.6.3/tests/test_startkit_provider.py +193 -0
- fastapi_startkit_auth-0.6.3/tests/test_store_names.py +49 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/tests/test_sync_regression.py +38 -13
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/tests/test_token_service.py +7 -3
- fastapi_startkit_auth-0.6.2/CHANGELOG.md +0 -147
- fastapi_startkit_auth-0.6.2/src/fastapi_startkit_auth/application.py +0 -38
- fastapi_startkit_auth-0.6.2/src/fastapi_startkit_auth/clients/repository.py +0 -71
- fastapi_startkit_auth-0.6.2/src/fastapi_startkit_auth/config.py +0 -109
- fastapi_startkit_auth-0.6.2/src/fastapi_startkit_auth/grants/client_credentials.py +0 -30
- fastapi_startkit_auth-0.6.2/src/fastapi_startkit_auth/manager.py +0 -406
- fastapi_startkit_auth-0.6.2/src/fastapi_startkit_auth/provider.py +0 -85
- fastapi_startkit_auth-0.6.2/src/fastapi_startkit_auth/routes.py +0 -368
- fastapi_startkit_auth-0.6.2/src/fastapi_startkit_auth/startkit.py +0 -41
- fastapi_startkit_auth-0.6.2/src/fastapi_startkit_auth/tokens/service.py +0 -316
- fastapi_startkit_auth-0.6.2/tests/conftest.py +0 -166
- fastapi_startkit_auth-0.6.2/tests/test_clients.py +0 -58
- fastapi_startkit_auth-0.6.2/tests/test_startkit_provider.py +0 -131
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/LICENSE +0 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/apitokens/__init__.py +0 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/apitokens/manager.py +0 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/apitokens/models.py +0 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/apitokens/orm.py +0 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/apitokens/repository.py +0 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/clients/__init__.py +0 -0
- /fastapi_startkit_auth-0.6.2/src/fastapi_startkit_auth/py.typed → /fastapi_startkit_auth-0.6.3/src/fastapi_startkit_auth/commands/__init__.py +0 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/concurrency.py +0 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/grants/__init__.py +0 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/grants/pkce.py +0 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/guards/__init__.py +0 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/middleware/__init__.py +0 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/passwords/__init__.py +0 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/passwords/broker.py +0 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/passwords/repository.py +0 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/providers/__init__.py +0 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/providers/base.py +0 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/providers/fields.py +0 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/providers/memory.py +0 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/providers/model.py +0 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/publishable/cors.py +0 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/publishable/migrations/2026_09_30_000001_create_sessions_table.py +0 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/publishable/migrations/2026_09_30_000002_create_personal_api_tokens_table.py +0 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/publishable/migrations/2026_09_30_000003_create_oauth_access_tokens_table.py +0 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/publishable/migrations/2026_09_30_000004_create_oauth_refresh_tokens_table.py +0 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/publishable/migrations/2026_09_30_000005_create_oauth_auth_codes_table.py +0 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/security/__init__.py +0 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/security/hashing.py +0 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/sessions/__init__.py +0 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/sessions/models.py +0 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/src/fastapi_startkit_auth/tokens/__init__.py +0 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/tests/test_hashing.py +0 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/tests/test_model_provider_postgres.py +0 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/tests/test_password_reset.py +0 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/tests/test_reset_throttle_enumeration.py +0 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/tests/test_session_store.py +0 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/tests/test_token_repository.py +0 -0
- {fastapi_startkit_auth-0.6.2 → fastapi_startkit_auth-0.6.3}/tests/test_user_provider.py +0 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `fastapi-startkit-auth` are documented in this file.
|
|
4
|
+
|
|
5
|
+
Release notes are also published on
|
|
6
|
+
[GitHub Releases](https://github.com/fastapi-startkit/fastapi-startkit-auth/releases).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
### Breaking changes and upgrade notes
|
|
11
|
+
|
|
12
|
+
Each item lists what changed and what to do.
|
|
13
|
+
|
|
14
|
+
- **Feature providers replace `Application` and `AuthServiceProvider`.**
|
|
15
|
+
Register `AuthProvider` first, then the features your guards use:
|
|
16
|
+
`AuthSessionProvider(SessionConfig)`, `AuthOAuth2Provider(OAuth2Config)` and
|
|
17
|
+
`AuthApiTokenProvider(ApiTokenConfig)`. Standalone apps call
|
|
18
|
+
`Provider(config).register(app)`; Startkit apps list the providers in the
|
|
19
|
+
application. *Migrate:* replace `Application([(AuthProvider, Config)])` with a
|
|
20
|
+
`FastAPI()` app plus the providers, and `AuthServiceProvider` with
|
|
21
|
+
`AuthProvider` + the feature providers.
|
|
22
|
+
- **Feature settings moved off `AuthConfig`.** The `session`, `api_tokens` and
|
|
23
|
+
`spa` dicts are rejected at startup; use `SessionConfig`, `ApiTokenConfig`
|
|
24
|
+
(`stateful_origins` turns on SPA mode) instead. *Migrate:* move each dict's
|
|
25
|
+
keys onto the matching dataclass and pass it to its provider.
|
|
26
|
+
- **OAuth settings moved to `OAuth2Config`.** `key`, `algorithm`, the `*_ttl`
|
|
27
|
+
values, `tokens`, `issuer`, `resources`, `scopes`, `pkce_methods` and
|
|
28
|
+
`require_pkce` now live on `OAuth2Config`. Setting them on `AuthConfig` still
|
|
29
|
+
works but emits a `DeprecationWarning`, and an explicit `OAuth2Config` value
|
|
30
|
+
wins. *Migrate:* move them to `OAuth2Config`; the fallback will be removed in a
|
|
31
|
+
future release.
|
|
32
|
+
- **Password grant is opt-in.** `OAuth2Config.grant_types` defaults to
|
|
33
|
+
`["authorization_code", "client_credentials", "refresh_token"]`; `/oauth/token`
|
|
34
|
+
with `grant_type=password` and `/token` answer `unsupported_grant_type`.
|
|
35
|
+
*Migrate:* add `"password"` to `grant_types` to keep it.
|
|
36
|
+
- **Client-less refresh tokens need the password grant.** Refresh tokens are now
|
|
37
|
+
bound to the issuing client: refreshing or revoking one requires that client's
|
|
38
|
+
authentication. Tokens without a client (issued by the password grant) are only
|
|
39
|
+
accepted while `"password"` is enabled, and client credentials sent alongside
|
|
40
|
+
one are authenticated (`invalid_client` if wrong). *Migrate:* send client credentials on
|
|
41
|
+
refresh; keep `"password"` enabled while old client-less tokens are in use.
|
|
42
|
+
- **Consent is mandatory.** `POST /oauth/authorize` returns `access_denied`
|
|
43
|
+
unless the body has `"approved": true`, and always returns `iss` (body and
|
|
44
|
+
redirect, RFC 9207). `response_type` other than `code` fails with
|
|
45
|
+
`unsupported_response_type`. *Migrate:* send `approved: true` after the user
|
|
46
|
+
consents; `GET /oauth/authorize` validates a request for your consent screen.
|
|
47
|
+
- **PKCE no longer defaults to S256.** A missing `code_challenge_method` means
|
|
48
|
+
`plain`, which the default `pkce_methods=["S256"]` refuses. RFC 7636 formats are
|
|
49
|
+
enforced: an S256 challenge is 43 base64url characters, a verifier 43–128
|
|
50
|
+
unreserved characters. *Migrate:* always send `code_challenge_method=S256` and
|
|
51
|
+
a verifier of at least 43 characters.
|
|
52
|
+
- **`client_credentials` is for confidential clients only**; public, revoked or
|
|
53
|
+
grant-disallowed clients get `unauthorized_client` / `invalid_client`.
|
|
54
|
+
Confidential clients must authenticate on the code exchange. *Migrate:* give
|
|
55
|
+
machine clients a secret and list the grant (or leave `grant_types` empty).
|
|
56
|
+
- **Introspection requires a confidential client.** *Migrate:* call
|
|
57
|
+
`/oauth/introspect` with a confidential client's credentials.
|
|
58
|
+
- **`/oauth/clients` HTTP routes are removed.** *Migrate:* create clients with
|
|
59
|
+
`auth:oauth2:client` (Startkit) or `manager.client_repository.register(...)`.
|
|
60
|
+
- **Password reset routes** move to their own router, mounted by `AuthProvider`
|
|
61
|
+
whenever `AuthConfig.passwords` is set (no OAuth needed). Paths are unchanged.
|
|
62
|
+
- **Default guard changed** from `"api"` to `"web"` and `AuthConfig.guards`
|
|
63
|
+
defaults to empty. *Migrate:* set `default` and `guards` explicitly.
|
|
64
|
+
- **CSRF is always on with sessions**, and also accepts the `_token` form field.
|
|
65
|
+
*Migrate:* send `X-XSRF-TOKEN` (or `_token`) on unsafe requests, or list paths
|
|
66
|
+
in `SessionConfig.csrf_exempt_paths`.
|
|
67
|
+
- **OAuth client store defaults to `"database"`** (the ORM `oauth_clients`
|
|
68
|
+
table). *Migrate:* publish and run the migrations, or set
|
|
69
|
+
`OAuth2Config(clients=OAuthClientsConfig(store="memory"))`.
|
|
70
|
+
- **The in-memory OAuth client store is async.** `InMemoryClientRepository`
|
|
71
|
+
methods (`register`, `add`, `find`, `all`, `authenticate`, `revoke`,
|
|
72
|
+
`delete`) are coroutines, like `OrmClientRepository`, and `store="memory"` now
|
|
73
|
+
makes the manager build the async refresh and authorization-code grants. A
|
|
74
|
+
synchronous custom store passed with `store="instance"` keeps the sync grants.
|
|
75
|
+
*Migrate:*
|
|
76
|
+
`await` client-store calls and the `handle` / `issue_code` of those grants.
|
|
77
|
+
- **Persistent stores are named `"database"`.** `store="orm"` still works for
|
|
78
|
+
sessions, API tokens, OAuth tokens and OAuth clients, but emits a
|
|
79
|
+
`DeprecationWarning` and will be removed in a future release. *Migrate:*
|
|
80
|
+
replace `store="orm"` with `store="database"`.
|
|
81
|
+
- **Custom token repositories** must implement `consume_refresh_token` and
|
|
82
|
+
`revoke_token_chain`, and `store_refresh_token` takes `family_id`.
|
|
83
|
+
- Error responses carry `Cache-Control: no-store`; `invalid_client` challenges
|
|
84
|
+
with `WWW-Authenticate: Basic`.
|
|
85
|
+
|
|
86
|
+
### Migrations
|
|
87
|
+
|
|
88
|
+
All additive; the v0.6.x migrations are unchanged. Publish and run them when
|
|
89
|
+
using the ORM stores:
|
|
90
|
+
|
|
91
|
+
- `2026_10_04_000001_create_oauth_clients_table` — the `oauth_clients` table.
|
|
92
|
+
- `2026_10_04_000002_add_family_id_to_oauth_refresh_tokens_table` — a nullable,
|
|
93
|
+
indexed `family_id` column on `oauth_refresh_tokens`.
|
|
94
|
+
- `2026_10_05_000001_add_scopes_to_oauth_clients_table` — a nullable `scopes`
|
|
95
|
+
column on `oauth_clients`; existing rows (NULL) stay unrestricted.
|
|
96
|
+
|
|
97
|
+
### Added
|
|
98
|
+
|
|
99
|
+
- Refresh token families with reuse detection: replaying a rotated refresh token
|
|
100
|
+
revokes the whole family, whoever presents it: the owning client, another
|
|
101
|
+
authenticated client, or no client at all for unbound password-grant tokens.
|
|
102
|
+
Refresh tokens are introspectable.
|
|
103
|
+
- `authentication.md`: the full authentication guide.
|
|
104
|
+
- `POST /oauth/revoke` (RFC 7009) revokes the token and its access/refresh chain;
|
|
105
|
+
`GET`/`DELETE /oauth/tokens` and `DELETE /oauth/tokens/{jti}` let a user list
|
|
106
|
+
and revoke their OAuth tokens. `DELETE /oauth/personal-access-tokens` revokes
|
|
107
|
+
all personal access tokens, which also accept a `ttl`.
|
|
108
|
+
- `/.well-known/oauth-authorization-server` metadata (RFC 8414).
|
|
109
|
+
- `OAuth2Config.default_scopes`, `grant_types`, `require_redirect_uri` and
|
|
110
|
+
`authorization_guard`.
|
|
111
|
+
- `OrmClientRepository` and the `auth:oauth2:client` command (`--public`,
|
|
112
|
+
`--name`, repeatable `--redirect-uri`).
|
|
113
|
+
- Per-client scope restrictions: `Client.scopes` lists the scopes a client may
|
|
114
|
+
request (empty means any). A request outside the list, including
|
|
115
|
+
`default_scopes` applied to a request naming none, fails with `invalid_scope`
|
|
116
|
+
on `/oauth/authorize`, the `client_credentials` grant and the password grant
|
|
117
|
+
when a client authenticates. `"*"` is not expanded when matching the allow-list, but a token granted `"*"`
|
|
118
|
+
passes every `require_scopes` check, so never give a client `"*"` (the command
|
|
119
|
+
refuses it).
|
|
120
|
+
Set it with `auth:oauth2:client --scopes`; client repositories gain `revoke`.
|
|
121
|
+
- `AuthMiddleware` and request-scoped facades: `Auth.user()`, `Session.token()`,
|
|
122
|
+
`ApiToken.create(...)` work on the class inside a request. `Auth.validate`
|
|
123
|
+
checks credentials without logging in.
|
|
124
|
+
- `auth` dependency; token guards fall back to the SPA session cookie.
|
|
125
|
+
- Guest sessions are persisted lazily, only when the response is sent.
|
|
126
|
+
|
|
127
|
+
### Added (resource indicators, #27)
|
|
128
|
+
|
|
129
|
+
- RFC 8707 resource indicators: `OAuth2Config.resources` lists the resources tokens
|
|
130
|
+
may be bound to. The token endpoint and `/oauth/authorize` accept `resource`;
|
|
131
|
+
the code and refresh token remember it and the access token carries it as `aud`.
|
|
132
|
+
Unknown or mismatched resources fail with `invalid_target`.
|
|
133
|
+
- Guards accept an `audience`: a passport guard with `"audience": "<uri>"` only
|
|
134
|
+
accepts tokens bound to that resource, and resource-bound tokens are refused by
|
|
135
|
+
guards without it.
|
|
136
|
+
- `OAuth2Config.issuer` stamps and verifies the `iss` claim.
|
|
137
|
+
- `OAuth2Config.scopes` is a scope catalog: when set, grants refuse unknown scopes
|
|
138
|
+
with `invalid_scope`.
|
|
139
|
+
- `OAuth2Config.pkce_methods` restricts `code_challenge_method` (e.g. `["S256"]`),
|
|
140
|
+
compared case-insensitively; an unknown `code_challenge_method` is now refused
|
|
141
|
+
at `/oauth/authorize` with `invalid_request`.
|
|
142
|
+
- `OAuth2Config.require_pkce` demands a `code_challenge` from confidential clients
|
|
143
|
+
too (OAuth 2.1 / MCP).
|
|
144
|
+
- A repeated `resource` parameter on the token endpoint fails with `invalid_target`.
|
|
145
|
+
- Building a guard whose `audience` is not listed in `OAuth2Config.resources` emits
|
|
146
|
+
a `UserWarning`.
|
|
147
|
+
- `GrantPolicy`, `InvalidScope` and `InvalidTarget` are exported; introspection
|
|
148
|
+
reports `aud` and `iss` when present.
|
|
149
|
+
- Migration `add_resource_to_oauth_tables` adds a nullable `resource` column to
|
|
150
|
+
`oauth_auth_codes` and `oauth_refresh_tokens`; publish and run it when using the
|
|
151
|
+
ORM store.
|
|
152
|
+
|
|
153
|
+
### Changed
|
|
154
|
+
|
|
155
|
+
- **Breaking:** persistence now goes through the fastapi-startkit ORM with models
|
|
156
|
+
shipped in `fastapi_startkit_auth.orm`; no raw SQL remains. Use
|
|
157
|
+
`{"store": "orm"}` (optional ORM `connection` name) for sessions, API tokens and
|
|
158
|
+
OAuth tokens.
|
|
159
|
+
- Requires `pyjwt>=2.10.1`.
|
|
160
|
+
- **Security:** refresh tokens are bound to the client they were issued to
|
|
161
|
+
(RFC 6749 §6). `RefreshTokenGrant.handle` takes `client_id` and fails with
|
|
162
|
+
`invalid_grant` when it differs from the token's client; the token endpoint
|
|
163
|
+
passes the authenticated client. Callers refreshing a client-issued token must
|
|
164
|
+
now send that client's credentials.
|
|
165
|
+
- **Breaking:** the `startkit` extra now requires `fastapi-startkit[database]>=0.60`.
|
|
166
|
+
- Run the published migrations (`provider:publish -p auth`, then `migrate`) to
|
|
167
|
+
create the tables; stores no longer create tables themselves.
|
|
168
|
+
|
|
169
|
+
### Removed
|
|
170
|
+
|
|
171
|
+
- **Breaking:** the `sql` / `async_sql` stores and the `SqlSessionStore`,
|
|
172
|
+
`SqlApiTokenRepository`, `AsyncSql*` classes and database adapters.
|
|
173
|
+
|
|
174
|
+
### Added
|
|
175
|
+
|
|
176
|
+
- `Client.provider` (and `register(provider=...)`) binds an OAuth client to a
|
|
177
|
+
user provider. `AuthManager.owner_provider(client_id)` resolves it, falling
|
|
178
|
+
back to the default guard's provider.
|
|
179
|
+
- `AuthManager.warm_up()` and `AsyncModelUserProvider.warm_up()`, run from the
|
|
180
|
+
app lifespan by `AuthProvider`, precompute the dummy hash at startup.
|
|
181
|
+
|
|
182
|
+
### Fixed
|
|
183
|
+
|
|
184
|
+
- Refresh, authorization-code exchange and introspection re-check the token
|
|
185
|
+
owner against the provider of the issuing client instead of always using the
|
|
186
|
+
default guard's provider. The password grant authenticates against the
|
|
187
|
+
client's provider, and `/oauth/authorize` rejects a client bound to another
|
|
188
|
+
provider.
|
|
189
|
+
- `AsyncModelUserProvider.is_active` only uses the threadpool for a sync
|
|
190
|
+
callable hook. Attribute checks and `async def` hooks run inline.
|
|
191
|
+
- The first unknown-user login no longer pays an extra bcrypt hash to create
|
|
192
|
+
the dummy hash.
|
|
193
|
+
|
|
194
|
+
## [0.4.0]
|
|
195
|
+
|
|
196
|
+
### Added
|
|
197
|
+
|
|
198
|
+
- Async support for async ORMs and drivers, side by side with the sync API:
|
|
199
|
+
- `AsyncModelUserProvider` (provider driver `"async_model"`) for models whose
|
|
200
|
+
`find` / `where(...).first()` / `save` are coroutines; bcrypt runs in a
|
|
201
|
+
worker thread.
|
|
202
|
+
- `AsyncPassportGuard`, `AsyncSessionGuard`, `AsyncTokenGuard`,
|
|
203
|
+
`AsyncTokenService`, `AsyncApiTokenManager`, `AsyncPasswordBroker`, the
|
|
204
|
+
`Async*Grant` variants and the `AsyncAuth` facade.
|
|
205
|
+
- `AuthManager` picks the async variant automatically when any provider
|
|
206
|
+
method or store method is async; sync configurations keep getting the sync
|
|
207
|
+
classes. Sync collaborators used from the async classes run in the
|
|
208
|
+
threadpool.
|
|
209
|
+
- `"async_sql"` stores for sessions (`AsyncSqlSessionStore`), personal API
|
|
210
|
+
tokens (`AsyncSqlApiTokenRepository`) and a new `AuthConfig.tokens` block
|
|
211
|
+
for OAuth access/refresh tokens and authorization codes
|
|
212
|
+
(`AsyncSqlTokenRepository`). They accept an asyncpg pool/connection, an
|
|
213
|
+
aiosqlite connection, or a zero-arg (async) factory. Supported backends are
|
|
214
|
+
PostgreSQL and SQLite. Refresh tokens and authorization codes are stored as
|
|
215
|
+
SHA-256 hashes; refresh revocation and code redemption are atomic and use
|
|
216
|
+
only portable SQL. Expired rows are purged via `expires_at` indexes.
|
|
217
|
+
- `fastapi-startkit` migrations for the five tables, published to
|
|
218
|
+
`databases/migrations/` by `AuthServiceProvider`.
|
|
219
|
+
- `password_key` provider option: the credentials key holding the plaintext
|
|
220
|
+
password, separate from the model column `password_field` (e.g.
|
|
221
|
+
`password_key="password"`, `password_field="hashed_password"`). Defaults to
|
|
222
|
+
`password_field`.
|
|
223
|
+
- `is_active` provider option (attribute name or callable; `async def` with
|
|
224
|
+
the `async_model` driver). Inactive or deleted users are rejected by the
|
|
225
|
+
password grant, the refresh and authorization-code grants, `Auth.attempt` /
|
|
226
|
+
`AsyncAuth.attempt` and every guard, and introspect as inactive.
|
|
227
|
+
- `AsyncMisconfiguration`: raised when a sync guard, grant, broker or `Auth`
|
|
228
|
+
receives an awaitable from a provider or store, so a half-async provider
|
|
229
|
+
fails closed. Sync providers reject an `async def` `is_active` hook.
|
|
230
|
+
- CI runs the asyncpg store and flow tests against a Postgres service.
|
|
231
|
+
- `Typing :: Typed` classifier (the package already ships `py.typed`).
|
|
232
|
+
|
|
233
|
+
### Changed
|
|
234
|
+
|
|
235
|
+
- **Breaking (signature):** `current_context` and `optional_user` are now
|
|
236
|
+
`async def` dependencies. `Depends(current_user)` and friends keep working
|
|
237
|
+
unchanged; code calling them directly as plain functions must now await
|
|
238
|
+
them. Sync guards still run in the threadpool.
|
|
239
|
+
- The built-in OAuth, password-reset and SPA csrf-cookie routes await async
|
|
240
|
+
collaborators; `SessionMiddleware` accepts an async session store.
|
|
241
|
+
- `Auth` (sync facade) raises `RuntimeError` for async session guards; use
|
|
242
|
+
`AsyncAuth` there.
|
|
243
|
+
- Minimum supported Python is now 3.10. 0.3.0 advertised `>=3.9` but fails on
|
|
244
|
+
3.9 because runtime-evaluated annotations use `X | None` syntax.
|
|
245
|
+
- The `startkit` extra is bounded to `fastapi-startkit>=0.51,<1.0`.
|
|
246
|
+
- The package `__init__` imports its public API directly instead of through a
|
|
247
|
+
lazy export table.
|
|
248
|
+
- The source distribution now ships the test suite and this changelog.
|
|
249
|
+
- `bin/release.sh` uploads to PyPI with `twine upload`, matching
|
|
250
|
+
fastapi-startkit; the tag-triggered trusted-publishing workflow is removed.
|
|
251
|
+
|
|
252
|
+
## [0.3.0]
|
|
253
|
+
|
|
254
|
+
First release published to PyPI. Contains the 0.2.0 feature set, built with
|
|
255
|
+
the `uv_build` backend; no functional changes.
|
|
256
|
+
|
|
257
|
+
## [0.2.0]
|
|
258
|
+
|
|
259
|
+
Adds three new authentication modes alongside the existing OAuth2/JWT server,
|
|
260
|
+
so the package now covers the full Laravel-style auth surface.
|
|
261
|
+
|
|
262
|
+
### Added
|
|
263
|
+
|
|
264
|
+
- Guard-driver registry in `AuthManager`: guards resolve their driver
|
|
265
|
+
(`token`, `session`, …) from a pluggable registry, enabling multiple guard
|
|
266
|
+
types in one application (Phase 0).
|
|
267
|
+
- Cookie/session authentication: `Auth` facade, `SessionGuard`, session
|
|
268
|
+
middleware, and in-memory + SQL session stores (Phase 1).
|
|
269
|
+
- SPA authentication with CSRF protection: `/__auth__/csrf-cookie` endpoint,
|
|
270
|
+
double-submit-cookie middleware, and an `AuthServiceProvider` that publishes
|
|
271
|
+
the `config/cors.py` stub via `provider:publish -p auth` (Phase 2).
|
|
272
|
+
- Sanctum-style API tokens: `TokenGuard`, opaque hashed tokens with abilities,
|
|
273
|
+
and in-memory + SQL token stores (Phase 3).
|
|
274
|
+
|
|
275
|
+
### Security / Hardening
|
|
276
|
+
|
|
277
|
+
- Session guard verifies against a constant-time dummy hash when no user is
|
|
278
|
+
found, closing a timing side-channel.
|
|
279
|
+
- SQL session and API-token stores prune expired rows; purge cadence is
|
|
280
|
+
configurable.
|
|
281
|
+
- CSRF cookie endpoint hardened; token repr for `NewApiToken` redacts the
|
|
282
|
+
plaintext value so it cannot leak into logs.
|
|
283
|
+
- DB-API connection vs. connection-factory detection now probes `execute()`
|
|
284
|
+
instead of `callable()`, fixing misdetection of raw `sqlite3.Connection`
|
|
285
|
+
objects in the SQL session and API-token stores.
|
|
286
|
+
|
|
287
|
+
## [0.1.0]
|
|
288
|
+
|
|
289
|
+
Initial release.
|
|
290
|
+
|
|
291
|
+
- Passport-style OAuth2 authorization server for FastAPI: authorization code
|
|
292
|
+
(with PKCE), password, client credentials, and refresh token grants
|
|
293
|
+
- JWT access tokens with scopes, token guards, and route dependencies
|
|
294
|
+
- Personal access tokens
|
|
295
|
+
- Pluggable user/client providers (in-memory and Masonite ORM)
|
|
296
|
+
- Password reset flow with throttling hardened against account enumeration
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-startkit-auth
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.3
|
|
4
4
|
Summary: Laravel-style authentication for FastAPI: OAuth2/JWT authorization server, cookie/session guard, SPA + CSRF protection, and Sanctum-style API tokens.
|
|
5
5
|
Keywords: fastapi,oauth2,jwt,auth,passport,sanctum,authentication,authorization,security,tokens,session,cookie,csrf,spa,starter-kit
|
|
6
6
|
Author: Bedram Tamang
|
|
@@ -21,7 +21,7 @@ Classifier: Topic :: Security
|
|
|
21
21
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
22
|
Classifier: Typing :: Typed
|
|
23
23
|
Requires-Dist: fastapi>=0.110
|
|
24
|
-
Requires-Dist: pyjwt>=2.
|
|
24
|
+
Requires-Dist: pyjwt>=2.10.1
|
|
25
25
|
Requires-Dist: bcrypt>=4.1
|
|
26
26
|
Requires-Dist: pydantic>=2
|
|
27
27
|
Requires-Dist: python-multipart>=0.0.9
|
|
@@ -51,17 +51,20 @@ to FastAPI: a config-driven guard/provider/passwords model layered on top of
|
|
|
51
51
|
OAuth2 grants and signed JWT access tokens (per the
|
|
52
52
|
[FastAPI security tutorial](https://fastapi.tiangolo.com/tutorial/security/oauth2-jwt/)).
|
|
53
53
|
|
|
54
|
+
The full guide — providers, sessions, API tokens, the OAuth 2.1 server, stores
|
|
55
|
+
and migrations — is in [authentication.md](authentication.md).
|
|
56
|
+
|
|
54
57
|
## Features
|
|
55
58
|
|
|
56
59
|
| Area | What you get |
|
|
57
60
|
| --- | --- |
|
|
58
|
-
| **Config layer** | `AuthConfig` (`default` / `guards` / `providers` / `passwords`) + `AuthProvider`
|
|
59
|
-
| **Password grant** |
|
|
60
|
-
| **Refresh tokens** |
|
|
61
|
+
| **Config layer** | `AuthConfig` (`default` / `guards` / `providers` / `passwords`) + `AuthProvider`, with `SessionConfig` / `OAuth2Config` / `ApiTokenConfig` feature providers |
|
|
62
|
+
| **Password grant** | Opt-in legacy password grant → signed JWT access tokens (`/oauth/token`, `/token`) |
|
|
63
|
+
| **Refresh tokens** | Client-bound opaque refresh tokens with **rotation** and reuse detection |
|
|
61
64
|
| **Personal access tokens** | Named, long-lived tokens with scopes/abilities |
|
|
62
65
|
| **Client credentials** | Machine-to-machine grant |
|
|
63
|
-
| **Authorization code + PKCE** |
|
|
64
|
-
| **Clients** |
|
|
66
|
+
| **Authorization code + PKCE** | OAuth 2.1 flow with S256 PKCE, explicit consent and `iss` (RFC 9207) |
|
|
67
|
+
| **Clients** | ORM client store and the `auth:oauth2:client` command |
|
|
65
68
|
| **Revocation & introspection** | RFC 7009 revoke + RFC 7662 introspect |
|
|
66
69
|
| **Password reset** | `password_reset_tokens` flow with configurable `expire` + `throttle` |
|
|
67
70
|
| **Guards & deps** | `current_user`, `optional_user`, `require_scopes` FastAPI dependencies |
|
|
@@ -81,7 +84,7 @@ Optional extras:
|
|
|
81
84
|
|
|
82
85
|
| Extra | Installs | Use when |
|
|
83
86
|
| --- | --- | --- |
|
|
84
|
-
| `startkit` | `fastapi-startkit[database]>=0.60,<1.0` (Python 3.12+) | Registering
|
|
87
|
+
| `startkit` | `fastapi-startkit[database]>=0.60,<1.0` (Python 3.12+) | Registering the auth providers in a Startkit app, publishing migrations via `provider:publish`, and the `"database"` stores |
|
|
85
88
|
| `masoniteorm` | `masonite-orm` | Using the `masoniteorm` user provider driver |
|
|
86
89
|
|
|
87
90
|
```bash
|
|
@@ -98,16 +101,16 @@ pins `cleo<2` while `fastapi-startkit` requires `cleo>=2.1`.
|
|
|
98
101
|
## Quickstart
|
|
99
102
|
|
|
100
103
|
```python
|
|
101
|
-
from fastapi import Depends
|
|
102
|
-
from fastapi_startkit_auth import
|
|
104
|
+
from fastapi import Depends, FastAPI
|
|
105
|
+
from fastapi_startkit_auth import (
|
|
106
|
+
AuthConfig, AuthOAuth2Provider, AuthProvider, OAuth2Config, current_user, require_scopes,
|
|
107
|
+
)
|
|
103
108
|
from myapp.models import User # any active-record-style model
|
|
104
109
|
|
|
105
110
|
|
|
106
111
|
class Config(AuthConfig):
|
|
107
|
-
key = "change-me-to-a-long-random-secret" # JWT signing key
|
|
108
|
-
|
|
109
112
|
default = {"guard": "api", "passwords": "users"}
|
|
110
|
-
guards = {"api": {"driver": "
|
|
113
|
+
guards = {"api": {"driver": "oauth2", "provider": "users"}}
|
|
111
114
|
providers = {"users": {"driver": "masoniteorm", "model": User}}
|
|
112
115
|
passwords = {
|
|
113
116
|
"users": {"provider": "users", "table": "password_reset_tokens",
|
|
@@ -115,49 +118,76 @@ class Config(AuthConfig):
|
|
|
115
118
|
}
|
|
116
119
|
|
|
117
120
|
|
|
118
|
-
app =
|
|
119
|
-
|
|
121
|
+
app = FastAPI()
|
|
122
|
+
AuthProvider(Config).register(app) # always first
|
|
123
|
+
AuthOAuth2Provider(OAuth2Config(key="change-me-to-a-long-random-secret")).register(app)
|
|
120
124
|
|
|
121
125
|
|
|
122
|
-
@
|
|
126
|
+
@app.get("/me")
|
|
123
127
|
def me(user=Depends(current_user)):
|
|
124
128
|
return user
|
|
125
129
|
|
|
126
130
|
|
|
127
|
-
@
|
|
131
|
+
@app.get("/reports")
|
|
128
132
|
def reports(ctx=Depends(require_scopes("reports:read"))):
|
|
129
133
|
return {"ok": True}
|
|
130
134
|
```
|
|
131
135
|
|
|
132
|
-
|
|
136
|
+
`current_user`, `optional_user`, `auth` and `require_scopes` resolve the request
|
|
137
|
+
through the default guard (`AuthConfig.default["guard"]`); use `Auth.user("name")`
|
|
138
|
+
to read another guard.
|
|
133
139
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
140
|
+
In a Startkit application, list `AuthProvider` and then the feature providers in
|
|
141
|
+
the application's providers instead:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from fastapi_startkit import Application
|
|
145
|
+
|
|
146
|
+
app = Application(
|
|
147
|
+
providers=[
|
|
148
|
+
(AuthProvider, Config),
|
|
149
|
+
(AuthOAuth2Provider, OAuth2Config(key="change-me-to-a-long-random-secret")),
|
|
150
|
+
]
|
|
151
|
+
)
|
|
137
152
|
```
|
|
138
153
|
|
|
154
|
+
See [Registering the providers](authentication.md#registering-the-providers).
|
|
155
|
+
|
|
139
156
|
## Configuration
|
|
140
157
|
|
|
141
|
-
`AuthConfig` mirrors Laravel's `config/auth.php
|
|
158
|
+
`AuthConfig` mirrors Laravel's `config/auth.php`: guards, user providers and
|
|
159
|
+
password brokers. Each feature has its own config, handed to its provider:
|
|
160
|
+
|
|
161
|
+
| Provider | Config | Enables |
|
|
162
|
+
| --- | --- | --- |
|
|
163
|
+
| `AuthSessionProvider` | `SessionConfig` | cookie sessions (`"session"` guards), CSRF |
|
|
164
|
+
| `AuthOAuth2Provider` | `OAuth2Config` | the OAuth 2.1 server (`"oauth2"` guards; `"passport"` is an alias) |
|
|
165
|
+
| `AuthApiTokenProvider` | `ApiTokenConfig` | opaque API tokens (`"token"` guards), SPA mode |
|
|
142
166
|
|
|
143
167
|
```python
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
#
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
168
|
+
OAuth2Config(
|
|
169
|
+
key=None, # JWT secret (required in production)
|
|
170
|
+
algorithm="HS256",
|
|
171
|
+
access_token_ttl=3600,
|
|
172
|
+
refresh_token_ttl=60 * 60 * 24 * 14,
|
|
173
|
+
authorization_code_ttl=600,
|
|
174
|
+
issuer=None, # stamps and verifies `iss`
|
|
175
|
+
resources=[], # RFC 8707 resource indicators
|
|
176
|
+
scopes={}, # scope catalog; empty accepts any scope
|
|
177
|
+
default_scopes=[],
|
|
178
|
+
pkce_methods=["S256"],
|
|
179
|
+
require_pkce=True,
|
|
180
|
+
require_redirect_uri=True,
|
|
181
|
+
grant_types=["authorization_code", "client_credentials", "refresh_token"],
|
|
182
|
+
tokens=OAuthTokensConfig(store="memory"),
|
|
183
|
+
clients=OAuthClientsConfig(store="database"), # the default; needs the migrations
|
|
184
|
+
)
|
|
159
185
|
```
|
|
160
186
|
|
|
187
|
+
Add `"password"` to `grant_types` to enable the legacy password grant (and the
|
|
188
|
+
`/token` endpoint). The OAuth settings formerly on `AuthConfig` (`key`,
|
|
189
|
+
`issuer`, `scopes`, ...) are still read from there with a `DeprecationWarning`.
|
|
190
|
+
|
|
161
191
|
### Provider drivers
|
|
162
192
|
|
|
163
193
|
| driver | meaning |
|
|
@@ -204,7 +234,7 @@ introspection re-check the token owner against it. A client without a
|
|
|
204
234
|
client bound to a different provider with `unauthorized_client`:
|
|
205
235
|
|
|
206
236
|
```python
|
|
207
|
-
client, secret = manager.client_repository.register(name="admin-panel", provider="admins")
|
|
237
|
+
client, secret = await manager.client_repository.register(name="admin-panel", provider="admins")
|
|
208
238
|
```
|
|
209
239
|
|
|
210
240
|
### ORM stores and migrations
|
|
@@ -217,20 +247,25 @@ in `fastapi-startkit[database]`). The package ships its own models
|
|
|
217
247
|
(`fastapi_startkit_auth.orm`) and uses no raw SQL:
|
|
218
248
|
|
|
219
249
|
```python
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
tokens
|
|
250
|
+
SessionConfig(store="database")
|
|
251
|
+
ApiTokenConfig(store="database")
|
|
252
|
+
OAuth2Config(tokens=OAuthTokensConfig(store="database", connection="auth")) # optional ORM connection name
|
|
223
253
|
```
|
|
224
254
|
|
|
255
|
+
`"database"` is the persistent store for all four configs. Only
|
|
256
|
+
`OAuthClientsConfig` uses it by default; `SessionConfig`, `ApiTokenConfig` and
|
|
257
|
+
`OAuthTokensConfig` default to `"memory"`.
|
|
258
|
+
|
|
225
259
|
`connection` names an entry of your database config; omit it to use the default
|
|
226
|
-
connection. Publish and run the migrations once per app
|
|
227
|
-
and
|
|
228
|
-
|
|
260
|
+
connection. Publish and run the migrations once per app. Each provider
|
|
261
|
+
publishes its own (all reversible and additive): sessions, personal API tokens,
|
|
262
|
+
and the OAuth access/refresh token, auth code and client tables plus the
|
|
263
|
+
`resource` and refresh `family_id` columns:
|
|
229
264
|
|
|
230
265
|
```bash
|
|
231
|
-
python artisan provider:publish -p auth # copies them to databases/migrations/
|
|
232
|
-
python artisan migrate
|
|
233
|
-
python artisan migrate:rollback
|
|
266
|
+
python artisan provider:publish -p auth-oauth2 # copies them to databases/migrations/
|
|
267
|
+
python artisan db:migrate
|
|
268
|
+
python artisan db:migrate:rollback # drops them again
|
|
234
269
|
```
|
|
235
270
|
|
|
236
271
|
Single-use guarantees rely on conditional `UPDATE`/`DELETE` statements issued
|
|
@@ -238,7 +273,8 @@ through the ORM query builder: refresh-token rotation and authorization-code
|
|
|
238
273
|
redemption each have exactly one winner under concurrency.
|
|
239
274
|
|
|
240
275
|
The former `sql` and `async_sql` stores were removed; configuring them raises a
|
|
241
|
-
`ValueError` pointing at `"
|
|
276
|
+
`ValueError` pointing at `"database"`. `"orm"` is a deprecated alias of
|
|
277
|
+
`"database"` and emits a `DeprecationWarning`. Use `"memory"` or an `"instance"` store for
|
|
242
278
|
sync setups.
|
|
243
279
|
|
|
244
280
|
In mixed setups (async stores with a sync provider or a sync session store),
|
|
@@ -256,13 +292,15 @@ one bcrypt verify, the same as a wrong password.
|
|
|
256
292
|
|
|
257
293
|
| Method & path | Purpose |
|
|
258
294
|
| --- | --- |
|
|
259
|
-
| `
|
|
260
|
-
| `POST /token` |
|
|
261
|
-
| `POST /
|
|
262
|
-
| `
|
|
263
|
-
| `POST /oauth/
|
|
264
|
-
| `POST
|
|
265
|
-
| `POST
|
|
295
|
+
| `GET /.well-known/oauth-authorization-server` | RFC 8414 server metadata |
|
|
296
|
+
| `POST /oauth/token` | Token endpoint: `authorization_code`, `refresh_token`, `client_credentials` (+ `password` when enabled) |
|
|
297
|
+
| `POST /token` | Simple password grant (only when `"password"` is enabled) |
|
|
298
|
+
| `GET /oauth/authorize` | Validate an auth-code request for your consent screen |
|
|
299
|
+
| `POST /oauth/authorize` | Issue a code once the user consents (`"approved": true`) → `code`, `iss` |
|
|
300
|
+
| `POST /oauth/introspect` | RFC 7662 introspection (**confidential client authentication**) |
|
|
301
|
+
| `POST /oauth/revoke` | RFC 7009 revocation of a token and its chain (**client authentication**) |
|
|
302
|
+
| `GET/DELETE /oauth/tokens`, `DELETE /oauth/tokens/{jti}` | The user's OAuth tokens |
|
|
303
|
+
| `POST/GET/DELETE /oauth/personal-access-tokens`, `DELETE .../{jti}` | Personal access tokens |
|
|
266
304
|
| `POST /password/email` | Trigger a password-reset token (delivered out-of-band; see below) |
|
|
267
305
|
| `POST /password/reset` | Reset the password with a token |
|
|
268
306
|
|
|
@@ -278,12 +316,21 @@ class AuthConfig(BaseAuthConfig):
|
|
|
278
316
|
# debug_expose_reset_token = True # DEV ONLY: echo the token in the response
|
|
279
317
|
```
|
|
280
318
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
319
|
+
PKCE is required by default (`require_pkce=True`, `pkce_methods=["S256"]`):
|
|
320
|
+
send `code_challenge_method=S256` with a 43-character challenge, and a verifier
|
|
321
|
+
of 43–128 characters at exchange. A missing method means `plain`.
|
|
322
|
+
|
|
323
|
+
Clients are created with `python artisan auth:oauth2:client --name app
|
|
324
|
+
--redirect-uri https://app/cb [--public] [--scopes "read write"]` or
|
|
325
|
+
`manager.client_repository.register(...)`; there are no `/oauth/clients` HTTP
|
|
326
|
+
routes. Refresh tokens are bound to their client. Replaying a rotated refresh
|
|
327
|
+
token revokes its whole family, whoever presents it; see
|
|
328
|
+
[Refresh tokens](authentication.md#refresh-tokens).
|
|
284
329
|
|
|
285
330
|
### Example: password grant
|
|
286
331
|
|
|
332
|
+
With `"password"` in `OAuth2Config.grant_types`:
|
|
333
|
+
|
|
287
334
|
```bash
|
|
288
335
|
curl -X POST localhost:8000/oauth/token \
|
|
289
336
|
-d grant_type=password -d username=ada@example.com -d password=secret -d scope="read write"
|
|
@@ -293,8 +340,8 @@ curl -X POST localhost:8000/oauth/token \
|
|
|
293
340
|
|
|
294
341
|
### Example: authorization code + PKCE
|
|
295
342
|
|
|
296
|
-
1. `POST /oauth/authorize` with
|
|
297
|
-
2. `POST /oauth/token` with `grant_type=authorization_code`, the `code`, and the `code_verifier
|
|
343
|
+
1. `POST /oauth/authorize` as the signed-in user (resolved through `OAuth2Config.authorization_guard`, default: the default guard) with `client_id`, `redirect_uri`, `approved: true`, `code_challenge` and `code_challenge_method: S256` → returns a single-use `code` and `iss`.
|
|
344
|
+
2. `POST /oauth/token` with `grant_type=authorization_code`, `client_id`, the `code`, `redirect_uri` and the `code_verifier` (plus the secret for a confidential client).
|
|
298
345
|
|
|
299
346
|
## Scopes / abilities
|
|
300
347
|
|