sanic-security 1.12.6__py3-none-any.whl → 1.13.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.
@@ -1,5 +1,6 @@
1
1
  import functools
2
2
  import re
3
+ import warnings
3
4
 
4
5
  from argon2 import PasswordHasher
5
6
  from argon2.exceptions import VerifyMismatchError
@@ -8,14 +9,16 @@ from sanic.log import logger
8
9
  from sanic.request import Request
9
10
  from tortoise.exceptions import DoesNotExist
10
11
 
11
- from sanic_security.configuration import config as security_config
12
+ from sanic_security.configuration import config as security_config, DEFAULT_CONFIG
12
13
  from sanic_security.exceptions import (
13
14
  CredentialsError,
14
15
  DeactivatedError,
15
16
  SecondFactorFulfilledError,
16
17
  ExpiredError,
18
+ AuditWarning,
17
19
  )
18
20
  from sanic_security.models import Account, AuthenticationSession, Role, TwoStepSession
21
+ from sanic_security.utils import get_ip
19
22
 
20
23
  """
21
24
  Copyright (c) 2020-present Nicholas Aidan Stewart
@@ -75,11 +78,10 @@ async def register(
75
78
  raise CredentialsError(
76
79
  "An account with this phone number may already exist.", 409
77
80
  )
78
- validate_password(request.form.get("password"))
79
81
  account = await Account.create(
80
82
  email=email_lower,
81
83
  username=request.form.get("username"),
82
- password=password_hasher.hash(request.form.get("password")),
84
+ password=password_hasher.hash(validate_password(request.form.get("password"))),
83
85
  phone=request.form.get("phone"),
84
86
  verified=verified,
85
87
  disabled=disabled,
@@ -122,10 +124,17 @@ async def login(
122
124
  account.password = password_hasher.hash(password)
123
125
  await account.save(update_fields=["password"])
124
126
  account.validate()
125
- return await AuthenticationSession.new(
127
+ authentication_session = await AuthenticationSession.new(
126
128
  request, account, requires_second_factor=require_second_factor
127
129
  )
130
+ logger.info(
131
+ f"Client {get_ip(request)} has logged into account {account.id} with authentication session {authentication_session.id}."
132
+ )
133
+ return authentication_session
128
134
  except VerifyMismatchError:
135
+ logger.warning(
136
+ f"Client {get_ip(request)} has failed to log into account {account.id}."
137
+ )
129
138
  raise CredentialsError("Incorrect password.", 401)
130
139
 
131
140
 
@@ -149,6 +158,10 @@ async def logout(request: Request) -> AuthenticationSession:
149
158
  raise DeactivatedError("Already logged out.", 403)
150
159
  authentication_session.active = False
151
160
  await authentication_session.save(update_fields=["active"])
161
+ logger.info(
162
+ f"Client {get_ip(request)} has logged out {"anonymously" if authentication_session.anonymous
163
+ else f"of account {authentication_session.bearer.id}"} with authentication session {authentication_session.id}."
164
+ )
152
165
  return authentication_session
153
166
 
154
167
 
@@ -177,7 +190,7 @@ async def fulfill_second_factor(request: Request) -> AuthenticationSession:
177
190
  raise SecondFactorFulfilledError()
178
191
  two_step_session = await TwoStepSession.decode(request)
179
192
  two_step_session.validate()
180
- await two_step_session.check_code(request, request.form.get("code"))
193
+ await two_step_session.check_code(request.form.get("code"))
181
194
  authentication_session.requires_second_factor = False
182
195
  await authentication_session.save(update_fields=["requires_second_factor"])
183
196
  return authentication_session
@@ -249,59 +262,6 @@ def requires_authentication(arg=None):
249
262
  return decorator(arg) if callable(arg) else decorator
250
263
 
251
264
 
252
- def attach_refresh_encoder(app: Sanic):
253
- """
254
- Automatically encodes the new/refreshed session returned during authentication when client's current session expires.
255
-
256
- Args:
257
- app: (Sanic): The main Sanic application instance.
258
- """
259
-
260
- @app.on_response
261
- async def refresh_encoder_middleware(request, response):
262
- if hasattr(request.ctx, "authentication_session"):
263
- authentication_session = request.ctx.authentication_session
264
- if authentication_session.is_refresh:
265
- authentication_session.encode(response)
266
-
267
-
268
- def create_initial_admin_account(app: Sanic) -> None:
269
- """
270
- Creates the initial admin account that can be logged into and has complete authoritative access.
271
-
272
- Args:
273
- app (Sanic): The main Sanic application instance.
274
- """
275
-
276
- @app.listener("before_server_start")
277
- async def create(app, loop):
278
- try:
279
- role = await Role.filter(name="Head Admin").get()
280
- except DoesNotExist:
281
- role = await Role.create(
282
- description="Has root abilities, assign sparingly.",
283
- permissions="*:*",
284
- name="Head Admin",
285
- )
286
- try:
287
- account = await Account.filter(
288
- email=security_config.INITIAL_ADMIN_EMAIL
289
- ).get()
290
- await account.fetch_related("roles")
291
- if role not in account.roles:
292
- await account.roles.add(role)
293
- logger.warning("Initial admin account role has been reinstated.")
294
- except DoesNotExist:
295
- account = await Account.create(
296
- username="Head-Admin",
297
- email=security_config.INITIAL_ADMIN_EMAIL,
298
- password=password_hasher.hash(security_config.INITIAL_ADMIN_PASSWORD),
299
- verified=True,
300
- )
301
- await account.roles.add(role)
302
- logger.info("Initial admin account created.")
303
-
304
-
305
265
  def validate_email(email: str) -> str:
306
266
  """
