django-pfx 1.4.dev14__tar.gz → 1.4.dev18__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 (133) hide show
  1. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/PKG-INFO +1 -1
  2. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/django_pfx.egg-info/PKG-INFO +1 -1
  3. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/doc/conf.py +2 -1
  4. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/doc/source/authentication.md +154 -0
  5. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/exceptions.py +14 -14
  6. django-pfx-1.4.dev18/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
  7. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.po +82 -44
  8. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/shortcuts.py +4 -4
  9. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/authentication_views.py +14 -9
  10. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/requirements.txt +1 -0
  11. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/test_auth_api.py +36 -26
  12. django-pfx-1.4.dev14/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
  13. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/.gitignore +0 -0
  14. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/.gitlab-ci.yml +0 -0
  15. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/.pre-commit-config.yaml +0 -0
  16. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/LICENSE +0 -0
  17. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/MANIFEST.in +0 -0
  18. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/README.md +0 -0
  19. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/django-admin-test +0 -0
  20. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/django_pfx.egg-info/SOURCES.txt +0 -0
  21. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/django_pfx.egg-info/dependency_links.txt +0 -0
  22. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/django_pfx.egg-info/requires.txt +0 -0
  23. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/django_pfx.egg-info/top_level.txt +0 -0
  24. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/doc/Makefile +0 -0
  25. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/doc/index.rst +0 -0
  26. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/doc/source/api.views.rst +0 -0
  27. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/doc/source/decorator.md +0 -0
  28. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/doc/source/generate_openapi.md +0 -0
  29. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/doc/source/getting_started.md +0 -0
  30. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/doc/source/internationalisation.md +0 -0
  31. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/doc/source/model.md +0 -0
  32. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/doc/source/pfx_views.md +0 -0
  33. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/doc/source/profiling.md +0 -0
  34. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/doc/source/settings.md +0 -0
  35. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/doc/source/testing.md +0 -0
  36. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/img/pfx.png +0 -0
  37. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/img/pfx.svg +0 -0
  38. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/__init__.py +0 -0
  39. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/__init__.py +0 -0
  40. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/apidoc/__init__.py +0 -0
  41. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/apidoc/parameters.py +0 -0
  42. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/apidoc/schema.py +0 -0
  43. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/apidoc/tags.py +0 -0
  44. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/apps.py +0 -0
  45. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/decorator/__init__.py +0 -0
  46. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/decorator/rest.py +0 -0
  47. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/default_settings.py +0 -0
  48. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/fields.py +0 -0
  49. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/http/__init__.py +0 -0
  50. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/http/json_response.py +0 -0
  51. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/management/__init__.py +0 -0
  52. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/management/commands/__init__.py +0 -0
  53. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/management/commands/makeapidoc.py +0 -0
  54. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/management/commands/profile.py +0 -0
  55. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/middleware/__init__.py +0 -0
  56. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/middleware/authentication.py +0 -0
  57. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/middleware/locale.py +0 -0
  58. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/middleware/profiling.py +0 -0
  59. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/models/__init__.py +0 -0
  60. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/models/cache_mixins.py +0 -0
  61. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/models/not_null_fields.py +0 -0
  62. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/models/otp_user_mixin.py +0 -0
  63. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/models/pfx_models.py +0 -0
  64. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/models/user_filtered_queryset_mixin.py +0 -0
  65. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/serializers/__init__.py +0 -0
  66. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/serializers/json.py +0 -0
  67. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/settings.py +0 -0
  68. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/storage/__init__.py +0 -0
  69. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/storage/s3_storage.py +0 -0
  70. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/templates/registration/password_reset_email.txt +0 -0
  71. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/templates/registration/password_reset_subject.txt +0 -0
  72. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/templates/registration/welcome_email.txt +0 -0
  73. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/templates/registration/welcome_subject.txt +0 -0
  74. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/test.py +0 -0
  75. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/urls.py +0 -0
  76. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/__init__.py +0 -0
  77. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/fields.py +0 -0
  78. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/filters_views.py +0 -0
  79. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/locale_views.py +0 -0
  80. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/parameters/__init__.py +0 -0
  81. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/parameters/date_format.py +0 -0
  82. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/parameters/groups.py +0 -0
  83. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/parameters/list_count.py +0 -0
  84. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/parameters/list_items.py +0 -0
  85. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/parameters/list_mode.py +0 -0
  86. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/parameters/list_order.py +0 -0
  87. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/parameters/list_search.py +0 -0
  88. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/parameters/media_redirect.py +0 -0
  89. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/parameters/meta_fields.py +0 -0
  90. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/parameters/meta_filters.py +0 -0
  91. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/parameters/meta_orders.py +0 -0
  92. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/parameters/subset.py +0 -0
  93. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/parameters/subset_limit.py +0 -0
  94. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/parameters/subset_offset.py +0 -0
  95. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/parameters/subset_page.py +0 -0
  96. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/parameters/subset_page_size.py +0 -0
  97. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/parameters/subset_page_subset.py +0 -0
  98. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/rest_views.py +0 -0
  99. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pyproject.toml +0 -0
  100. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/runtest.py +0 -0
  101. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/serve-doc +0 -0
  102. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/setup.cfg +0 -0
  103. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/setup.py +0 -0
  104. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/__init__.py +0 -0
  105. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/locale/fr/LC_MESSAGES/django.po +0 -0
  106. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/models.py +0 -0
  107. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/settings/__init__.py +0 -0
  108. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/settings/ci.py +0 -0
  109. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/settings/common.py +0 -0
  110. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/settings/dev.py +0 -0
  111. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/settings/dev_custom_example.py +0 -0
  112. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/settings/dev_default.py +0 -0
  113. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/__init__.py +0 -0
  114. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/basic_api_errors.py +0 -0
  115. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/basic_api_test.py +0 -0
  116. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/test_api_doc.py +0 -0
  117. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/test_body_mixin.py +0 -0
  118. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/test_cache.py +0 -0
  119. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/test_client.py +0 -0
  120. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/test_fields.py +0 -0
  121. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/test_filters.py +0 -0
  122. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/test_locale_api.py +0 -0
  123. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/test_perm_tests.py +0 -0
  124. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/test_perms_api.py +0 -0
  125. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/test_profiling_middleware.py +0 -0
  126. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/test_shortcuts.py +0 -0
  127. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/test_timezone_middleware.py +0 -0
  128. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/test_tools.py +0 -0
  129. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/test_user_queryset.py +0 -0
  130. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/test_view_decorators.py +0 -0
  131. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/test_view_fields.py +0 -0
  132. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/urls.py +0 -0
  133. {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/views.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-pfx
3
- Version: 1.4.dev14
3
+ Version: 1.4.dev18
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.dev14
3
+ Version: 1.4.dev18
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
@@ -53,7 +53,8 @@ release = '1.0'
53
53
  # ones.
54
54
  extensions = ['myst_parser',
55
55
  'sphinx.ext.autodoc',
56
- 'sphinx.ext.autosummary']
56
+ 'sphinx.ext.autosummary',
57
+ 'sphinxcontrib.mermaid']
57
58
  autodoc_member_order = 'bysource'
