django-pfx 1.4.dev26__tar.gz → 1.4.dev30__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/PKG-INFO +1 -1
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/django_pfx.egg-info/PKG-INFO +1 -1
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/django_pfx.egg-info/SOURCES.txt +2 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/doc/source/api.views.rst +18 -1
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/doc/source/authentication.md +42 -14
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/decorator/rest.py +1 -3
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/management/commands/makeapidoc.py +1 -2
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/middleware/profiling.py +1 -2
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/models/__init__.py +1 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/models/abstract_pfx_base_user.py +4 -1
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/models/login_ban.py +1 -3
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/models/otp_user_mixin.py +40 -3
- django-pfx-1.4.dev30/pfx/pfxcore/models/pfx_user.py +11 -0
- django-pfx-1.4.dev30/pfx/pfxcore/settings.py +22 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/shortcuts.py +1 -3
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/authentication_views.py +3 -4
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/__init__.py +1 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/test_auth_api.py +1 -1
- django-pfx-1.4.dev30/tests/tests/test_settings.py +23 -0
- django-pfx-1.4.dev26/pfx/pfxcore/settings.py +0 -11
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/.gitignore +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/.gitlab-ci.yml +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/.pre-commit-config.yaml +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/LICENSE +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/MANIFEST.in +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/README.md +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/django-admin-test +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/django_pfx.egg-info/dependency_links.txt +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/django_pfx.egg-info/requires.txt +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/django_pfx.egg-info/top_level.txt +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/doc/Makefile +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/doc/conf.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/doc/index.rst +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/doc/source/decorator.md +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/doc/source/generate_openapi.md +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/doc/source/getting_started.md +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/doc/source/internationalisation.md +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/doc/source/model.md +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/doc/source/pfx_views.md +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/doc/source/profiling.md +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/doc/source/settings.md +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/doc/source/testing.md +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/img/pfx.png +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/img/pfx.svg +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/__init__.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/__init__.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/apidoc/__init__.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/apidoc/parameters.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/apidoc/schema.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/apidoc/tags.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/apps.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/decorator/__init__.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/default_settings.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/exceptions.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/fields.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/http/__init__.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/http/json_response.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.po +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/management/__init__.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/management/commands/__init__.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/management/commands/profile.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/middleware/__init__.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/middleware/authentication.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/middleware/locale.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/models/cache_mixins.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/models/not_null_fields.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/models/pfx_models.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/models/user_filtered_queryset_mixin.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/serializers/__init__.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/serializers/json.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/storage/__init__.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/storage/s3_storage.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/templates/registration/otp_code_email.txt +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/templates/registration/otp_code_subject.txt +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/templates/registration/password_reset_email.txt +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/templates/registration/password_reset_subject.txt +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/templates/registration/welcome_email.txt +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/templates/registration/welcome_subject.txt +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/test.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/urls.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/__init__.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/fields.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/filters_views.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/locale_views.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/__init__.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/date_format.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/groups.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/list_count.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/list_items.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/list_mode.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/list_order.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/list_search.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/media_redirect.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/meta_fields.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/meta_filters.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/meta_orders.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/subset.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/subset_limit.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/subset_offset.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/subset_page.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/subset_page_size.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/subset_page_subset.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/rest_views.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/pyproject.toml +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/requirements.txt +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/runtest.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/serve-doc +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/setup.cfg +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/setup.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/__init__.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/locale/fr/LC_MESSAGES/django.po +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/models.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/settings/__init__.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/settings/ci.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/settings/common.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/settings/dev.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/settings/dev_custom_example.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/settings/dev_default.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/basic_api_errors.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/basic_api_test.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/test_api_doc.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/test_body_mixin.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/test_cache.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/test_client.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/test_fields.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/test_filters.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/test_locale_api.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/test_perm_tests.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/test_perms_api.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/test_profiling_middleware.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/test_shortcuts.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/test_timezone_middleware.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/test_tools.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/test_user_queryset.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/test_view_decorators.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/tests/test_view_fields.py +0 -0
- {django-pfx-1.4.dev26 → django-pfx-1.4.dev30}/tests/urls.py +0 -0
- {django-pfx-1.4.dev26 → 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
|
|
@@ -127,6 +128,7 @@ tests/tests/test_locale_api.py
|
|
|
127
128
|
tests/tests/test_perm_tests.py
|
|
128
129
|
tests/tests/test_perms_api.py
|
|
129
130
|
tests/tests/test_profiling_middleware.py
|
|
131
|
+
tests/tests/test_settings.py
|
|
130
132
|
tests/tests/test_shortcuts.py
|
|
131
133
|
tests/tests/test_timezone_middleware.py
|
|
132
134
|
tests/tests/test_tools.py
|
|
@@ -32,6 +32,18 @@ API Reference
|
|
|
32
32
|
:undoc-members:
|
|
33
33
|
:show-inheritance:
|
|
34
34
|
|
|
35
|
+
.. autoclass:: pfx.pfxcore.models.AbstractPFXBaseUser
|
|
36
|
+
:members:
|
|
37
|
+
:show-inheritance:
|
|
38
|
+
|
|
39
|
+
.. autoclass:: pfx.pfxcore.models.OtpUserMixin
|
|
40
|
+
:members:
|
|
41
|
+
:show-inheritance:
|
|
42
|
+
|
|
43
|
+
.. autoclass:: pfx.pfxcore.models.PFXUser
|
|
44
|
+
:members:
|
|
45
|
+
:show-inheritance:
|
|
46
|
+
|
|
35
47
|
``pfx.pfxcore.views``
|
|
36
48
|
*********************
|
|
37
49
|
|
|
@@ -43,12 +55,17 @@ Base services
|
|
|
43
55
|
:undoc-members:
|
|
44
56
|
:show-inheritance:
|
|
45
57
|
|
|
58
|
+
.. autoclass:: pfx.pfxcore.views.SignupView
|
|
59
|
+
:members:
|
|
60
|
+
:undoc-members:
|
|
61
|
+
:show-inheritance:
|
|
62
|
+
|
|
46
63
|
.. autoclass:: pfx.pfxcore.views.ForgottenPasswordView
|
|
47
64
|
:members:
|
|
48
65
|
:undoc-members:
|
|
49
66
|
:show-inheritance:
|
|
50
67
|
|
|
51
|
-
.. autoclass:: pfx.pfxcore.views.
|
|
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
|
|
|
@@ -7,11 +7,9 @@ from apispec.utils import deepupdate
|
|
|
7
7
|
|
|
8
8
|
from pfx.pfxcore.exceptions import APIError
|
|
9
9
|
from pfx.pfxcore.http import JsonResponse
|
|
10
|
-
from pfx.pfxcore.settings import
|
|
10
|
+
from pfx.pfxcore.settings import settings
|
|
11
11
|
from pfx.pfxcore.test import format_request
|
|
12
12
|
|
|
13
|
-
settings = PFXSettings()
|
|
14
|
-
|
|
15
13
|
logger = logging.getLogger(__name__)
|
|
16
14
|
|
|
17
15
|
|
|
@@ -11,10 +11,9 @@ from apispec.yaml_utils import load_operations_from_docstring
|
|
|
11
11
|
|
|
12
12
|
from pfx.pfxcore import __PFX_VIEWS__
|
|
13
13
|
from pfx.pfxcore.apidoc import __PARAMETERS__, __TAGS__, ParameterGroup
|
|
14
|
-
from pfx.pfxcore.settings import
|
|
14
|
+
from pfx.pfxcore.settings import settings
|
|
15
15
|
from pfx.pfxcore.views import FilterGroup, ModelMixin
|
|
16
16
|
|
|
17
|
-
settings = PFXSettings()
|
|
18
17
|
DEFAULT_TEMPLATE = dict(
|
|
19
18
|
title="PFX API",
|
|
20
19
|
version="1.0.0",
|
|
@@ -11,10 +11,9 @@ from django.utils.deprecation import MiddlewareMixin
|
|
|
11
11
|
|
|
12
12
|
import sqlparse
|
|
13
13
|
|
|
14
|
-
from pfx.pfxcore.settings import
|
|
14
|
+
from pfx.pfxcore.settings import settings
|
|
15
15
|
|
|
16
16
|
logger = logging.getLogger(__name__)
|
|
17
|
-
settings = PFXSettings()
|
|
18
17
|
rid = ContextVar('var')
|
|
19
18
|
|
|
20
19
|
|
|
@@ -2,6 +2,8 @@ from django.contrib.auth.models import AbstractBaseUser
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
class AbstractPFXBaseUser(AbstractBaseUser):
|
|
5
|
+
"""The base abstract user for PFX."""
|
|
6
|
+
|
|
5
7
|
class Meta:
|
|
6
8
|
abstract = True
|
|
7
9
|
|
|
@@ -10,6 +12,7 @@ class AbstractPFXBaseUser(AbstractBaseUser):
|
|
|
10
12
|
Return a user secret to sign JWT token.
|
|
11
13
|
|
|
12
14
|
If not empty, the JWT token validity depends on all values
|
|
13
|
-
user to build the return string.
|
|
15
|
+
user to build the return string. So, each time the returned value
|
|
16
|
+
changes, the previously issued tokens will no longer be valid.
|
|
14
17
|
"""
|
|
15
18
|
return self.password
|
|
@@ -4,9 +4,7 @@ from django.db import models
|
|
|
4
4
|
from django.utils import timezone
|
|
5
5
|
from django.utils.translation import gettext_lazy as _
|
|
6
6
|
|
|
7
|
-
from pfx.pfxcore.settings import
|
|
8
|
-
|
|
9
|
-
settings = PFXSettings()
|
|
7
|
+
from pfx.pfxcore.settings import settings
|
|
10
8
|
|
|
11
9
|
|
|
12
10
|
class LoginBanQuerySet(models.QuerySet):
|
|
@@ -4,29 +4,46 @@ from django.db import models
|
|
|
4
4
|
from django.utils import timezone
|
|
5
5
|
from django.utils.translation import gettext_lazy as _
|
|
6
6
|
|
|
7
|
-
from pfx.pfxcore.settings import
|
|
8
|
-
|
|
9
|
-
settings = PFXSettings()
|
|
7
|
+
from pfx.pfxcore.settings import settings
|
|
10
8
|
|
|
11
9
|
|
|
12
10
|
class OtpUserMixin(models.Model):
|
|
11
|
+
"""A mixin to enable OTP MFA on a user class."""
|
|
12
|
+
|
|
13
|
+
#: OTP secret token.
|
|
13
14
|
otp_secret_token = models.CharField(
|
|
14
15
|
_("OTP secret token"), max_length=32, null=True,
|
|
15
16
|
blank=True, unique=True)
|
|
17
|
+
#: Temporary OTP secret token (needs confirmation).
|
|
16
18
|
otp_secret_token_tmp = models.CharField(
|
|
17
19
|
_("Temporary OTP secret token"), max_length=32, null=True, blank=True)
|
|
20
|
+
#: HOTP count.
|
|
18
21
|
hotp_count = models.IntegerField(_("HOTP count"), default=0)
|
|
22
|
+
#: HOTP expiry.
|
|
19
23
|
hotp_expiry = models.DateTimeField(_("HOTP expiry"), default=timezone.now)
|
|
20
24
|
|
|
21
25
|
class Meta:
|
|
22
26
|
abstract = True
|
|
23
27
|
|
|
24
28
|
def enable_otp(self):
|
|
29
|
+
"""Activate OTP for this user.
|
|
30
|
+
|
|
31
|
+
Generates a new temporary OTP secret token. To complete activation,
|
|
32
|
+
call `confirm_otp` with a valid code.
|
|
33
|
+
"""
|
|
25
34
|
import pyotp
|
|
26
35
|
self.otp_secret_token_tmp = pyotp.random_base32()
|
|
27
36
|
self.save(update_fields=['otp_secret_token_tmp'])
|
|
28
37
|
|
|
29
38
|
def confirm_otp(self, otp_code):
|
|
39
|
+
"""Confirm OTP activation for this user.
|
|
40
|
+
|
|
41
|
+
Set the OTP secret token from the temporary one if the provided
|
|
42
|
+
code is valid.
|
|
43
|
+
|
|
44
|
+
:param otp_code: A valid OTP code for the temporary OTP secret key.
|
|
45
|
+
:returns: `True` if success, `False` otherwise.
|
|
46
|
+
"""
|
|
30
47
|
if self.is_otp_valid(otp_code, tmp=True):
|
|
31
48
|
self.otp_secret_token = self.otp_secret_token_tmp
|
|
32
49
|
self.otp_secret_token_tmp = None
|
|
@@ -36,10 +53,16 @@ class OtpUserMixin(models.Model):
|
|
|
36
53
|
return False
|
|
37
54
|
|
|
38
55
|
def disable_otp(self):
|
|
56
|
+
"""Disable OTP for this user.
|
|
57
|
+
|
|
58
|
+
Remove the OTP secret token.
|
|
59
|
+
"""
|
|
39
60
|
self.otp_secret_token = None
|
|
40
61
|
self.save(update_fields=['otp_secret_token'])
|
|
41
62
|
|
|
42
63
|
def get_otp_setup_uri(self, tmp=False):
|
|
64
|
+
"""Return the setup URL for OTP activation.
|
|
65
|
+
"""
|
|
43
66
|
import pyotp
|
|
44
67
|
return pyotp.totp.TOTP(
|
|
45
68
|
tmp and self.otp_secret_token_tmp or
|
|
@@ -47,6 +70,13 @@ class OtpUserMixin(models.Model):
|
|
|
47
70
|
name=self.email, issuer_name=settings.PFX_SITE_NAME)
|
|
48
71
|
|
|
49
72
|
def is_otp_valid(self, otp_code, tmp=False):
|
|
73
|
+
"""Verify an OTP code.
|
|
74
|
+
|
|
75
|
+
:param otp_code: A valid OTP code for the OTP secret key.
|
|
76
|
+
:param tmp: If `True`, verify the code with the temporary
|
|
77
|
+
OTP secret key.
|
|
78
|
+
:returns: `True` if the code is valid, `False` otherwise.
|
|
79
|
+
"""
|
|
50
80
|
import pyotp
|
|
51
81
|
totp = pyotp.parse_uri(self.get_otp_setup_uri(tmp=tmp))
|
|
52
82
|
valid = totp.verify(otp_code)
|
|
@@ -58,10 +88,17 @@ class OtpUserMixin(models.Model):
|
|
|
58
88
|
return valid
|
|
59
89
|
|
|
60
90
|
def get_user_jwt_signature_key(self):
|
|
91
|
+
"""Return a user secret to sign JWT token.
|
|
92
|
+
|
|
93
|
+
If the user inherit :class:`pfx.pfxcore.models.AbstractPFXBaseUser`,
|
|
94
|
+
add the OTP secret token to the user signature."""
|
|
61
95
|
return super().get_user_jwt_signature_key() + (
|
|
62
96
|
self.otp_secret_token or "")
|
|
63
97
|
|
|
64
98
|
def get_hotp_code(self):
|
|
99
|
+
"""Return a new valid HOTP code.
|
|
100
|
+
|
|
101
|
+
Increment the HOTP counter and reset the expiry."""
|
|
65
102
|
import pyotp
|
|
66
103
|
if not self.otp_secret_token:
|
|
67
104
|
raise Exception("OTP disabled")
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from django.contrib.auth.models import AbstractUser
|
|
2
|
+
|
|
3
|
+
from .abstract_pfx_base_user import AbstractPFXBaseUser
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PFXUser(AbstractUser, AbstractPFXBaseUser):
|
|
7
|
+
"""The Django User with PFX mixin.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
class Meta(AbstractUser.Meta):
|
|
11
|
+
swappable = "AUTH_USER_MODEL"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from django.conf import settings as django_settings
|
|
2
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
3
|
+
|
|
4
|
+
from . import default_settings
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PFXSettings:
|
|
8
|
+
def __getattr__(self, name):
|
|
9
|
+
try:
|
|
10
|
+
val = getattr(django_settings, name)
|
|
11
|
+
except AttributeError:
|
|
12
|
+
if hasattr(default_settings, name):
|
|
13
|
+
val = getattr(default_settings, name)
|
|
14
|
+
else:
|
|
15
|
+
raise
|
|
16
|
+
if name == "PFX_SECRET_KEY" and not val:
|
|
17
|
+
raise ImproperlyConfigured(
|
|
18
|
+
"The PFX_SECRET_KEY setting must not be empty.")
|
|
19
|
+
return val
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
settings = PFXSettings()
|
|
@@ -5,9 +5,7 @@ from datetime import date
|
|
|
5
5
|
from django.core.exceptions import ObjectDoesNotExist
|
|
6
6
|
from django.utils.translation import gettext_lazy as _
|
|
7
7
|
|
|
8
|
-
from
|
|
9
|
-
|
|
10
|
-
settings = PFXSettings()
|
|
8
|
+
from .settings import settings
|
|
11
9
|
|
|
12
10
|
logger = logging.getLogger(__name__)
|
|
13
11
|
|
|
@@ -32,7 +32,7 @@ from pfx.pfxcore.http import JsonResponse
|
|
|
32
32
|
from pfx.pfxcore.middleware.authentication import JWTTokenDecodeMixin
|
|
33
33
|
from pfx.pfxcore.models import CacheableMixin, OtpUserMixin
|
|
34
34
|
from pfx.pfxcore.models.login_ban import LoginBan
|
|
35
|
-
from pfx.pfxcore.settings import
|
|
35
|
+
from pfx.pfxcore.settings import settings
|
|
36
36
|
from pfx.pfxcore.shortcuts import delete_token_cookie
|
|
37
37
|
|
|
38
38
|
from .rest_views import (
|
|
@@ -42,7 +42,6 @@ from .rest_views import (
|
|
|
42
42
|
ModelMixin,
|
|
43
43
|
)
|
|
44
44
|
|
|
45
|
-
settings = PFXSettings()
|
|
46
45
|
logger = logging.getLogger(__name__)
|
|
47
46
|
UserModel = get_user_model()
|
|
48
47
|
AUTHENTICATION_TAG = Tag("Authentication")
|
|
@@ -784,7 +783,7 @@ class ForgottenPasswordView(SendMessageTokenMixin, BodyMixin, BaseRestView):
|
|
|
784
783
|
|
|
785
784
|
@rest_view("/auth/otp")
|
|
786
785
|
class OtpEmailView(BodyMixin, JWTTokenDecodeMixin, BaseRestView):
|
|
787
|
-
"""View for
|
|
786
|
+
"""View for the OTP code email service."""
|
|
788
787
|
#: The email template.
|
|
789
788
|
email_template_name = 'registration/otp_code_email.txt'
|
|
790
789
|
#: The email subject template.
|
|
@@ -853,7 +852,7 @@ class OtpEmailView(BodyMixin, JWTTokenDecodeMixin, BaseRestView):
|
|
|
853
852
|
'message': _('A new authentication code has been sent by email.')})
|
|
854
853
|
|
|
855
854
|
def send_otp_message(self, user):
|
|
856
|
-
"""Send an email to a user with an OTP code.
|
|
855
|
+
"""Send an email to a user with an OTP code to a user.
|
|
857
856
|
|
|
858
857
|
:param user: The user
|
|
859
858
|
"""
|
|
@@ -11,6 +11,7 @@ from .test_locale_api import LocaleAPITest
|
|
|
11
11
|
from .test_perm_tests import PermTestsTest
|
|
12
12
|
from .test_perms_api import PermsAPITest
|
|
13
13
|
from .test_profiling_middleware import ProfilingMiddlewareTest
|
|
14
|
+
from .test_settings import TestSettings
|
|
14
15
|
from .test_shortcuts import ShortcutTest
|
|
15
16
|
from .test_timezone_middleware import TimezoneMiddlewareTest
|
|
16
17
|
from .test_tools import TestTools
|
|
@@ -1247,7 +1247,7 @@ class AuthAPITest(TestAssertMixin, TransactionTestCase):
|
|
|
1247
1247
|
code_match = re.search(
|
|
1248
1248
|
r'Authentication code: (\d{6})', mail.outbox[0].body)
|
|
1249
1249
|
self.assertIsNotNone(code_match)
|
|
1250
|
-
otp_code =
|
|
1250
|
+
otp_code = code_match.group(1)
|
|
1251
1251
|
|
|
1252
1252
|
response = self.client.post('/api/auth/otp/login', dict(
|
|
1253
1253
|
token=otp_token,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from datetime import timedelta
|
|
2
|
+
|
|
3
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
4
|
+
from django.test import TestCase, override_settings
|
|
5
|
+
|
|
6
|
+
from pfx.pfxcore.test import TestAssertMixin
|
|
7
|
+
from pfx.pfxcore.views import AuthenticationView
|
|
8
|
+
from tests.models import User
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestSettings(TestAssertMixin, TestCase):
|
|
12
|
+
|
|
13
|
+
@override_settings(PFX_SECRET_KEY="")
|
|
14
|
+
def test_mandatory_pfx_secret_key(self):
|
|
15
|
+
user = User.objects.create_user(
|
|
16
|
+
username='user',
|
|
17
|
+
email="user@example.com",
|
|
18
|
+
password='test',
|
|
19
|
+
first_name='User',
|
|
20
|
+
last_name='Test')
|
|
21
|
+
|
|
22
|
+
with self.assertRaises(ImproperlyConfigured):
|
|
23
|
+
AuthenticationView()._prepare_token(user, timedelta())
|
|
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.dev26 → 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
|
{django-pfx-1.4.dev26 → 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.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/templates/registration/welcome_email.txt
RENAMED
|
File without changes
|
{django-pfx-1.4.dev26 → 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.dev26 → 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.dev26 → django-pfx-1.4.dev30}/pfx/pfxcore/views/parameters/subset_page_size.py
RENAMED
|
File without changes
|
{django-pfx-1.4.dev26 → 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
|