fastapi-startkit-auth 0.6.3__tar.gz → 0.8.0__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.
Files changed (135) hide show
  1. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/CHANGELOG.md +53 -1
  2. fastapi_startkit_auth-0.8.0/PKG-INFO +67 -0
  3. fastapi_startkit_auth-0.8.0/README.md +23 -0
  4. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/pyproject.toml +2 -2
  5. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/__init__.py +50 -24
  6. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/apitokens/manager.py +3 -77
  7. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/commands/oauth2_client.py +2 -2
  8. fastapi_startkit_auth-0.8.0/src/fastapi_startkit_auth/concurrency.py +22 -0
  9. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/config/__init__.py +6 -0
  10. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/config/auth.py +45 -19
  11. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/dependencies.py +51 -6
  12. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/exceptions.py +41 -16
  13. fastapi_startkit_auth-0.8.0/src/fastapi_startkit_auth/facade.py +95 -0
  14. fastapi_startkit_auth-0.8.0/src/fastapi_startkit_auth/grants/__init__.py +13 -0
  15. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/grants/authorization_code.py +10 -101
  16. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/grants/client_credentials.py +3 -21
  17. fastapi_startkit_auth-0.8.0/src/fastapi_startkit_auth/grants/password.py +56 -0
  18. fastapi_startkit_auth-0.8.0/src/fastapi_startkit_auth/grants/refresh.py +59 -0
  19. fastapi_startkit_auth-0.8.0/src/fastapi_startkit_auth/guards/__init__.py +13 -0
  20. fastapi_startkit_auth-0.8.0/src/fastapi_startkit_auth/guards/chain.py +77 -0
  21. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/guards/guard.py +22 -41
  22. fastapi_startkit_auth-0.8.0/src/fastapi_startkit_auth/guards/session.py +79 -0
  23. fastapi_startkit_auth-0.8.0/src/fastapi_startkit_auth/guards/token.py +63 -0
  24. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/manager.py +108 -110
  25. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/middleware/csrf.py +2 -2
  26. fastapi_startkit_auth-0.8.0/src/fastapi_startkit_auth/passwords/__init__.py +2 -0
  27. fastapi_startkit_auth-0.8.0/src/fastapi_startkit_auth/passwords/broker.py +70 -0
  28. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/policy.py +12 -12
  29. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/provider.py +4 -4
  30. fastapi_startkit_auth-0.8.0/src/fastapi_startkit_auth/providers/base.py +51 -0
  31. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/providers/fields.py +1 -8
  32. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/providers/memory.py +12 -10
  33. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/providers/model.py +3 -64
  34. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/routes.py +38 -38
  35. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/routes_password.py +2 -2
  36. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/security/jwt.py +2 -2
  37. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/sessions/facade.py +2 -2
  38. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/tokens/service.py +9 -182
  39. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/tests/conftest.py +2 -2
  40. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/tests/test_api_tokens.py +87 -87
  41. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/tests/test_async_auth.py +50 -61
  42. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/tests/test_async_refresh_race.py +9 -9
  43. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/tests/test_async_safety.py +93 -100
  44. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/tests/test_class_config.py +3 -3
  45. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/tests/test_client_scopes.py +15 -15
  46. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/tests/test_facade_api.py +13 -13
  47. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/tests/test_grants.py +43 -43
  48. fastapi_startkit_auth-0.8.0/tests/test_guard_dependency.py +298 -0
  49. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/tests/test_integration.py +24 -0
  50. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/tests/test_jwt.py +7 -7
  51. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/tests/test_manager.py +17 -17
  52. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/tests/test_model_provider.py +8 -8
  53. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/tests/test_model_provider_postgres.py +3 -23
  54. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/tests/test_multi_provider.py +21 -23
  55. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/tests/test_oauth21.py +58 -57
  56. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/tests/test_oauth2_client_command.py +3 -3
  57. fastapi_startkit_auth-0.8.0/tests/test_oauth_config_aliases.py +29 -0
  58. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/tests/test_oauth_hardening.py +67 -67
  59. fastapi_startkit_auth-0.8.0/tests/test_password_reset.py +88 -0
  60. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/tests/test_resource_binding.py +103 -103
  61. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/tests/test_security_fixes.py +6 -6
  62. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/tests/test_session_auth.py +37 -37
  63. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/tests/test_spa_csrf.py +5 -5
  64. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/tests/test_startkit_provider.py +3 -3
  65. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/tests/test_store_names.py +9 -9
  66. fastapi_startkit_auth-0.8.0/tests/test_token_service.py +114 -0
  67. fastapi_startkit_auth-0.8.0/tests/test_user_provider.py +76 -0
  68. fastapi_startkit_auth-0.6.3/PKG-INFO +0 -398
  69. fastapi_startkit_auth-0.6.3/README.md +0 -354
  70. fastapi_startkit_auth-0.6.3/src/fastapi_startkit_auth/concurrency.py +0 -48
  71. fastapi_startkit_auth-0.6.3/src/fastapi_startkit_auth/facade.py +0 -200
  72. fastapi_startkit_auth-0.6.3/src/fastapi_startkit_auth/grants/__init__.py +0 -17
  73. fastapi_startkit_auth-0.6.3/src/fastapi_startkit_auth/grants/password.py +0 -96
  74. fastapi_startkit_auth-0.6.3/src/fastapi_startkit_auth/grants/refresh.py +0 -92
  75. fastapi_startkit_auth-0.6.3/src/fastapi_startkit_auth/guards/__init__.py +0 -14
  76. fastapi_startkit_auth-0.6.3/src/fastapi_startkit_auth/guards/session.py +0 -149
  77. fastapi_startkit_auth-0.6.3/src/fastapi_startkit_auth/guards/token.py +0 -112
  78. fastapi_startkit_auth-0.6.3/src/fastapi_startkit_auth/passwords/__init__.py +0 -4
  79. fastapi_startkit_auth-0.6.3/src/fastapi_startkit_auth/passwords/broker.py +0 -131
  80. fastapi_startkit_auth-0.6.3/src/fastapi_startkit_auth/providers/base.py +0 -118
  81. fastapi_startkit_auth-0.6.3/tests/test_password_reset.py +0 -88
  82. fastapi_startkit_auth-0.6.3/tests/test_sync_regression.py +0 -227
  83. fastapi_startkit_auth-0.6.3/tests/test_token_service.py +0 -114
  84. fastapi_startkit_auth-0.6.3/tests/test_user_provider.py +0 -76
  85. /fastapi_startkit_auth-0.6.3/LICENSE → /fastapi_startkit_auth-0.8.0/LICENSE.md +0 -0
  86. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/apitokens/__init__.py +0 -0
  87. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/apitokens/facade.py +0 -0
  88. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/apitokens/models.py +0 -0
  89. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/apitokens/orm.py +0 -0
  90. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/apitokens/repository.py +0 -0
  91. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/clients/__init__.py +0 -0
  92. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/clients/credentials.py +0 -0
  93. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/clients/models.py +0 -0
  94. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/clients/orm.py +0 -0
  95. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/clients/repository.py +0 -0
  96. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/commands/__init__.py +0 -0
  97. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/grants/pkce.py +0 -0
  98. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/middleware/__init__.py +0 -0
  99. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/middleware/context.py +0 -0
  100. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/middleware/session.py +0 -0
  101. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/orm.py +0 -0
  102. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/passwords/repository.py +0 -0
  103. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/providers/__init__.py +0 -0
  104. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/publishable/cors.py +0 -0
  105. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/publishable/migrations/2026_09_30_000001_create_sessions_table.py +0 -0
  106. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/publishable/migrations/2026_09_30_000002_create_personal_api_tokens_table.py +0 -0
  107. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/publishable/migrations/2026_09_30_000003_create_oauth_access_tokens_table.py +0 -0
  108. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/publishable/migrations/2026_09_30_000004_create_oauth_refresh_tokens_table.py +0 -0
  109. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/publishable/migrations/2026_09_30_000005_create_oauth_auth_codes_table.py +0 -0
  110. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/publishable/migrations/2026_10_02_000001_add_resource_to_oauth_tables.py +0 -0
  111. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/publishable/migrations/2026_10_04_000001_create_oauth_clients_table.py +0 -0
  112. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/publishable/migrations/2026_10_04_000002_add_family_id_to_oauth_refresh_tokens_table.py +0 -0
  113. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/publishable/migrations/2026_10_05_000001_add_scopes_to_oauth_clients_table.py +0 -0
  114. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/py.typed +0 -0
  115. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/request_context.py +0 -0
  116. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/routes_spa.py +0 -0
  117. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/scoped_method.py +0 -0
  118. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/security/__init__.py +0 -0
  119. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/security/hashing.py +0 -0
  120. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/sessions/__init__.py +0 -0
  121. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/sessions/models.py +0 -0
  122. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/sessions/orm.py +0 -0
  123. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/sessions/state.py +0 -0
  124. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/sessions/store.py +0 -0
  125. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/tokens/__init__.py +0 -0
  126. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/tokens/models.py +0 -0
  127. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/tokens/orm.py +0 -0
  128. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/src/fastapi_startkit_auth/tokens/repository.py +0 -0
  129. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/tests/test_async_stores.py +0 -0
  130. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/tests/test_clients.py +0 -0
  131. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/tests/test_guard_registry.py +0 -0
  132. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/tests/test_hashing.py +0 -0
  133. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/tests/test_reset_throttle_enumeration.py +0 -0
  134. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/tests/test_session_store.py +0 -0
  135. {fastapi_startkit_auth-0.6.3 → fastapi_startkit_auth-0.8.0}/tests/test_token_repository.py +0 -0
