sanic-security 1.13.3__tar.gz → 1.13.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. {sanic_security-1.13.3/sanic_security.egg-info → sanic_security-1.13.4}/PKG-INFO +56 -24
  2. {sanic_security-1.13.3 → sanic_security-1.13.4}/README.md +54 -23
  3. {sanic_security-1.13.3 → sanic_security-1.13.4}/pyproject.toml +3 -2
  4. {sanic_security-1.13.3 → sanic_security-1.13.4}/sanic_security/authentication.py +5 -8
  5. {sanic_security-1.13.3 → sanic_security-1.13.4}/sanic_security/authorization.py +5 -3
  6. {sanic_security-1.13.3 → sanic_security-1.13.4}/sanic_security/configuration.py +3 -0
  7. {sanic_security-1.13.3 → sanic_security-1.13.4}/sanic_security/models.py +30 -18
  8. {sanic_security-1.13.3 → sanic_security-1.13.4}/sanic_security/test/server.py +8 -3
  9. {sanic_security-1.13.3 → sanic_security-1.13.4}/sanic_security/utils.py +13 -0
  10. {sanic_security-1.13.3 → sanic_security-1.13.4/sanic_security.egg-info}/PKG-INFO +56 -24
  11. {sanic_security-1.13.3 → sanic_security-1.13.4}/sanic_security.egg-info/requires.txt +1 -0
  12. {sanic_security-1.13.3 → sanic_security-1.13.4}/LICENSE +0 -0
  13. {sanic_security-1.13.3 → sanic_security-1.13.4}/sanic_security/__init__.py +0 -0
  14. {sanic_security-1.13.3 → sanic_security-1.13.4}/sanic_security/exceptions.py +0 -0
  15. {sanic_security-1.13.3 → sanic_security-1.13.4}/sanic_security/test/__init__.py +0 -0
  16. {sanic_security-1.13.3 → sanic_security-1.13.4}/sanic_security/test/tests.py +0 -0
  17. {sanic_security-1.13.3 → sanic_security-1.13.4}/sanic_security/verification.py +0 -0
  18. {sanic_security-1.13.3 → sanic_security-1.13.4}/sanic_security.egg-info/SOURCES.txt +0 -0
  19. {sanic_security-1.13.3 → sanic_security-1.13.4}/sanic_security.egg-info/dependency_links.txt +0 -0
  20. {sanic_security-1.13.3 → sanic_security-1.13.4}/sanic_security.egg-info/top_level.txt +0 -0
  21. {sanic_security-1.13.3 → sanic_security-1.13.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sanic-security
3
- Version: 1.13.3
3
+ Version: 1.13.4
4
4
  Summary: An async security library for the Sanic framework.
5
5
  Author-email: Aidan Stewart <me@na-stewart.com>
6
6
  Project-URL: Documentation, https://security.na-stewart.com/
@@ -20,6 +20,7 @@ Requires-Dist: captcha>=0.4
20
20
  Requires-Dist: pillow>=9.5.0
21
21
  Requires-Dist: argon2-cffi>=20.1.0
22
22
  Requires-Dist: sanic>=21.3.0
23
+ Requires-Dist: secure>=1.0.1
23
24
  Provides-Extra: dev
24
25
  Requires-Dist: httpx; extra == "dev"
25
26
  Requires-Dist: black; extra == "dev"
@@ -63,7 +64,7 @@ Requires-Dist: cryptography>=3.3.1; extra == "crypto"
63
64
  * [Configuration](#configuration)
64
65
  * [Usage](#usage)
65
66
  * [Authentication](#authentication)
66
- * [Captcha](#captcha)
67
+ * [CAPTCHA](#captcha)
67
68
  * [Two-step Verification](#two-step-verification)
68
69
  * [Authorization](#authorization)
69
70
  * [Testing](#testing)
@@ -76,21 +77,22 @@ Requires-Dist: cryptography>=3.3.1; extra == "crypto"
76
77
  <!-- ABOUT THE PROJECT -->
77
78
  ## About The Project
78
79
 
79
- Sanic Security is an authentication, authorization, and verification library designed for use with [Sanic](https://github.com/huge-success/sanic).
80
+ Sanic Security is an authentication, authorization, and verification library designed for use with the
81
+ [Sanic](https://github.com/huge-success/sanic) framework.
80
82
 
81
83
  * Login, registration, and authentication with refresh mechanisms
82
84
  * Role based authorization with wildcard permissions
85
+ * Image & audio CAPTCHA
83
86
  * Two-factor authentication
84
87
  * Two-step verification
85
- * Logging & Auditing
86
- * Captcha
88
+ * Logging & auditing
87
89
 
88
90
  Visit [security.na-stewart.com](https://security.na-stewart.com) for documentation.
89
91
 
90
92
  <!-- GETTING STARTED -->
91
93
  ## Getting Started
92
94
 
93
- In order to get started, please install [Pip](https://pypi.org/).
95
+ In order to get started, please install [PyPI](https://pypi.org/).
94
96
 
95
97
  ### Installation
96
98
 
@@ -109,14 +111,9 @@ as an extra requirement.
109
111
  pip3 install sanic-security[crypto]
110
112
  ````
111
113
 
112
- * For developers, fork Sanic Security and install development dependencies.
113
- ```shell
114
- pip3 install -e ".[dev]"
115
- ````
116
-
117
114
  * Update sanic-security if already installed.
118
115
  ```shell
119
- pip3 install --upgrade sanic-security
116
+ pip3 install sanic-security --upgrade
120
117
  ```
121
118
 
122
119
  ### Configuration
@@ -154,7 +151,8 @@ You can load environment variables with a different prefix via `config.load_envi
154
151
  | **SESSION_PREFIX** | tkn | Prefix attached to the beginning of session cookies. |
155
152
  | **MAX_CHALLENGE_ATTEMPTS** | 5 | The maximum amount of session challenge attempts allowed. |
156
153
  | **CAPTCHA_SESSION_EXPIRATION** | 60 | The amount of seconds till captcha session expiration on creation. Setting to 0 will disable expiration. |
157
- | **CAPTCHA_FONT** | captcha-font.ttf | The file path to the font being used for captcha generation. |
154
+ | **CAPTCHA_FONT** | captcha-font.ttf | The file path to the font being used for captcha generation. Several fonts can be used by separating them via comma. |
155
+ | **CAPTCHA_VOICE** | captcha-voice/ | The directory of the voice library being used for audio captcha generation. |
158
156
  | **TWO_STEP_SESSION_EXPIRATION** | 200 | The amount of seconds till two-step session expiration on creation. Setting to 0 will disable expiration. |
159
157
  | **AUTHENTICATION_SESSION_EXPIRATION** | 86400 | The amount of seconds till authentication session expiration on creation. Setting to 0 will disable expiration. |
160
158
  | **AUTHENTICATION_REFRESH_EXPIRATION** | 604800 | The amount of seconds till authentication refresh expiration. Setting to 0 will disable refresh mechanism. |
@@ -319,16 +317,40 @@ async def on_authenticate(request):
319
317
  return response
320
318
  ```
321
319
 
322
- ## Captcha
320
+ ## CAPTCHA
321
+
322
+ Protects against spam and malicious activities by ensuring that only real humans can complete certain actions, like
323
+ submitting a form or creating an account.
323
324
 
324
- A pre-existing font for captcha challenges is included in the Sanic Security repository. You may set your own font by
325
- downloading a .ttf font and defining the file's path in the configuration.
325
+ * Fonts
326
+
327
+ A font for captcha challenges is included in the repository. You can set a custom font by downloading a .ttf file and
328
+ specifying its path in the configuration.
326
329
 
327
330
  [1001 Free Fonts](https://www.1001fonts.com/)
328
331
 
329
- [Recommended Font](https://www.1001fonts.com/source-sans-pro-font.html)
332
+ * Voice Library
333
+
334
+ A voice library for captcha challenges is included in the repository. You can generate your own using `espeak` and
335
+ `ffmpeg`, then specify the library's directory in the configuration.
330
336
 
331
- * Request Captcha
337
+ ```bash
338
+ # Set the language code
339
+ export ESLANG=en
340
+
341
+ # Create a directory for the specified language code
342
+ mkdir "$ESLANG"
343
+
344
+ # Loop through each character (A-Z, 0-9) and create a directory for each
345
+ for i in {A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,0,1,2,3,4,5,6,7,8,9}; do
346
+ mkdir "$ESLANG/$i"
347
+ espeak -a 150 -s 100 -p 15 -v "$ESLANG" "$i" -w "$ESLANG/$i/orig_default.wav"
348
+ ffmpeg -i "$ESLANG/$i/orig_default.wav" -ar 8000 -ac 1 -acodec pcm_u8 "$ESLANG/$i/default.wav"
349
+ rm "$ESLANG/$i/orig_default.wav"
350
+ done
351
+ ```
352
+
353
+ * Request CAPTCHA
332
354
 
333
355
  ```python
334
356
  @app.get("api/security/captcha")
@@ -339,7 +361,16 @@ async def on_captcha_img_request(request):
339
361
  return response
340
362
  ```
341
363
 
342
- * Captcha
364
+ * Request CAPTCHA Audio
365
+
366
+ ```python
367
+ @app.get("api/security/captcha/audio")
368
+ async def on_captcha_audio_request(request):
369
+ captcha_session = await CaptchaSession.decode(request)
370
+ return captcha_session.get_audio() # Captcha: LJ0F3U
371
+ ```
372
+
373
+ * Attempt CAPTCHA
343
374
 
344
375
  | Key | Value |
345
376
  |-------------|--------|
@@ -352,7 +383,7 @@ async def on_captcha(request):
352
383
  return json("Captcha attempt successful!", captcha_session.json)
353
384
  ```
354
385
 
355
- * Requires Captcha (This method is not called directly and instead used as a decorator)
386
+ * Requires CAPTCHA (This method is not called directly and instead used as a decorator)
356
387
 
357
388
  | Key | Value |
358
389
  |-------------|--------|
@@ -367,7 +398,7 @@ async def on_captcha(request):
367
398
 
368
399
  ## Two-step Verification
369
400
 
370
- Two-step verification should be integrated with other custom functionality. For example, account verification during registration.
401
+ Two-step verification should be integrated with other custom functionalities, such as account verification during registration.
371
402
 
372
403
  * Request Two-step Verification
373
404
 
@@ -399,7 +430,7 @@ async def on_two_step_resend(request):
399
430
  return json("Verification code resend successful!", two_step_session.json)
400
431
  ```
401
432
 
402
- * Two-step Verification
433
+ * Attempt Two-step Verification
403
434
 
404
435
  | Key | Value |
405
436
  |----------|--------|
@@ -442,13 +473,14 @@ user's account; this simplifies common operations, such as adding a user, or cha
442
473
 
443
474
  Wildcard permissions support the concept of multiple levels or parts. For example, you could grant a user the permission
444
475
  `printer:query`, `printer:query,delete`, or `printer:*`.
476
+
445
477
  * Assign Role
446
478
 
447
479
  ```python
448
480
  await assign_role(
449
481
  "Chat Room Moderator",
450
482
  account,
451
- "channels:view,delete, account:suspend,mute, voice:*",
483
+ "channels:view,delete, voice:*, account:suspend,mute",
452
484
  "Can read and delete messages in all chat rooms, suspend and mute accounts, and control voice chat.",
453
485
  )
454
486
  ```
@@ -497,7 +529,7 @@ async def on_check_roles(request):
497
529
 
498
530
  * Make sure the test Sanic instance (`test/server.py`) is running on your machine.
499
531
 
500
- * Run the unit test client (`test/tests.py`) for results.
532
+ * Run the test client (`test/tests.py`) for results.
501
533
 
502
534
  ## Tortoise
503
535
 
@@ -32,7 +32,7 @@
32
32
  * [Configuration](#configuration)
33
33
  * [Usage](#usage)
34
34
  * [Authentication](#authentication)
35
- * [Captcha](#captcha)
35
+ * [CAPTCHA](#captcha)
36
36
  * [Two-step Verification](#two-step-verification)
37
37
  * [Authorization](#authorization)
38
38
  * [Testing](#testing)
@@ -45,21 +45,22 @@
45
45
  <!-- ABOUT THE PROJECT -->
46
46
  ## About The Project
47
47
 
48
- Sanic Security is an authentication, authorization, and verification library designed for use with [Sanic](https://github.com/huge-success/sanic).
48
+ Sanic Security is an authentication, authorization, and verification library designed for use with the
49
+ [Sanic](https://github.com/huge-success/sanic) framework.
49
50
 
50
51
  * Login, registration, and authentication with refresh mechanisms
51
52
  * Role based authorization with wildcard permissions
53
+ * Image & audio CAPTCHA
52
54
  * Two-factor authentication
53
55
  * Two-step verification
54
- * Logging & Auditing
55
- * Captcha
56
+ * Logging & auditing
56
57
 
57
58
  Visit [security.na-stewart.com](https://security.na-stewart.com) for documentation.
58
59
 
59
60
  <!-- GETTING STARTED -->
60
61
  ## Getting Started
61
62
 
62
- In order to get started, please install [Pip](https://pypi.org/).
63
+ In order to get started, please install [PyPI](https://pypi.org/).
63
64
 
64
65
  ### Installation
65
66
 
@@ -78,14 +79,9 @@ as an extra requirement.
78
79
  pip3 install sanic-security[crypto]
79
80
  ````
80
81
 
81
- * For developers, fork Sanic Security and install development dependencies.
82
- ```shell
83
- pip3 install -e ".[dev]"
84
- ````
85
-
86
82
  * Update sanic-security if already installed.
87
83
  ```shell
88
- pip3 install --upgrade sanic-security
84
+ pip3 install sanic-security --upgrade
89
85
  ```
90
86
 
91
87
  ### Configuration
@@ -123,7 +119,8 @@ You can load environment variables with a different prefix via `config.load_envi
123
119
  | **SESSION_PREFIX** | tkn | Prefix attached to the beginning of session cookies. |
124
120
  | **MAX_CHALLENGE_ATTEMPTS** | 5 | The maximum amount of session challenge attempts allowed. |
125
121
  | **CAPTCHA_SESSION_EXPIRATION** | 60 | The amount of seconds till captcha session expiration on creation. Setting to 0 will disable expiration. |
126
- | **CAPTCHA_FONT** | captcha-font.ttf | The file path to the font being used for captcha generation. |
122
+ | **CAPTCHA_FONT** | captcha-font.ttf | The file path to the font being used for captcha generation. Several fonts can be used by separating them via comma. |
123
+ | **CAPTCHA_VOICE** | captcha-voice/ | The directory of the voice library being used for audio captcha generation. |
127
124
  | **TWO_STEP_SESSION_EXPIRATION** | 200 | The amount of seconds till two-step session expiration on creation. Setting to 0 will disable expiration. |
128
125
  | **AUTHENTICATION_SESSION_EXPIRATION** | 86400 | The amount of seconds till authentication session expiration on creation. Setting to 0 will disable expiration. |
129
126
  | **AUTHENTICATION_REFRESH_EXPIRATION** | 604800 | The amount of seconds till authentication refresh expiration. Setting to 0 will disable refresh mechanism. |
@@ -288,16 +285,40 @@ async def on_authenticate(request):
288
285
  return response
289
286
  ```
290
287
 
291
- ## Captcha
288
+ ## CAPTCHA
289
+
290
+ Protects against spam and malicious activities by ensuring that only real humans can complete certain actions, like
291
+ submitting a form or creating an account.
292
292
 
293
- A pre-existing font for captcha challenges is included in the Sanic Security repository. You may set your own font by
294
- downloading a .ttf font and defining the file's path in the configuration.
293
+ * Fonts
294
+
295
+ A font for captcha challenges is included in the repository. You can set a custom font by downloading a .ttf file and
296
+ specifying its path in the configuration.
295
297
 
296
298
  [1001 Free Fonts](https://www.1001fonts.com/)
297
299
 
298
- [Recommended Font](https://www.1001fonts.com/source-sans-pro-font.html)
300
+ * Voice Library
301
+
302
+ A voice library for captcha challenges is included in the repository. You can generate your own using `espeak` and
303
+ `ffmpeg`, then specify the library's directory in the configuration.
299
304
 
300
- * Request Captcha
305
+ ```bash
306
+ # Set the language code
307
+ export ESLANG=en
308
+
309
+ # Create a directory for the specified language code
310
+ mkdir "$ESLANG"
311
+
312
+ # Loop through each character (A-Z, 0-9) and create a directory for each
313
+ for i in {A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,0,1,2,3,4,5,6,7,8,9}; do
314
+ mkdir "$ESLANG/$i"
315
+ espeak -a 150 -s 100 -p 15 -v "$ESLANG" "$i" -w "$ESLANG/$i/orig_default.wav"
316
+ ffmpeg -i "$ESLANG/$i/orig_default.wav" -ar 8000 -ac 1 -acodec pcm_u8 "$ESLANG/$i/default.wav"
317
+ rm "$ESLANG/$i/orig_default.wav"
318
+ done
319
+ ```
320
+
321
+ * Request CAPTCHA
301
322
 
302
323
  ```python
303
324
  @app.get("api/security/captcha")
@@ -308,7 +329,16 @@ async def on_captcha_img_request(request):
308
329
  return response
309
330
  ```
310
331
 
311
- * Captcha
332
+ * Request CAPTCHA Audio
333
+
334
+ ```python
335
+ @app.get("api/security/captcha/audio")
336
+ async def on_captcha_audio_request(request):
337
+ captcha_session = await CaptchaSession.decode(request)
338
+ return captcha_session.get_audio() # Captcha: LJ0F3U
339
+ ```
340
+
341
+ * Attempt CAPTCHA
312
342
 
313
343
  | Key | Value |
314
344
  |-------------|--------|
@@ -321,7 +351,7 @@ async def on_captcha(request):
321
351
  return json("Captcha attempt successful!", captcha_session.json)
322
352
  ```
323
353
 
324
- * Requires Captcha (This method is not called directly and instead used as a decorator)
354
+ * Requires CAPTCHA (This method is not called directly and instead used as a decorator)
325
355
 
326
356
  | Key | Value |
327
357
  |-------------|--------|
@@ -336,7 +366,7 @@ async def on_captcha(request):
336
366
 
337
367
  ## Two-step Verification
338
368
 
339
- Two-step verification should be integrated with other custom functionality. For example, account verification during registration.
369
+ Two-step verification should be integrated with other custom functionalities, such as account verification during registration.
340
370
 
341
371
  * Request Two-step Verification
342
372
 
@@ -368,7 +398,7 @@ async def on_two_step_resend(request):
368
398
  return json("Verification code resend successful!", two_step_session.json)
369
399
  ```
370
400
 
371
- * Two-step Verification
401
+ * Attempt Two-step Verification
372
402
 
373
403
  | Key | Value |
374
404
  |----------|--------|
@@ -411,13 +441,14 @@ user's account; this simplifies common operations, such as adding a user, or cha
411
441
 
412
442
  Wildcard permissions support the concept of multiple levels or parts. For example, you could grant a user the permission
413
443
  `printer:query`, `printer:query,delete`, or `printer:*`.
444
+
414
445
  * Assign Role
415
446
 
416
447
  ```python
417
448
  await assign_role(
418
449
  "Chat Room Moderator",
419
450
  account,
420
- "channels:view,delete, account:suspend,mute, voice:*",
451
+ "channels:view,delete, voice:*, account:suspend,mute",
421
452
  "Can read and delete messages in all chat rooms, suspend and mute accounts, and control voice chat.",
422
453
  )
423
454
  ```
@@ -466,7 +497,7 @@ async def on_check_roles(request):
466
497
 
467
498
  * Make sure the test Sanic instance (`test/server.py`) is running on your machine.
468
499
 
469
- * Run the unit test client (`test/tests.py`) for results.
500
+ * Run the test client (`test/tests.py`) for results.
470
501
 
471
502
  ## Tortoise
472
503
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sanic-security"
7
- version = "1.13.3"
7
+ version = "1.13.4"
8
8
  requires-python = ">=3.8"
9
9
  dependencies = [
10
10
  "tortoise-orm>=0.17.0",
@@ -12,7 +12,8 @@ dependencies = [
12
12
  "captcha>=0.4",
13
13
  "pillow>=9.5.0",
14
14
  "argon2-cffi>=20.1.0",
15
- "sanic>=21.3.0"
15
+ "sanic>=21.3.0",
16
+ "secure>=1.0.1"
16
17
  ]
17
18
  description = "An async security library for the Sanic framework."
18
19
  authors = [
@@ -2,7 +2,6 @@ import functools
2
2
  import re
3
3
  import warnings
4
4
 
5
- from argon2 import PasswordHasher
6
5
  from argon2.exceptions import VerifyMismatchError
7
6
  from sanic import Sanic
8
7
  from sanic.log import logger
@@ -18,7 +17,7 @@ from sanic_security.exceptions import (
18
17
  AuditWarning,
19
18
  )
20
19
  from sanic_security.models import Account, AuthenticationSession, Role, TwoStepSession
21
- from sanic_security.utils import get_ip
20
+ from sanic_security.utils import get_ip, password_hasher, secure_headers
22
21
 
23
22
  """
24
23
  Copyright (c) 2020-present Nicholas Aidan Stewart
@@ -42,8 +41,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
42
41
  SOFTWARE.
43
42
  """
44
43
 
45
- password_hasher = PasswordHasher()
46
-
47
44
 
48
45
  async def register(
49
46
  request: Request, verified: bool = False, disabled: bool = False
@@ -193,8 +190,7 @@ async def fulfill_second_factor(request: Request) -> AuthenticationSession:
193
190
  authentication_session.requires_second_factor = False
194
191
  await authentication_session.save(update_fields=["requires_second_factor"])
195
192
  logger.info(
196
- f"Client {get_ip(request)} has fulfilled authentication session {authentication_session.id} "
197
- "second factor."
193
+ f"Client {get_ip(request)} has fulfilled authentication session {authentication_session.id} second factor."
198
194
  )
199
195
  return authentication_session
200
196
 
@@ -288,7 +284,7 @@ def validate_password(password: str) -> str:
288
284
 
289
285
  def initialize_security(app: Sanic, create_root=True) -> None:
290
286
  """
291
- Audits configuration, creates root administrator account, and attaches refresh encoder middleware.
287
+ Audits configuration, creates root administrator account, and attaches response handler middleware.
292
288
 
293
289
  Args:
294
290
  app (Sanic): The main Sanic application instance.
@@ -296,7 +292,8 @@ def initialize_security(app: Sanic, create_root=True) -> None:
296
292
  """
297
293
 
298
294
  @app.on_response
299
- async def refresh_encoder_middleware(request, response):
295
+ async def response_handler_middleware(request, response):
296
+ secure_headers.set_headers(response)
300
297
  if hasattr(request.ctx, "authentication_session"):
301
298
  authentication_session = request.ctx.authentication_session
302
299
  if authentication_session.is_refresh:
@@ -71,7 +71,7 @@ async def check_permissions(
71
71
  if fnmatch(required_permission, role_permission):
72
72
  return authentication_session
73
73
  logger.warning(
74
- f"Client {get_ip(request)} with account {authentication_session.bearer.id} attempted an unauthorized action. "
74
+ f"Client {get_ip(request)} with account {authentication_session.bearer.id} attempted an unauthorized action."
75
75
  )
76
76
  raise AuthorizationError("Insufficient permissions required for this action.")
77
77
 
@@ -123,14 +123,16 @@ async def assign_role(
123
123
  Args:
124
124
  name (str): The name of the role associated with the account.
125
125
  account (Account): The account associated with the created role.
126
- permissions (str): The permissions of the role associated with the account. Permissions must be separated via comma and in wildcard format.
126
+ permissions (str): The permissions of the role associated with the account. Permissions must be separated via ", " and in wildcard format.
127
127
  description (str): The description of the role associated with the account.
128
128
  """
129
129
  try:
130
130
  role = await Role.filter(name=name).get()
131
131
  except DoesNotExist:
132
132
  role = await Role.create(
133
- description=description, permissions=permissions, name=name
133
+ name=name,
134
+ description=description,
135
+ permissions=permissions,
134
136
  )
135
137
  await account.roles.add(role)
136
138
  return role
@@ -36,6 +36,7 @@ DEFAULT_CONFIG = {
36
36
  "MAX_CHALLENGE_ATTEMPTS": 5,
37
37
  "CAPTCHA_SESSION_EXPIRATION": 60,
38
38
  "CAPTCHA_FONT": "captcha-font.ttf",
39
+ "CAPTCHA_VOICE": "captcha-voice/",
39
40
  "TWO_STEP_SESSION_EXPIRATION": 300,
40
41
  "AUTHENTICATION_SESSION_EXPIRATION": 86400,
41
42
  "AUTHENTICATION_REFRESH_EXPIRATION": 604800,
@@ -62,6 +63,7 @@ class Config(dict):
62
63
  MAX_CHALLENGE_ATTEMPTS (str): The maximum amount of session challenge attempts allowed.
63
64
  CAPTCHA_SESSION_EXPIRATION (int): The amount of seconds till captcha session expiration on creation. Setting to 0 will disable expiration.
64
65
  CAPTCHA_FONT (str): The file path to the font being used for captcha generation.
66
+ CAPTCHA_VOICE (str): The directory of the voice library being used for audio captcha generation.
65
67
  TWO_STEP_SESSION_EXPIRATION (int): The amount of seconds till two-step session expiration on creation. Setting to 0 will disable expiration.
66
68
  AUTHENTICATION_SESSION_EXPIRATION (int): The amount of seconds till authentication session expiration on creation. Setting to 0 will disable expiration.
67
69
  AUTHENTICATION_REFRESH_EXPIRATION (int): The amount of seconds till authentication session refresh expiration. Setting to 0 will disable refresh mechanism.
@@ -82,6 +84,7 @@ class Config(dict):
82
84
  MAX_CHALLENGE_ATTEMPTS: int
83
85
  CAPTCHA_SESSION_EXPIRATION: int
84
86
  CAPTCHA_FONT: str
87
+ CAPTCHA_VOICE: str
85
88
  TWO_STEP_SESSION_EXPIRATION: int
86
89
  AUTHENTICATION_SESSION_EXPIRATION: int
87
90
  AUTHENTICATION_REFRESH_EXPIRATION: int
@@ -1,11 +1,9 @@
1
1
  import base64
2
2
  import datetime
3
3
  import re
4
- from io import BytesIO
5
4
  from typing import Union
6
5
 
7
6
  import jwt
8
- from captcha.image import ImageCaptcha
9
7
  from jwt import DecodeError
10
8
  from sanic.request import Request
11
9
  from sanic.response import HTTPResponse, raw
@@ -15,7 +13,13 @@ from tortoise.validators import RegexValidator
15
13
 
16
14
  from sanic_security.configuration import config as security_config
17
15
  from sanic_security.exceptions import *
18
- from sanic_security.utils import get_ip, get_code, get_expiration_date
16
+ from sanic_security.utils import (
17
+ get_ip,
18
+ get_code,
19
+ get_expiration_date,
20
+ image_generator,
21
+ audio_generator,
22
+ )
19
23
 
20
24
  """
21
25
  Copyright (c) 2020-present Nicholas Aidan Stewart
@@ -68,7 +72,7 @@ class BaseModel(Model):
68
72
  @property
69
73
  def json(self) -> dict:
70
74
  """
71
- A JSON serializable dict to be used in a HTTP request or response.
75
+ A JSON serializable dict to be used in an HTTP request or response.
72
76
 
73
77
  Example:
74
78
  Below is an example of this method returning a dict to be used for JSON serialization.
@@ -119,7 +123,7 @@ class Account(BaseModel):
119
123
  )
120
124
  phone: str = fields.CharField(
121
125
  unique=True,
122
- max_length=14,
126
+ max_length=15,
123
127
  null=True,
124
128
  validators=[
125
129
  RegexValidator(r"^(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$", re.I)
@@ -189,9 +193,8 @@ class Account(BaseModel):
189
193
  NotFoundError
190
194
  """
191
195
  try:
192
- account = await Account.filter(email=email, deleted=False).get()
193
- return account
194
- except DoesNotExist:
196
+ return await Account.filter(email=email, deleted=False).get()
197
+ except (DoesNotExist, ValidationError):
195
198
  raise NotFoundError("Account with this email does not exist.")
196
199
 
197
200
  @staticmethod
@@ -209,8 +212,7 @@ class Account(BaseModel):
209
212
  NotFoundError
210
213
  """
211
214
  try:
212
- account = await Account.filter(username=username, deleted=False).get()
213
- return account
215
+ return await Account.filter(username=username, deleted=False).get()
214
216
  except (DoesNotExist, ValidationError):
215
217
  raise NotFoundError("Account with this username does not exist.")
216
218
 
@@ -230,7 +232,7 @@ class Account(BaseModel):
230
232
  """
231
233
  try:
232
234
  account = await Account.get_via_email(credential)
233
- except (NotFoundError, ValidationError) as e:
235
+ except NotFoundError as e:
234
236
  if security_config.ALLOW_LOGIN_WITH_USERNAME:
235
237
  account = await Account.get_via_username(credential)
236
238
  else:
@@ -281,9 +283,8 @@ class Account(BaseModel):
281
283
  NotFoundError
282
284
  """
283
285
  try:
284
- account = await Account.filter(phone=phone, deleted=False).get()
285
- return account
286
- except DoesNotExist:
286
+ return await Account.filter(phone=phone, deleted=False).get()
287
+ except (DoesNotExist, ValidationError):
287
288
  raise NotFoundError("Account with this phone number does not exist.")
288
289
 
289
290
 
@@ -574,10 +575,21 @@ class CaptchaSession(VerificationSession):
574
575
  Returns:
575
576
  captcha_image
576
577
  """
577
- image = ImageCaptcha(190, 90, fonts=[security_config.CAPTCHA_FONT])
578
- with BytesIO() as output:
579
- image.generate_image(self.code).save(output, format="JPEG")
580
- return raw(output.getvalue(), content_type="image/jpeg")
578
+ return raw(
579
+ image_generator.generate(self.code, "jpeg").getvalue(),
580
+ content_type="image/jpeg",
581
+ )
582
+
583
+ def get_audio(self) -> HTTPResponse:
584
+ """
585
+ Retrieves captcha audio file.
586
+
587
+ Returns:
588
+ captcha_audio
589
+ """
590
+ return raw(
591
+ bytes(audio_generator.generate(self.code)), content_type="audio/mpeg"
592
+ )
581
593
 
582
594
  class Meta:
583
595
  table = "captcha_session"
@@ -195,9 +195,14 @@ async def on_captcha_request(request):
195
195
  async def on_captcha_image(request):
196
196
  """Request captcha image."""
197
197
  captcha_session = await CaptchaSession.decode(request)
198
- response = captcha_session.get_image()
199
- captcha_session.encode(response)
200
- return response
198
+ return captcha_session.get_image()
199
+
200
+
201
+ @app.get("api/test/capt/audio")
202
+ async def on_captcha_audio(request):
203
+ """Request captcha audio."""
204
+ captcha_session = await CaptchaSession.decode(request)
205
+ return captcha_session.get_audio()
201
206
 
202
207
 
203
208
  @app.post("api/test/capt")
@@ -2,8 +2,14 @@ import datetime
2
2
  import random
3
3
  import string
4
4
 
5
+ from argon2 import PasswordHasher
6
+ from captcha.audio import AudioCaptcha
7
+ from captcha.image import ImageCaptcha
5
8
  from sanic.request import Request
6
9
  from sanic.response import json as sanic_json, HTTPResponse
10
+ from secure import Secure
11
+
12
+ from sanic_security.configuration import config
7
13
 
8
14
  """
9
15
  Copyright (c) 2020-Present Nicholas Aidan Stewart
@@ -27,6 +33,13 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
33
  SOFTWARE.
28
34
  """
29
35
 
36
+ image_generator = ImageCaptcha(
37
+ 190, 90, fonts=config.CAPTCHA_FONT.replace(" ", "").split(",")
38
+ )
39
+ audio_generator = AudioCaptcha(voicedir=config.CAPTCHA_VOICE)
40
+ password_hasher = PasswordHasher()
41
+ secure_headers = Secure.with_default_headers()
42
+
30
43
 
31
44
  def get_ip(request: Request) -> str:
32
45
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sanic-security
3
- Version: 1.13.3
3
+ Version: 1.13.4
4
4
  Summary: An async security library for the Sanic framework.
5
5
  Author-email: Aidan Stewart <me@na-stewart.com>
6
6
  Project-URL: Documentation, https://security.na-stewart.com/
@@ -20,6 +20,7 @@ Requires-Dist: captcha>=0.4
20
20
  Requires-Dist: pillow>=9.5.0
21
21
  Requires-Dist: argon2-cffi>=20.1.0
22
22
  Requires-Dist: sanic>=21.3.0
23
+ Requires-Dist: secure>=1.0.1
23
24
  Provides-Extra: dev
24
25
  Requires-Dist: httpx; extra == "dev"
25
26
  Requires-Dist: black; extra == "dev"
@@ -63,7 +64,7 @@ Requires-Dist: cryptography>=3.3.1; extra == "crypto"
63
64
  * [Configuration](#configuration)
64
65
  * [Usage](#usage)
65
66
  * [Authentication](#authentication)
66
- * [Captcha](#captcha)
67
+ * [CAPTCHA](#captcha)
67
68
  * [Two-step Verification](#two-step-verification)
68
69
  * [Authorization](#authorization)
69
70
  * [Testing](#testing)
@@ -76,21 +77,22 @@ Requires-Dist: cryptography>=3.3.1; extra == "crypto"
76
77
  <!-- ABOUT THE PROJECT -->
77
78
  ## About The Project
78
79
 
79
- Sanic Security is an authentication, authorization, and verification library designed for use with [Sanic](https://github.com/huge-success/sanic).
80
+ Sanic Security is an authentication, authorization, and verification library designed for use with the
81
+ [Sanic](https://github.com/huge-success/sanic) framework.
80
82
 
81
83
  * Login, registration, and authentication with refresh mechanisms
82
84
  * Role based authorization with wildcard permissions
85
+ * Image & audio CAPTCHA
83
86
  * Two-factor authentication
84
87
  * Two-step verification
85
- * Logging & Auditing
86
- * Captcha
88
+ * Logging & auditing
87
89
 
88
90
  Visit [security.na-stewart.com](https://security.na-stewart.com) for documentation.
89
91
 
90
92
  <!-- GETTING STARTED -->
91
93
  ## Getting Started
92
94
 
93
- In order to get started, please install [Pip](https://pypi.org/).
95
+ In order to get started, please install [PyPI](https://pypi.org/).
94
96
 
95
97
  ### Installation
96
98
 
@@ -109,14 +111,9 @@ as an extra requirement.
109
111
  pip3 install sanic-security[crypto]
110
112
  ````
111
113
 
112
- * For developers, fork Sanic Security and install development dependencies.
113
- ```shell
114
- pip3 install -e ".[dev]"
115
- ````
116
-
117
114
  * Update sanic-security if already installed.
118
115
  ```shell
119
- pip3 install --upgrade sanic-security
116
+ pip3 install sanic-security --upgrade
120
117
  ```
121
118
 
122
119
  ### Configuration
@@ -154,7 +151,8 @@ You can load environment variables with a different prefix via `config.load_envi
154
151
  | **SESSION_PREFIX** | tkn | Prefix attached to the beginning of session cookies. |
155
152
  | **MAX_CHALLENGE_ATTEMPTS** | 5 | The maximum amount of session challenge attempts allowed. |
156
153
  | **CAPTCHA_SESSION_EXPIRATION** | 60 | The amount of seconds till captcha session expiration on creation. Setting to 0 will disable expiration. |
157
- | **CAPTCHA_FONT** | captcha-font.ttf | The file path to the font being used for captcha generation. |
154
+ | **CAPTCHA_FONT** | captcha-font.ttf | The file path to the font being used for captcha generation. Several fonts can be used by separating them via comma. |
155
+ | **CAPTCHA_VOICE** | captcha-voice/ | The directory of the voice library being used for audio captcha generation. |
158
156
  | **TWO_STEP_SESSION_EXPIRATION** | 200 | The amount of seconds till two-step session expiration on creation. Setting to 0 will disable expiration. |
159
157
  | **AUTHENTICATION_SESSION_EXPIRATION** | 86400 | The amount of seconds till authentication session expiration on creation. Setting to 0 will disable expiration. |
160
158
  | **AUTHENTICATION_REFRESH_EXPIRATION** | 604800 | The amount of seconds till authentication refresh expiration. Setting to 0 will disable refresh mechanism. |
@@ -319,16 +317,40 @@ async def on_authenticate(request):
319
317
  return response
320
318
  ```
321
319
 
322
- ## Captcha
320
+ ## CAPTCHA
321
+
322
+ Protects against spam and malicious activities by ensuring that only real humans can complete certain actions, like
323
+ submitting a form or creating an account.
323
324
 
324
- A pre-existing font for captcha challenges is included in the Sanic Security repository. You may set your own font by
325
- downloading a .ttf font and defining the file's path in the configuration.
325
+ * Fonts
326
+
327
+ A font for captcha challenges is included in the repository. You can set a custom font by downloading a .ttf file and
328
+ specifying its path in the configuration.
326
329
 
327
330
  [1001 Free Fonts](https://www.1001fonts.com/)
328
331
 
329
- [Recommended Font](https://www.1001fonts.com/source-sans-pro-font.html)
332
+ * Voice Library
333
+
334
+ A voice library for captcha challenges is included in the repository. You can generate your own using `espeak` and
335
+ `ffmpeg`, then specify the library's directory in the configuration.
330
336
 
331
- * Request Captcha
337
+ ```bash
338
+ # Set the language code
339
+ export ESLANG=en
340
+
341
+ # Create a directory for the specified language code
342
+ mkdir "$ESLANG"
343
+
344
+ # Loop through each character (A-Z, 0-9) and create a directory for each
345
+ for i in {A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,0,1,2,3,4,5,6,7,8,9}; do
346
+ mkdir "$ESLANG/$i"
347
+ espeak -a 150 -s 100 -p 15 -v "$ESLANG" "$i" -w "$ESLANG/$i/orig_default.wav"
348
+ ffmpeg -i "$ESLANG/$i/orig_default.wav" -ar 8000 -ac 1 -acodec pcm_u8 "$ESLANG/$i/default.wav"
349
+ rm "$ESLANG/$i/orig_default.wav"
350
+ done
351
+ ```
352
+
353
+ * Request CAPTCHA
332
354
 
333
355
  ```python
334
356
  @app.get("api/security/captcha")
@@ -339,7 +361,16 @@ async def on_captcha_img_request(request):
339
361
  return response
340
362
  ```
341
363
 
342
- * Captcha
364
+ * Request CAPTCHA Audio
365
+
366
+ ```python
367
+ @app.get("api/security/captcha/audio")
368
+ async def on_captcha_audio_request(request):
369
+ captcha_session = await CaptchaSession.decode(request)
370
+ return captcha_session.get_audio() # Captcha: LJ0F3U
371
+ ```
372
+
373
+ * Attempt CAPTCHA
343
374
 
344
375
  | Key | Value |
345
376
  |-------------|--------|
@@ -352,7 +383,7 @@ async def on_captcha(request):
352
383
  return json("Captcha attempt successful!", captcha_session.json)
353
384
  ```
354
385
 
355
- * Requires Captcha (This method is not called directly and instead used as a decorator)
386
+ * Requires CAPTCHA (This method is not called directly and instead used as a decorator)
356
387
 
357
388
  | Key | Value |
358
389
  |-------------|--------|
@@ -367,7 +398,7 @@ async def on_captcha(request):
367
398
 
368
399
  ## Two-step Verification
369
400
 
370
- Two-step verification should be integrated with other custom functionality. For example, account verification during registration.
401
+ Two-step verification should be integrated with other custom functionalities, such as account verification during registration.
371
402
 
372
403
  * Request Two-step Verification
373
404
 
@@ -399,7 +430,7 @@ async def on_two_step_resend(request):
399
430
  return json("Verification code resend successful!", two_step_session.json)
400
431
  ```
401
432
 
402
- * Two-step Verification
433
+ * Attempt Two-step Verification
403
434
 
404
435
  | Key | Value |
405
436
  |----------|--------|
@@ -442,13 +473,14 @@ user's account; this simplifies common operations, such as adding a user, or cha
442
473
 
443
474
  Wildcard permissions support the concept of multiple levels or parts. For example, you could grant a user the permission
444
475
  `printer:query`, `printer:query,delete`, or `printer:*`.
476
+
445
477
  * Assign Role
446
478
 
447
479
  ```python
448
480
  await assign_role(
449
481
  "Chat Room Moderator",
450
482
  account,
451
- "channels:view,delete, account:suspend,mute, voice:*",
483
+ "channels:view,delete, voice:*, account:suspend,mute",
452
484
  "Can read and delete messages in all chat rooms, suspend and mute accounts, and control voice chat.",
453
485
  )
454
486
  ```
@@ -497,7 +529,7 @@ async def on_check_roles(request):
497
529
 
498
530
  * Make sure the test Sanic instance (`test/server.py`) is running on your machine.
499
531
 
500
- * Run the unit test client (`test/tests.py`) for results.
532
+ * Run the test client (`test/tests.py`) for results.
501
533
 
502
534
  ## Tortoise
503
535
 
@@ -4,6 +4,7 @@ captcha>=0.4
4
4
  pillow>=9.5.0
5
5
  argon2-cffi>=20.1.0
6
6
  sanic>=21.3.0
7
+ secure>=1.0.1
7
8
 
8
9
  [crypto]
9
10
  cryptography>=3.3.1
File without changes