sanic-security 1.13.1__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 +34 -21
- {sanic_security-1.13.1.dist-info → sanic_security-1.13.3.dist-info}/METADATA +4 -4
- {sanic_security-1.13.1.dist-info → sanic_security-1.13.3.dist-info}/RECORD +7 -7
- {sanic_security-1.13.1.dist-info → sanic_security-1.13.3.dist-info}/WHEEL +1 -1
- {sanic_security-1.13.1.dist-info → sanic_security-1.13.3.dist-info}/LICENSE +0 -0
- {sanic_security-1.13.1.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:
|
@@ -336,6 +355,7 @@ class Session(BaseModel):
|
|
336
355
|
"id": self.id,
|
337
356
|
"date_created": str(self.date_created),
|
338
357
|
"expiration_date": str(self.expiration_date),
|
358
|
+
"bearer": self.bearer.id if isinstance(self.bearer, Account) else None,
|
339
359
|
"ip": self.ip,
|
340
360
|
},
|
341
361
|
security_config.SECRET,
|
@@ -347,19 +367,14 @@ class Session(BaseModel):
|
|
347
367
|
httponly=security_config.SESSION_HTTPONLY,
|
348
368
|
samesite=security_config.SESSION_SAMESITE,
|
349
369
|
secure=security_config.SESSION_SECURE,
|
350
|
-
|
351
|
-
|
352
|
-
response.cookies.get_cookie(cookie).expires = (
|
370
|
+
domain=security_config.SESSION_DOMAIN,
|
371
|
+
expires=(
|
353
372
|
self.refresh_expiration_date
|
354
|
-
if (
|
355
|
-
|
356
|
-
and self.refresh_expiration_date
|
357
|
-
)
|
373
|
+
if hasattr(self, "refresh_expiration_date")
|
374
|
+
and self.refresh_expiration_date
|
358
375
|
else self.expiration_date
|
359
|
-
)
|
360
|
-
|
361
|
-
if security_config.SESSION_DOMAIN:
|
362
|
-
response.cookies.get_cookie(cookie).domain = security_config.SESSION_DOMAIN
|
376
|
+
),
|
377
|
+
)
|
363
378
|
|
364
379
|
@property
|
365
380
|
def json(self) -> dict:
|
@@ -368,9 +383,7 @@ class Session(BaseModel):
|
|
368
383
|
"date_created": str(self.date_created),
|
369
384
|
"date_updated": str(self.date_updated),
|
370
385
|
"expiration_date": str(self.expiration_date),
|
371
|
-
"bearer": (
|
372
|
-
self.bearer.username if isinstance(self.bearer, Account) else None
|
373
|
-
),
|
386
|
+
"bearer": self.bearer.id if isinstance(self.bearer, Account) else None,
|
374
387
|
"active": self.active,
|
375
388
|
}
|
376
389
|
|
@@ -490,7 +503,7 @@ class VerificationSession(Session):
|
|
490
503
|
"""
|
491
504
|
|
492
505
|
attempts: int = fields.IntField(default=0)
|
493
|
-
code: str = fields.CharField(max_length=
|
506
|
+
code: str = fields.CharField(max_length=6, default=get_code, null=True)
|
494
507
|
|
495
508
|
async def check_code(self, code: str) -> None:
|
496
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/
|
@@ -64,7 +64,7 @@ Requires-Dist: cryptography; extra == "dev"
|
|
64
64
|
* [Usage](#usage)
|
65
65
|
* [Authentication](#authentication)
|
66
66
|
* [Captcha](#captcha)
|
67
|
-
* [Two-
|
67
|
+
* [Two-step Verification](#two-step-verification)
|
68
68
|
* [Authorization](#authorization)
|
69
69
|
* [Testing](#testing)
|
70
70
|
* [Tortoise](#tortoise)
|
@@ -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
|