@@ -11,6 +11,26 @@ Release notes are also published on
11
11
 
12
12
  Each item lists what changed and what to do.
13
13
 
14
+ - **The package is async-only; every sync class is removed.** The sync
15
+ `Auth`, `SessionGuard`, `PassportGuard`, `TokenGuard`, `ChainGuard`,
16
+ `TokenService`, `ApiTokenManager`, `PasswordBroker`, `PasswordGrant`,
17
+ `ClientCredentialsGrant`, `AuthorizationCodeGrant`, `RefreshTokenGrant`,
18
+ `ModelUserProvider` and `UserProvider` are gone, and each `Async*` class now
19
+ carries the plain name (`AsyncAuth` is `Auth`, `AsyncTokenService` is
20
+ `TokenService`, and so on). `user_is_active_async`, `find_user_async`,
21
+ `active_user_async` and `AuthManager.owner_provider_async` lose their
22
+ `_async` suffix; `AuthManager.owner_provider` is now a coroutine.
23
+ `AsyncMisconfiguration`, `ensure_sync`, `is_async_provider`,
24
+ `guard_is_async`, `is_async` and `has_async_methods` are removed, and the
25
+ `"async_model"` provider driver is dropped in favour of `"model"`;
26
+ configuring it raises a `ValueError` naming the replacement. The `"model"`
27
+ driver accepts sync and async models: a sync model's `find`,
28
+ `where(...).first()` and `save()` run in the threadpool.
29
+ `InMemoryUserProvider` methods are coroutines. User-supplied stores,
30
+ listeners and `is_active` hooks may still be sync; they run in the
31
+ threadpool. *Migrate:* import the plain names, `await` every `Auth`, guard,
32
+ provider, broker, grant and token-service call, and change
33
+ `driver="async_model"` to `driver="model"`.
14
34
  - **Feature providers replace `Application` and `AuthServiceProvider`.**