58
59
 
59
60
  # Add any paths that contain templates here, relative to this directory.
@@ -48,12 +48,50 @@ To use the `CookieAuthenticationMiddleware`, you need to configure the following
48
48
 
49
49
  See the [MDN Website](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) for more details.
50
50
 
51
+ ### Multifactor Authentication
52
+ Multifactor authentication can be enabled in django-pfx Authentication API.
53
+
54
+ PFX currently provides MFA with One Time Password (OTP), compatible with FreeOTP,
55
+ Google Authenticator and other OTP app.
56
+
57
+ To enable this feature, install django-pfx with otp
58
+
59
+ ```bash
60
+ pip install django-pfx[otp]
61
+ ```
62
+
63
+ Then the user class must use the ```OtpUserMixin```
64
+
65
+ ```python
66
+ from django.contrib.auth.models import AbstractUser
67
+ from pfx.pfxcore.models import OtpUserMixin
68
+
69
+ class MyUser(OtpUserMixin, AbstractUser):
70
+ pass
71
+ ```
72
+
73
+ The user can then enable or disable the OTP auth using the [services documented below](#enable-mfa-otp).
74
+
51
75
  ## Services
52
76
 
53
77
  ### Login
54
78
  A login rest services with a `mode` parameter to choose between JWT bearer token or cookie authentication.
55
79
  In cookie mode, the JWT token is saved in an HTTP-only cookie.
56
80
 
81
+ ```{mermaid}
82
+
83
+ sequenceDiagram
84
+ participant App
85
+ participant API
86
+ App->>API: POST /auth/login
87
+ alt Authentication success
88
+ API->>App: 200 OK + cookie
89
+ else Authentication failed
90
+ API->>App: 401 Unautorized
91
+ end
92
+
93
+ ```
94
+
57
95
  **Request :** `POST` `/auth/login?mode=<mode>`
58
96
 
59
97
  **Request body:**
@@ -75,6 +113,122 @@ In cookie mode, the JWT token is saved in an HTTP-only cookie.
75
113
  | user | the user object |
76
114
 
77
115
 
116
+ ### Login + TOTP
117
+ If the user has enabled the TOTP login, the process is the same as above for the first step,
118
+ except that the login service returns a temporary JWT token valid only for the otp services.
119
+
120
+ ```{mermaid}
121
+
122
+ sequenceDiagram
123
+ participant App
124
+ participant API
125
+ App->>API: POST /auth/login
126
+ alt Login success
127
+ API->>App: 200 OK
128
+ note left of API: temporary jwt token
129
+ App->>API: POST /auth/otp/login
130
+ note right of App: temporary jwt token + OTP token in body.
131
+ alt OTP success
132
+ API->>App: 200 OK
133
+ note left of API: JWT token in cookie or in body <br/> + user in body
134
+ else OTP failed
135
+ API->>App: 401 Unautorized
136
+ end
137
+ else Login failed
138
+ API->>App: 401 Unautorized
139
+ end
140
+
141
+ ```
142
+
143
+ **Request :** `POST` `/auth/login?mode=<mode>`
144
+
145
+ **Request body:**
146
+
147
+ | Field | Description |
148
+ |-------------|-------------------------------------|
149
+ | username | the username |
150
+ | password | the password |
151
+ | remember_me | If true, use a long validity token. |
152
+
153
+ **Responses :**
154
+
155
+ * `HTTP 401` if the credentials are incorrect
156
+ * `HTTP 200` with the following body
157
+
158
+ | Field | Description |
159
+ |-------|-----------------------|
160
+ | token | a temporary jwt token |
161
+
162
+
163
+ **Request :** `POST` `/auth/otp/login?mode=<mode>`
164
+
165
+ **Request body:**
166
+
167
+ | Field | Description |
168
+ |-------------|-------------------------------------|
169
+ | token | the temporary jwt token |
170
+ | otp | the one time password (TOTP) |
171
+
172
+ **Responses :**
173
+
174
+ * `HTTP 401` if the temporary jwt token is incorrect
175
+ * `HTTP 403` if the otp is incorrect
176
+ * `HTTP 200` with the following body
177
+
178
+ | Field | Description |
179
+ |-------|----------------------------------------|
180
+ | token | the jwt token. (only if mode is 'jwt') |
181
+ | user | the user object |
182
+
183
+ ### Enable MFA OTP
184
+ Services to enable the MFA with OTP.
185
+ You have to call first the `activate` service to get the URI,
186
+ encode it as a QR code and present it in the UI of your software.
187
+ Then user then scans this QR code to add the OTP secret to his OTP App.
188
+ Finally, the `confirm` service must be called with an OTP code retrieved
189
+ in the OTP App to confirm the activation.
190
+
191
+ **Request :** `PUT` `/auth/otp/activate`
192
+
193
+ **Responses :**
194
+
195
+ * `HTTP 400` if the otp is already enabled
196
+ * `HTTP 200` with the following body
197
+
198
+ | Field | Description |
199
+ |-----------|---------------------------------------|
200
+ | setup_uri | the uri to enable the OTP application |
201
+
202
+ **Request :** `PUT` `/auth/otp/confirm`
203
+
204
+ **Request body:**
205
+
206
+ | Field | Description |
207
+ |-------------|---------------------------------------|
208
+ | otp_code | the otp code retrieved in the OTP app |
209
+
210
+ **Responses :**
211
+
212
+ * `HTTP 422` if the otp code is invalid
213
+ * `HTTP 200` if the otp code is valid
214
+
215
+ ### Disable MFA OTP
216
+ A service to disable the MFA with OTP.
217
+
218
+ **Request :** `PUT` `/auth/otp/disable`
219
+
220
+ **Request body:**
221
+
222
+ | Field | Description |
223
+ |-------------|---------------------------------------|
224
+ | otp_code | the otp code retrieved in the OTP app |
225
+
226
+ **Responses :**
227
+
228
+ * `HTTP 422` if the otp code is invalid
229
+ * `HTTP 200` if the otp code is valid
230
+
231
+
78
232
  ### Logout
79
233
  A service that deletes the authentication cookie if it exists.
80
234
 
@@ -19,17 +19,17 @@ class APIError(Exception):
19
19
  return res
20
20
 
21
21
 
22
- class JsonErrorAPIError(APIError):
23
- def __init__(self, json_error, status=422, **kwargs):
22
+ class ModelNotFoundAPIError(APIError):
23
+ def __init__(self, model, status=404, **kwargs):
24
24
  super().__init__(
25
- f(_("JSON Malformed {}").format(str(json_error))),
25
+ f(_("{model} not found."), model=model._meta.verbose_name),
26
26
  status=status, **kwargs)
27
27
 
28
28
 
29
- class ModelNotFoundAPIError(APIError):
30
- def __init__(self, model, status=404, **kwargs):
29
+ class JsonErrorAPIError(APIError):
30
+ def __init__(self, json_error, status=422, **kwargs):
31
31
  super().__init__(
32
- f(_("{model} not found."), model=model._meta.verbose_name),
32
+ f(_("JSON Malformed {}").format(str(json_error))),
33
33
  status=status, **kwargs)
34
34
 
35
35
 
@@ -50,17 +50,17 @@ class RelatedModelNotFoundAPIError(ModelValidationAPIError):
50
50
 
51
51
 
52
52
  class AuthenticationError(APIError):
53
- def __init__(self, message=None, status=401, **kwargs):
53
+ def __init__(self, message=None, status=422, **kwargs):
54
54
  super().__init__(
55
- f(message or _("Authentication Error")),
55
+ f(message or _("Login failed")),
56
56
  status=status, **kwargs)
57
57
 
58
58
 
59
59
  class UnauthorizedError(APIError):
60
- def __init__(self, **kwargs):
60
+ def __init__(self, message=None, status=401, **kwargs):
61
61
  super().__init__(
62
- f(_("Unauthorized")),
63
- status=401, **kwargs)
62
+ f(message or _("Unauthorized")),
63
+ status=status, **kwargs)
64
64
 