307
267
  Validates email format.
@@ -380,3 +340,77 @@ def validate_password(password: str) -> str:
380
340
  400,
381
341
  )
382
342
  return password
343
+
344
+
345
+ def initialize_security(app: Sanic, create_root=True) -> None:
346
+ """
347
+ Audits configuration, creates root administrator account, and attaches refresh encoder middleware.
348
+
349
+ Args:
350
+ app (Sanic): The main Sanic application instance.
351
+ create_root (bool): Determines root account creation on initialization.
352
+ """
353
+
354
+ @app.on_response
355
+ async def refresh_encoder_middleware(request, response):
356
+ if hasattr(request.ctx, "authentication_session"):
357
+ authentication_session = request.ctx.authentication_session
358
+ if authentication_session.is_refresh:
359
+ authentication_session.encode(response)
360
+
361
+ @app.listener("before_server_start")
362
+ async def audit_configuration(app, loop):
363
+ if security_config.SECRET == DEFAULT_CONFIG["SECRET"]:
364
+ warnings.warn("Secret should be changed from default.", AuditWarning)
365
+ if not security_config.SESSION_HTTPONLY:
366
+ warnings.warn("HttpOnly should be enabled.", AuditWarning)
367
+ if not security_config.SESSION_SECURE:
368
+ warnings.warn("Secure should be enabled.", AuditWarning)
369
+ if security_config.SESSION_SAMESITE.lower() == "none":
370
+ warnings.warn("SameSite should not be set to none.", AuditWarning)
371
+ if (
372
+ create_root
373
+ and security_config.INITIAL_ADMIN_EMAIL
374
+ == DEFAULT_CONFIG["INITIAL_ADMIN_EMAIL"]
375
+ ):
376
+ warnings.warn(
377
+ "Initial admin email should be changed from default.", AuditWarning
378
+ )
379
+ if (
380
+ create_root
381
+ and security_config.INITIAL_ADMIN_PASSWORD
382
+ == DEFAULT_CONFIG["INITIAL_ADMIN_PASSWORD"]
383
+ ):
384
+ warnings.warn(
385
+ "Initial admin password should be changed from default.", AuditWarning
386
+ )
387
+
388
+ @app.listener("before_server_start")
389
+ async def create_root_account(app, loop):
390
+ if not create_root:
391
+ return
392
+ try:
393
+ role = await Role.filter(name="Root").get()
394
+ except DoesNotExist:
395
+ role = await Role.create(
396
+ description="Has administrator abilities, assign sparingly.",
397
+ permissions="*:*",
398
+ name="Root",
399
+ )
400
+ try:
401
+ account = await Account.filter(
402
+ email=security_config.INITIAL_ADMIN_EMAIL
403
+ ).get()
404
+ await account.fetch_related("roles")
405
+ if role not in account.roles:
406
+ await account.roles.add(role)
407
+ logger.warning("Initial admin account role has been reinstated.")
408
+ except DoesNotExist:
409
+ account = await Account.create(
410
+ username="Root",
411
+ email=security_config.INITIAL_ADMIN_EMAIL,
412
+ password=password_hasher.hash(security_config.INITIAL_ADMIN_PASSWORD),
413
+ verified=True,
414
+ )
415
+ await account.roles.add(role)
416
+ logger.info("Initial admin account created.")
@@ -1,12 +1,14 @@
1
1
  import functools
2
2
  from fnmatch import fnmatch
3
3
 
4
+ from sanic.log import logger
4
5
  from sanic.request import Request
5
6
  from tortoise.exceptions import DoesNotExist
6
7
 
7
8
  from sanic_security.authentication import authenticate
8
9
  from sanic_security.exceptions import AuthorizationError, AnonymousError
9
10
  from sanic_security.models import Role, Account, AuthenticationSession
11
+ from sanic_security.utils import get_ip
10
12
 
11
13
  """
12
14
  Copyright (c) 2020-present Nicholas Aidan Stewart
@@ -65,6 +67,10 @@ async def check_permissions(
65
67
  ):
66
68
  if fnmatch(required_permission, role_permission):
67
69
  return authentication_session
70
+ logger.warning(
71
+ f"Client {get_ip(request)} with account {authentication_session.bearer.id} has insufficient permissions for "
72
+ "attempted action."
73
+ )
68
74
  raise AuthorizationError("Insufficient permissions required for this action.")
69
75
 
70
76
 
@@ -97,6 +103,10 @@ async def check_roles(request: Request, *required_roles: str) -> AuthenticationS
97
103
  for role in roles:
98
104
  if role.name in required_roles:
99
105
  return authentication_session
106
+ logger.warning(
107
+ f"Client {get_ip(request)} with account {authentication_session.bearer.id} has insufficient roles for "
108
+ "attempted action."
109
+ )
100
110
  raise AuthorizationError("Insufficient roles required for this action.")
101
111
 
102
112
 
@@ -24,15 +24,14 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
24
  SOFTWARE.
25
25
  """
26
26
 
