sanic-security 1.14.1__py3-none-any.whl → 1.15.1__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.
@@ -17,7 +17,7 @@ from sanic_security.exceptions import (
17
17
  AuditWarning,
18
18
  )
19
19
  from sanic_security.models import Account, AuthenticationSession, Role, TwoStepSession
20
- from sanic_security.utils import get_ip, password_hasher, secure_headers
20
+ from sanic_security.utils import get_ip, password_hasher
21
21
 
22
22
  """
23
23
  Copyright (c) 2020-present Nicholas Aidan Stewart
@@ -183,7 +183,7 @@ async def fulfill_second_factor(request: Request) -> AuthenticationSession:
183
183
  """
184
184
  authentication_session = await AuthenticationSession.decode(request)
185
185
  if not authentication_session.requires_second_factor:
186
- raise SecondFactorFulfilledError()
186
+ raise SecondFactorFulfilledError
187
187
  two_step_session = await TwoStepSession.decode(request)
188
188
  two_step_session.validate()
189
189
  await two_step_session.check_code(request.form.get("code"))
@@ -284,7 +284,7 @@ def validate_password(password: str) -> str:
284
284
 
285
285
  def initialize_security(app: Sanic, create_root=True) -> None:
286
286
  """
287
- Audits configuration, creates root administrator account, and attaches response handler middleware.
287
+ Audits configuration, creates root administrator account, and attaches refresh encoder middleware.
288
288
 
289
289
  Args:
290
290
  app (Sanic): The main Sanic application instance.
@@ -356,11 +356,8 @@ def initialize_security(app: Sanic, create_root=True) -> None:
356
356
  logger.info("Initial admin account created.")
357
357
 
358
358
  @app.on_response
359
- async def response_handler_middleware(request, response):
360
- if hasattr(request.ctx, "session"):
361
- secure_headers.set_headers(response)
362
- if (
363
- hasattr(request.ctx.session, "is_refresh")
364
- and request.ctx.session.is_refresh
365
- ):
366
- request.ctx.session.encode(response)
359
+ async def refresh_encoder_middleware(request, response):
360
+ if hasattr(request.ctx, "session") and getattr(
361
+ request.ctx.session, "is_refresh", False
362
+ ):
363
+ request.ctx.session.encode(response)
@@ -62,7 +62,7 @@ async def check_permissions(
62
62
  logger.warning(
63
63
  f"Client {get_ip(request)} attempted an unauthorized action anonymously."
64
64
  )
65
- raise AnonymousError()
65
+ raise AnonymousError
66
66
  roles = await authentication_session.bearer.roles.filter(deleted=False).all()
67
67
  for role in roles:
68
68
  for required_permission, role_permission in zip(
@@ -104,7 +104,7 @@ async def check_roles(request: Request, *required_roles: str) -> AuthenticationS
104
104
  logger.warning(
105
105
  f"Client {get_ip(request)} attempted an unauthorized action anonymously."
106
106
  )
107
- raise AnonymousError()
107
+ raise AnonymousError
108
108
  roles = await authentication_session.bearer.roles.filter(deleted=False).all()
109
109
  for role in roles:
110
110
  if role.name in required_roles:
@@ -33,8 +33,8 @@ DEFAULT_CONFIG = {
33
33
  "SESSION_DOMAIN": None,
34
34
  "SESSION_PREFIX": "tkn",
35
35
  "SESSION_ENCODING_ALGORITHM": "HS256",
36
- "MAX_CHALLENGE_ATTEMPTS": 5,
37
- "CAPTCHA_SESSION_EXPIRATION": 60,
36
+ "MAX_CHALLENGE_ATTEMPTS": 3,
37
+ "CAPTCHA_SESSION_EXPIRATION": 180,
38
38
  "CAPTCHA_FONT": "captcha-font.ttf",
39
39
  "CAPTCHA_VOICE": "captcha-voice/",
40
40
  "TWO_STEP_SESSION_EXPIRATION": 300,
@@ -145,15 +145,6 @@ class ExpiredError(SessionError):
145
145
  super().__init__("Session has expired.")
146
146
 
147
147
 
148
- class NotExpiredError(SessionError):
149
- """
150
- Raised when session needs to be expired.
151
- """
152
-
153
- def __init__(self):
154
- super().__init__("Session has not expired yet.", 403)
155
-
156
-
157
148
  class SecondFactorRequiredError(SessionError):
158
149
  """
159
150
  Raised when authentication session two-factor requirement isn't met.
sanic_security/models.py CHANGED
@@ -1,6 +1,8 @@
1
1
  import base64