65
65
 
66
66
  class ForbiddenError(APIError):
@@ -71,7 +71,7 @@ class ForbiddenError(APIError):
71
71
 
72
72
 
73
73
  class NotFoundError(APIError):
74
- def __init__(self, **kwargs):
74
+ def __init__(self, message=None, status=404, **kwargs):
75
75
  super().__init__(
76
- f(_("Resource not found")),
77
- status=404, **kwargs)
76
+ f(message or _("Resource not found")),
77
+ status=status, **kwargs)
@@ -7,7 +7,7 @@ msgid ""
7
7
  msgstr ""
8
8
  "Project-Id-Version: \n"
9
9
  "Report-Msgid-Bugs-To: \n"
10
- "POT-Creation-Date: 2022-05-23 13:42+0200\n"
10
+ "POT-Creation-Date: 2024-03-28 11:03+0100\n"
11
11
  "PO-Revision-Date: 2021-06-22 23:31+0200\n"
12
12
  "Last-Translator: \n"
13
13
  "Language-Team: \n"
@@ -18,22 +18,22 @@ msgstr ""
18
18
  "Plural-Forms: nplurals=2; plural=(n > 1);\n"
19
19
  "X-Generator: Poedit 2.3\n"
20
20
 
21
- #: decorator/rest.py:30
21
+ #: decorator/rest.py:43
22
22
  msgid "An internal server error occured."
