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.
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/PKG-INFO +1 -1
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/django_pfx.egg-info/PKG-INFO +1 -1
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/django_pfx.egg-info/SOURCES.txt +1 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/doc/source/api.views.rst +18 -1
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/doc/source/authentication.md +42 -14
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/models/__init__.py +1 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/models/abstract_pfx_base_user.py +4 -1
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/models/otp_user_mixin.py +39 -0
- django-pfx-1.4.dev30/pfx/pfxcore/models/pfx_user.py +11 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/authentication_views.py +2 -2
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/test_auth_api.py +1 -1
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/.gitignore +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/.gitlab-ci.yml +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/.pre-commit-config.yaml +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/LICENSE +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/MANIFEST.in +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/README.md +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/django-admin-test +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/django_pfx.egg-info/dependency_links.txt +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/django_pfx.egg-info/requires.txt +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/django_pfx.egg-info/top_level.txt +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/doc/Makefile +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/doc/conf.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/doc/index.rst +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/doc/source/decorator.md +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/doc/source/generate_openapi.md +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/doc/source/getting_started.md +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/doc/source/internationalisation.md +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/doc/source/model.md +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/doc/source/pfx_views.md +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/doc/source/profiling.md +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/doc/source/settings.md +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/doc/source/testing.md +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/img/pfx.png +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/img/pfx.svg +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/__init__.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/__init__.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/apidoc/__init__.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/apidoc/parameters.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/apidoc/schema.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/apidoc/tags.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/apps.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/decorator/__init__.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/decorator/rest.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/default_settings.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/exceptions.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/fields.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/http/__init__.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/http/json_response.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.po +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/management/__init__.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/management/commands/__init__.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/management/commands/makeapidoc.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/management/commands/profile.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/middleware/__init__.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/middleware/authentication.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/middleware/locale.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/middleware/profiling.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/models/cache_mixins.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/models/login_ban.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/models/not_null_fields.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/models/pfx_models.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/models/user_filtered_queryset_mixin.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/serializers/__init__.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/serializers/json.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/settings.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/shortcuts.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/storage/__init__.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/storage/s3_storage.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/templates/registration/otp_code_email.txt +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/templates/registration/otp_code_subject.txt +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/templates/registration/password_reset_email.txt +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/templates/registration/password_reset_subject.txt +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/templates/registration/welcome_email.txt +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/templates/registration/welcome_subject.txt +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/test.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/urls.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/__init__.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/fields.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/filters_views.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/locale_views.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/__init__.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/date_format.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/groups.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/list_count.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/list_items.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/list_mode.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/list_order.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/list_search.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/media_redirect.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/meta_fields.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/meta_filters.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/meta_orders.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/subset.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/subset_limit.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/subset_offset.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/subset_page.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/subset_page_size.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/subset_page_subset.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/rest_views.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pyproject.toml +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/requirements.txt +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/runtest.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/serve-doc +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/setup.cfg +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/setup.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/__init__.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/locale/fr/LC_MESSAGES/django.po +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/models.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/settings/__init__.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/settings/ci.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/settings/common.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/settings/dev.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/settings/dev_custom_example.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/settings/dev_default.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/__init__.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/basic_api_errors.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/basic_api_test.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/test_api_doc.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/test_body_mixin.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/test_cache.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/test_client.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/test_fields.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/test_filters.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/test_locale_api.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/test_perm_tests.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/test_perms_api.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/test_profiling_middleware.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/test_settings.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/test_shortcuts.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/test_timezone_middleware.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/test_tools.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/test_user_queryset.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/test_view_decorators.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/tests/test_view_fields.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/urls.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/tests/views.py +0 -0
|
@@ -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.
|
|
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 `
|
|
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
|
|
16
|
+
from pfx.pfxcore.models import AbstractPFXBaseUser
|
|
16
17
|
|
|
17
|
-
class MyUser(
|
|
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
|
-
* `
|
|
32
|
-
* `
|
|
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
|
|
77
|
+
Then the user class must use the {class}`pfx.pfxcore.models.OtpUserMixin`.
|
|
64
78
|
|
|
65
79
|
```python
|
|
66
|
-
from
|
|
67
|
-
from pfx.pfxcore.models import OtpUserMixin
|
|
80
|
+
from pfx.pfxcore.models import PFXUser, OtpUserMixin
|
|
68
81
|
|
|
69
|
-
class MyUser(OtpUserMixin,
|
|
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
|
|
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
|
|
319
|
+
Refer to {class}`pfx.pfxcore.views.SignupView` for more details.
|
|
292
320
|
|
|
293
321
|
**Request :** `POST` `/auth/signup`
|
|
294
322
|
|
|
@@ -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
|
|
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 =
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/models/user_filtered_queryset_mixin.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/templates/registration/otp_code_email.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/templates/registration/welcome_email.txt
RENAMED
|
File without changes
|
{django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/templates/registration/welcome_subject.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/media_redirect.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/subset_page_size.py
RENAMED
|
File without changes
|
{django-pfx-1.4.dev28 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/subset_page_subset.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|