2
2
  import datetime
3
+ import logging
3
4
  import re
5
+ import uuid
4
6
  from typing import Union
5
7
 
6
8
  import jwt
@@ -19,6 +21,7 @@ from sanic_security.utils import (
19
21
  get_expiration_date,
20
22
  image_generator,
21
23
  audio_generator,
24
+ is_expired,
22
25
  )
23
26
 
24
27
  """
@@ -55,7 +58,9 @@ class BaseModel(Model):
55
58
  deleted (bool): Renders the model filterable without removing from the database.
56
59
  """
57
60
 
58
- id: int = fields.IntField(pk=True)
61
+ id: str = fields.CharField(
62
+ pk=True, max_length=36, default=lambda: str(uuid.uuid4())
63
+ )
59
64
  date_created: datetime.datetime = fields.DatetimeField(auto_now_add=True)
60
65
  date_updated: datetime.datetime = fields.DatetimeField(auto_now=True)
61
66
  deleted: bool = fields.BooleanField(default=False)
@@ -67,7 +72,7 @@ class BaseModel(Model):
67
72
  Raises:
68
73
  SecurityError
69
74
  """
70
- raise NotImplementedError()
75
+ raise NotImplementedError
71
76
 
72
77
  @property
73
78
  def json(self) -> dict:
@@ -89,7 +94,7 @@ class BaseModel(Model):
89
94
  }
90
95
 
91
96
  """
92
- raise NotImplementedError()
97
+ raise NotImplementedError
93
98
 
94
99
  class Meta:
95
100
  abstract = True
@@ -148,9 +153,9 @@ class Account(BaseModel):
148
153
  if self.deleted:
149
154
  raise DeletedError("Account has been deleted.")
150
155
  elif not self.verified:
151
- raise UnverifiedError()
156
+ raise UnverifiedError
152
157
  elif self.disabled:
153
- raise DisabledError()
158
+ raise DisabledError
154
159
 
155
160
  async def disable(self):
156
161
  """
@@ -321,12 +326,9 @@ class Session(BaseModel):
321
326
  if self.deleted:
322
327
  raise DeletedError("Session has been deleted.")
323
328
  elif not self.active:
324
- raise DeactivatedError()
325
- elif (
326
- self.expiration_date
327
- and datetime.datetime.now(datetime.timezone.utc) >= self.expiration_date
328
- ):
329
- raise ExpiredError()
329
+ raise DeactivatedError
330
+ elif is_expired(self.expiration_date):
331
+ raise ExpiredError
330
332
 
331
333
  async def deactivate(self):
332
334
  """
@@ -416,7 +418,7 @@ class Session(BaseModel):
416
418
  Returns:
417
419
  session
418
420
  """
419
- raise NotImplementedError()
421
+ raise NotImplementedError
420
422
 
421
423
  @classmethod
422
424
  async def get_associated(cls, account: Account):
@@ -517,15 +519,15 @@ class VerificationSession(Session):
517
519
  ChallengeError
518
520
  MaxedOutChallengeError
519
521
  """
520
- if self.code != code.upper():
522
+ if not code or self.code != code.upper():
523
+ self.attempts += 1
521
524
  if self.attempts < security_config.MAX_CHALLENGE_ATTEMPTS:
522
- self.attempts += 1
523
525
  await self.save(update_fields=["attempts"])
524
526
  raise ChallengeError(
525
527
  "Your code does not match verification session code."
526
528
  )
527
529
  else:
528
- raise MaxedOutChallengeError()
530
+ raise MaxedOutChallengeError
529
531
  else:
530
532
  await self.deactivate()
531
533
 
@@ -540,9 +542,13 @@ class VerificationSession(Session):
540
542
  class TwoStepSession(VerificationSession):
541
543
  """Validates a client using a code sent via email or text."""
542
544
 
545
+ code: str = fields.CharField(
546
+ max_length=6, default=lambda: get_code(True), null=True
547
+ )
548
+
543
549
  @classmethod
544
550
  async def new(cls, request: Request, account: Account, **kwargs):
