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.
- sanic_security/authentication.py +92 -58
- sanic_security/authorization.py +10 -0
- sanic_security/configuration.py +2 -3
- sanic_security/exceptions.py +6 -0
- sanic_security/models.py +7 -14
- sanic_security/test/server.py +21 -59
- sanic_security/test/tests.py +27 -76
- sanic_security/verification.py +3 -3
- {sanic_security-1.12.6.dist-info → sanic_security-1.13.0.dist-info}/METADATA +11 -25
- sanic_security-1.13.0.dist-info/RECORD +16 -0
- sanic_security-1.12.6.dist-info/RECORD +0 -16
- {sanic_security-1.12.6.dist-info → sanic_security-1.13.0.dist-info}/LICENSE +0 -0
- {sanic_security-1.12.6.dist-info → sanic_security-1.13.0.dist-info}/WHEEL +0 -0
- {sanic_security-1.12.6.dist-info → sanic_security-1.13.0.dist-info}/top_level.txt +0 -0
sanic_security/authentication.py
CHANGED
@@ -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
|
-
|
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
|
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.")
|
sanic_security/authorization.py
CHANGED
@@ -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
|
|
sanic_security/configuration.py
CHANGED
@@ -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": "
|
30
|
+
"SESSION_SAMESITE": "Strict",
|
32
31
|
"SESSION_SECURE": True,
|
33
32
|
"SESSION_HTTPONLY": True,
|
34
33
|
"SESSION_DOMAIN": None,
|
35
|
-
"SESSION_PREFIX": "
|
34
|
+
"SESSION_PREFIX": "tkn",
|
36
35
|
"SESSION_ENCODING_ALGORITHM": "HS256",
|
37
36
|
"MAX_CHALLENGE_ATTEMPTS": 5,
|
38
37
|
"CAPTCHA_SESSION_EXPIRATION": 60,
|
sanic_security/exceptions.py
CHANGED
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,
|
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.
|
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
|
sanic_security/test/server.py
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
312
|
+
app.run(host="127.0.0.1", port=8000, workers=1)
|
sanic_security/test/tests.py
CHANGED
@@ -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": "
|
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={
|
sanic_security/verification.py
CHANGED
@@ -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
|
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
|
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
|
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.
|
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
|
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** |
|
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** |
|
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
|
-
|
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
|
-
|
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,,
|
File without changes
|
File without changes
|
File without changes
|