sanic-security 1.14.0__py3-none-any.whl → 1.15.0__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,20 +284,13 @@ 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.
291
291
  create_root (bool): Determines root account creation on initialization.
292
292
  """
293
293
 
294
- @app.on_response
295
- async def response_handler_middleware(request, response):
296
- if hasattr(request.ctx, "session"):
297
- secure_headers.set_headers(response)
298
- if request.ctx.session.is_refresh:
299
- request.ctx.session.encode(response)
300
-
301
294
  @app.listener("before_server_start")
302
295
  async def audit_configuration(app, loop):
303
296
  if security_config.SECRET == DEFAULT_CONFIG["SECRET"]:
@@ -361,3 +354,10 @@ def initialize_security(app: Sanic, create_root=True) -> None:
361
354
  )
362
355
  await account.roles.add(role)
363
356
  logger.info("Initial admin account created.")
357
+
358
+ @app.on_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,5 +1,6 @@
1
1
  import base64
2
2
  import datetime
3
+ import logging
3
4
  import re
4
5
  from typing import Union
5
6
 
@@ -19,6 +20,8 @@ from sanic_security.utils import (
19
20
  get_expiration_date,
20
21
  image_generator,
21
22
  audio_generator,
23
+ get_id,
24
+ is_expired,
22
25
  )
23
26
 
24
27
  """
@@ -55,7 +58,7 @@ 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(pk=True, max_length=36, default=get_id)
59
62
  date_created: datetime.datetime = fields.DatetimeField(auto_now_add=True)
60
63
  date_updated: datetime.datetime = fields.DatetimeField(auto_now=True)
61
64
  deleted: bool = fields.BooleanField(default=False)
@@ -67,7 +70,7 @@ class BaseModel(Model):
67
70
  Raises:
68
71
  SecurityError
69
72
  """
70
- raise NotImplementedError()
73
+ raise NotImplementedError
71
74
 
72
75
  @property
73
76
  def json(self) -> dict:
@@ -89,7 +92,7 @@ class BaseModel(Model):
89
92
  }
90
93
 
91
94
  """
92
- raise NotImplementedError()
95
+ raise NotImplementedError
93
96
 
94
97
  class Meta:
95
98
  abstract = True
@@ -148,9 +151,9 @@ class Account(BaseModel):
148
151
  if self.deleted:
149
152
  raise DeletedError("Account has been deleted.")
150
153
  elif not self.verified:
151
- raise UnverifiedError()
154
+ raise UnverifiedError
152
155
  elif self.disabled:
153
- raise DisabledError()
156
+ raise DisabledError
154
157
 
155
158
  async def disable(self):
156
159
  """
@@ -321,12 +324,9 @@ class Session(BaseModel):
321
324
  if self.deleted:
322
325
  raise DeletedError("Session has been deleted.")
323
326
  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()
327
+ raise DeactivatedError
328
+ elif is_expired(self.expiration_date):
329
+ raise ExpiredError
330
330
 
331
331
  async def deactivate(self):
332
332
  """
@@ -416,7 +416,7 @@ class Session(BaseModel):
416
416
  Returns:
417
417
  session
418
418
  """
419
- raise NotImplementedError()
419
+ raise NotImplementedError
420
420
 
421
421
  @classmethod
422
422
  async def get_associated(cls, account: Account):
@@ -517,15 +517,15 @@ class VerificationSession(Session):
517
517
  ChallengeError
518
518
  MaxedOutChallengeError
519
519
  """
520
- if self.code != code.upper():
520
+ if not code or self.code != code.upper():
521
+ self.attempts += 1
521
522
  if self.attempts < security_config.MAX_CHALLENGE_ATTEMPTS:
522
- self.attempts += 1
523
523
  await self.save(update_fields=["attempts"])
524
524
  raise ChallengeError(
525
525
  "Your code does not match verification session code."
526
526
  )
527
527
  else:
528
- raise MaxedOutChallengeError()
528
+ raise MaxedOutChallengeError
529
529
  else:
530
530
  await self.deactivate()
531
531
 