23
23
  msgstr "Une erreur interne du serveur est survenue."
24
24
 
25
- #: exceptions.py:25
26
- msgid "JSON Malformed {}"
27
- msgstr "JSON malformé {}"
28
-
29
- #: exceptions.py:32 exceptions.py:48
25
+ #: exceptions.py:25 exceptions.py:48
30
26
  #, python-brace-format
31
27
  msgid "{model} not found."
32
28
  msgstr "{model} non trouvé."
33
29
 
30
+ #: exceptions.py:32
31
+ msgid "JSON Malformed {}"
32
+ msgstr "JSON malformé {}"
33
+
34
34
  #: exceptions.py:55
35
- msgid "Authentication Error"
36
- msgstr "Erreur d'authentification"
35
+ msgid "Login failed"
36
+ msgstr "La connexion a échoué"
37
37
 
38
38
  #: exceptions.py:62
39
39
  msgid "Unauthorized"
@@ -44,16 +44,14 @@ msgid "Forbidden"
44
44
  msgstr "Interdit"
45
45
 
46
46
  #: exceptions.py:76
47
- #, fuzzy
48
- #| msgid "{model} not found."
49
47
  msgid "Resource not found"
50
- msgstr "{model} non trouvé."
48
+ msgstr "Ressource non trouvée"
51
49
 
52
- #: fields.py:68
50
+ #: fields.py:77
53
51
  msgid "Invalid value."
54
- msgstr ""
52
+ msgstr "Valeur invalide."
55
53
 
56
- #: fields.py:83
54
+ #: fields.py:92
57
55
  msgid ""
58
56
  "Invalid format, it can be a number in hours, “1:05”, “:05”, “1h 5m”, “1.5h” "
59
57
  "or “30m”."
@@ -61,20 +59,39 @@ msgstr ""
61
59
  "Format non valide, il peut s’agir d’un nombre en heures, « 1:05 », « :05 », "
62
60
  "« 1h 5m », « 1.5h » ou « 30m »."
63
61
 
64
- #: middleware/authentication.py:45 middleware/authentication.py:48
65
- #: middleware/authentication.py:53
66
- msgid "Authentication error"
67
- msgstr "Erreur d'authentification"
62
+ #: models/otp_user_mixin.py:8
63
+ msgid "OTP secret token"
64
+ msgstr "Jeton secret OTP"
68
65
 
