fastapi-fullauth 0.17.1__tar.gz → 0.17.2__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 (209) hide show
  1. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/CHANGELOG.md +8 -0
  2. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/PKG-INFO +1 -1
  3. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/docs/configuration.md +5 -0
  4. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/__init__.py +1 -1
  5. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/config.py +22 -13
  6. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/protection/challenges.py +7 -0
  7. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/protection/ratelimit.py +16 -2
  8. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/tests/test_config.py +48 -0
  9. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/tests/test_passkey.py +22 -0
  10. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/tests/test_security.py +56 -0
  11. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  12. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  13. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/.github/pull_request_template.md +0 -0
  14. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/.github/workflows/ci.yml +0 -0
  15. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/.github/workflows/docs.yml +0 -0
  16. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/.github/workflows/publish.yml +0 -0
  17. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/.gitignore +0 -0
  18. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/.python-version +0 -0
  19. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/AGENTS.md +0 -0
  20. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/CONTRIBUTING.md +0 -0
  21. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/LICENSE +0 -0
  22. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/Makefile +0 -0
  23. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/README.md +0 -0
  24. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/SECURITY.md +0 -0
  25. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/docs/adapters/beanie.md +0 -0
  26. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/docs/adapters/custom.md +0 -0
  27. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/docs/adapters/index.md +0 -0
  28. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/docs/adapters/sqlalchemy.md +0 -0
  29. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/docs/adapters/sqlmodel.md +0 -0
  30. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/docs/adapters/tortoise.md +0 -0
  31. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/docs/api-reference.md +0 -0
  32. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/docs/architecture.md +0 -0
  33. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/docs/auth/custom-claims.md +0 -0
  34. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/docs/auth/dependencies.md +0 -0
  35. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/docs/auth/hooks.md +0 -0
  36. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/docs/auth/passwords.md +0 -0
  37. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/docs/auth/sessions.md +0 -0
  38. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/docs/contributing.md +0 -0
  39. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/docs/customization.md +0 -0
  40. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/docs/frontend-integration.md +0 -0
  41. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/docs/getting-started.md +0 -0
  42. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/docs/index.md +0 -0
  43. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/docs/migrating-from-fastapi-users.md +0 -0
  44. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/docs/migrations.md +0 -0
  45. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/docs/oauth.md +0 -0
  46. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/docs/passkeys.md +0 -0
  47. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/docs/recipes.md +0 -0
  48. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/docs/security/middleware.md +0 -0
  49. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/docs/security/rate-limiting.md +0 -0
  50. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/docs/security/threat-model.md +0 -0
  51. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/docs/stylesheets/home.css +0 -0
  52. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/docs/testing.md +0 -0
  53. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/docs/troubleshooting.md +0 -0
  54. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/examples/beanie_app/__init__.py +0 -0
  55. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/examples/beanie_app/auth.py +0 -0
  56. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/examples/beanie_app/config.py +0 -0
  57. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/examples/beanie_app/main.py +0 -0
  58. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/examples/beanie_app/models.py +0 -0
  59. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/examples/beanie_app/routes.py +0 -0
  60. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/examples/sqlalchemy_app/__init__.py +0 -0
  61. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/examples/sqlalchemy_app/auth.py +0 -0
  62. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/examples/sqlalchemy_app/config.py +0 -0
  63. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/examples/sqlalchemy_app/main.py +0 -0
  64. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/examples/sqlalchemy_app/models.py +0 -0
  65. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/examples/sqlalchemy_app/routes.py +0 -0
  66. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/examples/sqlmodel_app/__init__.py +0 -0
  67. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/examples/sqlmodel_app/auth.py +0 -0
  68. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/examples/sqlmodel_app/config.py +0 -0
  69. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/examples/sqlmodel_app/main.py +0 -0
  70. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/examples/sqlmodel_app/models.py +0 -0
  71. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/examples/sqlmodel_app/routes.py +0 -0
  72. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/examples/tortoise_app/__init__.py +0 -0
  73. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/examples/tortoise_app/auth.py +0 -0
  74. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/examples/tortoise_app/config.py +0 -0
  75. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/examples/tortoise_app/main.py +0 -0
  76. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/examples/tortoise_app/models.py +0 -0
  77. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/examples/tortoise_app/routes.py +0 -0
  78. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/adapters/__init__.py +0 -0
  79. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/adapters/_sqlalchemy_base.py +0 -0
  80. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/adapters/base.py +0 -0
  81. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/adapters/beanie.py +0 -0
  82. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/adapters/sqlalchemy.py +0 -0
  83. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/adapters/sqlmodel.py +0 -0
  84. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/adapters/tortoise.py +0 -0
  85. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/backends/__init__.py +0 -0
  86. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/backends/base.py +0 -0
  87. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/backends/bearer.py +0 -0
  88. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/backends/cookie.py +0 -0
  89. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/cli.py +0 -0
  90. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/core/__init__.py +0 -0
  91. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/core/_redis.py +0 -0
  92. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/core/blacklist.py +0 -0
  93. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/core/crypto.py +0 -0
  94. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/core/tokens.py +0 -0
  95. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/dependencies/__init__.py +0 -0
  96. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/dependencies/current_user.py +0 -0
  97. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/dependencies/rbac.py +0 -0
  98. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/exceptions.py +0 -0
  99. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/flows/__init__.py +0 -0
  100. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/flows/change_password.py +0 -0
  101. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/flows/credentials.py +0 -0
  102. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/flows/email_verify.py +0 -0
  103. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/flows/login.py +0 -0
  104. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/flows/logout.py +0 -0
  105. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/flows/oauth.py +0 -0
  106. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/flows/passkey.py +0 -0
  107. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/flows/password_reset.py +0 -0
  108. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/flows/profile.py +0 -0
  109. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/flows/reauth.py +0 -0
  110. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/flows/refresh.py +0 -0
  111. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/flows/register.py +0 -0
  112. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/flows/sessions.py +0 -0
  113. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/flows/tokens.py +0 -0
  114. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/fullauth.py +0 -0
  115. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/hooks.py +0 -0
  116. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/middleware/__init__.py +0 -0
  117. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/middleware/csrf.py +0 -0
  118. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/middleware/ratelimit.py +0 -0
  119. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/middleware/security_headers.py +0 -0
  120. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/models/__init__.py +0 -0
  121. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/models/beanie/__init__.py +0 -0
  122. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/models/beanie/_common.py +0 -0
  123. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/models/beanie/base.py +0 -0
  124. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/models/beanie/oauth.py +0 -0
  125. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/models/beanie/passkey.py +0 -0
  126. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/models/beanie/permission.py +0 -0
  127. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/models/beanie/role.py +0 -0
  128. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/models/sqlalchemy/__init__.py +0 -0
  129. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/models/sqlalchemy/base.py +0 -0
  130. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/models/sqlalchemy/oauth.py +0 -0
  131. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/models/sqlalchemy/passkey.py +0 -0
  132. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/models/sqlalchemy/permission.py +0 -0
  133. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/models/sqlalchemy/role.py +0 -0
  134. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/models/sqlmodel/__init__.py +0 -0
  135. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/models/sqlmodel/base.py +0 -0
  136. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/models/sqlmodel/oauth.py +0 -0
  137. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/models/sqlmodel/passkey.py +0 -0
  138. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/models/sqlmodel/permission.py +0 -0
  139. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/models/sqlmodel/role.py +0 -0
  140. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/models/tortoise/__init__.py +0 -0
  141. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/models/tortoise/_common.py +0 -0
  142. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/models/tortoise/base.py +0 -0
  143. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/models/tortoise/oauth.py +0 -0
  144. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/models/tortoise/passkey.py +0 -0
  145. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/models/tortoise/permission.py +0 -0
  146. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/models/tortoise/role.py +0 -0
  147. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/oauth/__init__.py +0 -0
  148. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/oauth/base.py +0 -0
  149. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/oauth/discord.py +0 -0
  150. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/oauth/github.py +0 -0
  151. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/oauth/gitlab.py +0 -0
  152. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/oauth/google.py +0 -0
  153. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/protection/__init__.py +0 -0
  154. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/protection/lockout.py +0 -0
  155. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/py.typed +0 -0
  156. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/routers/__init__.py +0 -0
  157. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/routers/_schemas.py +0 -0
  158. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/routers/_transport.py +0 -0
  159. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/routers/admin.py +0 -0
  160. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/routers/auth.py +0 -0
  161. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/routers/oauth.py +0 -0
  162. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/routers/passkey.py +0 -0
  163. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/routers/profile.py +0 -0
  164. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/routers/sessions.py +0 -0
  165. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/routers/verify.py +0 -0
  166. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/types.py +0 -0
  167. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/utils.py +0 -0
  168. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/fastapi_fullauth/validators.py +0 -0
  169. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/mkdocs.yml +0 -0
  170. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/pyproject.toml +0 -0
  171. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/scripts/llmstxt_preprocess.py +0 -0
  172. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/skills/fastapi-fullauth/SKILL.md +0 -0
  173. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/skills/fastapi-fullauth/references/adapters.md +0 -0
  174. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/skills/fastapi-fullauth/references/api-reference.md +0 -0
  175. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/skills/fastapi-fullauth/references/composable-design.md +0 -0
  176. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/skills/fastapi-fullauth/references/getting-started.md +0 -0
  177. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/skills/fastapi-fullauth/references/hooks.md +0 -0
  178. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/skills/fastapi-fullauth/references/migrations.md +0 -0
  179. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/skills/fastapi-fullauth/references/oauth.md +0 -0
  180. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/skills/fastapi-fullauth/references/passkeys.md +0 -0
  181. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/skills/fastapi-fullauth/references/production.md +0 -0
  182. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/skills/fastapi-fullauth/references/rbac.md +0 -0
  183. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/skills/fastapi-fullauth/references/sessions.md +0 -0
  184. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/skills/fastapi-fullauth/references/testing.md +0 -0
  185. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/skills/fastapi-fullauth/references/troubleshooting.md +0 -0
  186. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/tests/__init__.py +0 -0
  187. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/tests/adapter_conformance.py +0 -0
  188. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/tests/conftest.py +0 -0
  189. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/tests/test_account_safety.py +0 -0
  190. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/tests/test_auth.py +0 -0
  191. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/tests/test_beanie_adapter.py +0 -0
  192. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/tests/test_cli.py +0 -0
  193. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/tests/test_cookie_backend.py +0 -0
  194. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/tests/test_crypto.py +0 -0
  195. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/tests/test_hooks.py +0 -0
  196. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/tests/test_oauth.py +0 -0
  197. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/tests/test_passkey_flows.py +0 -0
  198. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/tests/test_polish.py +0 -0
  199. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/tests/test_profile.py +0 -0
  200. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/tests/test_rbac.py +0 -0
  201. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/tests/test_session_revocation.py +0 -0
  202. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/tests/test_sessions.py +0 -0
  203. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/tests/test_sqlalchemy_adapter.py +0 -0
  204. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/tests/test_sqlmodel_adapter.py +0 -0
  205. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/tests/test_tokens.py +0 -0
  206. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/tests/test_tortoise_adapter.py +0 -0
  207. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/tests/test_tortoise_int_keys.py +0 -0
  208. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/tests/test_user_id_types.py +0 -0
  209. {fastapi_fullauth-0.17.1 → fastapi_fullauth-0.17.2}/uv.lock +0 -0
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.17.2
4
+
5
+ ### Fixed
6
+
7
+ - **The Redis challenge store raised on a non-positive TTL** where the in-memory one wrote an entry that was simply already expired. With `PASSKEY_CHALLENGE_TTL` set to `0`, beginning a passkey sign-in answered `500` on Redis, on a route that needs no account. Both backends now agree: nothing is stored and the next `pop()` returns `None`.
8
+ - **Numeric settings reject values that cannot work.** Durations, expiries, attempt counts and rate-limit caps are validated at construction, so a mistake fails at startup instead of somewhere unrelated later: `MAX_LOGIN_ATTEMPTS=0` locked every account on its first failed login, and `PASSKEY_CHALLENGE_TTL=0` raised from Redis mid-request. The three settings that document a meaningful zero keep it: `JWT_LEEWAY_SECONDS`, `PASSWORD_MAX_LENGTH` and `REAUTH_MAX_AGE_SECONDS`. **If your configuration holds one of these impossible values, the application now refuses to start and says which setting.**
9
+ - **Auth rate limits shared one bucket per IP on Redis.** Each route gets its own limiter with its own cap, but the key was the client IP alone, and every Redis-backed limiter writes into the same keyspace, so all six auth routes counted into a single sorted set. The smallest cap won for all of them: with `register` at 3, any three auth requests, a token refresh or a failed login included, returned `429` on the next registration attempt for the rest of the window. It also broke the separation `reauth` was given in 0.17.0, where failed password checks were meant not to touch the sign-in budget. The key now carries the route name. Only Redis was affected; the in-memory backend keeps its counts per limiter instance, which is why the test suite never saw it, and why the new regression test runs against both backends. **Note:** the key format changed, so existing counters reset once on deploy.
10
+
3
11
  ## 0.17.1