545
- return await TwoStepSession.create(
551
+ return await cls.create(
546
552
  **kwargs,
547
553
  ip=get_ip(request),
548
554
  bearer=account,
@@ -560,7 +566,7 @@ class CaptchaSession(VerificationSession):
560
566
 
561
567
  @classmethod
562
568
  async def new(cls, request: Request, **kwargs):
563
- return await CaptchaSession.create(
569
+ return await cls.create(
564
570
  **kwargs,
565
571
  ip=get_ip(request),
566
572
  expiration_date=get_expiration_date(
@@ -621,45 +627,36 @@ class AuthenticationSession(Session):
621
627
  """
622
628
  super().validate()
623
629
  if self.requires_second_factor:
624
- raise SecondFactorRequiredError()
630
+ raise SecondFactorRequiredError
625
631
 
626
632
  async def refresh(self, request: Request):
627
633
  """
628
- Refreshes session if expired and within refresh date.
634
+ Refreshes session if within refresh date.
629
635
 
630
636
  Args:
631
637
  request (Request): Sanic request parameter.
632
638
 
633
639
  Raises:
634
- DeletedError
635
640
  ExpiredError
636
- DeactivatedError
637
- SecondFactorRequiredError
638
- NotExpiredError
639
641
 
640
642
  Returns:
641
643
  session
642
644
  """
643
- try:
644
- self.validate()
645
- raise NotExpiredError()
646
- except ExpiredError as e:
647
- if (
648
- self.refresh_expiration_date
649
- and datetime.datetime.now(datetime.timezone.utc)
650
- <= self.refresh_expiration_date
651
- ):
652
- self.active = False
653
- await self.save(update_fields=["active"])
654
- return await self.new(request, self.bearer, True)
655
- else:
656
- raise e
645
+ if not is_expired(self.refresh_expiration_date):
646
+ self.active = False
647
+ await self.save(update_fields=["active"])
648
+ logging.info(
649
+ f"Client {get_ip(request)} has refreshed authentication session {self.id}."
650
+ )
651
+ return await self.new(request, self.bearer, True)
652
+ else:
653
+ raise ExpiredError
657
654
 
658
655
  @classmethod
659
656
  async def new(
660
657
  cls, request: Request, account: Account = None, is_refresh=False, **kwargs
661
658
  ):
662
- authentication_session = await AuthenticationSession.create(
659
+ authentication_session = await cls.create(
663
660
  **kwargs,
664
661
  bearer=account,
665
662
  ip=get_ip(request),
@@ -692,7 +689,7 @@ class Role(BaseModel):
692
689
  permissions: str = fields.CharField(max_length=255, null=True)
693
690
 
694
691
  def validate(self) -> None:
695
- raise NotImplementedError()
692
+ raise NotImplementedError
696
693
 
697
694
  @property
698
695
  def json(self) -> dict:
sanic_security/utils.py CHANGED
@@ -1,13 +1,12 @@
1
1
  import datetime
2
2
  import random
3
- import string
3
+ from string import ascii_uppercase, digits
4
4
 
5
5
  from argon2 import PasswordHasher
6
6
  from captcha.audio import AudioCaptcha
7
7
  from captcha.image import ImageCaptcha
8
8
  from sanic.request import Request
9
9
  from sanic.response import json as sanic_json, HTTPResponse
10
- from secure import Secure
11
10
 
12
11
  from sanic_security.configuration import config
13
12
 
@@ -38,7 +37,6 @@ image_generator = ImageCaptcha(
38
37
  )
39
38
  audio_generator = AudioCaptcha(voicedir=config.CAPTCHA_VOICE)
40
39
  password_hasher = PasswordHasher()
41
- secure_headers = Secure.with_default_headers()
42
40
 
43
41
 
44
42
  def get_ip(request: Request) -> str:
@@ -54,35 +52,33 @@ def get_ip(request: Request) -> str:
54
52
  return request.remote_addr or request.ip
55
53
 
56
54
 
57
- def get_code() -> str:
55
+ def get_code(digits_only: bool = False) -> str:
58
56
  """
59
57
  Generates random code to be used for verification.
60
58
 
59
+ Args:
60
+ digits_only: Determines if code should only contain digits.
61
+
61
62
  Returns:
62
63
  code
63
64
  """
64
65
  return "".join(
65
- random.choice(string.ascii_uppercase + string.digits) for _ in range(6)
66
+ random.choice(("" if digits_only else ascii_uppercase) + digits)
67
+ for _ in range(6)
66
68
  )
67
69
 
68
70
 
69
- def json(
70
- message: str, data, status_code: int = 200
71
- ) -> HTTPResponse: # May be causing fixture error bc of json property
71
+ def is_expired(date):
72
72
  """
73
- A preformatted Sanic json response.
73
+ Checks if current date has surpassed the date passed into the function.
74
74
 
75
75
  Args:
76
- message (int): Message describing data or relaying human-readable information.
77
- data (Any): Raw information to be used by client.
78
- status_code (int): HTTP response code.
76
+ date: The date being checked for expiration.
79
77
 
80
78
  Returns:
81
- json
79
+ is_expired
82
80
  """
83
- return sanic_json(
84
- {"message": message, "code": status_code, "data": data}, status=status_code
85
- )
81
+ return date and datetime.datetime.now(datetime.timezone.utc) >= date
86
82
 
87
83
 
88
84
  def get_expiration_date(seconds: int) -> datetime.datetime:
@@ -100,3 +96,22 @@ def get_expiration_date(seconds: int) -> datetime.datetime:
100
96
  if seconds > 0
101
97
  else None
102
98
  )
99
+
100
+
101
+ def json(
102
+ message: str, data, status_code: int = 200
103
+ ) -> HTTPResponse: # May be causing fixture error bc of json property
104
+ """
105
+ A preformatted Sanic json response.
106
+
107
+ Args:
108
+ message (int): Message describing data or relaying human-readable information.
109
+ data (Any): Raw information to be used by client.
110
+ status_code (int): HTTP response code.
111
+
112
+ Returns:
113
+ json
114
+ """
115
+ return sanic_json(
116
+ {"message": message, "code": status_code, "data": data}, status=status_code
117
+ )
@@ -164,7 +164,7 @@ async def verify_account(request: Request) -> TwoStepSession:
164
164
  """
165
165
  two_step_session = await TwoStepSession.decode(request)
166
166
  if two_step_session.bearer.verified:
167
- raise VerifiedError()
167
+ raise VerifiedError
168
168
  two_step_session.validate()
169
169
  try:
170
170
  await two_step_session.check_code(request.form.get("code"))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sanic-security
3
- Version: 1.14.1
3
+ Version: 1.15.1
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,7 +20,6 @@ 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
24
23
  Provides-Extra: dev
25
24
  Requires-Dist: httpx; extra == "dev"
26
25
  Requires-Dist: black; extra == "dev"
@@ -149,11 +148,11 @@ You can load environment variables with a different prefix via `config.load_envi
149
148
  | **SESSION_DOMAIN** | None | The Domain attribute of session cookies. |
150
149
  | **SESSION_ENCODING_ALGORITHM** | HS256 | The algorithm used to encode and decode session JWT's. |
151
150
  | **SESSION_PREFIX** | tkn | Prefix attached to the beginning of session cookies. |
152
- | **MAX_CHALLENGE_ATTEMPTS** | 5 | The maximum amount of session challenge attempts allowed. |
153
- | **CAPTCHA_SESSION_EXPIRATION** | 60 | The amount of seconds till captcha session expiration on creation. Setting to 0 will disable expiration. |
151
+ | **MAX_CHALLENGE_ATTEMPTS** | 3 | The maximum amount of session challenge attempts allowed. |
152
+ | **CAPTCHA_SESSION_EXPIRATION** | 180 | The amount of seconds till captcha session expiration on creation. Setting to 0 will disable expiration. |
154
153
  | **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
154
  | **CAPTCHA_VOICE** | captcha-voice/ | The directory of the voice library being used for audio captcha generation. |
156
- | **TWO_STEP_SESSION_EXPIRATION** | 200 | The amount of seconds till two-step session expiration on creation. Setting to 0 will disable expiration. |
155
+ | **TWO_STEP_SESSION_EXPIRATION** | 300 | The amount of seconds till two-step session expiration on creation. Setting to 0 will disable expiration. |
157
156
  | **AUTHENTICATION_SESSION_EXPIRATION** | 86400 | The amount of seconds till authentication session expiration on creation. Setting to 0 will disable expiration. |
158
157
  | **AUTHENTICATION_REFRESH_EXPIRATION** | 604800 | The amount of seconds till authentication refresh expiration. Setting to 0 will disable refresh mechanism. |
159
158
  | **ALLOW_LOGIN_WITH_USERNAME** | False | Allows login via username; unique constraint is disabled when set to false. |
@@ -196,8 +195,7 @@ async def on_register(request):
196
195
  account.email, two_step_session.code # Code = 24KF19
197
196
  ) # Custom method for emailing verification code.
198
197
  response = json(
199
- "Registration successful! Email verification required.",
200
- two_step_session.json,
198
+ "Registration successful! Email verification required.", account.json
201
199
  )
202
200
  two_step_session.encode(response)
203
201
  return response
@@ -215,7 +213,9 @@ Verifies the client's account via two-step session code.
215
213
  @app.put("api/security/verify")
216
214
  async def on_verify(request):
217
215
  two_step_session = await verify_account(request)
218
- return json("You have verified your account and may login!", two_step_session.json)
216
+ return json(
217
+ "You have verified your account and may login!", two_step_session.bearer.json
218
+ )
219
219
  ```
220
220
 
221
221
  * Login (With two-factor authentication)
@@ -237,7 +237,7 @@ async def on_login(request):
237
237
  ) # Custom method for emailing verification code.
238
238
  response = json(
239
239
  "Login successful! Two-factor authentication required.",
240
- authentication_session.json,
240
+ authentication_session.bearer.json,
241
241
  )
242
242
  authentication_session.encode(response)
243
243
  two_step_session.encode(response)
@@ -260,7 +260,7 @@ async def on_two_factor_authentication(request):
260
260
  authentication_session = await fulfill_second_factor(request)
261
261
  response = json(
262
262
  "Authentication session second-factor fulfilled! You are now authenticated.",
263
- authentication_session.json,
263
+ authentication_session.bearer.json,
264
264
  )
265
265
  return response
266
266
  ```
@@ -0,0 +1,16 @@
1
+ sanic_security/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ sanic_security/authentication.py,sha256=3Pit1B4PN_MRvwAlhYPHl_6DGD9JVpdPjgjiyKQJanM,13145
3
+ sanic_security/authorization.py,sha256=jAxfDT9cHN_zpMKcA3oYFZ5Eu2KItnMJZ7oPcqmMwrw,7537
4
+ sanic_security/configuration.py,sha256=_E66ts5g9t_XHW9ZAnr48rWVcZmGNu_DWGDxm_AVVWE,5681
5
+ sanic_security/exceptions.py,sha256=9zISLyAvP6qN8sNR8e5qxKP__FA4NLIXCun_fEKndOw,5297
6
+ sanic_security/models.py,sha256=TytuQTjfKslPr893-mCYSzsRK7gfJtXmDz656iCCM0k,22530
7
+ sanic_security/utils.py,sha256=Il5MjFzVe975yx_CV2HV_LVQYl2W3XYDRGtCG5CQA8Q,3531
8
+ sanic_security/verification.py,sha256=9bi8-NZ8GE3rcuELZ63yh18zDg8RxvxGPkhAu5SzLn0,8692
9
+ sanic_security/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ sanic_security/test/server.py,sha256=RjL9Kfvkfqpm5TXWwFQKKa0J4hfTKgwI6U0s_TAKO8w,11984
11
+ sanic_security/test/tests.py,sha256=bW5fHJfsCrg8eBmcSqVMLm0R5XRL1ou-XJJRgz09GOE,21993
12
+ sanic_security-1.15.1.dist-info/LICENSE,sha256=sXlJs9_mG-dCkPfWsDnuzydJWagS82E2gYtkVH9enHA,1100
13
+ sanic_security-1.15.1.dist-info/METADATA,sha256=EINxdSgG_LLkPu3QqXo1CMe_-GTQk5QDhO3b3909quI,23247
14
+ sanic_security-1.15.1.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
15
+ sanic_security-1.15.1.dist-info/top_level.txt,sha256=ZybkhHXSjfzhmv8XeqLvnNmLmv21Z0oPX6Ep4DJN8b0,15
16
+ sanic_security-1.15.1.dist-info/RECORD,,
@@ -1,16 +0,0 @@
1
- sanic_security/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- sanic_security/authentication.py,sha256=ksHa4E82kNXNRKGSqeO28xfQWdF2pJDxUaaQy9snKwU,13299
3
- sanic_security/authorization.py,sha256=QvLsaOWu3te0Y75tqChrEXQP5CR92nsRU7LXiUWy5cw,7541
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=olmpP2AwXKILRVRnDf7AMRJuK5Fs_i5ESHXSH94A-Yk,8694
9
- sanic_security/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- sanic_security/test/server.py,sha256=RjL9Kfvkfqpm5TXWwFQKKa0J4hfTKgwI6U0s_TAKO8w,11984
11
- sanic_security/test/tests.py,sha256=bW5fHJfsCrg8eBmcSqVMLm0R5XRL1ou-XJJRgz09GOE,21993
12
- sanic_security-1.14.1.dist-info/LICENSE,sha256=sXlJs9_mG-dCkPfWsDnuzydJWagS82E2gYtkVH9enHA,1100
13
- sanic_security-1.14.1.dist-info/METADATA,sha256=It9-aEffADsbk5GJGO0SEG2DUd1wTq7FRl64l5oMcdA,23259
14
- sanic_security-1.14.1.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
15
- sanic_security-1.14.1.dist-info/top_level.txt,sha256=ZybkhHXSjfzhmv8XeqLvnNmLmv21Z0oPX6Ep4DJN8b0,15
16
- sanic_security-1.14.1.dist-info/RECORD,,