69
- #: middleware/authentication.py:50
70
- msgid "Token has expired"
71
- msgstr "Le jeton a expiré"
66
+ #: models/otp_user_mixin.py:11
67
+ msgid "Temporary OTP secret token"
68
+ msgstr "Jeton secret OTP temporaire"
72
69
 
73
70
  #: models/pfx_models.py:14
74
71
  #, python-format
75
72
  msgid "%(model_name)s with this %(field_labels)s already exists."
76
73
  msgstr "%(model_name)s avec ce/cette %(field_labels)s éxiste déjà."
77
74
 
75
+ #: shortcuts.py:52
76
+ #, python-brace-format
77
+ msgid "{key} must be an integer number."
78
+ msgstr "{key} doit être un nombre entier."
79
+
80
+ #: shortcuts.py:68
81
+ #, python-brace-format
82
+ msgid "{key} must be a number."
83
+ msgstr "{key} doit être un nombre."
84
+
85
+ #: shortcuts.py:84
86
+ #, python-brace-format
87
+ msgid "{key} must be a date."
88
+ msgstr "{key} doit être une date."
89
+
90
+ #: shortcuts.py:105
91
+ #, python-brace-format
92
+ msgid "{key} must be “true”, “false”, “1”, “0” or empty."
93
+ msgstr "{key} doit être « true », « false », « 1 », « 0 » ou vide"
94
+
78
95
  #: templates/registration/password_reset_email.txt:2
79
96
  #, python-format
80
97
  msgid ""
@@ -122,19 +139,43 @@ msgstr "Bienvenue sur %(site_name)s."
122
139
  msgid "Welcome on %(site_name)s"
123
140
  msgstr "Bienvenue sur %(site_name)s"
124
141
 
125
- #: views/authentication_views.py:101 views/authentication_views.py:155
142
+ #: views/authentication_views.py:221 views/authentication_views.py:378
126
143
  msgid "password updated successfully"
127
144
  msgstr "le mot de passe a été mis à jour avec succès"
128
145
 
129
- #: views/authentication_views.py:106
146
+ #: views/authentication_views.py:226
130
147
  msgid "Incorrect password"
131
148
  msgstr "Mot de passe incorrect"
132
149
 
133
- #: views/authentication_views.py:109 views/authentication_views.py:164
150
+ #: views/authentication_views.py:229 views/authentication_views.py:387
134
151
  msgid "Empty password is not allowed"
135
152
  msgstr "Un mot de passe vide n’est pas autorisé"
136
153
 
137
- #: views/authentication_views.py:268
154
+ #: views/authentication_views.py:313
155
+ msgid "User and token are valid"
156
+ msgstr "L'utilisateur et le token sont valides"
157
+
158
+ #: views/authentication_views.py:315
159
+ msgid "User or token is invalid"
160
+ msgstr "L'utilisateur ou le token est invalide"
161
+
162
+ #: views/authentication_views.py:420
163
+ msgid "OTP is already activated"
164
+ msgstr "L'OTP est déjà activé"
165
+
166
+ #: views/authentication_views.py:458
167
+ msgid "OTP is activated"
168
+ msgstr "L'OTP est activé"
169
+
170
+ #: views/authentication_views.py:459 views/authentication_views.py:493
171
+ msgid "Invalid OTP code"
172
+ msgstr "Code OTP invalide"
173
+
174
+ #: views/authentication_views.py:492
175
+ msgid "OTP is disabled"
176
+ msgstr "L'OTP est désactivé"
177
+
178
+ #: views/authentication_views.py:740
138
179
  msgid ""
139
180
  "If the email address you entered is correct, you will receive an email from "
140
181
  "us with instructions to reset your password."
@@ -143,35 +184,32 @@ msgstr ""
143
184
  "un courrier électronique de notre part contenant des instructions pour "
144
185
  "réinitialiser votre mot de passe."
145
186
 
146
- #: views/rest_views.py:139
187
+ #: views/filters_views.py:79
188
+ #, python-brace-format
189
+ msgid "Invalid value for {filter} filter"
190
+ msgstr "Valeur invalide pour le filtre {filter}"
191
+
192
+ #: views/rest_views.py:235
193
+ #, python-brace-format
194
+ msgid "{obj} cannot be deleted because it is referenced by other objects."
195
+ msgstr ""
196
+ "{obj} ne peut pas être supprimé car il est référencé par d’autres objets."
197
+
198
+ #: views/rest_views.py:329
147
199
  #, python-brace-format
148
200
  msgid "{model} {obj} created."
149
201
  msgstr "{model} {obj} créé."
150
202
 
