sanic-security 1.13.2__py3-none-any.whl → 1.13.4__py3-none-any.whl
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.
- sanic_security/authentication.py +36 -94
- sanic_security/authorization.py +5 -3
- sanic_security/configuration.py +3 -0
- sanic_security/models.py +60 -36
- sanic_security/test/server.py +8 -3
- sanic_security/utils.py +13 -0
- {sanic_security-1.13.2.dist-info → sanic_security-1.13.4.dist-info}/METADATA +57 -25
- sanic_security-1.13.4.dist-info/RECORD +16 -0
- sanic_security-1.13.2.dist-info/RECORD +0 -16
- {sanic_security-1.13.2.dist-info → sanic_security-1.13.4.dist-info}/LICENSE +0 -0
- {sanic_security-1.13.2.dist-info → sanic_security-1.13.4.dist-info}/WHEEL +0 -0
- {sanic_security-1.13.2.dist-info → sanic_security-1.13.4.dist-info}/top_level.txt +0 -0
sanic_security/authentication.py
CHANGED
@@ -2,12 +2,11 @@ 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
|
9
8
|
from sanic.request import Request
|
10
|
-
from tortoise.exceptions import DoesNotExist
|
9
|
+
from tortoise.exceptions import DoesNotExist, ValidationError, IntegrityError
|
11
10
|
|
12
11
|
from sanic_security.configuration import config as security_config, DEFAULT_CONFIG
|
13
12
|
from sanic_security.exceptions import (
|
@@ -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
|
@@ -62,32 +59,31 @@ async def register(
|
|
62
59
|
Raises:
|
63
60
|
CredentialsError
|
64
61
|
"""
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
62
|
+
try:
|
63
|
+
account = await Account.create(
|
64
|
+
email=request.form.get("email").lower(),
|
65
|
+
username=request.form.get("username"),
|
66
|
+
password=password_hasher.hash(
|
67
|
+
validate_password(request.form.get("password"))
|
68
|
+
),
|
69
|
+
phone=request.form.get("phone"),
|
70
|
+
verified=verified,
|
71
|
+
disabled=disabled,
|
72
|
+
)
|
73
|
+
logger.info(f"Client {get_ip(request)} has registered account {account.id}.")
|
74
|
+
return account
|
75
|
+
except ValidationError as e:
|
78
76
|
raise CredentialsError(
|
79
|
-
"
|
77
|
+
"Username must be 3-32 characters long and can only include _ or -."
|
78
|
+
if "username" in e.args[0]
|
79
|
+
else "Invalid email or phone number."
|
80
|
+
)
|
81
|
+
except IntegrityError as e:
|
82
|
+
raise CredentialsError(
|
83
|
+
f"An account with this {"username" if "username" in str(e.args[0]) else "email or phone number"} "
|
84
|
+
"may already exist.",
|
85
|
+
409,
|
80
86
|
)
|
81
|
-
account = await Account.create(
|
82
|
-
email=email_lower,
|
83
|
-
username=request.form.get("username"),
|
84
|
-
password=password_hasher.hash(validate_password(request.form.get("password"))),
|
85
|
-
phone=request.form.get("phone"),
|
86
|
-
verified=verified,
|
87
|
-
disabled=disabled,
|
88
|
-
)
|
89
|
-
logger.info(f"Client {get_ip(request)} has registered account {account.id}.")
|
90
|
-
return account
|
91
87
|
|
92
88
|
|
93
89
|
async def login(
|
@@ -194,8 +190,7 @@ async def fulfill_second_factor(request: Request) -> AuthenticationSession:
|
|
194
190
|
authentication_session.requires_second_factor = False
|
195
191
|
await authentication_session.save(update_fields=["requires_second_factor"])
|
196
192
|
logger.info(
|
197
|
-
f"Client {get_ip(request)} has fulfilled authentication session {authentication_session.id} "
|
198
|
-
"second factor."
|
193
|
+
f"Client {get_ip(request)} has fulfilled authentication session {authentication_session.id} second factor."
|
199
194
|
)
|
200
195
|
return authentication_session
|
201
196
|
|
@@ -266,65 +261,6 @@ def requires_authentication(arg=None):
|
|
266
261
|
return decorator(arg) if callable(arg) else decorator
|
267
262
|
|
268
263
|
|
269
|
-
def validate_email(email: str) -> str:
|
270
|
-
"""
|
271
|
-
Validates email format.
|
272
|
-
|
273
|
-
Args:
|
274
|
-
email (str): Email being validated.
|
275
|
-
|
276
|
-
Returns:
|
277
|
-
email
|
278
|
-
|
279
|
-
Raises:
|
280
|
-
CredentialsError
|
281
|
-
"""
|
282
|
-
if not re.search(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$", email):
|
283
|
-
raise CredentialsError("Please use a valid email address.", 400)
|
284
|
-
return email
|
285
|
-
|
286
|
-
|
287
|
-
def validate_username(username: str) -> str:
|
288
|
-
"""
|
289
|
-
Validates username format.
|
290
|
-
|
291
|
-
Args:
|
292
|
-
username (str): Username being validated.
|
293
|
-
|
294
|
-
Returns:
|
295
|
-
username
|
296
|
-
|
297
|
-
Raises:
|
298
|
-
CredentialsError
|
299
|
-
"""
|
300
|
-
if not re.search(r"^[A-Za-z0-9_-]{3,32}$", username):
|
301
|
-
raise CredentialsError(
|
302
|
-
"Username must be between 3-32 characters and not contain any special characters other than _ or -.",
|
303
|
-
400,
|
304
|
-
)
|
305
|
-
return username
|
306
|
-
|
307
|
-
|
308
|
-
def validate_phone(phone: str) -> str:
|
309
|
-
"""
|
310
|
-
Validates phone number format.
|
311
|
-
|
312
|
-
Args:
|
313
|
-
phone (str): Phone number being validated.
|
314
|
-
|
315
|
-
Returns:
|
316
|
-
phone
|
317
|
-
|
318
|
-
Raises:
|
319
|
-
CredentialsError
|
320
|
-
"""
|
321
|
-
if phone and not re.search(
|
322
|
-
r"^(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$", phone
|
323
|
-
):
|
324
|
-
raise CredentialsError("Please use a valid phone number.", 400)
|
325
|
-
return phone
|
326
|
-
|
327
|
-
|
328
264
|
def validate_password(password: str) -> str:
|
329
265
|
"""
|
330
266
|
Validates password requirements.
|
@@ -348,7 +284,7 @@ def validate_password(password: str) -> str:
|
|
348
284
|
|
349
285
|
def initialize_security(app: Sanic, create_root=True) -> None:
|
350
286
|
"""
|
351
|
-
Audits configuration, creates root administrator account, and attaches
|
287
|
+
Audits configuration, creates root administrator account, and attaches response handler middleware.
|
352
288
|
|
353
289
|
Args:
|
354
290
|
app (Sanic): The main Sanic application instance.
|
@@ -356,7 +292,8 @@ def initialize_security(app: Sanic, create_root=True) -> None:
|
|
356
292
|
"""
|
357
293
|
|
358
294
|
@app.on_response
|
359
|
-
async def
|
295
|
+
async def response_handler_middleware(request, response):
|
296
|
+
secure_headers.set_headers(response)
|
360
297
|
if hasattr(request.ctx, "authentication_session"):
|
361
298
|
authentication_session = request.ctx.authentication_session
|
362
299
|
if authentication_session.is_refresh:
|
@@ -370,8 +307,13 @@ def initialize_security(app: Sanic, create_root=True) -> None:
|
|
370
307
|
warnings.warn("HttpOnly should be enabled.", AuditWarning)
|
371
308
|
if not security_config.SESSION_SECURE:
|
372
309
|
warnings.warn("Secure should be enabled.", AuditWarning)
|
373
|
-
if
|
374
|
-
|
310
|
+
if (
|
311
|
+
not security_config.SESSION_SAMESITE
|
312
|
+
or security_config.SESSION_SAMESITE.lower() == "none"
|
313
|
+
):
|
314
|
+
warnings.warn("SameSite should not be none.", AuditWarning)
|
315
|
+
if not security_config.SESSION_DOMAIN:
|
316
|
+
warnings.warn("Domain should not be none.", AuditWarning)
|
375
317
|
if (
|
376
318
|
create_root
|
377
319
|
and security_config.INITIAL_ADMIN_EMAIL
|
sanic_security/authorization.py
CHANGED
@@ -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
|
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
|
-
|
133
|
+
name=name,
|
134
|
+
description=description,
|
135
|
+
permissions=permissions,
|
134
136
|
)
|
135
137
|
await account.roles.add(role)
|
136
138
|
return role
|
sanic_security/configuration.py
CHANGED
@@ -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
|
sanic_security/models.py
CHANGED
@@ -1,19 +1,25 @@
|
|
1
1
|
import base64
|
2
2
|
import datetime
|
3
|
-
|
3
|
+
import re
|
4
4
|
from typing import Union
|
5
5
|
|
6
6
|
import jwt
|
7
|
-
from captcha.image import ImageCaptcha
|
8
7
|
from jwt import DecodeError
|
9
8
|
from sanic.request import Request
|
10
9
|
from sanic.response import HTTPResponse, raw
|
11
10
|
from tortoise import fields, Model
|
12
|
-
from tortoise.exceptions import DoesNotExist
|
11
|
+
from tortoise.exceptions import DoesNotExist, ValidationError
|
12
|
+
from tortoise.validators import RegexValidator
|
13
13
|
|
14
14
|
from sanic_security.configuration import config as security_config
|
15
15
|
from sanic_security.exceptions import *
|
16
|
-
from sanic_security.utils import
|
16
|
+
from sanic_security.utils import (
|
17
|
+
get_ip,
|
18
|
+
get_code,
|
19
|
+
get_expiration_date,
|
20
|
+
image_generator,
|
21
|
+
audio_generator,
|
22
|
+
)
|
17
23
|
|
18
24
|
"""
|
19
25
|
Copyright (c) 2020-present Nicholas Aidan Stewart
|
@@ -66,7 +72,7 @@ class BaseModel(Model):
|
|
66
72
|
@property
|
67
73
|
def json(self) -> dict:
|
68
74
|
"""
|
69
|
-
A JSON serializable dict to be used in
|
75
|
+
A JSON serializable dict to be used in an HTTP request or response.
|
70
76
|
|
71
77
|
Example:
|
72
78
|
Below is an example of this method returning a dict to be used for JSON serialization.
|
@@ -103,9 +109,26 @@ class Account(BaseModel):
|
|
103
109
|
roles (ManyToManyRelation[Role]): Roles associated with this account.
|
104
110
|
"""
|
105
111
|
|
106
|
-
username: str = fields.CharField(
|
107
|
-
|
108
|
-
|
112
|
+
username: str = fields.CharField(
|
113
|
+
unique=security_config.ALLOW_LOGIN_WITH_USERNAME,
|
114
|
+
max_length=32,
|
115
|
+
validators=[RegexValidator(r"^[A-Za-z0-9_-]*$", re.I)],
|
116
|
+
)
|
117
|
+
email: str = fields.CharField(
|
118
|
+
unique=True,
|
119
|
+
max_length=255,
|
120
|
+
validators=[
|
121
|
+
RegexValidator(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$", re.I)
|
122
|
+
],
|
123
|
+
)
|
124
|
+
phone: str = fields.CharField(
|
125
|
+
unique=True,
|
126
|
+
max_length=15,
|
127
|
+
null=True,
|
128
|
+
validators=[
|
129
|
+
RegexValidator(r"^(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$", re.I)
|
130
|
+
],
|
131
|
+
)
|
109
132
|
password: str = fields.CharField(max_length=255)
|
110
133
|
disabled: bool = fields.BooleanField(default=False)
|
111
134
|
verified: bool = fields.BooleanField(default=False)
|
@@ -170,9 +193,8 @@ class Account(BaseModel):
|
|
170
193
|
NotFoundError
|
171
194
|
"""
|
172
195
|
try:
|
173
|
-
|
174
|
-
|
175
|
-
except DoesNotExist:
|
196
|
+
return await Account.filter(email=email, deleted=False).get()
|
197
|
+
except (DoesNotExist, ValidationError):
|
176
198
|
raise NotFoundError("Account with this email does not exist.")
|
177
199
|
|
178
200
|
@staticmethod
|
@@ -190,9 +212,8 @@ class Account(BaseModel):
|
|
190
212
|
NotFoundError
|
191
213
|
"""
|
192
214
|
try:
|
193
|
-
|
194
|
-
|
195
|
-
except DoesNotExist:
|
215
|
+
return await Account.filter(username=username, deleted=False).get()
|
216
|
+
except (DoesNotExist, ValidationError):
|
196
217
|
raise NotFoundError("Account with this username does not exist.")
|
197
218
|
|
198
219
|
@staticmethod
|
@@ -262,9 +283,8 @@ class Account(BaseModel):
|
|
262
283
|
NotFoundError
|
263
284
|
"""
|
264
285
|
try:
|
265
|
-
|
266
|
-
|
267
|
-
except DoesNotExist:
|
286
|
+
return await Account.filter(phone=phone, deleted=False).get()
|
287
|
+
except (DoesNotExist, ValidationError):
|
268
288
|
raise NotFoundError("Account with this phone number does not exist.")
|
269
289
|
|
270
290
|
|
@@ -348,19 +368,14 @@ class Session(BaseModel):
|
|
348
368
|
httponly=security_config.SESSION_HTTPONLY,
|
349
369
|
samesite=security_config.SESSION_SAMESITE,
|
350
370
|
secure=security_config.SESSION_SECURE,
|
351
|
-
|
352
|
-
|
353
|
-
response.cookies.get_cookie(cookie).expires = (
|
371
|
+
domain=security_config.SESSION_DOMAIN,
|
372
|
+
expires=(
|
354
373
|
self.refresh_expiration_date
|
355
|
-
if (
|
356
|
-
|
357
|
-
and self.refresh_expiration_date
|
358
|
-
)
|
374
|
+
if hasattr(self, "refresh_expiration_date")
|
375
|
+
and self.refresh_expiration_date
|
359
376
|
else self.expiration_date
|
360
|
-
)
|
361
|
-
|
362
|
-
if security_config.SESSION_DOMAIN:
|
363
|
-
response.cookies.get_cookie(cookie).domain = security_config.SESSION_DOMAIN
|
377
|
+
),
|
378
|
+
)
|
364
379
|
|
365
380
|
@property
|
366
381
|
def json(self) -> dict:
|
@@ -369,9 +384,7 @@ class Session(BaseModel):
|
|
369
384
|
"date_created": str(self.date_created),
|
370
385
|
"date_updated": str(self.date_updated),
|
371
386
|
"expiration_date": str(self.expiration_date),
|
372
|
-
"bearer": (
|
373
|
-
self.bearer.username if isinstance(self.bearer, Account) else None
|
374
|
-
),
|
387
|
+
"bearer": self.bearer.id if isinstance(self.bearer, Account) else None,
|
375
388
|
"active": self.active,
|
376
389
|
}
|
377
390
|
|
@@ -491,7 +504,7 @@ class VerificationSession(Session):
|
|
491
504
|
"""
|
492
505
|
|
493
506
|
attempts: int = fields.IntField(default=0)
|
494
|
-
code: str = fields.CharField(max_length=
|
507
|
+
code: str = fields.CharField(max_length=6, default=get_code, null=True)
|
495
508
|
|
496
509
|
async def check_code(self, code: str) -> None:
|
497
510
|
"""
|
@@ -562,10 +575,21 @@ class CaptchaSession(VerificationSession):
|
|
562
575
|
Returns:
|
563
576
|
captcha_image
|
564
577
|
"""
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
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
|
+
)
|
569
593
|
|
570
594
|
class Meta:
|
571
595
|
table = "captcha_session"
|
sanic_security/test/server.py
CHANGED
@@ -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
|
-
|
199
|
-
|
200
|
-
|
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")
|
sanic_security/utils.py
CHANGED
@@ -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
|
+
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: crypto
|
24
25
|
Requires-Dist: cryptography>=3.3.1; extra == "crypto"
|
25
26
|
Provides-Extra: dev
|
@@ -63,7 +64,7 @@ Requires-Dist: cryptography; extra == "dev"
|
|
63
64
|
* [Configuration](#configuration)
|
64
65
|
* [Usage](#usage)
|
65
66
|
* [Authentication](#authentication)
|
66
|
-
* [
|
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; extra == "dev"
|
|
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
|
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
|
-
*
|
86
|
-
* Logging
|
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 [
|
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
|
116
|
+
pip3 install sanic-security --upgrade
|
120
117
|
```
|
121
118
|
|
122
119
|
### Configuration
|
@@ -154,11 +151,12 @@ 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. |
|
161
|
-
| **ALLOW_LOGIN_WITH_USERNAME** | False | Allows login via username
|
159
|
+
| **ALLOW_LOGIN_WITH_USERNAME** | False | Allows login via username; unique constraint is disabled when set to false. |
|
162
160
|
| **INITIAL_ADMIN_EMAIL** | admin@example.com | Email used when creating the initial admin account. |
|
163
161
|
| **INITIAL_ADMIN_PASSWORD** | admin123 | Password used when creating the initial admin account. |
|
164
162
|
|
@@ -319,16 +317,40 @@ async def on_authenticate(request):
|
|
319
317
|
return response
|
320
318
|
```
|
321
319
|
|
322
|
-
##
|
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
|
-
|
325
|
-
|
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
|
-
|
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
|
-
|
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
|
-
*
|
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
|
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
|
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
|
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
|
532
|
+
* Run the test client (`test/tests.py`) for results.
|
501
533
|
|
502
534
|
## Tortoise
|
503
535
|
|
@@ -0,0 +1,16 @@
|
|
1
|
+
sanic_security/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
sanic_security/authentication.py,sha256=6pOiZJmnej0no-at7irkrh8YCupAdJVQOJEgfT2cc5k,13214
|
3
|
+
sanic_security/authorization.py,sha256=ddJWqGJbFIqII5pUW5SxI7h4EyVB-EhrbGM7jsQutOI,7559
|
4
|
+
sanic_security/configuration.py,sha256=h-Kh4PalJpjbDcZvVHCzxX5l-GnldP3Fr8OlgGCZNHY,5680
|
5
|
+
sanic_security/exceptions.py,sha256=JiCaBR2kjE1Cj0fc_08y-32fqJJXa_1UCw205T4_RTY,5493
|
6
|
+
sanic_security/models.py,sha256=bK5daR6Iq7V7aqNSzksH6DGrCXMj2e4feNRhlxlFQMg,22722
|
7
|
+
sanic_security/utils.py,sha256=tgewsCAkNl_NkobHaDlZNIgVopQPiD8SWb6UC6tBYNs,3151
|
8
|
+
sanic_security/verification.py,sha256=js2PkqJU6o46atslJ76n-_cYoY5iz5fbyiV0OFwoySo,8668
|
9
|
+
sanic_security/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
+
sanic_security/test/server.py,sha256=Rh_L12HPCfagvAyqkHziBD1C4WHAKZ9ht4mTCpX2Yik,12240
|
11
|
+
sanic_security/test/tests.py,sha256=bW5fHJfsCrg8eBmcSqVMLm0R5XRL1ou-XJJRgz09GOE,21993
|
12
|
+
sanic_security-1.13.4.dist-info/LICENSE,sha256=sXlJs9_mG-dCkPfWsDnuzydJWagS82E2gYtkVH9enHA,1100
|
13
|
+
sanic_security-1.13.4.dist-info/METADATA,sha256=T_LTsAXGsFWZFSntcTzKoEaoPRpz1JIHwLjcPc0Havc,24248
|
14
|
+
sanic_security-1.13.4.dist-info/WHEEL,sha256=R06PA3UVYHThwHvxuRWMqaGcr-PuniXahwjmQRFMEkY,91
|
15
|
+
sanic_security-1.13.4.dist-info/top_level.txt,sha256=ZybkhHXSjfzhmv8XeqLvnNmLmv21Z0oPX6Ep4DJN8b0,15
|
16
|
+
sanic_security-1.13.4.dist-info/RECORD,,
|
@@ -1,16 +0,0 @@
|
|
1
|
-
sanic_security/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
sanic_security/authentication.py,sha256=h0Yq2hWFv1GZoqhPQBmoJnqywGQd6fOYu7zyaqfv6wQ,14432
|
3
|
-
sanic_security/authorization.py,sha256=1SHx4cU_ibC0o_nEDDYURH_l_K6Q66M0SLzpRQrsIXc,7534
|
4
|
-
sanic_security/configuration.py,sha256=br2hI3MHsTBh3yfPer5f3bkKSWfQdCeqfLqWmaDNVoM,5510
|
5
|
-
sanic_security/exceptions.py,sha256=JiCaBR2kjE1Cj0fc_08y-32fqJJXa_1UCw205T4_RTY,5493
|
6
|
-
sanic_security/models.py,sha256=l3cO5-S1bmE0tqhFXBC9z4IWTCnYNeRvw1VzIFLeaHE,22363
|
7
|
-
sanic_security/utils.py,sha256=XAUNalcTi53qTz0D8xiDyDyRlq7Z7ffNBzUONJZqe90,2705
|
8
|
-
sanic_security/verification.py,sha256=js2PkqJU6o46atslJ76n-_cYoY5iz5fbyiV0OFwoySo,8668
|
9
|
-
sanic_security/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
-
sanic_security/test/server.py,sha256=79HaNIH1skWTrh2gIbh8WWVNxvYqPA5GlQ8AqRaCsXQ,12094
|
11
|
-
sanic_security/test/tests.py,sha256=bW5fHJfsCrg8eBmcSqVMLm0R5XRL1ou-XJJRgz09GOE,21993
|
12
|
-
sanic_security-1.13.2.dist-info/LICENSE,sha256=sXlJs9_mG-dCkPfWsDnuzydJWagS82E2gYtkVH9enHA,1100
|
13
|
-
sanic_security-1.13.2.dist-info/METADATA,sha256=mcHljpRvZGGTk_hRi2Sf-lqF-QyZIrhn_lNZGkbNjUI,23011
|
14
|
-
sanic_security-1.13.2.dist-info/WHEEL,sha256=R06PA3UVYHThwHvxuRWMqaGcr-PuniXahwjmQRFMEkY,91
|
15
|
-
sanic_security-1.13.2.dist-info/top_level.txt,sha256=ZybkhHXSjfzhmv8XeqLvnNmLmv21Z0oPX6Ep4DJN8b0,15
|
16
|
-
sanic_security-1.13.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|