sanic-security 1.13.2__py3-none-any.whl → 1.13.3__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 +31 -86
- sanic_security/models.py +33 -21
- {sanic_security-1.13.2.dist-info → sanic_security-1.13.3.dist-info}/METADATA +3 -3
- {sanic_security-1.13.2.dist-info → sanic_security-1.13.3.dist-info}/RECORD +7 -7
- {sanic_security-1.13.2.dist-info → sanic_security-1.13.3.dist-info}/LICENSE +0 -0
- {sanic_security-1.13.2.dist-info → sanic_security-1.13.3.dist-info}/WHEEL +0 -0
- {sanic_security-1.13.2.dist-info → sanic_security-1.13.3.dist-info}/top_level.txt +0 -0
sanic_security/authentication.py
CHANGED
@@ -7,7 +7,7 @@ from argon2.exceptions import VerifyMismatchError
|
|
7
7
|
from sanic import Sanic
|
8
8
|
from sanic.log import logger
|
9
9
|
from sanic.request import Request
|
10
|
-
from tortoise.exceptions import DoesNotExist
|
10
|
+
from tortoise.exceptions import DoesNotExist, ValidationError, IntegrityError
|
11
11
|
|
12
12
|
from sanic_security.configuration import config as security_config, DEFAULT_CONFIG
|
13
13
|
from sanic_security.exceptions import (
|
@@ -62,32 +62,31 @@ async def register(
|
|
62
62
|
Raises:
|
63
63
|
CredentialsError
|
64
64
|
"""
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
65
|
+
try:
|
66
|
+
account = await Account.create(
|
67
|
+
email=request.form.get("email").lower(),
|
68
|
+
username=request.form.get("username"),
|
69
|
+
password=password_hasher.hash(
|
70
|
+
validate_password(request.form.get("password"))
|
71
|
+
),
|
72
|
+
phone=request.form.get("phone"),
|
73
|
+
verified=verified,
|
74
|
+
disabled=disabled,
|
75
|
+
)
|
76
|
+
logger.info(f"Client {get_ip(request)} has registered account {account.id}.")
|
77
|
+
return account
|
78
|
+
except ValidationError as e:
|
78
79
|
raise CredentialsError(
|
79
|
-
"
|
80
|
+
"Username must be 3-32 characters long and can only include _ or -."
|
81
|
+
if "username" in e.args[0]
|
82
|
+
else "Invalid email or phone number."
|
83
|
+
)
|
84
|
+
except IntegrityError as e:
|
85
|
+
raise CredentialsError(
|
86
|
+
f"An account with this {"username" if "username" in str(e.args[0]) else "email or phone number"} "
|
87
|
+
"may already exist.",
|
88
|
+
409,
|
80
89
|
)
|
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
90
|
|
92
91
|
|
93
92
|
async def login(
|
@@ -266,65 +265,6 @@ def requires_authentication(arg=None):
|
|
266
265
|
return decorator(arg) if callable(arg) else decorator
|
267
266
|
|
268
267
|
|
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
268
|
def validate_password(password: str) -> str:
|
329
269
|
"""
|
330
270
|
Validates password requirements.
|
@@ -370,8 +310,13 @@ def initialize_security(app: Sanic, create_root=True) -> None:
|
|
370
310
|
warnings.warn("HttpOnly should be enabled.", AuditWarning)
|
371
311
|
if not security_config.SESSION_SECURE:
|
372
312
|
warnings.warn("Secure should be enabled.", AuditWarning)
|
373
|
-
if
|
374
|
-
|
313
|
+
if (
|
314
|
+
not security_config.SESSION_SAMESITE
|
315
|
+
or security_config.SESSION_SAMESITE.lower() == "none"
|
316
|
+
):
|
317
|
+
warnings.warn("SameSite should not be none.", AuditWarning)
|
318
|
+
if not security_config.SESSION_DOMAIN:
|
319
|
+
warnings.warn("Domain should not be none.", AuditWarning)
|
375
320
|
if (
|
376
321
|
create_root
|
377
322
|
and security_config.INITIAL_ADMIN_EMAIL
|
sanic_security/models.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
import base64
|
2
2
|
import datetime
|
3
|
+
import re
|
3
4
|
from io import BytesIO
|
4
5
|
from typing import Union
|
5
6
|
|
@@ -9,7 +10,8 @@ from jwt import DecodeError
|
|
9
10
|
from sanic.request import Request
|
10
11
|
from sanic.response import HTTPResponse, raw
|
11
12
|
from tortoise import fields, Model
|
12
|
-
from tortoise.exceptions import DoesNotExist
|
13
|
+
from tortoise.exceptions import DoesNotExist, ValidationError
|
14
|
+
from tortoise.validators import RegexValidator
|
13
15
|
|
14
16
|
from sanic_security.configuration import config as security_config
|
15
17
|
from sanic_security.exceptions import *
|
@@ -103,9 +105,26 @@ class Account(BaseModel):
|
|
103
105
|
roles (ManyToManyRelation[Role]): Roles associated with this account.
|
104
106
|
"""
|
105
107
|
|
106
|
-
username: str = fields.CharField(
|
107
|
-
|
108
|
-
|
108
|
+
username: str = fields.CharField(
|
109
|
+
unique=security_config.ALLOW_LOGIN_WITH_USERNAME,
|
110
|
+
max_length=32,
|
111
|
+
validators=[RegexValidator(r"^[A-Za-z0-9_-]*$", re.I)],
|
112
|
+
)
|
113
|
+
email: str = fields.CharField(
|
114
|
+
unique=True,
|
115
|
+
max_length=255,
|
116
|
+
validators=[
|
117
|
+
RegexValidator(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$", re.I)
|
118
|
+
],
|
119
|
+
)
|
120
|
+
phone: str = fields.CharField(
|
121
|
+
unique=True,
|
122
|
+
max_length=14,
|
123
|
+
null=True,
|
124
|
+
validators=[
|
125
|
+
RegexValidator(r"^(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$", re.I)
|
126
|
+
],
|
127
|
+
)
|
109
128
|
password: str = fields.CharField(max_length=255)
|
110
129
|
disabled: bool = fields.BooleanField(default=False)
|
111
130
|
verified: bool = fields.BooleanField(default=False)
|
@@ -192,7 +211,7 @@ class Account(BaseModel):
|
|
192
211
|
try:
|
193
212
|
account = await Account.filter(username=username, deleted=False).get()
|
194
213
|
return account
|
195
|
-
except DoesNotExist:
|
214
|
+
except (DoesNotExist, ValidationError):
|
196
215
|
raise NotFoundError("Account with this username does not exist.")
|
197
216
|
|
198
217
|
@staticmethod
|
@@ -211,7 +230,7 @@ class Account(BaseModel):
|
|
211
230
|
"""
|
212
231
|
try:
|
213
232
|
account = await Account.get_via_email(credential)
|
214
|
-
except NotFoundError as e:
|
233
|
+
except (NotFoundError, ValidationError) as e:
|
215
234
|
if security_config.ALLOW_LOGIN_WITH_USERNAME:
|
216
235
|
account = await Account.get_via_username(credential)
|
217
236
|
else:
|
@@ -348,19 +367,14 @@ class Session(BaseModel):
|
|
348
367
|
httponly=security_config.SESSION_HTTPONLY,
|
349
368
|
samesite=security_config.SESSION_SAMESITE,
|
350
369
|
secure=security_config.SESSION_SECURE,
|
351
|
-
|
352
|
-
|
353
|
-
response.cookies.get_cookie(cookie).expires = (
|
370
|
+
domain=security_config.SESSION_DOMAIN,
|
371
|
+
expires=(
|
354
372
|
self.refresh_expiration_date
|
355
|
-
if (
|
356
|
-
|
357
|
-
and self.refresh_expiration_date
|
358
|
-
)
|
373
|
+
if hasattr(self, "refresh_expiration_date")
|
374
|
+
and self.refresh_expiration_date
|
359
375
|
else self.expiration_date
|
360
|
-
)
|
361
|
-
|
362
|
-
if security_config.SESSION_DOMAIN:
|
363
|
-
response.cookies.get_cookie(cookie).domain = security_config.SESSION_DOMAIN
|
376
|
+
),
|
377
|
+
)
|
364
378
|
|
365
379
|
@property
|
366
380
|
def json(self) -> dict:
|
@@ -369,9 +383,7 @@ class Session(BaseModel):
|
|
369
383
|
"date_created": str(self.date_created),
|
370
384
|
"date_updated": str(self.date_updated),
|
371
385
|
"expiration_date": str(self.expiration_date),
|
372
|
-
"bearer": (
|
373
|
-
self.bearer.username if isinstance(self.bearer, Account) else None
|
374
|
-
),
|
386
|
+
"bearer": self.bearer.id if isinstance(self.bearer, Account) else None,
|
375
387
|
"active": self.active,
|
376
388
|
}
|
377
389
|
|
@@ -491,7 +503,7 @@ class VerificationSession(Session):
|
|
491
503
|
"""
|
492
504
|
|
493
505
|
attempts: int = fields.IntField(default=0)
|
494
|
-
code: str = fields.CharField(max_length=
|
506
|
+
code: str = fields.CharField(max_length=6, default=get_code, null=True)
|
495
507
|
|
496
508
|
async def check_code(self, code: str) -> None:
|
497
509
|
"""
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: sanic-security
|
3
|
-
Version: 1.13.
|
3
|
+
Version: 1.13.3
|
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/
|
@@ -82,8 +82,8 @@ Sanic Security is an authentication, authorization, and verification library des
|
|
82
82
|
* Role based authorization with wildcard permissions
|
83
83
|
* Two-factor authentication
|
84
84
|
* Two-step verification
|
85
|
+
* Logging & Auditing
|
85
86
|
* Captcha
|
86
|
-
* Logging
|
87
87
|
|
88
88
|
Visit [security.na-stewart.com](https://security.na-stewart.com) for documentation.
|
89
89
|
|
@@ -158,7 +158,7 @@ You can load environment variables with a different prefix via `config.load_envi
|
|
158
158
|
| **TWO_STEP_SESSION_EXPIRATION** | 200 | The amount of seconds till two-step session expiration on creation. Setting to 0 will disable expiration. |
|
159
159
|
| **AUTHENTICATION_SESSION_EXPIRATION** | 86400 | The amount of seconds till authentication session expiration on creation. Setting to 0 will disable expiration. |
|
160
160
|
| **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
|
161
|
+
| **ALLOW_LOGIN_WITH_USERNAME** | False | Allows login via username; unique constraint is disabled when set to false. |
|
162
162
|
| **INITIAL_ADMIN_EMAIL** | admin@example.com | Email used when creating the initial admin account. |
|
163
163
|
| **INITIAL_ADMIN_PASSWORD** | admin123 | Password used when creating the initial admin account. |
|
164
164
|
|
@@ -1,16 +1,16 @@
|
|
1
1
|
sanic_security/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
sanic_security/authentication.py,sha256=
|
2
|
+
sanic_security/authentication.py,sha256=Rc2QqAjY17EialuML-wAThIrOxK8FKIFq927bst70IU,13218
|
3
3
|
sanic_security/authorization.py,sha256=1SHx4cU_ibC0o_nEDDYURH_l_K6Q66M0SLzpRQrsIXc,7534
|
4
4
|
sanic_security/configuration.py,sha256=br2hI3MHsTBh3yfPer5f3bkKSWfQdCeqfLqWmaDNVoM,5510
|
5
5
|
sanic_security/exceptions.py,sha256=JiCaBR2kjE1Cj0fc_08y-32fqJJXa_1UCw205T4_RTY,5493
|
6
|
-
sanic_security/models.py,sha256=
|
6
|
+
sanic_security/models.py,sha256=XF9u3RIU76M19QzNiM4C7LoLHe4uv6tfcELGH4W7L5k,22637
|
7
7
|
sanic_security/utils.py,sha256=XAUNalcTi53qTz0D8xiDyDyRlq7Z7ffNBzUONJZqe90,2705
|
8
8
|
sanic_security/verification.py,sha256=js2PkqJU6o46atslJ76n-_cYoY5iz5fbyiV0OFwoySo,8668
|
9
9
|
sanic_security/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
10
|
sanic_security/test/server.py,sha256=79HaNIH1skWTrh2gIbh8WWVNxvYqPA5GlQ8AqRaCsXQ,12094
|
11
11
|
sanic_security/test/tests.py,sha256=bW5fHJfsCrg8eBmcSqVMLm0R5XRL1ou-XJJRgz09GOE,21993
|
12
|
-
sanic_security-1.13.
|
13
|
-
sanic_security-1.13.
|
14
|
-
sanic_security-1.13.
|
15
|
-
sanic_security-1.13.
|
16
|
-
sanic_security-1.13.
|
12
|
+
sanic_security-1.13.3.dist-info/LICENSE,sha256=sXlJs9_mG-dCkPfWsDnuzydJWagS82E2gYtkVH9enHA,1100
|
13
|
+
sanic_security-1.13.3.dist-info/METADATA,sha256=PUPuAXTXIRtjpdBzYWl9Pz8S3m-VdJB2skIEnHqQdGM,23022
|
14
|
+
sanic_security-1.13.3.dist-info/WHEEL,sha256=R06PA3UVYHThwHvxuRWMqaGcr-PuniXahwjmQRFMEkY,91
|
15
|
+
sanic_security-1.13.3.dist-info/top_level.txt,sha256=ZybkhHXSjfzhmv8XeqLvnNmLmv21Z0oPX6Ep4DJN8b0,15
|
16
|
+
sanic_security-1.13.3.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|