151
- #: views/rest_views.py:140
203
+ #: views/rest_views.py:330
152
204
  #, python-brace-format
153
205
  msgid "{model} {obj} updated."
154
206
  msgstr "{model} {obj} modifié."
155
207
 
156
- #: views/rest_views.py:241
157
- #, python-brace-format
158
- msgid "Meta {meta} does not exists."
159
- msgstr "La meta {meta} n’éxiste pas."
160
-
161
- #: views/rest_views.py:396
208
+ #: views/rest_views.py:1076
162
209
  #, python-brace-format
163
210
  msgid "{model} {obj} deleted."
164
211
  msgstr "{model} {obj} supprimé."
165
212
 
166
- #: views/rest_views.py:400
167
- #, python-brace-format
168
- msgid "{obj} cannot be deleted because it is referenced by other objects."
169
- msgstr ""
170
- "{obj} ne peut pas être supprimé car il est référencé par d’autres objets."
171
-
172
- #: views/rest_views.py:422 views/rest_views.py:432
213
+ #: views/rest_views.py:1138 views/rest_views.py:1166
173
214
  msgid "Unexpected storage error"
174
215
  msgstr "Erreur de stockage inattendue"
175
-
176
- #~ msgid "A message has been sent"
177
- #~ msgstr "Un message a été envoyé"
@@ -49,7 +49,7 @@ def get_int(data, key, default=None):
49
49
  return parse_int(data.get(key))
50
50
  except ValueError:
51
51
  from pfx.pfxcore.exceptions import APIError
52
- raise APIError(f(_("{key} must be an integer value."), key=key))
52
+ raise APIError(f(_("{key} must be an integer number."), key=key))
53
53
 
54
54
 
55
55
  def parse_float(value):
@@ -65,7 +65,7 @@ def get_float(data, key, default=None):
65
65
  return parse_float(data.get(key))
66
66
  except ValueError:
67
67
  from pfx.pfxcore.exceptions import APIError
68
- raise APIError(f(_("{key} must be an float value."), key=key))
68
+ raise APIError(f(_("{key} must be a number."), key=key))
69
69
 
70
70
 
71
71
  def parse_date(value):
@@ -81,7 +81,7 @@ def get_date(data, key, default=None):
81
81
  return parse_date(data.get(key))
82
82
  except ValueError:
83
83
  from pfx.pfxcore.exceptions import APIError
84
- raise APIError(f(_("{key} must be a date value."), key=key))
84
+ raise APIError(f(_("{key} must be a date."), key=key))
85
85
 
86
86
 
87
87
  def parse_bool(value):
@@ -102,7 +102,7 @@ def get_bool(data, key, default=None):
102
102
  except ValueError:
103
103
  from pfx.pfxcore.exceptions import APIError
104
104
  raise APIError(f(
105
- _("{key} must be 'true'|'false'|'1'|'0' or empty."),
105
+ _("{key} must be true”, “false”, “1”, “0 or empty."),
106
106
  key=key))
107
107
 
108
108
 
@@ -26,6 +26,7 @@ from pfx.pfxcore.exceptions import (
26
26
  AuthenticationError,
27
27
  ForbiddenError,
28
28
  NotFoundError,
29
+ UnauthorizedError,
29
30
  )
30
31
  from pfx.pfxcore.http import JsonResponse
31
32
  from pfx.pfxcore.middleware.authentication import JWTTokenDecodeMixin
