django-pfx 1.4.dev26__tar.gz → 1.4.dev30__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 (139) hide show
  1. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/PKG-INFO +1 -1
  2. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/django_pfx.egg-info/PKG-INFO +1 -1
  3. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/django_pfx.egg-info/SOURCES.txt +2 -0
  4. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/doc/source/api.views.rst +18 -1
  5. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/doc/source/authentication.md +42 -14
  6. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/decorator/rest.py +1 -3
  7. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/management/commands/makeapidoc.py +1 -2
  8. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/middleware/profiling.py +1 -2
  9. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/models/__init__.py +1 -0
  10. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/models/abstract_pfx_base_user.py +4 -1
  11. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/models/login_ban.py +1 -3
  12. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/models/otp_user_mixin.py +40 -3
  13. django-pfx-1.4.dev30/pfx/pfxcore/models/pfx_user.py +11 -0
  14. django-pfx-1.4.dev30/pfx/pfxcore/settings.py +22 -0
  15. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/shortcuts.py +1 -3
  16. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/authentication_views.py +3 -4
  17. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/__init__.py +1 -0
  18. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/test_auth_api.py +1 -1
  19. django-pfx-1.4.dev30/tests/tests/test_settings.py +23 -0
  20. django-pfx-1.4.dev26/pfx/pfxcore/settings.py +0 -11
  21. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/.gitignore +0 -0
  22. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/.gitlab-ci.yml +0 -0
  23. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/.pre-commit-config.yaml +0 -0
  24. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/LICENSE +0 -0
  25. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/MANIFEST.in +0 -0
  26. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/README.md +0 -0
  27. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/django-admin-test +0 -0
  28. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/django_pfx.egg-info/dependency_links.txt +0 -0
  29. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/django_pfx.egg-info/requires.txt +0 -0
  30. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/django_pfx.egg-info/top_level.txt +0 -0
  31. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/doc/Makefile +0 -0
  32. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/doc/conf.py +0 -0
  33. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/doc/index.rst +0 -0
  34. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/doc/source/decorator.md +0 -0
  35. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/doc/source/generate_openapi.md +0 -0
  36. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/doc/source/getting_started.md +0 -0
  37. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/doc/source/internationalisation.md +0 -0
  38. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/doc/source/model.md +0 -0
  39. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/doc/source/pfx_views.md +0 -0
  40. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/doc/source/profiling.md +0 -0
  41. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/doc/source/settings.md +0 -0
  42. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/doc/source/testing.md +0 -0
  43. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/img/pfx.png +0 -0
  44. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/img/pfx.svg +0 -0
  45. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/__init__.py +0 -0
  46. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/__init__.py +0 -0
  47. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/apidoc/__init__.py +0 -0
  48. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/apidoc/parameters.py +0 -0
  49. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/apidoc/schema.py +0 -0
  50. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/apidoc/tags.py +0 -0
  51. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/apps.py +0 -0
  52. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/decorator/__init__.py +0 -0
  53. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/default_settings.py +0 -0
  54. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/exceptions.py +0 -0
  55. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/fields.py +0 -0
  56. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/http/__init__.py +0 -0
  57. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/http/json_response.py +0 -0
  58. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
  59. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.po +0 -0
  60. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/management/__init__.py +0 -0
  61. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/management/commands/__init__.py +0 -0
  62. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/management/commands/profile.py +0 -0
  63. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/middleware/__init__.py +0 -0
  64. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/middleware/authentication.py +0 -0
  65. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/middleware/locale.py +0 -0
  66. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/models/cache_mixins.py +0 -0
  67. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/models/not_null_fields.py +0 -0
  68. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/models/pfx_models.py +0 -0
  69. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/models/user_filtered_queryset_mixin.py +0 -0
  70. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/serializers/__init__.py +0 -0
  71. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/serializers/json.py +0 -0
  72. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/storage/__init__.py +0 -0
  73. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/storage/s3_storage.py +0 -0
  74. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/templates/registration/otp_code_email.txt +0 -0
  75. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/templates/registration/otp_code_subject.txt +0 -0
  76. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/templates/registration/password_reset_email.txt +0 -0
  77. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/templates/registration/password_reset_subject.txt +0 -0
  78. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/templates/registration/welcome_email.txt +0 -0
  79. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/templates/registration/welcome_subject.txt +0 -0
  80. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/test.py +0 -0
  81. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/urls.py +0 -0
  82. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/__init__.py +0 -0
  83. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/fields.py +0 -0
  84. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/filters_views.py +0 -0
  85. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/locale_views.py +0 -0
  86. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/__init__.py +0 -0
  87. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/date_format.py +0 -0
  88. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/groups.py +0 -0
  89. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/list_count.py +0 -0
  90. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/list_items.py +0 -0
  91. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/list_mode.py +0 -0
  92. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/list_order.py +0 -0
  93. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/list_search.py +0 -0
  94. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/media_redirect.py +0 -0
  95. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/meta_fields.py +0 -0
  96. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/meta_filters.py +0 -0
  97. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/meta_orders.py +0 -0
  98. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/subset.py +0 -0
  99. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/subset_limit.py +0 -0
  100. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/subset_offset.py +0 -0
  101. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/subset_page.py +0 -0
  102. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/subset_page_size.py +0 -0
  103. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/subset_page_subset.py +0 -0
  104. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/rest_views.py +0 -0
  105. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pyproject.toml +0 -0
  106. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/requirements.txt +0 -0
  107. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/runtest.py +0 -0
  108. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/serve-doc +0 -0
  109. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/setup.cfg +0 -0
  110. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/setup.py +0 -0
  111. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/__init__.py +0 -0
  112. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/locale/fr/LC_MESSAGES/django.po +0 -0
  113. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/models.py +0 -0
  114. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/settings/__init__.py +0 -0
  115. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/settings/ci.py +0 -0
  116. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/settings/common.py +0 -0
  117. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/settings/dev.py +0 -0
  118. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/settings/dev_custom_example.py +0 -0
  119. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/settings/dev_default.py +0 -0
  120. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/basic_api_errors.py +0 -0
  121. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/basic_api_test.py +0 -0
  122. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/test_api_doc.py +0 -0
  123. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/test_body_mixin.py +0 -0
  124. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/test_cache.py +0 -0
  125. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/test_client.py +0 -0
  126. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/test_fields.py +0 -0
  127. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/test_filters.py +0 -0
  128. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/test_locale_api.py +0 -0
  129. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/test_perm_tests.py +0 -0
  130. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/test_perms_api.py +0 -0
  131. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/test_profiling_middleware.py +0 -0
  132. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/test_shortcuts.py +0 -0
  133. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/test_timezone_middleware.py +0 -0
  134. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/test_tools.py +0 -0
  135. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/test_user_queryset.py +0 -0
  136. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/test_view_decorators.py +0 -0
  137. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/test_view_fields.py +0 -0
  138. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/urls.py +0 -0
  139. {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/views.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-pfx
3
- Version: 1.4.dev26
3
+ Version: 1.4.dev30
4
4
  Summary: Django PFX is a toolkit designed to streamline the development of RESTful APIs using the Django framework.
5
5
  Author: Hervé Martinet
6
6
  Author-email: herve.martinet@gmail.com
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-pfx
3
- Version: 1.4.dev26
3
+ Version: 1.4.dev30
4
4
  Summary: Django PFX is a toolkit designed to streamline the development of RESTful APIs using the Django framework.
5
5
  Author: Hervé Martinet
6
6
  Author-email: herve.martinet@gmail.com
@@ -67,6 +67,7 @@ pfx/pfxcore/models/login_ban.py
67
67
  pfx/pfxcore/models/not_null_fields.py
68
68
  pfx/pfxcore/models/otp_user_mixin.py
69
69
  pfx/pfxcore/models/pfx_models.py
70
+ pfx/pfxcore/models/pfx_user.py
70
71
  pfx/pfxcore/models/user_filtered_queryset_mixin.py
71
72
  pfx/pfxcore/serializers/__init__.py
72
73
  pfx/pfxcore/serializers/json.py
@@ -127,6 +128,7 @@ tests/tests/test_locale_api.py
127
128
  tests/tests/test_perm_tests.py
128
129
  tests/tests/test_perms_api.py
129
130
  tests/tests/test_profiling_middleware.py
131
+ tests/tests/test_settings.py
130
132
  tests/tests/test_shortcuts.py
131
133
  tests/tests/test_timezone_middleware.py
132
134
  tests/tests/test_tools.py
@@ -32,6 +32,18 @@ API Reference
32
32
  :undoc-members:
33
33
  :show-inheritance:
34
34
 
35
+ .. autoclass:: pfx.pfxcore.models.AbstractPFXBaseUser
36
+ :members:
37
+ :show-inheritance:
38
+
39
+ .. autoclass:: pfx.pfxcore.models.OtpUserMixin
40
+ :members:
41
+ :show-inheritance:
42
+
43
+ .. autoclass:: pfx.pfxcore.models.PFXUser
44
+ :members:
45
+ :show-inheritance:
46
+
35
47
  ``pfx.pfxcore.views``
36
48
  *********************
37
49
 
@@ -43,12 +55,17 @@ Base services
43
55
  :undoc-members:
44
56
  :show-inheritance:
45
57
 
58
+ .. autoclass:: pfx.pfxcore.views.SignupView
59
+ :members:
60
+ :undoc-members:
61
+ :show-inheritance:
62
+
46
63
  .. autoclass:: pfx.pfxcore.views.ForgottenPasswordView
47
64
  :members:
48
65
  :undoc-members:
49
66
  :show-inheritance:
50
67
 
51
- .. autoclass:: pfx.pfxcore.views.SignupView
68
+ .. autoclass:: pfx.pfxcore.views.OtpEmailView
52
69
  :members:
53
70
  :undoc-members:
54
71
  :show-inheritance:
@@ -1,20 +1,21 @@
1
1
  # Authentication
2
2
 
3
3
  Django PFX offers services and middlewares for managing user authentication in your API.
4
- These services replicate some of the functionalities provided by the `django.contrib.auth`
4
+ These services replicate some of the functionalities provided by the {mod}`django.contrib.auth`
5
5
  package but in the form of RESTful services.
6
6
  They utilize the same user model and authentication backend features,
7
7
  including password validation and hashing.
8
8
 
9
9
  ## User Model
10
10
 
11
- You have the option to use the standard `django.contrib.auth.models.User`,
11
+ You have the option to use the standard Django User with {class}`pfx.pfxcore.models.PFXUser`
12
+ (which is a {class}`django.contrib.auth.models.User` with PFX required mixins),
12
13
  but you may prefer to use your own model. To do this, create your own user class.
13
14
 
14
15
  ```python
15
- from django.contrib.auth.models import AbstractUser
16
+ from pfx.pfxcore.models import AbstractPFXBaseUser
16
17
 
17
- class MyUser(AbstractUser):
18
+ class MyUser(AbstractPFXBaseUser):
18
19
  pass
19
20
  ```
20
21
 
@@ -28,8 +29,8 @@ AUTH_USER_MODEL = "myapp.MyUser"
28
29
 
29
30
  There are two authentication modes available: cookie and bearer token. You can activate either or both by enabling the following middlewares:
30
31
 
31
- * `'pfx.pfxcore.middleware.AuthenticationMiddleware'` (bearer token)
32
- * `'pfx.pfxcore.middleware.CookieAuthenticationMiddleware'` (cookie)
32
+ * {class}`pfx.pfxcore.middleware.AuthenticationMiddleware` (bearer token)
33
+ * {class}`pfx.pfxcore.middleware.CookieAuthenticationMiddleware` (cookie)
33
34
 
34
35
  ### Token Validity
35
36
 
@@ -40,7 +41,7 @@ You can customize token validity by configuring these parameters:
40
41
 
41
42
  ### Cookie Settings
42
43
 
43
- To use the `CookieAuthenticationMiddleware`, you need to configure the following settings:
44
+ To use the {class}`pfx.pfxcore.middleware.CookieAuthenticationMiddleware`, you need to configure the following settings:
44
45
 
45
46
  * `PFX_COOKIE_DOMAIN`: The cookie domain
46
47
  * `PFX_COOKIE_SECURE`: `Secure` attribute of the cookie (`True`/`False`)
@@ -48,28 +49,55 @@ To use the `CookieAuthenticationMiddleware`, you need to configure the following
48
49
 
49
50
  See the [MDN Website](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) for more details.
50
51
 
52
+ ### Temporary bans
53
+
54
+ Users will be temporarily banned after several unsuccessful login attempts.
55
+
56
+ In the event of a temporary ban, login services will respond with the HTTP code `429` and
57
+ the `Retry-After` header.
58
+
59
+ #### Settings
60
+
61
+ * `PFX_LOGIN_BAN_FAILED_NUMBER`: The number of failed login attempts before banning. To deactivate the ban completely, set `0` (optional, default `5`).
62
+ * `PFX_LOGIN_BAN_SECONDS_START`: The number of seconds for the first ban (optional, default `60`).
63
+ * `PFX_LOGIN_BAN_SECONDS_STEP`: The number of seconds to be added to the previous ban for consecutive bans (optional, default `60`).
64
+
51
65
  ### Multifactor Authentication
52
66
  Multifactor authentication can be enabled in django-pfx Authentication API.
53
67
 
54
68
  PFX currently provides MFA with One Time Password (OTP), compatible with FreeOTP,
55
69
  Google Authenticator and other OTP app.
56
70
 
57
- To enable this feature, install django-pfx with otp
71
+ To enable this feature, install django-pfx with otp.
58
72
 
59
73
  ```bash
60
74
  pip install django-pfx[otp]
61
75
  ```
62
76
 
63
- Then the user class must use the ```OtpUserMixin```
77
+ Then the user class must use the {class}`pfx.pfxcore.models.OtpUserMixin`.
64
78
 
65
79
  ```python
66
- from django.contrib.auth.models import AbstractUser
67
- from pfx.pfxcore.models import OtpUserMixin
80
+ from pfx.pfxcore.models import PFXUser, OtpUserMixin
68
81
 
69
- class MyUser(OtpUserMixin, AbstractUser):
82
+ class MyUser(OtpUserMixin, PFXUser):
70
83
  pass
71
84
  ```
72
85
 
86
+ or
87
+
88
+ ```python
89
+ from pfx.pfxcore.models import AbstractPFXBaseUser, OtpUserMixin
90
+
91
+ class MyUser(OtpUserMixin, AbstractPFXBaseUser):
92
+ pass
93
+ ```
94
+
95
+ #### Settings
96
+
97
+ * `PFX_TOKEN_OTP_VALIDITY`: Validity for OTP tokens (corresponds to the maximum time to enter
98
+ an OTP code after logging in with a password) (optional, default `{'minutes': 15}`)
99
+ * `PFX_HOTP_CODE_VALIDITY`: Validity of HOTP codes in minutes (used to send code by email) (optional, default `15`).
100
+
73
101
  The user can then enable or disable the OTP auth using the [services documented below](#enable-mfa-otp).
74
102
 
75
103
  ## Services
@@ -263,7 +291,7 @@ like so: `https://example.com/reset-password?token={token}&uidb64={uidb64}`.
263
291
  Your reset page should then call the "set password" service with these two parameters.
264
292
 
265
293
  You can override this class if you need to customize the email templates.
266
- Refer to the [API doc](api.views.rst#pfx.pfxcore.views.ForgottenPasswordView) for more details.
294
+ Refer to {class}`pfx.pfxcore.views.ForgottenPasswordView` for more details.
267
295
 
268
296
  **Request :** `POST` `/auth/forgotten-password`
269
297
 
@@ -288,7 +316,7 @@ like so: `https://example.com/reset-password?token={token}&uidb64={uidb64}`.
288
316
  Your reset page should then call the "set password" service with these two parameters.
289
317
 
290
318
  You can override this class if you need to customize the user or email templates.
291
- Refer to the [API doc](api.views.rst#pfx.pfxcore.views.SignupView) for more details.
319
+ Refer to {class}`pfx.pfxcore.views.SignupView` for more details.
292
320
 
293
321
  **Request :** `POST` `/auth/signup`
294
322
 
@@ -7,11 +7,9 @@ from apispec.utils import deepupdate
7
7
 
8
8
  from pfx.pfxcore.exceptions import APIError
9
9
  from pfx.pfxcore.http import JsonResponse
10
- from pfx.pfxcore.settings import PFXSettings
10
+ from pfx.pfxcore.settings import settings
11
11
  from pfx.pfxcore.test import format_request
12
12
 
13
- settings = PFXSettings()
14
-
15
13
  logger = logging.getLogger(__name__)
16
14
 
17
15
 
@@ -11,10 +11,9 @@ from apispec.yaml_utils import load_operations_from_docstring
11
11
 
12
12
  from pfx.pfxcore import __PFX_VIEWS__
13
13
  from pfx.pfxcore.apidoc import __PARAMETERS__, __TAGS__, ParameterGroup
14
- from pfx.pfxcore.settings import PFXSettings
14
+ from pfx.pfxcore.settings import settings
15
15
  from pfx.pfxcore.views import FilterGroup, ModelMixin
16
16
 
17
- settings = PFXSettings()
18
17
  DEFAULT_TEMPLATE = dict(
19
18
  title="PFX API",
20
19
  version="1.0.0",
@@ -11,10 +11,9 @@ from django.utils.deprecation import MiddlewareMixin
11
11
 
12
12
  import sqlparse
13
13
 
14
- from pfx.pfxcore.settings import PFXSettings
14
+ from pfx.pfxcore.settings import settings
15
15
 
16
16
  logger = logging.getLogger(__name__)
17
- settings = PFXSettings()
18
17
  rid = ContextVar('var')
19
18
 
20
19
 
@@ -13,4 +13,5 @@ from .pfx_models import (
13
13
  PFXModelMixin,
14
14
  UniqueConstraint,
15
15
  )
16
+ from .pfx_user import PFXUser
16
17
  from .user_filtered_queryset_mixin import UserFilteredQuerySetMixin
@@ -2,6 +2,8 @@ from django.contrib.auth.models import AbstractBaseUser
2
2
 
3
3
 
4
4
  class AbstractPFXBaseUser(AbstractBaseUser):
5
+ """The base abstract user for PFX."""
6
+
5
7
  class Meta:
6
8
  abstract = True
7
9
 
@@ -10,6 +12,7 @@ class AbstractPFXBaseUser(AbstractBaseUser):
10
12
  Return a user secret to sign JWT token.
11
13
 
12
14
  If not empty, the JWT token validity depends on all values
13
- user to build the return string.
15
+ user to build the return string. So, each time the returned value
16
+ changes, the previously issued tokens will no longer be valid.
14
17
  """
15
18
  return self.password
@@ -4,9 +4,7 @@ from django.db import models
4
4
  from django.utils import timezone
5
5
  from django.utils.translation import gettext_lazy as _
6
6
 
7
- from pfx.pfxcore.settings import PFXSettings
8
-
9
- settings = PFXSettings()
7
+ from pfx.pfxcore.settings import settings
10
8
 
11
9
 
12
10
  class LoginBanQuerySet(models.QuerySet):
@@ -4,29 +4,46 @@ from django.db import models
4
4
  from django.utils import timezone
5
5
  from django.utils.translation import gettext_lazy as _
6
6
 
7
- from pfx.pfxcore.settings import PFXSettings
8
-
9
- settings = PFXSettings()
7
+ from pfx.pfxcore.settings import settings
10
8
 
11
9
 
12
10
  class OtpUserMixin(models.Model):
11
+ """A mixin to enable OTP MFA on a user class."""
12
+
13
+ #: OTP secret token.
13
14
  otp_secret_token = models.CharField(
14
15
  _("OTP secret token"), max_length=32, null=True,
15
16
  blank=True, unique=True)
17
+ #: Temporary OTP secret token (needs confirmation).
16
18
  otp_secret_token_tmp = models.CharField(
17
19
  _("Temporary OTP secret token"), max_length=32, null=True, blank=True)
20
+ #: HOTP count.
18
21
  hotp_count = models.IntegerField(_("HOTP count"), default=0)
22
+ #: HOTP expiry.
19
23
  hotp_expiry = models.DateTimeField(_("HOTP expiry"), default=timezone.now)
20
24
 
21
25
  class Meta:
22
26
  abstract = True
23
27
 
24
28
  def enable_otp(self):
29
+ """Activate OTP for this user.
30
+
31
+ Generates a new temporary OTP secret token. To complete activation,
32
+ call `confirm_otp` with a valid code.
33
+ """
25
34
  import pyotp
26
35
  self.otp_secret_token_tmp = pyotp.random_base32()
27
36
  self.save(update_fields=['otp_secret_token_tmp'])
28
37
 
29
38
  def confirm_otp(self, otp_code):
39
+ """Confirm OTP activation for this user.
40
+
41
+ Set the OTP secret token from the temporary one if the provided
42
+ code is valid.
43
+
44
+ :param otp_code: A valid OTP code for the temporary OTP secret key.
45
+ :returns: `True` if success, `False` otherwise.
46
+ """
30
47
  if self.is_otp_valid(otp_code, tmp=True):
31
48
  self.otp_secret_token = self.otp_secret_token_tmp
32
49
  self.otp_secret_token_tmp = None
@@ -36,10 +53,16 @@ class OtpUserMixin(models.Model):
36
53
  return False
37
54
 
38
55
  def disable_otp(self):
56
+ """Disable OTP for this user.
57
+
58
+ Remove the OTP secret token.
59
+ """
39
60
  self.otp_secret_token = None
40
61
  self.save(update_fields=['otp_secret_token'])
41
62
 
42
63
  def get_otp_setup_uri(self, tmp=False):
64
+ """Return the setup URL for OTP activation.
65
+ """
43
66
  import pyotp
44
67
  return pyotp.totp.TOTP(
45
68
  tmp and self.otp_secret_token_tmp or
@@ -47,6 +70,13 @@ class OtpUserMixin(models.Model):
47
70
  name=self.email, issuer_name=settings.PFX_SITE_NAME)
48
71
 
49
72
  def is_otp_valid(self, otp_code, tmp=False):
73
+ """Verify an OTP code.
74
+
75
+ :param otp_code: A valid OTP code for the OTP secret key.
76
+ :param tmp: If `True`, verify the code with the temporary
77
+ OTP secret key.
78
+ :returns: `True` if the code is valid, `False` otherwise.
79
+ """
50
80
  import pyotp
51
81
  totp = pyotp.parse_uri(self.get_otp_setup_uri(tmp=tmp))
52
82
  valid = totp.verify(otp_code)
@@ -58,10 +88,17 @@ class OtpUserMixin(models.Model):
58
88
  return valid
59
89
 
60
90
  def get_user_jwt_signature_key(self):
91
+ """Return a user secret to sign JWT token.
92
+
93
+ If the user inherit :class:`pfx.pfxcore.models.AbstractPFXBaseUser`,
94
+ add the OTP secret token to the user signature."""
61
95
  return super().get_user_jwt_signature_key() + (
62
96
  self.otp_secret_token or "")
63
97
 
64
98
  def get_hotp_code(self):
99
+ """Return a new valid HOTP code.
100
+
101
+ Increment the HOTP counter and reset the expiry."""
65
102
  import pyotp
66
103
  if not self.otp_secret_token:
67
104
  raise Exception("OTP disabled")
@@ -0,0 +1,11 @@
1
+ from django.contrib.auth.models import AbstractUser
2
+
3
+ from .abstract_pfx_base_user import AbstractPFXBaseUser
4
+
5
+
6
+ class PFXUser(AbstractUser, AbstractPFXBaseUser):
7
+ """The Django User with PFX mixin.
8
+ """
9
+
10
+ class Meta(AbstractUser.Meta):
11
+ swappable = "AUTH_USER_MODEL"
@@ -0,0 +1,22 @@
1
+ from django.conf import settings as django_settings
2
+ from django.core.exceptions import ImproperlyConfigured
3
+
4
+ from . import default_settings
5
+
6
+
7
+ class PFXSettings:
8
+ def __getattr__(self, name):
9
+ try:
10
+ val = getattr(django_settings, name)
11
+ except AttributeError:
12
+ if hasattr(default_settings, name):
13
+ val = getattr(default_settings, name)
14
+ else:
15
+ raise
16
+ if name == "PFX_SECRET_KEY" and not val:
17
+ raise ImproperlyConfigured(
18
+ "The PFX_SECRET_KEY setting must not be empty.")
19
+ return val
20
+
21
+
22
+ settings = PFXSettings()
@@ -5,9 +5,7 @@ from datetime import date
5
5
  from django.core.exceptions import ObjectDoesNotExist
6
6
  from django.utils.translation import gettext_lazy as _
7
7
 
8
- from pfx.pfxcore.settings import PFXSettings
9
-
10
- settings = PFXSettings()
8
+ from .settings import settings
11
9
 
12
10
  logger = logging.getLogger(__name__)
13
11
 
@@ -32,7 +32,7 @@ from pfx.pfxcore.http import JsonResponse
32
32
  from pfx.pfxcore.middleware.authentication import JWTTokenDecodeMixin
33
33
  from pfx.pfxcore.models import CacheableMixin, OtpUserMixin
34
34
  from pfx.pfxcore.models.login_ban import LoginBan
35
- from pfx.pfxcore.settings import PFXSettings
35
+ from pfx.pfxcore.settings import settings
36
36
  from pfx.pfxcore.shortcuts import delete_token_cookie
37
37
 
38
38
  from .rest_views import (
@@ -42,7 +42,6 @@ from .rest_views import (
42
42
  ModelMixin,
43
43
  )
44
44
 
45
- settings = PFXSettings()
46
45
  logger = logging.getLogger(__name__)
47
46
  UserModel = get_user_model()
48
47
  AUTHENTICATION_TAG = Tag("Authentication")
@@ -784,7 +783,7 @@ class ForgottenPasswordView(SendMessageTokenMixin, BodyMixin, BaseRestView):
784
783
 
785
784
  @rest_view("/auth/otp")
786
785
  class OtpEmailView(BodyMixin, JWTTokenDecodeMixin, BaseRestView):
787
- """View for forgotten password service."""
786
+ """View for the OTP code email service."""
788
787
  #: The email template.
789
788
  email_template_name = 'registration/otp_code_email.txt'
790
789
  #: The email subject template.
@@ -853,7 +852,7 @@ class OtpEmailView(BodyMixin, JWTTokenDecodeMixin, BaseRestView):
853
852
  'message': _('A new authentication code has been sent by email.')})
854
853
 
855
854
  def send_otp_message(self, user):
856
- """Send an email to a user with an OTP code.
855
+ """Send an email to a user with an OTP code to a user.
857
856
 
858
857
  :param user: The user
859
858
  """
@@ -11,6 +11,7 @@ from .test_locale_api import LocaleAPITest
11
11
  from .test_perm_tests import PermTestsTest
12
12
  from .test_perms_api import PermsAPITest
13
13
  from .test_profiling_middleware import ProfilingMiddlewareTest
14
+ from .test_settings import TestSettings
14
15
  from .test_shortcuts import ShortcutTest
15
16
  from .test_timezone_middleware import TimezoneMiddlewareTest
16
17
  from .test_tools import TestTools
@@ -1247,7 +1247,7 @@ class AuthAPITest(TestAssertMixin, TransactionTestCase):
1247
1247
  code_match = re.search(
1248
1248
  r'Authentication code: (\d{6})', mail.outbox[0].body)
1249
1249
  self.assertIsNotNone(code_match)
1250
- otp_code = int(code_match.group(1))
1250
+ otp_code = code_match.group(1)
1251
1251
 
1252
1252
  response = self.client.post('/api/auth/otp/login', dict(
1253
1253
  token=otp_token,
@@ -0,0 +1,23 @@
1
+ from datetime import timedelta
2
+
3
+ from django.core.exceptions import ImproperlyConfigured
4
+ from django.test import TestCase, override_settings
5
+
6
+ from pfx.pfxcore.test import TestAssertMixin
7
+ from pfx.pfxcore.views import AuthenticationView
8
+ from tests.models import User
9
+
10
+
11
+ class TestSettings(TestAssertMixin, TestCase):
12
+
13
+ @override_settings(PFX_SECRET_KEY="")
14
+ def test_mandatory_pfx_secret_key(self):
15
+ user = User.objects.create_user(
16
+ username='user',
17
+ email="user@example.com",
18
+ password='test',
19
+ first_name='User',
20
+ last_name='Test')
21
+
22
+ with self.assertRaises(ImproperlyConfigured):
23
+ AuthenticationView()._prepare_token(user, timedelta())
@@ -1,11 +0,0 @@
1
- from django.conf import settings
2
-
3
- from . import default_settings
4
-
5
-
6
- class PFXSettings:
7
- def __getattr__(self, attr):
8
- try:
9
- return getattr(settings, attr)
10
- except AttributeError:
11
- return getattr(default_settings, attr)
File without changes
File without changes
File without changes
File without changes
File without changes