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.
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/PKG-INFO +1 -1
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/django_pfx.egg-info/PKG-INFO +1 -1
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/doc/conf.py +2 -1
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/doc/source/authentication.md +154 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/exceptions.py +14 -14
- django-pfx-1.4.dev18/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.po +82 -44
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/shortcuts.py +4 -4
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/authentication_views.py +14 -9
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/requirements.txt +1 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/test_auth_api.py +36 -26
- django-pfx-1.4.dev14/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/.gitignore +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/.gitlab-ci.yml +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/.pre-commit-config.yaml +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/LICENSE +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/MANIFEST.in +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/README.md +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/django-admin-test +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/django_pfx.egg-info/SOURCES.txt +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/django_pfx.egg-info/dependency_links.txt +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/django_pfx.egg-info/requires.txt +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/django_pfx.egg-info/top_level.txt +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/doc/Makefile +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/doc/index.rst +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/doc/source/api.views.rst +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/doc/source/decorator.md +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/doc/source/generate_openapi.md +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/doc/source/getting_started.md +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/doc/source/internationalisation.md +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/doc/source/model.md +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/doc/source/pfx_views.md +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/doc/source/profiling.md +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/doc/source/settings.md +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/doc/source/testing.md +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/img/pfx.png +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/img/pfx.svg +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/__init__.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/__init__.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/apidoc/__init__.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/apidoc/parameters.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/apidoc/schema.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/apidoc/tags.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/apps.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/decorator/__init__.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/decorator/rest.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/default_settings.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/fields.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/http/__init__.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/http/json_response.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/management/__init__.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/management/commands/__init__.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/management/commands/makeapidoc.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/management/commands/profile.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/middleware/__init__.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/middleware/authentication.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/middleware/locale.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/middleware/profiling.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/models/__init__.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/models/cache_mixins.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/models/not_null_fields.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/models/otp_user_mixin.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/models/pfx_models.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/models/user_filtered_queryset_mixin.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/serializers/__init__.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/serializers/json.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/settings.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/storage/__init__.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/storage/s3_storage.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/templates/registration/password_reset_email.txt +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/templates/registration/password_reset_subject.txt +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/templates/registration/welcome_email.txt +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/templates/registration/welcome_subject.txt +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/test.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/urls.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/__init__.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/fields.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/filters_views.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/locale_views.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/parameters/__init__.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/parameters/date_format.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/parameters/groups.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/parameters/list_count.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/parameters/list_items.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/parameters/list_mode.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/parameters/list_order.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/parameters/list_search.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/parameters/media_redirect.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/parameters/meta_fields.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/parameters/meta_filters.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/parameters/meta_orders.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/parameters/subset.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/parameters/subset_limit.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/parameters/subset_offset.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/parameters/subset_page.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/parameters/subset_page_size.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/parameters/subset_page_subset.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/rest_views.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pyproject.toml +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/runtest.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/serve-doc +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/setup.cfg +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/setup.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/__init__.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/locale/fr/LC_MESSAGES/django.po +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/models.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/settings/__init__.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/settings/ci.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/settings/common.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/settings/dev.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/settings/dev_custom_example.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/settings/dev_default.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/__init__.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/basic_api_errors.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/basic_api_test.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/test_api_doc.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/test_body_mixin.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/test_cache.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/test_client.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/test_fields.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/test_filters.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/test_locale_api.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/test_perm_tests.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/test_perms_api.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/test_profiling_middleware.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/test_shortcuts.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/test_timezone_middleware.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/test_tools.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/test_user_queryset.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/test_view_decorators.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/tests/test_view_fields.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/urls.py +0 -0
- {django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/tests/views.py +0 -0
|
@@ -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
|
|
23
|
-
def __init__(self,
|
|
22
|
+
class ModelNotFoundAPIError(APIError):
|
|
23
|
+
def __init__(self, model, status=404, **kwargs):
|
|
24
24
|
super().__init__(
|
|
25
|
-
f(_("
|
|
25
|
+
f(_("{model} not found."), model=model._meta.verbose_name),
|
|
26
26
|
status=status, **kwargs)
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
class
|
|
30
|
-
def __init__(self,
|
|
29
|
+
class JsonErrorAPIError(APIError):
|
|
30
|
+
def __init__(self, json_error, status=422, **kwargs):
|
|
31
31
|
super().__init__(
|
|
32
|
-
f(_("{
|
|
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=
|
|
53
|
+
def __init__(self, message=None, status=422, **kwargs):
|
|
54
54
|
super().__init__(
|
|
55
|
-
f(message or _("
|
|
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=
|
|
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=
|
|
76
|
+
f(message or _("Resource not found")),
|
|
77
|
+
status=status, **kwargs)
|
|
Binary file
|
|
@@ -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:
|
|
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:
|
|
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 "
|
|
36
|
-
msgstr "
|
|
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 "
|
|
48
|
+
msgstr "Ressource non trouvée"
|
|
51
49
|
|
|
52
|
-
#: fields.py:
|
|
50
|
+
#: fields.py:77
|
|
53
51
|
msgid "Invalid value."
|
|
54
|
-
msgstr ""
|
|
52
|
+
msgstr "Valeur invalide."
|
|
55
53
|
|
|
56
|
-
#: fields.py:
|
|
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
|
-
#:
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
#:
|
|
70
|
-
msgid "
|
|
71
|
-
msgstr "
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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/
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
68
|
-
"""
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
525
|
+
description: If the token is missing, invalid or expired.
|
|
523
526
|
403:
|
|
524
|
-
description: If the
|
|
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
|
|
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.
|
|
543
|
+
return self.login_failed_response()
|
|
539
544
|
|
|
540
545
|
def get_user(self, uidb64):
|
|
541
546
|
"""Get user by token
|
|
@@ -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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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,
|
|
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
|
-
|
|
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',
|
|
Binary file
|
|
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.dev14 → django-pfx-1.4.dev18}/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
|
|
File without changes
|
{django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/templates/registration/welcome_email.txt
RENAMED
|
File without changes
|
{django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/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.dev14 → django-pfx-1.4.dev18}/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.dev14 → django-pfx-1.4.dev18}/pfx/pfxcore/views/parameters/subset_page_size.py
RENAMED
|
File without changes
|
{django-pfx-1.4.dev14 → django-pfx-1.4.dev18}/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
|