15
35
  Register `AuthProvider` first, then the features your guards use:
16
36
  `AuthSessionProvider(SessionConfig)`, `AuthOAuth2Provider(OAuth2Config)` and
@@ -66,7 +86,7 @@ Each item lists what changed and what to do.
66
86
  in `SessionConfig.csrf_exempt_paths`.
67
87
  - **OAuth client store defaults to `"database"`** (the ORM `oauth_clients`
68
88
  table). *Migrate:* publish and run the migrations, or set
69
- `OAuth2Config(clients=OAuthClientsConfig(store="memory"))`.
89
+ `OAuth2Config(clients=OAuthClientConfig(store="memory"))`.
70
90
  - **The in-memory OAuth client store is async.** `InMemoryClientRepository`
71
91
  methods (`register`, `add`, `find`, `all`, `authenticate`, `revoke`,
72
92
  `delete`) are coroutines, like `OrmClientRepository`, and `store="memory"` now
@@ -74,6 +94,14 @@ Each item lists what changed and what to do.
74
94
  synchronous custom store passed with `store="instance"` keeps the sync grants.
75
95
  *Migrate:*
76
96
  `await` client-store calls and the `handle` / `issue_code` of those grants.
97
+ - **Exception classes take an `Exception` suffix.** `AuthError` is now
98
+ `AuthException`, and every subclass gets the suffix (`InvalidToken` →
99
+ `InvalidTokenException`, `InsufficientScope` → `MissingScopeException`, and so
100
+ on). Wire error codes and HTTP statuses are unchanged. The old names remain as
101
+ deprecated aliases. *Migrate:* import the `*Exception` names.
102
+ - **Bad password on the password grant raises `InvalidCredentialsException`**, a
103
+ subclass of `InvalidGrantException`. The response is unchanged (`invalid_grant`,
104
+ 400), so existing `except InvalidGrantException` blocks still catch it.
77
105
  - **Persistent stores are named `"database"`.** `store="orm"` still works for
78
106
  sessions, API tokens, OAuth tokens and OAuth clients, but emits a
79
107
  `DeprecationWarning` and will be removed in a future release. *Migrate:*
@@ -82,6 +110,18 @@ Each item lists what changed and what to do.
82
110
  `revoke_token_chain`, and `store_refresh_token` takes `family_id`.
83
111
  - Error responses carry `Cache-Control: no-store`; `invalid_client` challenges
84
112
  with `WWW-Authenticate: Basic`.
113
+ - **Class-level `Auth.guard(name)` is a FastAPI dependency.**
114
+ `Auth.guard(name)` / `AsyncAuth.guard(name)` on the class now returns a
115
+ `GuardDependency`: `user = Depends(Auth.guard("api"))` resolves the user
116
+ through that guard and raises 401 otherwise, and equal names compare equal,
117
+ so `app.dependency_overrides[Auth.guard("api")]` works. Inside a request,
118
+ attribute access (`provider`, `user_from_token`, `authenticate`, ...) still
119
+ reaches the named guard, but the value is no longer the guard object itself,
120
+ so identity and `isinstance` checks fail, and outside a request attributes
121
+ raise `AttributeError`. On an instance (`auth.guard(name)` from
122
+ `Depends(Auth.scoped)`) it still returns the guard. *Migrate:* where you need
123
+ the guard object, use `auth.guard(name)` from `Depends(Auth.scoped)` or
124
+ `manager.guard(name)`.
85
125
 
86
126
  ### Migrations
87
127
 
@@ -96,6 +136,18 @@ using the ORM stores:
96
136
 
97
137
  ### Added
98
138
 
139
+ - `ChainGuard(guards=[...])` (`{"driver": "chain"}`): authenticates through the
140
+ first member guard that accepts the request, e.g. a bearer token backed by
141
+ the browser session. A token the request presents is final for every member
142
+ type (oauth2 bearer, or an API token in `Authorization` or a custom header):
143
+ when the member that reads it rejects it, the chain fails instead of trying
144
+ the session. `user_from_token` skips session members.
145
+ - `OAuth2Guard(first_party_only=True)` refuses access tokens issued to an OAuth
146
+ client (`client_id` set), so a first-party API accepts only the
147
+ application's own login and personal tokens.
148
+ - `AuthManager.on_authenticated(listener)`: `listener(request, user, guard)`
149
+ runs whenever a guard dependency resolves a user, before the route handler,
150
+ for activity tracking or request-scoped state.
99
151
  - Refresh token families with reuse detection: replaying a rotated refresh token