@@ -64,8 +65,8 @@ class AuthenticationView(
64
65
  #: The token generator.
65
66
  token_generator = default_token_generator
66
67
 
67
- def login_error_response(self):
68
- """Raise the error for login error.
68
+ def login_failed_response(self):
69
+ """Return the response for login failed.
69
70
 
70
71
  Can be overridden to customize the error."""
71
72
  raise AuthenticationError()
@@ -112,7 +113,7 @@ class AuthenticationView(
112
113
  enum: ['jwt', 'cookie']
113
114
  default: 'jwt'
114
115
  responses:
115
- 401:
116
+ 422:
116
117
  description: The credentials are not valid.
117
118
  """
118
119
  data = self.deserialize_body()
@@ -121,7 +122,7 @@ class AuthenticationView(
121
122
  if isinstance(user, CacheableMixin):
122
123
  user.cache_delete()
123
124
  if user is None:
124
- return self.login_error_response()
125
+ return self.login_failed_response()
125
126
  mode = self.request.GET.get('mode', 'jwt')
126
127
  remember_me = data.get('remember_me', False)
127
128
  if isinstance(user, OtpUserMixin) and user.otp_secret_token:
@@ -384,7 +385,7 @@ class AuthenticationView(
384
385
  return JsonResponse(
385
386
  ValidationError(dict(
386
387
  password=_("Empty password is not allowed"))), status=422)
387
- raise AuthenticationError()
388
+ raise UnauthorizedError()
388
389
 
389
390
  @method_decorator(never_cache)
390
391
  @rest_api("/otp/activate", public=False, method="put")
@@ -518,10 +519,12 @@ class AuthenticationView(
518
519
  type: string
519
520
  description: a valid OTP code
520
521
  responses:
522
+ 422:
523
+ description: If OTP code is missing, invalid or expired.
521
524
  401:
522
- description: If OTP code is not valid.
525
+ description: If the token is missing, invalid or expired.
523
526
  403:
524
- description: If the token is not valid or expired.
527
+ description: If the OTP is disabled for this user.
525
528
 
526
529
  """
527
530
  data = self.deserialize_body()
@@ -529,13 +532,15 @@ class AuthenticationView(
529
532
  user, mode, remember_me = self.decode_jwt(
530
533
  data.get('token'), otp_login=True)
531
534
  except Exception:
532
- raise ForbiddenError()
535
+ raise UnauthorizedError()
533
536
  if not isinstance(user, OtpUserMixin):
534
537
  logger.error("User must inherit OtpUserMixin to activate OTP")
535
538
  raise NotFoundError()
539
+ if not user.otp_secret_token:
540
+ raise ForbiddenError()
536
541
  if user.is_otp_valid(data.get('otp_code')):
537
542
  return self._login_success(user, mode, remember_me)
538
- return self.login_error_response()
543
+ return self.login_failed_response()
539
544
 
540
545
  def get_user(self, uidb64):
541
546
  """Get user by token
@@ -14,6 +14,7 @@ apispec
14
14
  sphinx
15
15
  myst-parser
16
16
  sphinx-rtd-theme
17
+ sphinxcontrib-mermaid
17
18
  dill
18
19
  apispec
19
20
  pyyaml
@@ -39,12 +39,12 @@ class AuthAPITest(TestAssertMixin, TransactionTestCase):
39
39
  '/api/auth/login', {
40
40
  'username': 'jrr.tolkien',
41
41
  'password': 'WRONG PASSWORD'})
42
- self.assertRC(response, 401)
42
+ self.assertRC(response, 422)
43
43
 
44
44
  def test_emtpy_login(self):
45
45
  response = self.client.post(
46
46
  '/api/auth/login', {})
47
- self.assertRC(response, 401)
47
+ self.assertRC(response, 422)
48
48
 
49
49
  @override_settings(PFX_TOKEN_SHORT_VALIDITY={'minutes': 30})
50
50
  def test_valid_login(self):
@@ -639,7 +639,6 @@ class AuthAPITest(TestAssertMixin, TransactionTestCase):
639
639
  'uidb64': 'WRONG UID',
640
640
  'password': 'NEW PASSWORD',
641
641
  })
642
-
643
642
  self.assertRC(response, 401)
644
643
 
645
644
  # Then try with a valid token and uid
@@ -792,6 +791,11 @@ class AuthAPITest(TestAssertMixin, TransactionTestCase):
792
791
  '/api/books')
793
792
  self.assertRC(response, 200)
794
793
 
794
+ def enable_otp(self, user):
795
+ user.otp_secret_token = pyotp.random_base32()
796
+ user.save()
797
+ return pyotp.totp.TOTP(self.user1.otp_secret_token)
798
+
795
799
  def test_otp_activate(self):
796
800
  self.client.login(username='jrr.tolkien', password='RIGHT PASSWORD')
797
801
 
@@ -835,8 +839,7 @@ class AuthAPITest(TestAssertMixin, TransactionTestCase):
835
839
  def test_otp_activate_already_activated(self):
836
840
  self.client.login(username='jrr.tolkien', password='RIGHT PASSWORD')
837
841
 
838
- self.user1.otp_secret_token = pyotp.random_base32()
839
- self.user1.save()
842
+ self.enable_otp(self.user1)
840
843
 
841
844
  response = self.client.put('/api/auth/otp/activate')
842
845
  self.assertRC(response, 400)
@@ -845,9 +848,7 @@ class AuthAPITest(TestAssertMixin, TransactionTestCase):
845
848
  def test_otp_disable(self):
846
849
  self.client.login(username='jrr.tolkien', password='RIGHT PASSWORD')
847
850
 
848
- self.user1.otp_secret_token = pyotp.random_base32()
849
- self.user1.save()
850
- totp = pyotp.totp.TOTP(self.user1.otp_secret_token)
851
+ totp = self.enable_otp(self.user1)
851
852
 
852
853
  response = self.client.put('/api/auth/otp/disable', dict(
853
854
  otp_code=totp.now()))
@@ -860,8 +861,7 @@ class AuthAPITest(TestAssertMixin, TransactionTestCase):
860
861
  def test_otp_disable_bad_code(self):