4
12
 
5
13
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: fastapi-fullauth
3
- Version: 0.17.1
3
+ Version: 0.17.2
4
4
  Summary: Production-grade, async-native authentication and authorization library for FastAPI
5
5
  Project-URL: Changelog, https://github.com/mdfarhankc/fastapi-fullauth/blob/main/CHANGELOG.md
6
6
  Project-URL: Documentation, https://mdfarhankc.github.io/fastapi-fullauth
@@ -124,6 +124,11 @@ If you want to be defensively explicit that no file is ever read, pass `FullAuth
124
124
 
125
125
  ## Reference
126
126
 
127
+ Durations, expiries, attempt counts and rate-limit caps must be at least 1, and a
128
+ value that cannot work is rejected when the config is constructed rather than
129
+ failing somewhere unrelated later. `JWT_LEEWAY_SECONDS`, `PASSWORD_MAX_LENGTH`
130
+ and `REAUTH_MAX_AGE_SECONDS` accept `0`, which means something for each of them.
131
+
127
132
  ### Core
128
133
 
129
134
  | Option | Type | Default | Description |
@@ -1,4 +1,4 @@
1
- __version__ = "0.17.1"
1
+ __version__ = "0.17.2"
2
2
 
3
3
  from fastapi_fullauth.config import AuthRateLimits, FullAuthConfig
4
4
  from fastapi_fullauth.fullauth import FullAuth
@@ -40,6 +40,14 @@ class AuthRateLimits:
40
40
  # bucket, so failed attempts cannot exhaust an IP's sign-in budget.
41
41
  reauth: int = 5
42
42
 
43
+ def __post_init__(self) -> None:
44
+ for name, value in vars(self).items():
45
+ if value < 1:
46
+ raise ValueError(
47
+ f"AUTH_RATE_LIMITS.{name} must be at least 1; "
48
+ f"{value} would reject every request to that route."
49
+ )
50
+
43
51
 
44
52
  class FullAuthConfig(BaseSettings):
45
53
  model_config = SettingsConfigDict(
@@ -61,19 +69,20 @@ class FullAuthConfig(BaseSettings):
61
69
  SECRET_KEY: str | None = None
62
70
  ALGORITHM: Literal["HS256", "HS384", "HS512"] = "HS256"
63
71
 
64
- ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
65
- REFRESH_TOKEN_EXPIRE_DAYS: int = 30
72
+ ACCESS_TOKEN_EXPIRE_MINUTES: int = Field(default=30, ge=1)
73
+ REFRESH_TOKEN_EXPIRE_DAYS: int = Field(default=30, ge=1)
66
74
  REFRESH_TOKEN_ROTATION: bool = True
67
- JWT_LEEWAY_SECONDS: int = 30
75
+ JWT_LEEWAY_SECONDS: int = Field(default=30, ge=0)
68
76
 
69
- PASSWORD_RESET_EXPIRE_MINUTES: int = 15
70
- EMAIL_VERIFY_EXPIRE_MINUTES: int = 1440
77
+ PASSWORD_RESET_EXPIRE_MINUTES: int = Field(default=15, ge=1)
78
+ EMAIL_VERIFY_EXPIRE_MINUTES: int = Field(default=1440, ge=1)
71
79
 
72
80
  PASSWORD_HASH_ALGORITHM: Literal["argon2id", "bcrypt"] = "argon2id"
73
- PASSWORD_MIN_LENGTH: int = 8
81
+ PASSWORD_MIN_LENGTH: int = Field(default=8, ge=1)
74
82
  # Upper bound so an attacker can't spend the server's CPU hashing a huge
75
83
  # body. bcrypt caps at 72 bytes anyway; argon2 will hash whatever it is given.
76
- PASSWORD_MAX_LENGTH: int = 4096
84
+ # 0 disables the cap, hence ge=0 rather than ge=1.
85
+ PASSWORD_MAX_LENGTH: int = Field(default=4096, ge=0)
77
86
 
78
87
  LOGIN_FIELD: str = "email"
79
88
  # When True, login runs a dummy password verify on unknown-user / no-password
@@ -86,12 +95,12 @@ class FullAuthConfig(BaseSettings):
86
95
  # destructive action (deleting the account, setting a first password).
87
96
  # Supplying the current password proves it too. 0 accepts only the password,
88
97
  # which locks passwordless accounts out of those actions.
89
- REAUTH_MAX_AGE_SECONDS: int = 300
98
+ REAUTH_MAX_AGE_SECONDS: int = Field(default=300, ge=0)
90
99
 
91
100
  LOCKOUT_ENABLED: bool = True
92
101
  LOCKOUT_BACKEND: Literal["memory", "redis"] = "memory"
93
- MAX_LOGIN_ATTEMPTS: int = 5
94
- LOCKOUT_DURATION_MINUTES: int = 15
102
+ MAX_LOGIN_ATTEMPTS: int = Field(default=5, ge=1)
103
+ LOCKOUT_DURATION_MINUTES: int = Field(default=15, ge=1)
95
104
 
96
105
  RATE_LIMIT_BACKEND: Literal["memory", "redis"] = "memory"
97
106
  TRUSTED_PROXY_HEADERS: Annotated[list[str], NoDecode] = []
@@ -104,14 +113,14 @@ class FullAuthConfig(BaseSettings):
104
113
 
105
114
  AUTH_RATE_LIMIT_ENABLED: bool = True
106
115
  AUTH_RATE_LIMITS: AuthRateLimits = Field(default_factory=AuthRateLimits)
107
- AUTH_RATE_LIMIT_WINDOW_SECONDS: int = 60
116
+ AUTH_RATE_LIMIT_WINDOW_SECONDS: int = Field(default=60, ge=1)
108
117
 
109
118
  REDIS_URL: str | None = None
110
119
 
111
120
  BLACKLIST_ENABLED: bool = True
112
121
  BLACKLIST_BACKEND: Literal["memory", "redis"] = "memory"
113
122
 
114
- OAUTH_STATE_EXPIRE_SECONDS: int = 300
123
+ OAUTH_STATE_EXPIRE_SECONDS: int = Field(default=300, ge=1)
115
124
  OAUTH_AUTO_LINK_BY_EMAIL: bool = True
116
125
  OAUTH_PKCE_ENABLED: bool = True
117
126
 
@@ -124,7 +133,7 @@ class FullAuthConfig(BaseSettings):
124
133
  PASSKEY_RP_NAME: str | None = None
125
134
  PASSKEY_ORIGINS: Annotated[list[str], NoDecode] = []
126
135
  PASSKEY_CHALLENGE_BACKEND: Literal["memory", "redis"] = "memory"
127
- PASSKEY_CHALLENGE_TTL: int = 60
136
+ PASSKEY_CHALLENGE_TTL: int = Field(default=60, ge=1)
128
137
  PASSKEY_REQUIRE_USER_VERIFICATION: bool = True
129
138
 
130
139
  API_PREFIX: str = "/api/v1"
@@ -82,6 +82,13 @@ class RedisChallengeStore(ChallengeStore):
82
82
  self._prefix = "fullauth:challenge:"
83
83
 
84
84
  async def store(self, key: str, challenge: str, ttl: int = 60) -> None:
85
+ # Redis rejects a non-positive expiry outright, where the in-memory store
86
+ # just writes an entry that is already expired. Skip the write instead,
87
+ # so both backends answer None to the next pop() rather than one of them
88
+ # raising into a 500 on a route that needs no account.
89
+ if ttl <= 0:
90
+ logger.warning("Challenge not stored; ttl=%s is not positive", ttl)
91
+ return
85
92
  await self._redis.set(f"{self._prefix}{key}", challenge, ex=ttl)
86
93
 
87
94
  async def pop(self, key: str) -> str | None:
@@ -257,10 +257,24 @@ class AuthRateLimiter:
257
257
 
258
258
  async def check(self, route_name: str, client_ip: str) -> None:
259
259
  limiter = self._limiters.get(route_name)
260
- if limiter and not await limiter.is_allowed(client_ip):
260
+ if limiter is None:
261
+ return
262
+
263
+ # The route belongs in the key, not just in the limiter lookup. Every
264
+ # Redis-backed limiter shares one keyspace, so keying on the IP alone
265
+ # put all six routes in a single bucket and the smallest cap, register
266
+ # at 3, became the effective limit for all of them: three refreshes
267
+ # locked out registration for a minute. The in-memory backend keeps its
268
+ # counts per instance, which is why only Redis showed it. Namespacing
269
+ # here rather than inside a backend keeps custom backends working
270
+ # unchanged, and keeps these buckets clear of any limiter the
271
+ # application passes to RateLimitMiddleware.
272
+ key = f"{route_name}:{client_ip}"
273
+
274
+ if not await limiter.is_allowed(key):
261
275
  from fastapi import HTTPException
262
276
 
263
- reset_in = await limiter.reset_time(client_ip)
277
+ reset_in = await limiter.reset_time(key)
264
278
  logger.warning("Auth rate limit exceeded: route=%s, ip=%s", route_name, client_ip)
265
279
  raise HTTPException(
266
280
  status_code=429,
@@ -7,6 +7,7 @@ import warnings
7
7
  import pytest
8
8
  from fastapi import FastAPI, Request
9
9
  from httpx import ASGITransport, AsyncClient
10
+ from pydantic import ValidationError
10
11
  from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
11
12
  from sqlmodel import SQLModel
12
13
 
@@ -15,6 +16,8 @@ from fastapi_fullauth.dependencies import current_user
15
16
  from fastapi_fullauth.types import CreateUserSchema, UserSchema
16
17
  from tests.conftest import make_test_adapter
17
18
 
19
+ SECRET = "test-secret-key-that-is-long-enough-32b"
20
+
18
21
 
19
22
  async def _make_db():
20
23
  engine = create_async_engine("sqlite+aiosqlite://", echo=False)
@@ -974,3 +977,48 @@ async def test_cookie_backend_with_csrf_no_warning(config, adapter):
974
977
  with warnings.catch_warnings():
975
978
  warnings.simplefilter("error")
976
979
  fa.init_app(app)
980
+
981
+
982
+ # ── Numeric settings reject values that cannot work ──────────────────
983
+
984
+
985
+ @pytest.mark.parametrize(
986
+ "setting",
987
+ [
988
+ "ACCESS_TOKEN_EXPIRE_MINUTES",
989
+ "REFRESH_TOKEN_EXPIRE_DAYS",
990
+ "PASSWORD_RESET_EXPIRE_MINUTES",
991
+ "EMAIL_VERIFY_EXPIRE_MINUTES",
992
+ "MAX_LOGIN_ATTEMPTS",
993
+ "LOCKOUT_DURATION_MINUTES",
994
+ "AUTH_RATE_LIMIT_WINDOW_SECONDS",
995
+ "OAUTH_STATE_EXPIRE_SECONDS",
996
+ "PASSKEY_CHALLENGE_TTL",
997
+ ],
998
+ )
999
+ def test_non_positive_durations_and_counts_are_rejected(setting):
1000
+ """Zero or negative here is never meaningful, and the failure it causes is
1001
+ remote from the cause: PASSKEY_CHALLENGE_TTL=0 raised from Redis on a public
1002
+ route, and MAX_LOGIN_ATTEMPTS=0 locked every account on its first failure.
1003
+ Fail at construction instead."""
1004
+ with pytest.raises(ValidationError):
1005
+ FullAuthConfig(SECRET_KEY=SECRET, **{setting: 0})
1006
+
1007
+
1008
+ def test_the_meaningful_zeros_are_still_accepted():
1009
+ """Three settings document zero as a real choice, so they must keep it."""
1010
+ config = FullAuthConfig(
1011
+ SECRET_KEY=SECRET,
1012
+ JWT_LEEWAY_SECONDS=0, # clock-sensitive tests rely on this
1013
+ PASSWORD_MAX_LENGTH=0, # disables the cap
1014
+ REAUTH_MAX_AGE_SECONDS=0, # accept only the current password
1015
+ )
1016
+ assert config.JWT_LEEWAY_SECONDS == 0
1017
+ assert config.PASSWORD_MAX_LENGTH == 0
1018
+ assert config.REAUTH_MAX_AGE_SECONDS == 0
1019
+
1020
+
1021
+ def test_a_rate_limit_cap_below_one_is_rejected():
1022
+ """A cap of 0 rejects every request to that route, which nobody means."""
1023
+ with pytest.raises(ValueError, match="at least 1"):
1024
+ AuthRateLimits(login=0)
@@ -278,3 +278,25 @@ async def test_challenge_store_evicts_challenges_that_were_never_used():
278
278
  await store.store("fresh", "c", ttl=60)
279
279
 
280
280
  assert list(store._store) == ["fresh"]
281
+
282
+
283
+ @pytest.mark.asyncio
284
+ async def test_challenge_stores_agree_on_a_non_positive_ttl():
285
+ """Redis rejects a non-positive expiry where the in-memory store writes an
286
+ entry that is already expired. Production picks the Redis backend whenever
287
+ REDIS_URL is set, so a divergence here is a 500 on a route that needs no
288
+ account, reachable by setting PASSKEY_CHALLENGE_TTL badly."""
289
+ import fakeredis.aioredis
290
+
291
+ from fastapi_fullauth.protection.challenges import RedisChallengeStore
292
+
293
+ memory = InMemoryChallengeStore()
294
+ redis = RedisChallengeStore.__new__(RedisChallengeStore)
295
+ redis._redis = fakeredis.aioredis.FakeRedis(decode_responses=True)
296
+ redis._prefix = "fullauth:challenge:"
297
+
298
+ for ttl in (0, -5):
299
+ await memory.store("k", "challenge", ttl=ttl)
300
+ await redis.store("k", "challenge", ttl=ttl)
301
+ assert await memory.pop("k") is None
302
+ assert await redis.pop("k") is None
@@ -790,3 +790,59 @@ async def test_in_memory_sweep_never_drops_live_entries():
790
790
  await blacklist.add("trigger", ttl_seconds=10)
791
791
  assert await blacklist.is_blacklisted("long-lived")
792
792
  assert "short" not in blacklist._blacklisted
793
+
794
+
795
+ # ── Auth rate limits are per route ───────────────────────────────────
796
+
797
+
798
+ def _auth_limiter(backend: str):
799
+ """An AuthRateLimiter on the given backend, with Redis faked in place.
800
+
801
+ The Redis limiters are built by the real code path, then pointed at one
802
+ shared fake server, which is what makes a cross-route collision visible.
803
+ """
804
+ from fastapi_fullauth import FullAuthConfig
805
+ from fastapi_fullauth.protection.ratelimit import AuthRateLimiter
806
+
807
+ kwargs = {"SECRET_KEY": "test-secret-key-that-is-long-enough-32b", "BACKEND": backend}
808
+ if backend == "redis":
809
+ kwargs["REDIS_URL"] = "redis://rate-limit-test:6379/0"
810
+ limiter = AuthRateLimiter(FullAuthConfig(**kwargs))
811
+
812
+ if backend == "redis":
813
+ import fakeredis.aioredis
814
+
815
+ server = fakeredis.aioredis.FakeServer()
816
+ for each in limiter._limiters.values():
817
+ each._redis = fakeredis.aioredis.FakeRedis(server=server, decode_responses=True)
818
+ return limiter
819
+
820
+
821
+ @pytest.mark.parametrize("backend", ["memory", "redis"])
822
+ @pytest.mark.asyncio
823
+ async def test_auth_routes_do_not_share_a_rate_limit_bucket(backend):
824
+ """Every Redis limiter shares one keyspace, so the key has to carry the
825
+ route. Keyed on the IP alone, all six routes counted into one bucket and the
826
+ smallest cap won: three refreshes locked out registration. The in-memory
827
+ backend counts per instance and never showed it, hence both backends here."""
828
+ from fastapi import HTTPException
829
+
830
+ limiter = _auth_limiter(backend)
831
+ ip = "203.0.113.9"
832
+
833
+ # refresh allows 30; spend enough to exhaust register's cap of 3.
834
+ for _ in range(5):
835
+ await limiter.check("refresh", ip)
836
+
837
+ # register has not been called at all, so it must still have its full cap.
838
+ for _ in range(3):
839
+ await limiter.check("register", ip)
840
+
841
+ with pytest.raises(HTTPException) as exc:
842
+ await limiter.check("register", ip)
843
+ assert exc.value.status_code == 429
844
+
845
+ # And exhausting register must leave the documented separate bucket alone.
846
+ await limiter.check("reauth", ip)
847
+
848
+ await limiter.aclose()