100
152
  revokes the whole family, whoever presents it: the owning client, another
101
153
  authenticated client, or no client at all for unbound password-grant tokens.
@@ -0,0 +1,67 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastapi-startkit-auth
3
+ Version: 0.8.0
4
+ Summary: Laravel-style authentication for FastAPI: OAuth2/JWT authorization server, cookie/session guard, SPA + CSRF protection, and Sanctum-style API tokens.
5
+ Keywords: fastapi,oauth2,jwt,auth,passport,sanctum,authentication,authorization,security,tokens,session,cookie,csrf,spa,starter-kit
6
+ Author: Bedram Tamang
7
+ Author-email: Bedram Tamang <tmgbedu@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE.md
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Framework :: FastAPI
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Internet :: WWW/HTTP :: Session
20
+ Classifier: Topic :: Security
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Typing :: Typed
23
+ Requires-Dist: fastapi>=0.110
24
+ Requires-Dist: pyjwt>=2.10.1
25
+ Requires-Dist: bcrypt>=4.1
26
+ Requires-Dist: pydantic>=2
27
+ Requires-Dist: python-multipart>=0.0.9
28
+ Requires-Dist: masonite-orm>=2.18 ; extra == 'masoniteorm'
29
+ Requires-Dist: fastapi-startkit[database]>=0.60,<1.0 ; python_full_version >= '3.12' and extra == 'startkit'
30
+ Requires-Dist: pytest>=8 ; extra == 'test'
31
+ Requires-Dist: pytest-asyncio>=0.23 ; extra == 'test'
32
+ Requires-Dist: httpx>=0.27 ; extra == 'test'
33
+ Requires-Dist: aiosqlite>=0.20 ; extra == 'test'
34
+ Requires-Dist: asyncpg>=0.29 ; extra == 'test'
35
+ Requires-Python: >=3.10
36
+ Project-URL: Homepage, https://github.com/fastapi-startkit/fastapi-startkit-auth
37
+ Project-URL: Repository, https://github.com/fastapi-startkit/fastapi-startkit-auth
38
+ Project-URL: Issues, https://github.com/fastapi-startkit/fastapi-startkit-auth/issues
39
+ Project-URL: Changelog, https://github.com/fastapi-startkit/fastapi-startkit-auth/blob/main/CHANGELOG.md
40
+ Provides-Extra: masoniteorm
41
+ Provides-Extra: startkit
42
+ Provides-Extra: test
43
+ Description-Content-Type: text/markdown
44
+
45
+ # FastAPI Startkit Auth
46
+
47
+ FastAPI Startkit Auth provides authentication for FastAPI: an OAuth2/JWT authorization server, cookie/session guards, SPA + CSRF protection, and API tokens.
48
+
49
+ ## Official Documentation
50
+
51
+ Documentation for FastAPI Startkit Auth can be found on the [FastAPI Startkit website](https://fastapi-startkit.github.io/docs/authentication).
52
+
53
+ ## Contributing
54
+
55
+ Thank you for considering contributing to FastAPI Startkit Auth! The contribution guide can be found in the [FastAPI Startkit documentation](https://fastapi-startkit.github.io/docs/authentication).
56
+
57
+ ## Code of Conduct
58
+
59
+ In order to ensure that the FastAPI Startkit community is welcoming to all, please review and abide by the code of conduct in the [FastAPI Startkit documentation](https://fastapi-startkit.github.io/docs/authentication).
60
+
61
+ ## Security Vulnerabilities
62
+
63
+ If you discover a security vulnerability within FastAPI Startkit Auth, please report it privately to the maintainers rather than opening a public issue. Details can be found in the [FastAPI Startkit documentation](https://fastapi-startkit.github.io/docs/authentication).
64
+
65
+ ## License
66
+
67
+ FastAPI Startkit Auth is open-sourced software licensed under the [MIT license](LICENSE.md).
@@ -0,0 +1,23 @@
1
+ # FastAPI Startkit Auth
2
+
3
+ FastAPI Startkit Auth provides authentication for FastAPI: an OAuth2/JWT authorization server, cookie/session guards, SPA + CSRF protection, and API tokens.
4
+
5
+ ## Official Documentation
6
+
7
+ Documentation for FastAPI Startkit Auth can be found on the [FastAPI Startkit website](https://fastapi-startkit.github.io/docs/authentication).
8
+
9
+ ## Contributing
10
+
11
+ Thank you for considering contributing to FastAPI Startkit Auth! The contribution guide can be found in the [FastAPI Startkit documentation](https://fastapi-startkit.github.io/docs/authentication).
12
+
13
+ ## Code of Conduct
14
+
15
+ In order to ensure that the FastAPI Startkit community is welcoming to all, please review and abide by the code of conduct in the [FastAPI Startkit documentation](https://fastapi-startkit.github.io/docs/authentication).
16
+
17
+ ## Security Vulnerabilities
18
+
19
+ If you discover a security vulnerability within FastAPI Startkit Auth, please report it privately to the maintainers rather than opening a public issue. Details can be found in the [FastAPI Startkit documentation](https://fastapi-startkit.github.io/docs/authentication).
20
+
21
+ ## License
22
+
23
+ FastAPI Startkit Auth is open-sourced software licensed under the [MIT license](LICENSE.md).
@@ -1,10 +1,10 @@
1
1
  [project]
2
2
  name = "fastapi-startkit-auth"
3
- version = "0.6.3"
3
+ version = "0.8.0"
4
4
  description = "Laravel-style authentication for FastAPI: OAuth2/JWT authorization server, cookie/session guard, SPA + CSRF protection, and Sanctum-style API tokens."
5
5
  readme = "README.md"
6
6
  license = "MIT"
7
- license-files = ["LICENSE"]
7
+ license-files = ["LICENSE.md"]
8
8
  keywords = [
9
9
  "fastapi",
10
10
  "oauth2",
@@ -8,21 +8,23 @@ Passport while staying idiomatic FastAPI:
8
8
  from __future__ import annotations
9
9
 
10
10
  from .apitokens.facade import ApiToken
11
- from .apitokens.manager import ApiTokenManager, AsyncApiTokenManager, NewApiToken
11
+ from .apitokens.manager import ApiTokenManager, NewApiToken
12
12
  from .apitokens.models import ApiTokenRecord
13
13
  from .apitokens.repository import ApiTokenRepository, InMemoryApiTokenRepository
14
- from .concurrency import AsyncMisconfiguration
15
14
  from .clients.models import Client
16
15
  from .clients.repository import InMemoryClientRepository
17
16
  from .config import (
18
17
  ApiTokenConfig,
19
18
  AuthConfig,
20
19
  OAuth2Config,
20
+ OAuthClientConfig,
21
21
  OAuthClientsConfig,
22
+ OAuthTokenConfig,
22
23
  OAuthTokensConfig,
23
24
  SessionConfig,
24
25
  )
25
26
  from .dependencies import (
27
+ GuardDependency,
26
28
  auth,
27
29
  current_user,
28
30
  optional_user,
@@ -31,44 +33,61 @@ from .dependencies import (
31
33
  )
32
34
  from .exceptions import (
33
35
  AccessDenied,
36
+ AccessDeniedException,
37
+ AuthenticationException,
34
38
  AuthError,
39
+ AuthException,
35
40
  CsrfTokenMismatch,
41
+ CsrfTokenMismatchException,
36
42
  InsufficientScope,
37
43
  InvalidClient,
44
+ InvalidClientException,
45
+ InvalidCredentialsException,
38
46
  InvalidGrant,
47
+ InvalidGrantException,
39
48
  InvalidScope,
49
+ InvalidScopeException,
40
50
  InvalidSession,
41
51
  InvalidTarget,
52
+ InvalidTargetException,
42
53
  InvalidToken,
54
+ InvalidTokenException,
55
+ MissingScopeException,
43
56
  UnauthorizedClient,
57
+ UnauthorizedClientException,
44
58
  UnsupportedGrantType,
59
+ UnsupportedGrantTypeException,
45
60
  UnsupportedResponseType,
61
+ UnsupportedResponseTypeException,
46
62
  )
47
63
  from .policy import GrantPolicy
48
- from .facade import AsyncAuth, Auth
49
- from .guards.guard import AsyncPassportGuard, PassportGuard
50
- from .guards.session import AsyncSessionGuard, SessionGuard
51
- from .guards.token import AsyncTokenGuard, TokenGuard
64
+ from .facade import Auth
65
+ from .guards.chain import ChainGuard
66
+ from .guards.guard import PassportGuard
67
+ from .guards.session import SessionGuard
68
+ from .guards.token import TokenGuard
52
69
  from .manager import AuthManager, FeatureNotRegistered
53
70
  from .middleware.context import AuthMiddleware
54
71
  from .middleware.csrf import CsrfMiddleware
55
72
  from .middleware.session import SessionMiddleware
56
- from .passwords.broker import AsyncPasswordBroker, PasswordBroker
73
+ from .passwords.broker import PasswordBroker
57
74
  from .provider import AuthApiTokenProvider, AuthOAuth2Provider, AuthProvider, AuthSessionProvider
58
75
  from .providers.memory import InMemoryUserProvider
59
- from .providers.model import AsyncModelUserProvider, ModelUserProvider
76
+ from .providers.model import ModelUserProvider
60
77
  from .sessions.facade import Session
61
78
  from .sessions.models import SessionRecord
62
79
  from .sessions.store import InMemorySessionStore, SessionStore
63
- from .tokens.service import AsyncTokenService, TokenService
80
+ from .tokens.service import TokenService
64
81
 
65
- __version__ = "0.6.3"
82
+ __version__ = "0.8.0"
66
83
 
67
84
  __all__ = (
68
85
  "AuthConfig",
69
86
  "SessionConfig",
70
87
  "OAuth2Config",
88
+ "OAuthClientConfig",
71
89
  "OAuthClientsConfig",
90
+ "OAuthTokenConfig",
72
91
  "OAuthTokensConfig",
73
92
  "ApiTokenConfig",
74
93
  "AuthProvider",
@@ -85,23 +104,37 @@ __all__ = (
85
104
  "InMemoryClientRepository",
86
105
  "OrmClientRepository",
87
106
  "OrmSessionStore",
88
- "AccessDenied",
89
- "UnauthorizedClient",
90
- "UnsupportedGrantType",
91
- "UnsupportedResponseType",
107
+ "AccessDeniedException",
108
+ "UnauthorizedClientException",
109
+ "UnsupportedGrantTypeException",
110
+ "UnsupportedResponseTypeException",
92
111
  "current_user",
112
+ "GuardDependency",
93
113
  "optional_user",
94
114
  "require_scopes",
95
115
  "require_abilities",
116
+ "AuthException",
117
+ "InvalidGrantException",
118
+ "InvalidClientException",
119
+ "InvalidCredentialsException",
120
+ "InvalidTokenException",
121
+ "MissingScopeException",
122
+ "InvalidScopeException",
123
+ "InvalidTargetException",
124
+ "GrantPolicy",
125
+ "AuthenticationException",
126
+ "CsrfTokenMismatchException",
127
+ "AccessDenied",
128
+ "UnauthorizedClient",
129
+ "UnsupportedGrantType",
130
+ "UnsupportedResponseType",
96
131
  "AuthError",
97
- "AsyncMisconfiguration",
98
132
  "InvalidGrant",
99
133
  "InvalidClient",
100
134
  "InvalidToken",
101
135
  "InsufficientScope",
102
136
  "InvalidScope",
103
137
  "InvalidTarget",
104
- "GrantPolicy",
105
138
  "InvalidSession",
106
139
  "CsrfTokenMismatch",
107
140
  "Auth",
@@ -117,19 +150,12 @@ __all__ = (
117
150
  "ApiTokenRecord",
118
151
  "ApiTokenRepository",
119
152
  "InMemoryApiTokenRepository",
120
- "AsyncAuth",
121
153
  "PassportGuard",
122
- "AsyncPassportGuard",
123
- "AsyncSessionGuard",
124
- "AsyncTokenGuard",
125
- "AsyncApiTokenManager",
154
+ "ChainGuard",
126
155
  "PasswordBroker",
127
- "AsyncPasswordBroker",
128
156
  "InMemoryUserProvider",
129
157
  "ModelUserProvider",
130
- "AsyncModelUserProvider",
131
158
  "TokenService",
132
- "AsyncTokenService",
133
159
  )
134
160
 
135
161
 
@@ -1,15 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import secrets
4
- import threading
5
4
  import time
6
5
  from dataclasses import dataclass
7
6
  from typing import Any
8
7
 
9
8
  from ..concurrency import call
10
- from ..exceptions import InvalidToken
9
+ from ..exceptions import InvalidTokenException
11
10
  from .models import ApiTokenRecord
12
- from .repository import ApiTokenRepository, generate_token_secret, hash_token_secret
11
+ from .repository import generate_token_secret, hash_token_secret
13
12
 
14
13
  # Every verification failure raises this exact message so responses (and their
15
14
  # timing, via the dummy compare below) never reveal whether the id exists.
@@ -29,7 +28,7 @@ def _check_secret(record: ApiTokenRecord | None, secret: str) -> None:
29
28
  expected = record.token_hash if record is not None else _DUMMY_HASH
30
29
  matched = secrets.compare_digest(hash_token_secret(secret), expected)
31
30
  if record is None or not matched or record.expired:
32
- raise InvalidToken(_GENERIC_FAILURE)
31
+ raise InvalidTokenException(_GENERIC_FAILURE)
33
32
 
34
33
 
35
34
  def _creation_fields(
@@ -69,79 +68,6 @@ class NewApiToken:
69
68
 
70
69
 
71
70
  class ApiTokenManager:
72
- """Issues, verifies, revokes, and lists opaque API tokens.
73
-
74
- Exposed as ``AuthManager.api_tokens``; apps wire their own management
75
- routes around it (the package ships none, per the app-owns-routes
76
- decision).
77
- """
78
-
79
- def __init__(
80
- self,
81
- repository: ApiTokenRepository,
82
- default_ttl: float | None = None,
83
- purge_interval: float = 300,
84
- ) -> None:
85
- self.repository = repository
86
- self._default_ttl = default_ttl
87
- self._purge_interval = purge_interval
88
- self._purge_lock = threading.Lock()
89
- self._last_purge = time.time()
90
-
91
- def _maybe_purge(self) -> None:
92
- """Drop expired records periodically so stores stay bounded without a
93
- scheduler (mirrors the purge-on-issue pattern of ``TokenService``)."""
94
- now = time.time()
95
- with self._purge_lock:
96
- if now - self._last_purge < self._purge_interval:
97
- return
98
- self._last_purge = now
99
- self.repository.purge_expired()
100
-
101
- def create(
102
- self,
103
- user_id: Any,
104
- name: str | None = None,
105
- abilities: list[str] | None = None,
106
- expires_at: float | None = None,
107
- ) -> NewApiToken:
108
- """Issue a token for ``user_id``; the plaintext is returned exactly once.
109
-
110
- ``expires_at`` overrides the configured default ttl; ``None`` with no
111
- configured ttl means the token never expires. ``abilities`` default to
112
- the unrestricted ``["*"]``.
113
- """
114
- self._maybe_purge()
115
- secret = generate_token_secret()
116
- record = self.repository.create(
117
- **_creation_fields(user_id, secret, name, abilities, expires_at, self._default_ttl)
118
- )
119
- return NewApiToken(record=record, plain_text=f"{record.id}|{secret}")
120
-
121
- def verify(self, token: str) -> ApiTokenRecord:
122
- """Resolve ``"{id}|{secret}"`` to its live record or raise 401.
123
-
124
- Lookup is by id (O(1), no scan); the secret comparison is constant-time
125
- against the stored hash — or against a dummy hash when the id is
126
- unknown, so the miss path costs the same as a mismatch.
127
- """
128
- token_id, secret = _split_token(token)
129
- record = self.repository.find(token_id) if token_id else None
130
- _check_secret(record, secret)
131
- self.repository.touch(record.id)
132
- return record
133
-
134
- def revoke(self, token_id: str) -> bool:
135
- return self.repository.revoke(token_id)
136
-
137
- def revoke_all(self, user_id: Any) -> int:
138
- return self.repository.revoke_all_for_user(user_id)
139
-
140
- def tokens_for(self, user_id: Any) -> list[ApiTokenRecord]:
141
- return self.repository.list_for_user(user_id)
142
-
143
-
144
- class AsyncApiTokenManager:
145
71
  def __init__(self, repository: Any, default_ttl: float | None = None, purge_interval: float = 300) -> None:
146
72
  self.repository = repository
147
73
  self._default_ttl = default_ttl
@@ -5,7 +5,7 @@ from cleo.helpers import option
5
5
  from fastapi_startkit.console import Command
6
6
 
7
7
  from ..concurrency import call
8
- from ..exceptions import InvalidScope
8
+ from ..exceptions import InvalidScopeException
9
9
 
10
10
 
11
11
  def _valid_redirect(uri: str | None) -> bool:
@@ -45,7 +45,7 @@ class OAuth2ClientCommand(Command):
45
45
  return 1
46
46
  try:
47
47
  manager.grant_policy.check_scopes(scopes)
48
- except InvalidScope as error:
48
+ except InvalidScopeException as error:
49
49
  self.line_error(str(error))
50
50
  return 1
51
51
  client, secret = asyncio.run(
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from typing import Any, Callable
5
+
6
+ from starlette.concurrency import run_in_threadpool
7
+
8
+
9
+ async def resolve(value: Any) -> Any:
10
+ if inspect.isawaitable(value):
11
+ return await value
12
+ return value
13
+
14
+
15
+ async def call(function: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
16
+ if is_coroutine_callable(function):
17
+ return await function(*args, **kwargs)
18
+ return await resolve(await run_in_threadpool(function, *args, **kwargs))
19
+
20
+
21
+ def is_coroutine_callable(obj: Any) -> bool:
22
+ return inspect.iscoroutinefunction(obj) or inspect.iscoroutinefunction(getattr(obj, "__call__", None))
@@ -4,9 +4,12 @@ from .auth import (
4
4
  ApiTokenConfig,
5
5
  ApiTokenGuard,
6
6
  AuthConfig,
7
+ ChainGuard,
7
8
  OAuth2Config,
8
9
  OAuth2Guard,
10
+ OAuthClientConfig,
9
11
  OAuthClientsConfig,
12
+ OAuthTokenConfig,
10
13
  OAuthTokensConfig,
11
14
  SessionConfig,
12
15
  SessionGuard,
@@ -19,12 +22,15 @@ __all__ = (
19
22
  "AuthConfig",
20
23
  "SessionConfig",
21
24
  "OAuth2Config",
25
+ "OAuthClientConfig",
22
26
  "OAuthClientsConfig",
27
+ "OAuthTokenConfig",
23
28
  "OAuthTokensConfig",
24
29
  "ApiTokenConfig",
25
30
  "SessionGuard",
26
31
  "OAuth2Guard",
27
32
  "ApiTokenGuard",
33
+ "ChainGuard",
28
34
  "DEFAULT_GRANT_TYPES",
29
35
  "DEPRECATED_OAUTH2_ATTRIBUTES",
30
36
  "as_config_class",
@@ -8,14 +8,14 @@ from typing import Any, Literal, TypeVar
8
8
  ConfigT = TypeVar("ConfigT")
9
9
 
10
10
 
11
- def _canonical_store(store: str, section: str) -> str:
12
- if store == "orm":
11
+ def _canonical_driver(driver: str, section: str) -> str:
12
+ if driver == "orm":
13
13
  # stacklevel 4: this helper, __post_init__, the dataclass __init__, then the caller.
14
14
  warnings.warn(
15
- f'{section}(store="orm") is deprecated; use store="database".', DeprecationWarning, stacklevel=4
15
+ f'{section}(driver="orm") is deprecated; use driver="database".', DeprecationWarning, stacklevel=4
16
16
  )
17
17
  return "database"
18
- return store
18
+ return driver
19
19
 
20
20
 
21
21
  @dataclass
@@ -28,6 +28,7 @@ class SessionGuard:
28
28
  class OAuth2Guard:
29
29
  provider: str = "users"
30
30
  audience: str | None = None
31
+ first_party_only: bool = False
31
32
  driver: Literal["oauth2"] = "oauth2"
32
33
 
33
34
 
@@ -37,15 +38,23 @@ class ApiTokenGuard:
37
38
  driver: Literal["token"] = "token"
38
39
 
39
40
 
41
+ @dataclass
42
+ class ChainGuard:
43
+ """Authenticates through the first of ``guards`` (guard names) that accepts the request."""
44
+
45
+ guards: list[str] = field(default_factory=list)
46
+ driver: Literal["chain"] = "chain"
47
+
48
+
40
49
  @dataclass
41
50
  class SessionConfig:
42
51
  """Cookie sessions, enabled by ``AuthSessionProvider``.
43
52
 
44
- ``store`` is "memory", "database" (the ORM ``sessions`` table; "orm" is a
53
+ ``driver`` is "memory", "database" (the ORM ``sessions`` table; "orm" is a
45
54
  deprecated alias) or "instance" (a ready-made store under ``instance``).
46
55
  """
47
56
 
48
- store: str = "memory"
57
+ driver: str = "memory"
49
58
  connection: str | None = None
50
59
  instance: Any = None
51
60
  cookie: str = "startkit_session"
@@ -63,7 +72,7 @@ class SessionConfig:
63
72
  csrf_exempt_paths: list[str] = field(default_factory=list)
64
73
 
65
74
  def __post_init__(self) -> None:
66
- self.store = _canonical_store(self.store, "SessionConfig")
75
+ self.driver = _canonical_driver(self.driver, "SessionConfig")
67
76
 
68
77
 
69
78
  @dataclass
@@ -74,7 +83,7 @@ class ApiTokenConfig:
74
83
  accept the ``session_guard`` cookie session from those origins.
75
84
  """
76
85
 
77
- store: str = "memory"
86
+ driver: str = "memory"
78
87
  connection: str | None = None
79
88
  instance: Any = None
80
89
  header: str = "Authorization"
@@ -84,7 +93,7 @@ class ApiTokenConfig:
84
93
  session_guard: str = "web"
85
94
 
86
95
  def __post_init__(self) -> None:
87
- self.store = _canonical_store(self.store, "ApiTokenConfig")
96
+ self.driver = _canonical_driver(self.driver, "ApiTokenConfig")
88
97
 
89
98
  @property
90
99
  def stateful(self) -> bool:
@@ -92,25 +101,42 @@ class ApiTokenConfig:
92
101
 
93
102
 
94
103
  @dataclass
95
- class OAuthClientsConfig:
104
+ class OAuthClientConfig:
96
105
  """Where OAuth clients live: "database" (the ORM ``oauth_clients`` table), "memory" or "instance"."""
97
106
 
98
- store: str = "database"
107
+ driver: str = "database"
99
108
  connection: str | None = None
100
109
  instance: Any = None
101
110
 
102
111
  def __post_init__(self) -> None:
103
- self.store = _canonical_store(self.store, "OAuthClientsConfig")
112
+ self.driver = _canonical_driver(self.driver, "OAuthClientConfig")
104
113
 
105
114
 
106
115
  @dataclass
107
- class OAuthTokensConfig:
108
- store: str = "memory"
116
+ class OAuthTokenConfig:
117
+ driver: str = "memory"
109
118
  connection: str | None = None
110
119
  instance: Any = None
111
120
 
112
121
  def __post_init__(self) -> None:
113
- self.store = _canonical_store(self.store, "OAuthTokensConfig")
122
+ self.driver = _canonical_driver(self.driver, "OAuthTokenConfig")
123
+
124
+
125
+ def _deprecated_alias(config_type: type[ConfigT], old_name: str) -> type[ConfigT]:
126
+ class Alias(config_type):
127
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
128
+ warnings.warn(
129
+ f"{old_name} is deprecated; use {config_type.__name__}.", DeprecationWarning, stacklevel=2
130
+ )
131
+ super().__init__(*args, **kwargs)
132
+
133
+ Alias.__name__ = old_name
134
+ Alias.__qualname__ = old_name
135
+ return Alias
136
+
137
+
138
+ OAuthClientsConfig = _deprecated_alias(OAuthClientConfig, "OAuthClientsConfig")
139
+ OAuthTokensConfig = _deprecated_alias(OAuthTokenConfig, "OAuthTokensConfig")
114
140
 
115
141
 
116
142
  DEFAULT_GRANT_TYPES = ("authorization_code", "client_credentials", "refresh_token")
@@ -145,12 +171,12 @@ class OAuth2Config:
145
171
  require_redirect_uri: bool = True
146
172
  grant_types: list[str] = field(default_factory=lambda: list(DEFAULT_GRANT_TYPES))
147
173
  authorization_guard: str | None = None
148
- tokens: OAuthTokensConfig = field(default_factory=OAuthTokensConfig)
149
- clients: OAuthClientsConfig = field(default_factory=OAuthClientsConfig)
174
+ tokens: OAuthTokenConfig = field(default_factory=OAuthTokenConfig)
175
+ clients: OAuthClientConfig = field(default_factory=OAuthClientConfig)
150
176
 
151
177
  def __post_init__(self) -> None:
152
- self.tokens = resolve_config(self.tokens, OAuthTokensConfig)
153
- self.clients = resolve_config(self.clients, OAuthClientsConfig)
178
+ self.tokens = resolve_config(self.tokens, OAuthTokenConfig)
179
+ self.clients = resolve_config(self.clients, OAuthClientConfig)
154
180
 
155
181
 
156
182
  # v0.6 AuthConfig attributes that moved to OAuth2Config; still read, with a warning.