27
-
28
27
  DEFAULT_CONFIG = {
29
28
  "SECRET": "This is a big secret. Shhhhh",
30
29
  "PUBLIC_SECRET": None,
31
- "SESSION_SAMESITE": "strict",
30
+ "SESSION_SAMESITE": "Strict",
32
31
  "SESSION_SECURE": True,
33
32
  "SESSION_HTTPONLY": True,
34
33
  "SESSION_DOMAIN": None,
35
- "SESSION_PREFIX": "token",
34
+ "SESSION_PREFIX": "tkn",
36
35
  "SESSION_ENCODING_ALGORITHM": "HS256",
37
36
  "MAX_CHALLENGE_ATTEMPTS": 5,
38
37
  "CAPTCHA_SESSION_EXPIRATION": 60,
@@ -206,3 +206,9 @@ class AnonymousError(AuthorizationError):
206
206
 
207
207
  def __init__(self):
208
208
  super().__init__("Session is anonymous.")
209
+
210
+
211
+ class AuditWarning(Warning):
212
+ """
213
+ Raised when configuration is invalid.
214
+ """
sanic_security/models.py CHANGED
@@ -6,7 +6,6 @@ from typing import Union
6
6
  import jwt
7
7
  from captcha.image import ImageCaptcha
8
8
  from jwt import DecodeError
9
- from sanic.log import logger
10
9
  from sanic.request import Request
11
10
  from sanic.response import HTTPResponse, raw
12
11
  from tortoise import fields, Model
@@ -493,13 +492,12 @@ class VerificationSession(Session):
493
492
  attempts: int = fields.IntField(default=0)
494
493
  code: str = fields.CharField(max_length=10, default=get_code, null=True)
495
494
 
496
- async def check_code(self, request: Request, code: str) -> None:
495
+ async def check_code(self, code: str) -> None:
497
496
  """
498
497
  Checks if code passed is equivalent to the session code.
499
498
 
500
499
  Args:
501
500
  code (str): Code being cross-checked with session code.
502
- request (Request): Sanic request parameter.
503
501
 
504
502
  Raises:
505
503
  ChallengeError
@@ -513,13 +511,9 @@ class VerificationSession(Session):
513
511
  "Your code does not match verification session code."
514
512
  )
515
513
  else:
516
- logger.warning(
517
- f"Client ({get_ip(request)}) has maxed out on session challenge attempts"
518
- )
519
514
  raise MaxedOutChallengeError()
520
515
  else:
521
- self.active = False
522
- await self.save(update_fields=["active"])
516
+ await self.deactivate()
523
517
 
524
518
  @classmethod
525
519
  async def new(cls, request: Request, account: Account, **kwargs):
@@ -530,9 +524,7 @@ class VerificationSession(Session):
530
524
 
531
525
 
532
526
  class TwoStepSession(VerificationSession):
533
- """
534
- Validates a client using a code sent via email or text.
535
- """
527
+ """Validates a client using a code sent via email or text."""
536
528
 
537
529
  @classmethod
538
530
  async def new(cls, request: Request, account: Account, **kwargs):
@@ -550,9 +542,7 @@ class TwoStepSession(VerificationSession):
550
542
 
551
543
 
552
544
  class CaptchaSession(VerificationSession):
553
- """
554
- Validates a client with a captcha challenge.
555
- """
545
+ """Validates a client with a captcha challenge."""
556
546
 
557
547
  @classmethod
558
548
  async def new(cls, request: Request, **kwargs):
@@ -612,6 +602,9 @@ class AuthenticationSession(Session):
612
602
  """
613
603
  Refreshes session if expired and within refresh date.
614
604
 
605
+ Args:
606
+ request (Request): Sanic request parameter.
607
+
615
608
  Raises:
616
609
  DeletedError
617
610
  ExpiredError
@@ -10,9 +10,8 @@ from sanic_security.authentication import (
10
10
  register,
11
11
  requires_authentication,
12
12
  logout,
13
- create_initial_admin_account,
14
13
  fulfill_second_factor,
15
- attach_refresh_encoder,
14
+ initialize_security,
16
15
  )
17
16
  from sanic_security.authorization import (
18
17
  assign_role,
@@ -62,9 +61,7 @@ password_hasher = PasswordHasher()
62
61
 
63
62
  @app.post("api/test/auth/register")
64
63
  async def on_register(request):
65
- """
66
- Register an account with email and password.
67
- """
64
+ """Register an account with email and password."""
68
65
  account = await register(
69
66
  request,
70
67
  verified=request.form.get("verified") == "true",
@@ -83,9 +80,7 @@ async def on_register(request):
83
80
 
84
81
  @app.post("api/test/auth/verify")
85
82
  async def on_verify(request):
86
- """
87
- Verifies client account.
88
- """
83
+ """Verifies client account."""
89
84
  two_step_session = await verify_account(request)
90
85
  return json(
91
86
  "You have verified your account and may login!", two_step_session.bearer.json
@@ -94,9 +89,7 @@ async def on_verify(request):
94
89
 
95
90
  @app.post("api/test/auth/login")
96
91
  async def on_login(request):
97
- """
98
- Login to an account with an email and password.
99
- """
92
+ """Login to an account with an email and password."""
100
93
  two_factor_authentication = request.args.get("two-factor-authentication") == "true"
101
94
  authentication_session = await login(
102
95
  request, require_second_factor=two_factor_authentication
@@ -118,9 +111,7 @@ async def on_login(request):
118
111
 
119
112
  @app.post("api/test/auth/login/anon")
120
113
  async def on_login_anonymous(request):
121
- """
122
- Login as anonymous user.
123
- """
114
+ """Login as anonymous user."""
124
115
  authentication_session = await AuthenticationSession.new(request)
125
116
  response = json(
126
117
  "Anonymous user now associated with session!", authentication_session.json
@@ -131,9 +122,7 @@ async def on_login_anonymous(request):
131
122
 
132
123
  @app.post("api/test/auth/validate-2fa")
133
124
  async def on_two_factor_authentication(request):
134
- """
135
- Fulfills client authentication session's second factor requirement.
136
- """
125
+ """Fulfills client authentication session's second factor requirement."""
137
126
  authentication_session = await fulfill_second_factor(request)
138
127
  response = json(
139
128
  "Authentication session second-factor fulfilled! You are now authenticated.",
@@ -145,9 +134,7 @@ async def on_two_factor_authentication(request):
145
134
 
146
135
  @app.post("api/test/auth/logout")
147
136
  async def on_logout(request):
148
- """
149
- Logout of currently logged in account.
150
- """
137
+ """Logout of currently logged in account."""
151
138
  authentication_session = await logout(request)
152
139
  response = json("Logout successful!", authentication_session.json)
153
140
  return response
@@ -156,9 +143,7 @@ async def on_logout(request):
156
143
  @app.post("api/test/auth")
157
144
  @requires_authentication
158
145
  async def on_authenticate(request):
159
- """
160
- Authenticate client session and account.
161
- """
146
+ """Authenticate client session and account."""
162
147
  authentication_session = request.ctx.authentication_session
163
148
  response = json(
164
149
  "Authenticated!",
@@ -177,9 +162,7 @@ async def on_authenticate(request):
177
162
  @app.post("api/test/auth/expire")
178
163
  @requires_authentication
179
164
  async def on_authentication_expire(request):
180
- """
181
- Expire client's session.
182
- """
165
+ """Expire client's session."""
183
166
  authentication_session = request.ctx.authentication_session
184
167
  authentication_session.expiration_date = datetime.datetime.now(datetime.UTC)
185
168
  await authentication_session.save(update_fields=["expiration_date"])
@@ -189,9 +172,7 @@ async def on_authentication_expire(request):
189
172
  @app.post("api/test/auth/associated")
190
173
  @requires_authentication
191
174
  async def on_get_associated_authentication_sessions(request):
192
- """
193
- Retrieves authentication sessions associated with logged in account.
194
- """
175
+ """Retrieves authentication sessions associated with logged in account."""
195
176
  authentication_sessions = await AuthenticationSession.get_associated(
196
177
  request.ctx.authentication_session.bearer
197
178
  )
@@ -203,9 +184,7 @@ async def on_get_associated_authentication_sessions(request):
203
184
 
204
185
  @app.get("api/test/capt/request")
205
186
  async def on_captcha_request(request):
206
- """
207
- Request captcha with solution in response.
208
- """
187
+ """Request captcha with solution in response."""
209
188
  captcha_session = await request_captcha(request)
210
189
  response = json("Captcha request successful!", captcha_session.code)
211
190
  captcha_session.encode(response)
@@ -214,9 +193,7 @@ async def on_captcha_request(request):
214
193
 
215
194
  @app.get("api/test/capt/image")
216
195
  async def on_captcha_image(request):
217
- """
218
- Request captcha image.
219
- """
196
+ """Request captcha image."""
220
197
  captcha_session = await CaptchaSession.decode(request)
221
198
  response = captcha_session.get_image()
222
199
  captcha_session.encode(response)
@@ -226,17 +203,13 @@ async def on_captcha_image(request):
226
203
  @app.post("api/test/capt")
227
204
  @requires_captcha
228
205
  async def on_captcha_attempt(request):
229
- """
230
- Attempt captcha challenge.
231
- """
206
+ """Attempt captcha challenge."""
232
207
  return json("Captcha attempt successful!", request.ctx.captcha_session.json)
233
208
 
234
209
 
235
210
  @app.post("api/test/two-step/request")
236
211
  async def on_request_verification(request):
237
- """
238
- Request two-step verification with code in the response.
239
- """
212
+ """Request two-step verification with code in the response."""
240
213
  two_step_session = await request_two_step_verification(request)
241
214
  response = json("Verification request successful!", two_step_session.code)
242
215
  two_step_session.encode(response)
@@ -246,9 +219,7 @@ async def on_request_verification(request):
246
219
  @app.post("api/test/two-step")
247
220
  @requires_two_step_verification
248
221
  async def on_verification_attempt(request):
249
- """
250
- Attempt two-step verification challenge.
251
- """
222
+ """Attempt two-step verification challenge."""
252
223
  return json(
253
224
  "Two step verification attempt successful!", request.ctx.two_step_session.json
254
225
  )
@@ -257,9 +228,7 @@ async def on_verification_attempt(request):
257
228
  @app.post("api/test/auth/roles")
258
229
  @requires_authentication
259
230
  async def on_authorization(request):
260
- """
261
- Check if client is authorized with sufficient roles and permissions.
262
- """
231
+ """Check if client is authorized with sufficient roles and permissions."""
263
232
  await check_roles(request, request.form.get("role"))
264
233
  if request.form.get("permissions_required"):
265
234
  await check_permissions(
@@ -271,9 +240,7 @@ async def on_authorization(request):
271
240
  @app.post("api/test/auth/roles/assign")
272
241
  @requires_authentication
273
242
  async def on_role_assign(request):
274
- """
275
- Assign authenticated account a role.
276
- """
243
+ """Assign authenticated account a role."""
277
244
  await assign_role(
278
245
  request.form.get("name"),
279
246
  request.ctx.authentication_session.bearer,
@@ -285,9 +252,7 @@ async def on_role_assign(request):
285
252
 
286
253
  @app.post("api/test/account")
287
254
  async def on_account_creation(request):
288
- """
289
- Quick account creation.
290
- """
255
+ """Quick account creation."""
291
256
  account = await Account.create(
292
257
  username=request.form.get("username"),
293
258
  email=request.form.get("email").lower(),
@@ -301,9 +266,7 @@ async def on_account_creation(request):
301
266
 
302
267
  @app.exception(SecurityError)
303
268
  async def on_security_error(request, exception):
304
- """
305
- Handles security errors with correct response.
306
- """
269
+ """Handles security errors with correct response."""
307
270
  traceback.print_exc()
308
271
  return exception.json
309
272
 
@@ -344,7 +307,6 @@ register_tortoise(
344
307
  modules={"models": ["sanic_security.models"]},
345
308
  generate_schemas=True,
346
309
  )
347
- attach_refresh_encoder(app)
348
- create_initial_admin_account(app)
310
+ initialize_security(app, True)
349
311
  if __name__ == "__main__":
350
- app.run(host="127.0.0.1", port=8000, workers=1, debug=True)
312
+ app.run(host="127.0.0.1", port=8000, workers=1)
@@ -30,9 +30,7 @@ SOFTWARE.
30
30
 
31
31
 
32
32
  class RegistrationTest(TestCase):
33
- """
34
- Registration tests.
35
- """
33
+ """Registration tests."""
36
34
 
37
35
  def setUp(self):
38
36
  self.client = httpx.Client()
@@ -62,9 +60,7 @@ class RegistrationTest(TestCase):
62
60
  return registration_response
63
61
 
64
62
  def test_registration(self):
65
- """
66
- Account registration and login.
67
- """
63
+ """Account registration and login."""
68
64
  registration_response = self.register(
69
65
  "account_registration@register.test",
70
66
  "account_registration",
@@ -80,9 +76,7 @@ class RegistrationTest(TestCase):
80
76
  assert login_response.status_code == 200, login_response.text
81
77
 
82
78
  def test_invalid_registration(self):
83
- """
84
- Registration with an intentionally invalid email, username, and phone.
85
- """
79
+ """Registration with an intentionally invalid email, username, and phone."""
86
80
  invalid_email_registration_response = self.register(
87
81
  "invalid_register.test", "invalid_register", False, True
88
82
  )
@@ -112,9 +106,7 @@ class RegistrationTest(TestCase):
112
106
  ), too_many_characters_registration_response.text
113
107
 
114
108
  def test_registration_disabled(self):
115
- """
116
- Registration and login with a disabled account.
117
- """
109
+ """Registration and login with a disabled account."""
118
110
  registration_response = self.register(
119
111
  "disabled@register.test", "disabled", True, True
120
112
  )
@@ -126,9 +118,7 @@ class RegistrationTest(TestCase):
126
118
  assert "DisabledError" in login_response.text, login_response.text
127
119
 
128
120
  def test_registration_unverified(self):
129
- """
130
- Registration and login with an unverified account.
131
- """
121
+ """Registration and login with an unverified account."""
132
122
  registration_response = self.register(
133
123
  "unverified@register.test", "unverified", False, False
134
124
  )
@@ -140,9 +130,7 @@ class RegistrationTest(TestCase):
140
130
  assert "UnverifiedError" in login_response.text, login_response.text
141
131
 
142
132
  def test_registration_unverified_disabled(self):
143
- """
144
- Registration and login with an unverified and disabled account.
145
- """
133
+ """Registration and login with an unverified and disabled account."""
146
134
  registration_response = self.register(
147
135
  "unverified_disabled@register.test", "unverified_disabled", True, False
148
136
  )
@@ -155,9 +143,7 @@ class RegistrationTest(TestCase):
155
143
 
156
144
 
157
145
  class LoginTest(TestCase):
158
- """
159
- Login tests.
160
- """
146
+ """Login tests."""
161
147
 
162
148
  def setUp(self):
163
149
  self.client = httpx.Client()
@@ -166,9 +152,7 @@ class LoginTest(TestCase):
166
152
  self.client.close()
167
153
 
168
154
  def test_login(self):
169
- """
170
- Login with an email and password.
171
- """
155
+ """Login with an email and password."""
172
156
  self.client.post(
173
157
  "http://127.0.0.1:8000/api/test/account",
174
158
  data={"email": "email_pass@login.test", "username": "email_pass"},
@@ -184,9 +168,7 @@ class LoginTest(TestCase):
184
168
  assert authenticate_response.status_code == 200, authenticate_response.text
185
169
 
186
170
  def test_login_with_username(self):
187
- """
188
- Login with a username instead of an email and password.
189
- """
171
+ """Login with a username instead of an email and password."""
190
172
  self.client.post(
191
173
  "http://127.0.0.1:8000/api/test/account",
192
174
  data={"email": "user_pass@login.test", "username": "user_pass"},
@@ -202,9 +184,7 @@ class LoginTest(TestCase):
202
184
  assert authenticate_response.status_code == 200, authenticate_response.text
203
185
 
204
186
  def test_invalid_login(self):
205
- """
206
- Login with an intentionally incorrect password and into a non existent account.
207
- """
187
+ """Login with an intentionally incorrect password and into a non-existent account."""
208
188
  self.client.post(
209
189
  "http://127.0.0.1:8000/api/test/account",
210
190
  data={"email": "incorrect_pass@login.test", "username": "incorrect_pass"},
@@ -225,9 +205,7 @@ class LoginTest(TestCase):
225
205
  ), unavailable_account_login_response
226
206
 
227
207
  def test_logout(self):
228
- """
229
- Logout of logged in account and attempt to authenticate.
230
- """
208
+ """Logout of logged in account and attempt to authenticate."""
231
209
  self.client.post(
232
210
  "http://127.0.0.1:8000/api/test/account",
233
211
  data={"email": "logout@login.test", "username": "logout"},
@@ -244,9 +222,7 @@ class LoginTest(TestCase):
244
222
  assert authenticate_response.status_code == 401, authenticate_response.text
245
223
 
246
224
  def test_initial_admin_login(self):
247
- """
248
- Initial admin account login and authorization.
249
- """
225
+ """Initial admin account login and authorization."""
250
226
  login_response = self.client.post(
251
227
  "http://127.0.0.1:8000/api/test/auth/login",
252
228
  auth=("admin@login.test", "admin123"),
@@ -255,7 +231,7 @@ class LoginTest(TestCase):
255
231
  permitted_authorization_response = self.client.post(
256
232
  "http://127.0.0.1:8000/api/test/auth/roles",
257
233
  data={
258
- "role": "Head Admin",
234
+ "role": "Root",
259
235
  "permissions_required": "perm1:create,add, perm2:*",
260
236
  },
261
237
  )
@@ -264,9 +240,7 @@ class LoginTest(TestCase):
264
240
  ), permitted_authorization_response.text
265
241
 
266
242
  def test_two_factor_login(self):
267
- """
268
- Test login with two-factor authentication requirement.
269
- """
243
+ """Test login with two-factor authentication requirement."""
270
244
  self.client.post(
271
245
  "http://127.0.0.1:8000/api/test/account",
272
246
  data={"email": "two-factor@login.test", "username": "two-factor"},
@@ -295,9 +269,7 @@ class LoginTest(TestCase):
295
269
  assert authenticate_response.status_code == 200, authenticate_response.text
296
270
 
297
271
  def test_anonymous_login(self):
298
- """
299
- Test login of anonymous user.
300
- """
272
+ """Test login of anonymous user."""
301
273
  anon_login_response = self.client.post(
302
274
  "http://127.0.0.1:8000/api/test/auth/login/anon"
303
275
  )
@@ -311,9 +283,7 @@ class LoginTest(TestCase):
311
283
 
312
284
 
313
285
  class VerificationTest(TestCase):
314
- """
315
- Two-step verification and captcha tests.
316
- """
286
+ """Two-step verification and captcha tests."""
317
287
 
318
288
  def setUp(self):
319
289
  self.client = httpx.Client()
@@ -322,9 +292,7 @@ class VerificationTest(TestCase):
322
292
  self.client.close()
323
293
 
324
294
  def test_captcha(self):
325
- """
326
- Captcha request and attempt.
327
- """
295
+ """Captcha request and attempt."""
328
296
  captcha_request_response = self.client.get(
329
297
  "http://127.0.0.1:8000/api/test/capt/request"
330
298
  )
@@ -344,9 +312,7 @@ class VerificationTest(TestCase):
344
312
  ), captcha_attempt_response.text
345
313
 
346
314
  def test_two_step_verification(self):
347
- """
348
- Two-step verification request and attempt.
349
- """
315
+ """Two-step verification request and attempt."""
350
316
  self.client.post(
351
317
  "http://127.0.0.1:8000/api/test/account",
352
318
  data={"email": "two_step@verification.test", "username": "two_step"},
@@ -382,9 +348,7 @@ class VerificationTest(TestCase):
382
348
  ), two_step_verification_no_email_request_response.text
383
349
 
384
350
  def test_account_verification(self):
385
- """
386
- Account registration and verification process with successful login.
387
- """
351
+ """Account registration and verification process with successful login."""
388
352
  registration_response = self.client.post(
389
353
  "http://127.0.0.1:8000/api/test/auth/register",
390
354
  data={
@@ -404,9 +368,7 @@ class VerificationTest(TestCase):
404
368
 
405
369
 
406
370
  class AuthorizationTest(TestCase):
407
- """
408
- Role and permissions based authorization tests.
409
- """
371
+ """Role and permissions based authorization tests."""
410
372
 
411
373
  def setUp(self):
412
374
  self.client = httpx.Client()
@@ -415,9 +377,7 @@ class AuthorizationTest(TestCase):
415
377
  self.client.close()
416
378
 
417
379
  def test_permissions_authorization(self):
418
- """
419
- Authorization with permissions.
420
- """
380
+ """Authorization with permissions."""
421
381
  self.client.post(
422
382
  "http://127.0.0.1:8000/api/test/account",
423
383
  data={"email": "permissions@authorization.test", "username": "permissions"},
@@ -455,9 +415,7 @@ class AuthorizationTest(TestCase):
455
415
  ), prohibited_authorization_response.text
456
416
 
457
417
  def test_roles_authorization(self):
458
- """
459
- Authorization with roles.
460
- """
418
+ """Authorization with roles."""
461
419
  self.client.post(
462
420
  "http://127.0.0.1:8000/api/test/account",
463
421
  data={"email": "roles@authorization.test", "username": "roles"},
@@ -488,6 +446,7 @@ class AuthorizationTest(TestCase):
488
446
  ), prohibited_authorization_response.text
489
447
 
490
448
  def test_anonymous_authorization(self):
449
+ """Authorization with anonymous client."""
491
450
  anon_login_response = self.client.post(
492
451
  "http://127.0.0.1:8000/api/test/auth/login/anon"
493
452
  )
@@ -506,9 +465,7 @@ class AuthorizationTest(TestCase):
506
465
 
507
466
 
508
467
  class MiscTest(TestCase):
509
- """
510
- Miscellaneous tests that cannot be categorized.
511
- """
468
+ """Miscellaneous tests that cannot be categorized."""
512
469
 
513
470
  def setUp(self):
514
471
  self.client = httpx.Client()
@@ -517,18 +474,14 @@ class MiscTest(TestCase):
517
474
  self.client.close()
518
475
 
519
476
  def test_environment_variable_load(self):
520
- """
521
- Config loads environment variables.
522
- """
477
+ """Config loads environment variables."""
523
478
  os.environ["SANIC_SECURITY_SECRET"] = "test-secret"
524
479
  security_config = Config()
525
480
  security_config.load_environment_variables()
526
481
  assert security_config.SECRET == "test-secret"
527
482
 
528
483
  def test_get_associated_sessions(self):
529
- """
530
- Retrieve sessions associated to logged in account.
531
- """
484
+ """Retrieve sessions associated to logged in account."""
532
485
  self.client.post(
533
486
  "http://127.0.0.1:8000/api/test/account",
534
487
  data={
@@ -549,9 +502,7 @@ class MiscTest(TestCase):
549
502
  ), retrieve_associated_response.text
550
503
 
551
504
  def test_authentication_refresh(self):
552
- """
553
- Test automatic authentication refresh.
554
- """
505
+ """Test automatic authentication refresh."""
555
506
  self.client.post(
556
507
  "http://127.0.0.1:8000/api/test/account",
557
508
  data={
@@ -89,7 +89,7 @@ async def two_step_verification(request: Request) -> TwoStepSession:
89
89
  two_step_session = await TwoStepSession.decode(request)
90
90
  two_step_session.validate()
91
91
  two_step_session.bearer.validate()
92
- await two_step_session.check_code(request, request.form.get("code"))
92
+ await two_step_session.check_code(request.form.get("code"))
93
93
  return two_step_session
94
94
 
95
95
 
@@ -153,7 +153,7 @@ async def verify_account(request: Request) -> TwoStepSession:
153
153
  if two_step_session.bearer.verified:
154
154
  raise VerifiedError()
155
155
  two_step_session.validate()
156
- await two_step_session.check_code(request, request.form.get("code"))
156
+ await two_step_session.check_code(request.form.get("code"))
157
157
  two_step_session.bearer.verified = True
158
158
  await two_step_session.bearer.save(update_fields=["verified"])
159
159
  return two_step_session
@@ -197,7 +197,7 @@ async def captcha(request: Request) -> CaptchaSession:
197
197
  """
198
198
  captcha_session = await CaptchaSession.decode(request)
199
199
  captcha_session.validate()
200
- await captcha_session.check_code(request, request.form.get("captcha"))
200
+ await captcha_session.check_code(request.form.get("captcha"))
201
201
  return captcha_session
202
202
 
203
203
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sanic-security
3
- Version: 1.12.6
3
+ Version: 1.13.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/
@@ -64,7 +64,7 @@ Requires-Dist: cryptography; extra == "dev"
64
64
  * [Usage](#usage)
65
65
  * [Authentication](#authentication)
66
66
  * [Captcha](#captcha)
67
- * [Two Step Verification](#two-step-verification)
67
+ * [Two-Step Verification](#two-step-verification)
68
68
  * [Authorization](#authorization)
69
69
  * [Testing](#testing)
70
70
  * [Tortoise](#tortoise)
@@ -145,12 +145,12 @@ You can load environment variables with a different prefix via `config.load_envi
145
145
  |---------------------------------------|------------------------------|----------------------------------------------------------------------------------------------------------------------------------|
146
146
  | **SECRET** | This is a big secret. Shhhhh | The secret used for generating and signing JWTs. This should be a string unique to your application. Keep it safe. |
147
147
  | **PUBLIC_SECRET** | None | The secret used for verifying and decoding JWTs and can be publicly shared. This should be a string unique to your application. |
148
- | **SESSION_SAMESITE** | strict | The SameSite attribute of session cookies. |
148
+ | **SESSION_SAMESITE** | Strict | The SameSite attribute of session cookies. |
149
149
  | **SESSION_SECURE** | True | The Secure attribute of session cookies. |
150
150
  | **SESSION_HTTPONLY** | True | The HttpOnly attribute of session cookies. HIGHLY recommended that you do not turn this off, unless you know what you are doing. |
151
151
  | **SESSION_DOMAIN** | None | The Domain attribute of session cookies. |
152
152
  | **SESSION_ENCODING_ALGORITHM** | HS256 | The algorithm used to encode and decode session JWT's. |
153
- | **SESSION_PREFIX** | token | Prefix attached to the beginning of session cookies. |
153
+ | **SESSION_PREFIX** | tkn | Prefix attached to the beginning of session cookies. |
154
154
  | **MAX_CHALLENGE_ATTEMPTS** | 5 | The maximum amount of session challenge attempts allowed. |
155
155
  | **CAPTCHA_SESSION_EXPIRATION** | 60 | The amount of seconds till captcha session expiration on creation. Setting to 0 will disable expiration. |
156
156
  | **CAPTCHA_FONT** | captcha-font.ttf | The file path to the font being used for captcha generation. |
@@ -166,19 +166,16 @@ You can load environment variables with a different prefix via `config.load_envi
166
166
  Sanic Security's authentication and verification functionality is session based. A new session will be created for the user after the user logs in or requests some form of verification (two-step, captcha). The session data is then encoded into a JWT and stored on a cookie on the user’s browser. The session cookie is then sent
167
167
  along with every subsequent request. The server can then compare the session stored on the cookie against the session information stored in the database to verify user’s identity and send a response with the corresponding state.
168
168
 
169
- The tables in the below examples represent example [request form-data](https://sanicframework.org/en/guide/basics/request.html#form).
170
-
171
- ## Authentication
172
-
173
- * Initial Administrator Account
174
-
175
- Creates initial admin account, you should modify its credentials in config!
176
-
169
+ * Initialize sanic-security as follows:
177
170
  ```python
178
- create_initial_admin_account(app)
171
+ initialize_security(app)
179
172
  if __name__ == "__main__":
180
- app.run(host="127.0.0.1", port=8000)
173
+ app.run(host="127.0.0.1", port=8000, workers=1, debug=True)
181
174
  ```
175
+
176
+ The tables in the below examples represent example [request form-data](https://sanicframework.org/en/guide/basics/request.html#form).
177
+
178
+ ## Authentication
182
179
 
183
180
  * Registration (With two-step account verification)
184
181
 
@@ -321,17 +318,6 @@ async def on_authenticate(request):
321
318
  return response
322
319
  ```
323
320
 
324
- * Refresh Encoder
325
-
326
- A new/refreshed session is returned during authentication when the client's current session expires and it
327
- requires encoding. This should be be done automatically via middleware.
328
-
329
- ```python
330
- attach_refresh_encoder(app)
331
- if __name__ == "__main__":
332
- app.run(host="127.0.0.1", port=8000)
333
- ```
334
-
335
321
  ## Captcha
336
322
 
337
323
  A pre-existing font for captcha challenges is included in the Sanic Security repository. You may set your own font by
@@ -0,0 +1,16 @@
1
+ sanic_security/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ sanic_security/authentication.py,sha256=ZY4WJDUXbwDdaH_2Ovc9gkR1jCgw52yB9enYp_1LypM,14334
3
+ sanic_security/authorization.py,sha256=XbRrnsx-Yqpiemf3bn_djIYIe1khdnfToB7DsBheLtk,7338
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=v3tJyL420HEdZXqJCq9uPPSivuYXuQNtqf9QC9wF0TU,22274
7
+ sanic_security/utils.py,sha256=XAUNalcTi53qTz0D8xiDyDyRlq7Z7ffNBzUONJZqe90,2705
8
+ sanic_security/verification.py,sha256=ebT7QaytHAsw-IKA13W9wyCoqoBAYKgmFA1QJ80N2bE,7476
9
+ sanic_security/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ sanic_security/test/server.py,sha256=DmwP5dBs2Sq2qk4UZgWbAqzm96YXvLGI9jWeqzluy_c,12082
11
+ sanic_security/test/tests.py,sha256=bW5fHJfsCrg8eBmcSqVMLm0R5XRL1ou-XJJRgz09GOE,21993
12
+ sanic_security-1.13.0.dist-info/LICENSE,sha256=sXlJs9_mG-dCkPfWsDnuzydJWagS82E2gYtkVH9enHA,1100
13
+ sanic_security-1.13.0.dist-info/METADATA,sha256=VeYHXoqKPrbNAfub2NW3RT78Rt1WSUBCKj2gVJrHplE,23000
14
+ sanic_security-1.13.0.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
15
+ sanic_security-1.13.0.dist-info/top_level.txt,sha256=ZybkhHXSjfzhmv8XeqLvnNmLmv21Z0oPX6Ep4DJN8b0,15
16
+ sanic_security-1.13.0.dist-info/RECORD,,
@@ -1,16 +0,0 @@
1
- sanic_security/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- sanic_security/authentication.py,sha256=obMKNnJXleeBGXqmsm1y5jFNI-FrW9krdO5SD6yOstE,12598
3
- sanic_security/authorization.py,sha256=aQztMiZG9LDctr_C6QEzO5qScwbxpiLk96XVxwdCChM,6921
4
- sanic_security/configuration.py,sha256=p44nTSrBQQSJZYN6qJEod_Ettf90rRNlmPxmNzxqQ9A,5514
5
- sanic_security/exceptions.py,sha256=MTPF4tm_68Nmf_z06RHH_6DTiC_CNiLER1jzEoW1dFk,5398
6
- sanic_security/models.py,sha256=nj5iYHzPZzdLs5dc3j6kdeScSk1SASizfK58Sa5YN8E,22527
7
- sanic_security/utils.py,sha256=XAUNalcTi53qTz0D8xiDyDyRlq7Z7ffNBzUONJZqe90,2705
8
- sanic_security/verification.py,sha256=vrxYborEOBKEirOHczul9WYub5j6T2ldXE1gsoA8iyY,7503
9
- sanic_security/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- sanic_security/test/server.py,sha256=pwqsDS81joMdxIynivaNPCCMamv9qzAjknfZ01ZxQHc,12380
11
- sanic_security/test/tests.py,sha256=6TUp5GVYIR27qCzwIw2qt7DvW7ohxj-seYpnpeMbuno,22407
12
- sanic_security-1.12.6.dist-info/LICENSE,sha256=sXlJs9_mG-dCkPfWsDnuzydJWagS82E2gYtkVH9enHA,1100
13
- sanic_security-1.12.6.dist-info/METADATA,sha256=aiKkOtkYiexSjoB4uysSQwxAVqRGAQnultZKvx5srAs,23382
14
- sanic_security-1.12.6.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
15
- sanic_security-1.12.6.dist-info/top_level.txt,sha256=ZybkhHXSjfzhmv8XeqLvnNmLmv21Z0oPX6Ep4DJN8b0,15
16
- sanic_security-1.12.6.dist-info/RECORD,,