django-pfx 1.4.dev28__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 (138) hide show
  1. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/PKG-INFO +1 -1
  2. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/django_pfx.egg-info/PKG-INFO +1 -1
  3. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/django_pfx.egg-info/SOURCES.txt +1 -0
  4. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/doc/source/api.views.rst +18 -1
  5. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/doc/source/authentication.md +42 -14
  6. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/models/__init__.py +1 -0
  7. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/models/abstract_pfx_base_user.py +4 -1
  8. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/models/otp_user_mixin.py +39 -0
  9. django-pfx-1.4.dev30/pfx/pfxcore/models/pfx_user.py +11 -0
  10. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/authentication_views.py +2 -2
  11. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/test_auth_api.py +1 -1
  12. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/.gitignore +0 -0
  13. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/.gitlab-ci.yml +0 -0
  14. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/.pre-commit-config.yaml +0 -0
  15. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/LICENSE +0 -0
  16. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/MANIFEST.in +0 -0
  17. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/README.md +0 -0
  18. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/django-admin-test +0 -0
  19. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/django_pfx.egg-info/dependency_links.txt +0 -0
  20. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/django_pfx.egg-info/requires.txt +0 -0
  21. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/django_pfx.egg-info/top_level.txt +0 -0
  22. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/doc/Makefile +0 -0
  23. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/doc/conf.py +0 -0
  24. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/doc/index.rst +0 -0
  25. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/doc/source/decorator.md +0 -0
  26. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/doc/source/generate_openapi.md +0 -0
  27. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/doc/source/getting_started.md +0 -0
  28. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/doc/source/internationalisation.md +0 -0
  29. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/doc/source/model.md +0 -0
  30. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/doc/source/pfx_views.md +0 -0
  31. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/doc/source/profiling.md +0 -0
  32. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/doc/source/settings.md +0 -0
  33. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/doc/source/testing.md +0 -0
  34. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/img/pfx.png +0 -0
  35. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/img/pfx.svg +0 -0
  36. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/__init__.py +0 -0
  37. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/__init__.py +0 -0
  38. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/apidoc/__init__.py +0 -0
  39. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/apidoc/parameters.py +0 -0
  40. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/apidoc/schema.py +0 -0
  41. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/apidoc/tags.py +0 -0
  42. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/apps.py +0 -0
  43. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/decorator/__init__.py +0 -0
  44. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/decorator/rest.py +0 -0
  45. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/default_settings.py +0 -0
  46. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/exceptions.py +0 -0
  47. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/fields.py +0 -0
  48. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/http/__init__.py +0 -0
  49. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/http/json_response.py +0 -0
  50. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
  51. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.po +0 -0
  52. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/management/__init__.py +0 -0
  53. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/management/commands/__init__.py +0 -0
  54. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/management/commands/makeapidoc.py +0 -0
  55. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/management/commands/profile.py +0 -0
  56. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/middleware/__init__.py +0 -0
  57. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/middleware/authentication.py +0 -0
  58. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/middleware/locale.py +0 -0
  59. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/middleware/profiling.py +0 -0
  60. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/models/cache_mixins.py +0 -0
  61. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/models/login_ban.py +0 -0
  62. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/models/not_null_fields.py +0 -0
  63. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/models/pfx_models.py +0 -0
  64. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/models/user_filtered_queryset_mixin.py +0 -0
  65. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/serializers/__init__.py +0 -0
  66. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/serializers/json.py +0 -0
  67. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/settings.py +0 -0
  68. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/shortcuts.py +0 -0
  69. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/storage/__init__.py +0 -0
  70. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/storage/s3_storage.py +0 -0
  71. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/templates/registration/otp_code_email.txt +0 -0
  72. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/templates/registration/otp_code_subject.txt +0 -0
  73. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/templates/registration/password_reset_email.txt +0 -0
  74. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/templates/registration/password_reset_subject.txt +0 -0
  75. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/templates/registration/welcome_email.txt +0 -0
  76. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/templates/registration/welcome_subject.txt +0 -0
  77. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/test.py +0 -0
  78. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/urls.py +0 -0
  79. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/__init__.py +0 -0
  80. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/fields.py +0 -0
  81. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/filters_views.py +0 -0
  82. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/locale_views.py +0 -0
  83. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/__init__.py +0 -0
  84. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/date_format.py +0 -0
  85. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/groups.py +0 -0
  86. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/list_count.py +0 -0
  87. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/list_items.py +0 -0
  88. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/list_mode.py +0 -0
  89. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/list_order.py +0 -0
  90. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/list_search.py +0 -0
  91. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/media_redirect.py +0 -0
  92. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/meta_fields.py +0 -0
  93. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/meta_filters.py +0 -0
  94. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/meta_orders.py +0 -0
  95. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/subset.py +0 -0
  96. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/subset_limit.py +0 -0
  97. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/subset_offset.py +0 -0
  98. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/subset_page.py +0 -0
  99. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/subset_page_size.py +0 -0
  100. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/subset_page_subset.py +0 -0
  101. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/rest_views.py +0 -0
  102. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pyproject.toml +0 -0
  103. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/requirements.txt +0 -0
  104. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/runtest.py +0 -0
  105. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/serve-doc +0 -0
  106. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/setup.cfg +0 -0
  107. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/setup.py +0 -0
  108. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/__init__.py +0 -0
  109. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/locale/fr/LC_MESSAGES/django.po +0 -0
  110. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/models.py +0 -0
  111. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/settings/__init__.py +0 -0
  112. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/settings/ci.py +0 -0
  113. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/settings/common.py +0 -0
  114. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/settings/dev.py +0 -0
  115. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/settings/dev_custom_example.py +0 -0
  116. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/settings/dev_default.py +0 -0
  117. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/__init__.py +0 -0
  118. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/basic_api_errors.py +0 -0
  119. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/basic_api_test.py +0 -0
  120. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/test_api_doc.py +0 -0
  121. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/test_body_mixin.py +0 -0
  122. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/test_cache.py +0 -0
  123. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/test_client.py +0 -0
  124. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/test_fields.py +0 -0
  125. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/test_filters.py +0 -0
  126. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/test_locale_api.py +0 -0
  127. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/test_perm_tests.py +0 -0
  128. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/test_perms_api.py +0 -0
  129. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/test_profiling_middleware.py +0 -0
  130. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/test_settings.py +0 -0
  131. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/test_shortcuts.py +0 -0
  132. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/test_timezone_middleware.py +0 -0
  133. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/test_tools.py +0 -0
  134. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/test_user_queryset.py +0 -0
  135. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/test_view_decorators.py +0 -0
  136. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/test_view_fields.py +0 -0
  137. {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/urls.py +0 -0
  138. {django-pfx-1.4.dev28 → 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.dev28
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.dev28
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
@@ -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
 
@@ -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
@@ -8,23 +8,42 @@ from pfx.pfxcore.settings import settings
8
8
 
9
9
 
10
10
  class OtpUserMixin(models.Model):
11
+ """A mixin to enable OTP MFA on a user class."""
12
+
13
+ #: OTP secret token.
11
14
  otp_secret_token = models.CharField(
12
15
  _("OTP secret token"), max_length=32, null=True,
13
16
  blank=True, unique=True)
17
+ #: Temporary OTP secret token (needs confirmation).
14
18
  otp_secret_token_tmp = models.CharField(
15
19
  _("Temporary OTP secret token"), max_length=32, null=True, blank=True)
20
+ #: HOTP count.
16
21
  hotp_count = models.IntegerField(_("HOTP count"), default=0)
22
+ #: HOTP expiry.
17
23
  hotp_expiry = models.DateTimeField(_("HOTP expiry"), default=timezone.now)
18
24
 
19
25
  class Meta:
20
26
  abstract = True
21
27
 
22
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
+ """
23
34
  import pyotp
24
35
  self.otp_secret_token_tmp = pyotp.random_base32()
25
36
  self.save(update_fields=['otp_secret_token_tmp'])
26
37
 
27
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
+ """
28
47
  if self.is_otp_valid(otp_code, tmp=True):
29
48
  self.otp_secret_token = self.otp_secret_token_tmp
30
49
  self.otp_secret_token_tmp = None
@@ -34,10 +53,16 @@ class OtpUserMixin(models.Model):
34
53
  return False
35
54
 
36
55
  def disable_otp(self):
56
+ """Disable OTP for this user.
57
+
58
+ Remove the OTP secret token.
59
+ """
37
60
  self.otp_secret_token = None
38
61
  self.save(update_fields=['otp_secret_token'])
39
62
 
40
63
  def get_otp_setup_uri(self, tmp=False):
64
+ """Return the setup URL for OTP activation.
65
+ """
41
66
  import pyotp
42
67
  return pyotp.totp.TOTP(
43
68
  tmp and self.otp_secret_token_tmp or
@@ -45,6 +70,13 @@ class OtpUserMixin(models.Model):
45
70
  name=self.email, issuer_name=settings.PFX_SITE_NAME)
46
71
 
47
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
+ """
48
80
  import pyotp
49
81
  totp = pyotp.parse_uri(self.get_otp_setup_uri(tmp=tmp))
50
82
  valid = totp.verify(otp_code)
@@ -56,10 +88,17 @@ class OtpUserMixin(models.Model):
56
88
  return valid
57
89
 
58
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."""
59
95
  return super().get_user_jwt_signature_key() + (
60
96
  self.otp_secret_token or "")
61
97
 
62
98
  def get_hotp_code(self):
99
+ """Return a new valid HOTP code.
100
+
101
+ Increment the HOTP counter and reset the expiry."""
63
102
  import pyotp
64
103
  if not self.otp_secret_token:
65
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"
@@ -783,7 +783,7 @@ class ForgottenPasswordView(SendMessageTokenMixin, BodyMixin, BaseRestView):
783
783
 
784
784
  @rest_view("/auth/otp")
785
785
  class OtpEmailView(BodyMixin, JWTTokenDecodeMixin, BaseRestView):
786
- """View for forgotten password service."""
786
+ """View for the OTP code email service."""
787
787
  #: The email template.
788
788
  email_template_name = 'registration/otp_code_email.txt'
789
789
  #: The email subject template.
@@ -852,7 +852,7 @@ class OtpEmailView(BodyMixin, JWTTokenDecodeMixin, BaseRestView):
852
852
  'message': _('A new authentication code has been sent by email.')})
853
853
 
854
854
  def send_otp_message(self, user):
855
- """Send an email to a user with an OTP code.
855
+ """Send an email to a user with an OTP code to a user.
856
856
 
857
857
  :param user: The user
858
858
  """
@@ -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,
File without changes
File without changes
File without changes
File without changes
File without changes