@@ -621,39 +621,30 @@ class AuthenticationSession(Session):
621
621
  """
622
622
  super().validate()
623
623
  if self.requires_second_factor:
624
- raise SecondFactorRequiredError()
624
+ raise SecondFactorRequiredError
625
625
 
626
626
  async def refresh(self, request: Request):
627
627
  """
628
- Refreshes session if expired and within refresh date.
628
+ Refreshes session if within refresh date.
629
629
 
630
630
  Args:
631
631
  request (Request): Sanic request parameter.
632
632
 
633
633
  Raises:
634
- DeletedError
635
634
  ExpiredError
636
- DeactivatedError
637
- SecondFactorRequiredError
638
- NotExpiredError
639
635
 
640
636
  Returns:
641
637
  session
642
638
  """
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
639
+ if not is_expired(self.refresh_expiration_date):
640
+ self.active = False
641
+ await self.save(update_fields=["active"])
642
+ logging.warning(
643
+ f"Client {get_ip(request)} has refreshed authentication session {self.id}."
644
+ )
645
+ return await self.new(request, self.bearer, True)
646
+ else:
647
+ raise ExpiredError
657
648
 
658
649
  @classmethod
659
650
  async def new(
@@ -692,7 +683,7 @@ class Role(BaseModel):
692
683
  permissions: str = fields.CharField(max_length=255, null=True)
693
684
 
694
685
  def validate(self) -> None:
695
- raise NotImplementedError()
686
+ raise NotImplementedError
696
687
 
697
688
  @property
698
689
  def json(self) -> dict:
@@ -128,7 +128,6 @@ async def on_two_factor_authentication(request):
128
128
  "Authentication session second-factor fulfilled! You are now authenticated.",
129
129
  authentication_session.bearer.json,
130
130
  )
131
- authentication_session.encode(response)
132
131
  return response
133
132
 
134
133
 
sanic_security/utils.py CHANGED
@@ -1,13 +1,13 @@
1
1
  import datetime
2
2
  import random
3
3
  import string
4
+ import uuid
4
5
 
5
6
  from argon2 import PasswordHasher
6
7
  from captcha.audio import AudioCaptcha
7
8
  from captcha.image import ImageCaptcha
8
9
  from sanic.request import Request
9
10
  from sanic.response import json as sanic_json, HTTPResponse
10
- from secure import Secure
11
11
 
12
12
  from sanic_security.configuration import config
13
13
 
@@ -38,7 +38,6 @@ image_generator = ImageCaptcha(
38
38
  )
39
39
  audio_generator = AudioCaptcha(voicedir=config.CAPTCHA_VOICE)
40
40
  password_hasher = PasswordHasher()
41
- secure_headers = Secure.with_default_headers()
42
41
 
43
42
 
44
43
  def get_ip(request: Request) -> str:
@@ -66,23 +65,27 @@ def get_code() -> str:
66
65
  )
67
66
 
68
67
 
69
- def json(
70
- message: str, data, status_code: int = 200
71
- ) -> HTTPResponse: # May be causing fixture error bc of json property
68
+ def get_id() -> str:
72
69
  """
73
- A preformatted Sanic json response.
70
+ Generates uuid to be used for primary key.
71
+
72
+ Returns:
73
+ id
74
+ """
75
+ return str(uuid.uuid4())
76
+
77
+
78
+ def is_expired(date):
79
+ """
80
+ Checks if current date has surpassed the date passed into the function.
74
81
 
75
82
  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.
83
+ date: The date being checked for expiration.
79
84
 
80
85
  Returns:
81
- json
86
+ is_expired
82
87
  """
83
- return sanic_json(
84
- {"message": message, "code": status_code, "data": data}, status=status_code
85
- )
88
+ return date and datetime.datetime.now(datetime.timezone.utc) >= date
86
89
 
87
90
 
88
91
  def get_expiration_date(seconds: int) -> datetime.datetime:
@@ -100,3 +103,22 @@ def get_expiration_date(seconds: int) -> datetime.datetime:
100
103
  if seconds > 0
101
104
  else None
102
105
  )
106
+
107
+
108
+ def json(
109
+ message: str, data, status_code: int = 200
110
+ ) -> HTTPResponse: # May be causing fixture error bc of json property
111
+ """
112
+ A preformatted Sanic json response.
113
+
114
+ Args:
115
+ message (int): Message describing data or relaying human-readable information.
116
+ data (Any): Raw information to be used by client.
117
+ status_code (int): HTTP response code.
118
+
119
+ Returns:
120
+ json
121
+ """
122
+ return sanic_json(
123
+ {"message": message, "code": status_code, "data": data}, status=status_code
124
+ )
@@ -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.0
3
+ Version: 1.15.0
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,15 +20,14 @@ 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
- Provides-Extra: crypto
25
- Requires-Dist: cryptography>=3.3.1; extra == "crypto"
26
23
  Provides-Extra: dev
27
24
  Requires-Dist: httpx; extra == "dev"
28
25
  Requires-Dist: black; extra == "dev"
29
26
  Requires-Dist: blacken-docs; extra == "dev"
30
27
  Requires-Dist: pdoc3; extra == "dev"
31
28
  Requires-Dist: cryptography; extra == "dev"
29
+ Provides-Extra: crypto
30
+ Requires-Dist: cryptography>=3.3.1; extra == "crypto"
32
31
 
33
32
  <!-- PROJECT SHIELDS -->
34
33
  <!--
@@ -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
@@ -212,10 +210,12 @@ Verifies the client's account via two-step session code.
212
210
  | **code** | 24KF19 |
213
211
 
214
212
  ```python
215
- @app.post("api/security/verify")
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)
@@ -255,14 +255,13 @@ Fulfills client authentication session's second factor requirement via two-step
255
255
  | **code** | XGED2U |
256
256
 
257
257
  ```python
258
- @app.post("api/security/fulfill-2fa")
258
+ @app.put("api/security/fulfill-2fa")
259
259
  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
- authentication_session.encode(response)
266
265
  return response
267
266
  ```
268
267
 
@@ -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=v4ZnvZonwrgxnXKPHLQYNtEDJAE2Ri0TGEAbLX0VWTo,22429
7
+ sanic_security/utils.py,sha256=Y_qs3UVuOXHnDWelAxNC6VLeB5lMeSv3xRty9xYlHOw,3538
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.0.dist-info/LICENSE,sha256=sXlJs9_mG-dCkPfWsDnuzydJWagS82E2gYtkVH9enHA,1100
13
+ sanic_security-1.15.0.dist-info/METADATA,sha256=7bxyaqAFp0hm6ldrbuux1Dwth8OtXVcffKelbV8zkl8,23247
14
+ sanic_security-1.15.0.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
15
+ sanic_security-1.15.0.dist-info/top_level.txt,sha256=ZybkhHXSjfzhmv8XeqLvnNmLmv21Z0oPX6Ep4DJN8b0,15
16
+ sanic_security-1.15.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.5.0)
2
+ Generator: setuptools (75.6.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,16 +0,0 @@
1
- sanic_security/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- sanic_security/authentication.py,sha256=dq7Yt_1xm9_LSZgMZkyzgcvG46NUQsysEb3s5pgO7BE,13165
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=hLvT2mG1VXeV7nE6r1avoSOTa1qMSS6jL0qTizyCdOY,12029
11
- sanic_security/test/tests.py,sha256=bW5fHJfsCrg8eBmcSqVMLm0R5XRL1ou-XJJRgz09GOE,21993
12
- sanic_security-1.14.0.dist-info/LICENSE,sha256=sXlJs9_mG-dCkPfWsDnuzydJWagS82E2gYtkVH9enHA,1100
13
- sanic_security-1.14.0.dist-info/METADATA,sha256=ynu6jYXbs9s7ZQi8l2Z2Ilm93Yu8JxQukx-swbOcQIc,23306
14
- sanic_security-1.14.0.dist-info/WHEEL,sha256=R06PA3UVYHThwHvxuRWMqaGcr-PuniXahwjmQRFMEkY,91
15
- sanic_security-1.14.0.dist-info/top_level.txt,sha256=ZybkhHXSjfzhmv8XeqLvnNmLmv21Z0oPX6Ep4DJN8b0,15
16
- sanic_security-1.14.0.dist-info/RECORD,,