sanic-security 1.11.7__py3-none-any.whl → 1.12.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sanic_security/authentication.py +144 -140
- sanic_security/authorization.py +50 -42
- sanic_security/configuration.py +26 -20
- sanic_security/exceptions.py +48 -22
- sanic_security/models.py +114 -64
- sanic_security/test/server.py +78 -26
- sanic_security/test/tests.py +82 -15
- sanic_security/utils.py +24 -20
- sanic_security/verification.py +20 -19
- sanic_security-1.12.1.dist-info/LICENSE +21 -0
- {sanic_security-1.11.7.dist-info → sanic_security-1.12.1.dist-info}/METADATA +622 -591
- sanic_security-1.12.1.dist-info/RECORD +16 -0
- {sanic_security-1.11.7.dist-info → sanic_security-1.12.1.dist-info}/WHEEL +2 -1
- sanic_security-1.12.1.dist-info/top_level.txt +1 -0
- sanic_security-1.11.7.dist-info/LICENSE +0 -661
- sanic_security-1.11.7.dist-info/RECORD +0 -15
sanic_security/exceptions.py
CHANGED
@@ -3,21 +3,25 @@ from sanic.exceptions import SanicException
|
|
3
3
|
from sanic_security.utils import json
|
4
4
|
|
5
5
|
"""
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
6
|
+
Copyright (c) 2020-present Nicholas Aidan Stewart
|
7
|
+
|
8
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
9
|
+
of this software and associated documentation files (the "Software"), to deal
|
10
|
+
in the Software without restriction, including without limitation the rights
|
11
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
12
|
+
copies of the Software, and to permit persons to whom the Software is
|
13
|
+
furnished to do so, subject to the following conditions:
|
14
|
+
|
15
|
+
The above copyright notice and this permission notice shall be included in all
|
16
|
+
copies or substantial portions of the Software.
|
17
|
+
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
19
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
20
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
21
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
22
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
23
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
24
|
+
SOFTWARE.
|
21
25
|
"""
|
22
26
|
|
23
27
|
|
@@ -53,7 +57,16 @@ class DeletedError(SecurityError):
|
|
53
57
|
"""
|
54
58
|
|
55
59
|
def __init__(self, message):
|
56
|
-
super().__init__(message,
|
60
|
+
super().__init__(message, 404)
|
61
|
+
|
62
|
+
|
63
|
+
class CredentialsError(SecurityError):
|
64
|
+
"""
|
65
|
+
Raised when credentials are invalid.
|
66
|
+
"""
|
67
|
+
|
68
|
+
def __init__(self, message, code=400):
|
69
|
+
super().__init__(message, code)
|
57
70
|
|
58
71
|
|
59
72
|
class AccountError(SecurityError):
|
@@ -115,7 +128,11 @@ class DeactivatedError(SessionError):
|
|
115
128
|
Raised when session is deactivated.
|
116
129
|
"""
|
117
130
|
|
118
|
-
def __init__(
|
131
|
+
def __init__(
|
132
|
+
self,
|
133
|
+
message: str = "Session has been deactivated or refreshed.",
|
134
|
+
code: int = 401,
|
135
|
+
):
|
119
136
|
super().__init__(message, code)
|
120
137
|
|
121
138
|
|
@@ -125,7 +142,16 @@ class ExpiredError(SessionError):
|
|
125
142
|
"""
|
126
143
|
|
127
144
|
def __init__(self):
|
128
|
-
super().__init__("Session has expired")
|
145
|
+
super().__init__("Session has expired.")
|
146
|
+
|
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)
|
129
155
|
|
130
156
|
|
131
157
|
class SecondFactorRequiredError(SessionError):
|
@@ -173,10 +199,10 @@ class AuthorizationError(SecurityError):
|
|
173
199
|
super().__init__(message, 403)
|
174
200
|
|
175
201
|
|
176
|
-
class
|
202
|
+
class AnonymousError(AuthorizationError):
|
177
203
|
"""
|
178
|
-
Raised when
|
204
|
+
Raised when attempting to authorize an anonymous user.
|
179
205
|
"""
|
180
206
|
|
181
|
-
def __init__(self
|
182
|
-
super().__init__(
|
207
|
+
def __init__(self):
|
208
|
+
super().__init__("Session is anonymous.")
|
sanic_security/models.py
CHANGED
@@ -16,21 +16,25 @@ from sanic_security.exceptions import *
|
|
16
16
|
from sanic_security.utils import get_ip, get_code, get_expiration_date
|
17
17
|
|
18
18
|
"""
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
19
|
+
Copyright (c) 2020-present Nicholas Aidan Stewart
|
20
|
+
|
21
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
22
|
+
of this software and associated documentation files (the "Software"), to deal
|
23
|
+
in the Software without restriction, including without limitation the rights
|
24
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
25
|
+
copies of the Software, and to permit persons to whom the Software is
|
26
|
+
furnished to do so, subject to the following conditions:
|
27
|
+
|
28
|
+
The above copyright notice and this permission notice shall be included in all
|
29
|
+
copies or substantial portions of the Software.
|
30
|
+
|
31
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
32
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
33
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
34
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
35
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
36
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
37
|
+
SOFTWARE.
|
34
38
|
"""
|
35
39
|
|
36
40
|
|
@@ -109,19 +113,6 @@ class Account(BaseModel):
|
|
109
113
|
"models.Role", through="account_role"
|
110
114
|
)
|
111
115
|
|
112
|
-
@property
|
113
|
-
def json(self) -> dict:
|
114
|
-
return {
|
115
|
-
"id": self.id,
|
116
|
-
"date_created": str(self.date_created),
|
117
|
-
"date_updated": str(self.date_updated),
|
118
|
-
"email": self.email,
|
119
|
-
"username": self.username,
|
120
|
-
"phone": self.phone,
|
121
|
-
"disabled": self.disabled,
|
122
|
-
"verified": self.verified,
|
123
|
-
}
|
124
|
-
|
125
116
|
def validate(self) -> None:
|
126
117
|
"""
|
127
118
|
Raises an error with respect to account state.
|
@@ -151,6 +142,19 @@ class Account(BaseModel):
|
|
151
142
|
self.disabled = True
|
152
143
|
await self.save(update_fields=["disabled"])
|
153
144
|
|
145
|
+
@property
|
146
|
+
def json(self) -> dict:
|
147
|
+
return {
|
148
|
+
"id": self.id,
|
149
|
+
"date_created": str(self.date_created),
|
150
|
+
"date_updated": str(self.date_updated),
|
151
|
+
"email": self.email,
|
152
|
+
"username": self.username,
|
153
|
+
"phone": self.phone,
|
154
|
+
"disabled": self.disabled,
|
155
|
+
"verified": self.verified,
|
156
|
+
}
|
157
|
+
|
154
158
|
@staticmethod
|
155
159
|
async def get_via_email(email: str):
|
156
160
|
"""
|
@@ -233,19 +237,6 @@ class Session(BaseModel):
|
|
233
237
|
def __init__(self, **kwargs):
|
234
238
|
super().__init__(**kwargs)
|
235
239
|
|
236
|
-
@property
|
237
|
-
def json(self) -> dict:
|
238
|
-
return {
|
239
|
-
"id": self.id,
|
240
|
-
"date_created": str(self.date_created),
|
241
|
-
"date_updated": str(self.date_updated),
|
242
|
-
"expiration_date": str(self.expiration_date),
|
243
|
-
"bearer": self.bearer.username
|
244
|
-
if isinstance(self.bearer, Account)
|
245
|
-
else None,
|
246
|
-
"active": self.active,
|
247
|
-
}
|
248
|
-
|
249
240
|
def validate(self) -> None:
|
250
241
|
"""
|
251
242
|
Raises an error with respect to session state.
|
@@ -257,13 +248,13 @@ class Session(BaseModel):
|
|
257
248
|
"""
|
258
249
|
if self.deleted:
|
259
250
|
raise DeletedError("Session has been deleted.")
|
251
|
+
elif not self.active:
|
252
|
+
raise DeactivatedError()
|
260
253
|
elif (
|
261
254
|
self.expiration_date
|
262
255
|
and datetime.datetime.now(datetime.timezone.utc) >= self.expiration_date
|
263
256
|
):
|
264
257
|
raise ExpiredError()
|
265
|
-
elif not self.active:
|
266
|
-
raise DeactivatedError()
|
267
258
|
|
268
259
|
async def deactivate(self):
|
269
260
|
"""
|
@@ -285,23 +276,22 @@ class Session(BaseModel):
|
|
285
276
|
Args:
|
286
277
|
response (HTTPResponse): Sanic response used to store JWT into a cookie on the client.
|
287
278
|
"""
|
288
|
-
payload = {
|
289
|
-
"id": self.id,
|
290
|
-
"date_created": str(self.date_created),
|
291
|
-
"expiration_date": str(self.expiration_date),
|
292
|
-
"ip": self.ip,
|
293
|
-
}
|
294
279
|
cookie = (
|
295
280
|
f"{security_config.SESSION_PREFIX}_{self.__class__.__name__.lower()[:7]}"
|
296
281
|
)
|
297
282
|
encoded_session = jwt.encode(
|
298
|
-
|
283
|
+
{
|
284
|
+
"id": self.id,
|
285
|
+
"date_created": str(self.date_created),
|
286
|
+
"expiration_date": str(self.expiration_date),
|
287
|
+
"ip": self.ip,
|
288
|
+
},
|
289
|
+
security_config.SECRET,
|
290
|
+
security_config.SESSION_ENCODING_ALGORITHM,
|
299
291
|
)
|
300
|
-
if isinstance(encoded_session, bytes):
|
301
|
-
encoded_session = encoded_session.decode()
|
302
292
|
response.cookies.add_cookie(
|
303
293
|
cookie,
|
304
|
-
encoded_session,
|
294
|
+
str(encoded_session),
|
305
295
|
httponly=security_config.SESSION_HTTPONLY,
|
306
296
|
samesite=security_config.SESSION_SAMESITE,
|
307
297
|
secure=security_config.SESSION_SECURE,
|
@@ -311,6 +301,29 @@ class Session(BaseModel):
|
|
311
301
|
if security_config.SESSION_DOMAIN:
|
312
302
|
response.cookies.get_cookie(cookie).domain = security_config.SESSION_DOMAIN
|
313
303
|
|
304
|
+
@property
|
305
|
+
def json(self) -> dict:
|
306
|
+
return {
|
307
|
+
"id": self.id,
|
308
|
+
"date_created": str(self.date_created),
|
309
|
+
"date_updated": str(self.date_updated),
|
310
|
+
"expiration_date": str(self.expiration_date),
|
311
|
+
"bearer": (
|
312
|
+
self.bearer.username if isinstance(self.bearer, Account) else None
|
313
|
+
),
|
314
|
+
"active": self.active,
|
315
|
+
}
|
316
|
+
|
317
|
+
@property
|
318
|
+
def anonymous(self) -> bool:
|
319
|
+
"""
|
320
|
+
Determines if an account is associated with session.
|
321
|
+
|
322
|
+
Returns:
|
323
|
+
anonymous
|
324
|
+
"""
|
325
|
+
return self.bearer is None
|
326
|
+
|
314
327
|
@classmethod
|
315
328
|
async def new(
|
316
329
|
cls,
|
@@ -345,7 +358,7 @@ class Session(BaseModel):
|
|
345
358
|
Raises:
|
346
359
|
NotFoundError
|
347
360
|
"""
|
348
|
-
sessions = await cls.filter(bearer=account).
|
361
|
+
sessions = await cls.filter(bearer=account, deleted=False).all()
|
349
362
|
if not sessions:
|
350
363
|
raise NotFoundError("No sessions associated to account were found.")
|
351
364
|
return sessions
|
@@ -373,9 +386,7 @@ class Session(BaseModel):
|
|
373
386
|
else:
|
374
387
|
return jwt.decode(
|
375
388
|
cookie,
|
376
|
-
security_config.SECRET
|
377
|
-
if not security_config.PUBLIC_SECRET
|
378
|
-
else security_config.PUBLIC_SECRET,
|
389
|
+
security_config.PUBLIC_SECRET or security_config.SECRET,
|
379
390
|
security_config.SESSION_ENCODING_ALGORITHM,
|
380
391
|
)
|
381
392
|
except DecodeError as e:
|
@@ -421,10 +432,6 @@ class VerificationSession(Session):
|
|
421
432
|
attempts: int = fields.IntField(default=0)
|
422
433
|
code: str = fields.CharField(max_length=10, default=get_code, null=True)
|
423
434
|
|
424
|
-
@classmethod
|
425
|
-
async def new(cls, request: Request, account: Account, **kwargs):
|
426
|
-
raise NotImplementedError
|
427
|
-
|
428
435
|
async def check_code(self, request: Request, code: str) -> None:
|
429
436
|
"""
|
430
437
|
Checks if code passed is equivalent to the session code.
|
@@ -453,6 +460,10 @@ class VerificationSession(Session):
|
|
453
460
|
self.active = False
|
454
461
|
await self.save(update_fields=["active"])
|
455
462
|
|
463
|
+
@classmethod
|
464
|
+
async def new(cls, request: Request, account: Account, **kwargs):
|
465
|
+
raise NotImplementedError
|
466
|
+
|
456
467
|
class Meta:
|
457
468
|
abstract = True
|
458
469
|
|
@@ -513,10 +524,14 @@ class AuthenticationSession(Session):
|
|
513
524
|
Used to authenticate and identify a client.
|
514
525
|
|
515
526
|
Attributes:
|
527
|
+
refresh_expiration_date (bool): Date and time the session can no longer be refreshed.
|
516
528
|
requires_second_factor (bool): Determines if session requires a second factor.
|
529
|
+
is_refresh (bool): Will only be true once when instantiated during refresh of expired session.
|
517
530
|
"""
|
518
531
|
|
532
|
+
refresh_expiration_date: datetime.datetime = fields.DatetimeField(null=True)
|
519
533
|
requires_second_factor: bool = fields.BooleanField(default=False)
|
534
|
+
is_refresh: bool = False
|
520
535
|
|
521
536
|
def validate(self) -> None:
|
522
537
|
"""
|
@@ -532,16 +547,51 @@ class AuthenticationSession(Session):
|
|
532
547
|
if self.requires_second_factor:
|
533
548
|
raise SecondFactorRequiredError()
|
534
549
|
|
550
|
+
async def refresh(self, request: Request):
|
551
|
+
"""
|
552
|
+
Refreshes session if expired and within refresh date.
|
553
|
+
|
554
|
+
Raises:
|
555
|
+
DeletedError
|
556
|
+
ExpiredError
|
557
|
+
DeactivatedError
|
558
|
+
SecondFactorRequiredError
|
559
|
+
NotExpiredError
|
560
|
+
|
561
|
+
Returns:
|
562
|
+
session
|
563
|
+
"""
|
564
|
+
try:
|
565
|
+
self.validate()
|
566
|
+
raise NotExpiredError()
|
567
|
+
except ExpiredError as e:
|
568
|
+
if (
|
569
|
+
datetime.datetime.now(datetime.timezone.utc)
|
570
|
+
<= self.refresh_expiration_date
|
571
|
+
):
|
572
|
+
self.active = False
|
573
|
+
await self.save(update_fields=["active"])
|
574
|
+
return await self.new(request, self.bearer, True)
|
575
|
+
else:
|
576
|
+
raise e
|
577
|
+
|
535
578
|
@classmethod
|
536
|
-
async def new(
|
537
|
-
|
579
|
+
async def new(
|
580
|
+
cls, request: Request, account: Account = None, is_refresh=False, **kwargs
|
581
|
+
):
|
582
|
+
authentication_session = await AuthenticationSession.create(
|
538
583
|
**kwargs,
|
539
584
|
bearer=account,
|
540
585
|
ip=get_ip(request),
|
541
586
|
expiration_date=get_expiration_date(
|
542
587
|
security_config.AUTHENTICATION_SESSION_EXPIRATION
|
543
588
|
),
|
589
|
+
refresh_expiration_date=get_expiration_date(
|
590
|
+
security_config.AUTHENTICATION_REFRESH_EXPIRATION
|
591
|
+
),
|
544
592
|
)
|
593
|
+
authentication_session.is_refresh = is_refresh
|
594
|
+
return authentication_session
|
545
595
|
|
546
596
|
class Meta:
|
547
597
|
table = "authentication_session"
|
@@ -554,7 +604,7 @@ class Role(BaseModel):
|
|
554
604
|
Attributes:
|
555
605
|
name (str): Name of the role.
|
556
606
|
description (str): Description of the role.
|
557
|
-
permissions (str): Permissions of the role. Must be separated via comma and in wildcard format (printer:query,
|
607
|
+
permissions (str): Permissions of the role. Must be separated via comma + space and in wildcard format (printer:query, dashboard:info,delete).
|
558
608
|
"""
|
559
609
|
|
560
610
|
name: str = fields.CharField(unique=True, max_length=255)
|
sanic_security/test/server.py
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
import datetime
|
2
|
+
import traceback
|
3
|
+
|
1
4
|
from argon2 import PasswordHasher
|
2
5
|
from sanic import Sanic, text
|
3
6
|
from tortoise.contrib.sanic import register_tortoise
|
@@ -16,7 +19,7 @@ from sanic_security.authorization import (
|
|
16
19
|
check_roles,
|
17
20
|
)
|
18
21
|
from sanic_security.configuration import config as security_config
|
19
|
-
from sanic_security.exceptions import SecurityError
|
22
|
+
from sanic_security.exceptions import SecurityError
|
20
23
|
from sanic_security.models import Account, CaptchaSession, AuthenticationSession
|
21
24
|
from sanic_security.utils import json
|
22
25
|
from sanic_security.verification import (
|
@@ -28,27 +31,34 @@ from sanic_security.verification import (
|
|
28
31
|
)
|
29
32
|
|
30
33
|
"""
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
34
|
+
Copyright (c) 2020-present Nicholas Aidan Stewart
|
35
|
+
|
36
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
37
|
+
of this software and associated documentation files (the "Software"), to deal
|
38
|
+
in the Software without restriction, including without limitation the rights
|
39
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
40
|
+
copies of the Software, and to permit persons to whom the Software is
|
41
|
+
furnished to do so, subject to the following conditions:
|
42
|
+
|
43
|
+
The above copyright notice and this permission notice shall be included in all
|
44
|
+
copies or substantial portions of the Software.
|
45
|
+
|
46
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
47
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
48
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
49
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
50
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
51
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
52
|
+
SOFTWARE.
|
46
53
|
"""
|
47
54
|
|
48
55
|
app = Sanic("sanic-security-test")
|
49
56
|
password_hasher = PasswordHasher()
|
50
57
|
|
51
58
|
|
59
|
+
# TODO: Testing for new functionality.
|
60
|
+
|
61
|
+
|
52
62
|
@app.post("api/test/auth/register")
|
53
63
|
async def on_register(request):
|
54
64
|
"""
|
@@ -100,7 +110,20 @@ async def on_login(request):
|
|
100
110
|
)
|
101
111
|
two_step_session.encode(response)
|
102
112
|
else:
|
103
|
-
response = json("Login successful!", authentication_session.
|
113
|
+
response = json("Login successful!", authentication_session.json)
|
114
|
+
authentication_session.encode(response)
|
115
|
+
return response
|
116
|
+
|
117
|
+
|
118
|
+
@app.post("api/test/auth/login/anon")
|
119
|
+
async def on_login_anonymous(request):
|
120
|
+
"""
|
121
|
+
Login as anonymous user.
|
122
|
+
"""
|
123
|
+
authentication_session = await AuthenticationSession.new(request)
|
124
|
+
response = json(
|
125
|
+
"Anonymous user now associated with session!", authentication_session.json
|
126
|
+
)
|
104
127
|
authentication_session.encode(response)
|
105
128
|
return response
|
106
129
|
|
@@ -125,21 +148,53 @@ async def on_logout(request):
|
|
125
148
|
Logout of currently logged in account.
|
126
149
|
"""
|
127
150
|
authentication_session = await logout(request)
|
128
|
-
response = json("Logout successful!", authentication_session.
|
151
|
+
response = json("Logout successful!", authentication_session.json)
|
129
152
|
return response
|
130
153
|
|
131
154
|
|
132
155
|
@app.post("api/test/auth")
|
133
|
-
@requires_authentication
|
156
|
+
@requires_authentication
|
134
157
|
async def on_authenticate(request):
|
135
158
|
"""
|
136
159
|
Authenticate client session and account.
|
137
160
|
"""
|
138
|
-
|
139
|
-
|
161
|
+
authentication_session = request.ctx.authentication_session
|
162
|
+
response = json(
|
163
|
+
"Authenticated!",
|
164
|
+
{
|
165
|
+
"bearer": (
|
166
|
+
authentication_session.bearer.json
|
167
|
+
if not authentication_session.anonymous
|
168
|
+
else None
|
169
|
+
),
|
170
|
+
"refresh": authentication_session.is_refresh,
|
171
|
+
},
|
172
|
+
)
|
140
173
|
return response
|
141
174
|
|
142
175
|
|
176
|
+
@app.on_response
|
177
|
+
async def authentication_refresh_encoder(request, response):
|
178
|
+
try:
|
179
|
+
authentication_session = request.ctx.authentication_session
|
180
|
+
if authentication_session.is_refresh:
|
181
|
+
authentication_session.encode(response)
|
182
|
+
except AttributeError:
|
183
|
+
pass
|
184
|
+
|
185
|
+
|
186
|
+
@app.post("api/test/auth/expire")
|
187
|
+
@requires_authentication
|
188
|
+
async def on_authentication_expire(request):
|
189
|
+
"""
|
190
|
+
Expire client's session.
|
191
|
+
"""
|
192
|
+
authentication_session = request.ctx.authentication_session
|
193
|
+
authentication_session.expiration_date = datetime.datetime.now(datetime.UTC)
|
194
|
+
await authentication_session.save(update_fields=["expiration_date"])
|
195
|
+
return json("Authentication expired!", authentication_session.json)
|
196
|
+
|
197
|
+
|
143
198
|
@app.post("api/test/auth/associated")
|
144
199
|
@requires_authentication
|
145
200
|
async def on_get_associated_authentication_sessions(request):
|
@@ -242,16 +297,12 @@ async def on_account_creation(request):
|
|
242
297
|
"""
|
243
298
|
Quick account creation.
|
244
299
|
"""
|
245
|
-
if await Account.filter(email=request.form.get("email").lower()).exists():
|
246
|
-
raise CredentialsError("An account with this email already exists.", 409)
|
247
|
-
elif await Account.filter(username=request.form.get("username")).exists():
|
248
|
-
raise CredentialsError("An account with this username already exists.", 409)
|
249
300
|
account = await Account.create(
|
250
301
|
username=request.form.get("username"),
|
251
302
|
email=request.form.get("email").lower(),
|
252
303
|
password=password_hasher.hash("password"),
|
253
304
|
verified=True,
|
254
|
-
|
305
|
+
disabled=False,
|
255
306
|
)
|
256
307
|
response = json("Account creation successful!", account.json)
|
257
308
|
return response
|
@@ -262,6 +313,7 @@ async def on_security_error(request, exception):
|
|
262
313
|
"""
|
263
314
|
Handles security errors with correct response.
|
264
315
|
"""
|
316
|
+
traceback.print_exc()
|
265
317
|
return exception.json
|
266
318
|
|
267
319
|
|