861
862
  self.client.login(username='jrr.tolkien', password='RIGHT PASSWORD')
862
863
 
863
- self.user1.otp_secret_token = pyotp.random_base32()
864
- self.user1.save()
864
+ self.enable_otp(self.user1)
865
865
 
866
866
  response = self.client.put('/api/auth/otp/disable', dict(
867
867
  otp_code='-'))
@@ -873,9 +873,7 @@ class AuthAPITest(TestAssertMixin, TransactionTestCase):
873
873
 
874
874
  @override_settings(PFX_TOKEN_SHORT_VALIDITY={'minutes': 30})
875
875
  def test_otp_login(self):
876
- self.user1.otp_secret_token = pyotp.random_base32()
877
- self.user1.save()
878
- totp = pyotp.totp.TOTP(self.user1.otp_secret_token)
876
+ totp = self.enable_otp(self.user1)
879
877
 
880
878
  with freeze_time("2023-05-01 08:00:00"):
881
879
  response = self.client.post('/api/auth/login', dict(
@@ -909,9 +907,7 @@ class AuthAPITest(TestAssertMixin, TransactionTestCase):
909
907
 
910
908
  @override_settings(PFX_TOKEN_LONG_VALIDITY={'days': 1})
911
909
  def test_otp_login_remember_me(self):
912
- self.user1.otp_secret_token = pyotp.random_base32()
913
- self.user1.save()
914
- totp = pyotp.totp.TOTP(self.user1.otp_secret_token)
910
+ totp = self.enable_otp(self.user1)
915
911
 
916
912
  with freeze_time("2023-05-01 08:00:00"):
917
913
  response = self.client.post('/api/auth/login', dict(
@@ -945,9 +941,7 @@ class AuthAPITest(TestAssertMixin, TransactionTestCase):
945
941
  datetime(2023, 5, 2, 8, 10))
946
942
 
947
943
  def test_otp_login_expired_token(self):
948
- self.user1.otp_secret_token = pyotp.random_base32()
949
- self.user1.save()
950
- totp = pyotp.totp.TOTP(self.user1.otp_secret_token)
944
+ totp = self.enable_otp(self.user1)
951
945
 
952
946
  with freeze_time("2023-05-01 08:00:00"):
953
947
  response = self.client.post('/api/auth/login', dict(
@@ -962,12 +956,11 @@ class AuthAPITest(TestAssertMixin, TransactionTestCase):
962
956
  response = self.client.post('/api/auth/otp/login', dict(
963
957
  token=token,
964
958
  otp_code=totp.now()))
965
- self.assertRC(response, 403)
959
+ self.assertRC(response, 401)
966
960
 
967
961
  @override_settings(PFX_TOKEN_SHORT_VALIDITY={'minutes': 30})
968
962
  def test_otp_login_invalid_code(self):
969
- self.user1.otp_secret_token = pyotp.random_base32()
970
- self.user1.save()
963
+ self.enable_otp(self.user1)
971
964
 
972
965
  response = self.client.post('/api/auth/login', dict(
973
966
  username='jrr.tolkien',
@@ -980,12 +973,29 @@ class AuthAPITest(TestAssertMixin, TransactionTestCase):
980
973
  response = self.client.post('/api/auth/otp/login', dict(
981
974
  token=token,
982
975
  otp_code='-'))
983
- self.assertRC(response, 401)
976
+ self.assertRC(response, 422)
977
+
978
+ @override_settings(PFX_TOKEN_SHORT_VALIDITY={'minutes': 30})
979
+ def test_otp_login_disabled(self):
980
+ totp = self.enable_otp(self.user1)
981
+
982
+ response = self.client.post('/api/auth/login', dict(
983
+ username='jrr.tolkien',
984
+ password='RIGHT PASSWORD'))
985
+ self.assertRC(response, 200)
986
+ self.assertJE(response, 'need_otp', True)
987
+ self.assertJEExists(response, 'token')
988
+ token = self.get_val(response, 'token')
989
+
990
+ self.user1.disable_otp()
991
+
992
+ response = self.client.post('/api/auth/otp/login', dict(
993
+ token=token,
994
+ otp_code=totp.now()))
995
+ self.assertRC(response, 403)
984
996
 
985
997
  def test_otp_token_rejected(self):
986
- self.user1.otp_secret_token = pyotp.random_base32()
987
- self.user1.save()
988
- totp = pyotp.totp.TOTP(self.user1.otp_secret_token)
998
+ totp = self.enable_otp(self.user1)
989
999
 
990
1000
  response = self.client.post('/api/auth/login', dict(
991
1001
  username='jrr.tolkien',
File without changes
File without changes
File without changes
File without changes
File without changes