sanic-security 1.12.3__py3-none-any.whl → 1.12.5__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 +28 -24
- sanic_security/exceptions.py +1 -1
- sanic_security/models.py +61 -5
- sanic_security/test/server.py +2 -8
- sanic_security/test/tests.py +1 -3
- {sanic_security-1.12.3.dist-info → sanic_security-1.12.5.dist-info}/METADATA +32 -35
- sanic_security-1.12.5.dist-info/RECORD +16 -0
- {sanic_security-1.12.3.dist-info → sanic_security-1.12.5.dist-info}/WHEEL +1 -1
- sanic_security-1.12.3.dist-info/RECORD +0 -16
- {sanic_security-1.12.3.dist-info → sanic_security-1.12.5.dist-info}/LICENSE +0 -0
- {sanic_security-1.12.3.dist-info → sanic_security-1.12.5.dist-info}/top_level.txt +0 -0
sanic_security/authentication.py
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
import base64
|
2
1
|
import functools
|
3
2
|
import re
|
4
3
|
|
@@ -11,7 +10,6 @@ from tortoise.exceptions import DoesNotExist
|
|
11
10
|
|
12
11
|
from sanic_security.configuration import config as security_config
|
13
12
|
from sanic_security.exceptions import (
|
14
|
-
NotFoundError,
|
15
13
|
CredentialsError,
|
16
14
|
DeactivatedError,
|
17
15
|
SecondFactorFulfilledError,
|
@@ -90,15 +88,19 @@ async def register(
|
|
90
88
|
|
91
89
|
|
92
90
|
async def login(
|
93
|
-
request: Request,
|
91
|
+
request: Request,
|
92
|
+
account: Account = None,
|
93
|
+
require_second_factor: bool = False,
|
94
|
+
password: str = None,
|
94
95
|
) -> AuthenticationSession:
|
95
96
|
"""
|
96
97
|
Login with email or username (if enabled) and password.
|
97
98
|
|
98
99
|
Args:
|
99
|
-
request (Request): Sanic request parameter
|
100
|
-
account (Account): Account being logged into, overrides
|
100
|
+
request (Request): Sanic request parameter, login credentials are retrieved via the authorization header.
|
101
|
+
account (Account): Account being logged into, overrides account retrieved via email or username.
|
101
102
|
require_second_factor (bool): Determines authentication session second factor requirement on login.
|
103
|
+
password (str): Overrides user's password attempt retrieved via the authorization header.
|
102
104
|
|
103
105
|
Returns:
|
104
106
|
authentication_session
|
@@ -110,24 +112,10 @@ async def login(
|
|
110
112
|
UnverifiedError
|
111
113
|
DisabledError
|
112
114
|
"""
|
113
|
-
if request.headers.get("Authorization"):
|
114
|
-
authorization_type, credentials = request.headers.get("Authorization").split()
|
115
|
-
if authorization_type == "Basic":
|
116
|
-
email_or_username, password = (
|
117
|
-
base64.b64decode(credentials).decode().split(":")
|
118
|
-
)
|
119
|
-
else:
|
120
|
-
raise CredentialsError("Invalid authorization type.")
|
121
|
-
else:
|
122
|
-
raise CredentialsError("Credentials not provided.")
|
123
115
|
if not account:
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
if security_config.ALLOW_LOGIN_WITH_USERNAME:
|
128
|
-
account = await Account.get_via_username(email_or_username)
|
129
|
-
else:
|
130
|
-
raise e
|
116
|
+
account, password = await Account.get_via_header(request)
|
117
|
+
elif not password:
|
118
|
+
raise CredentialsError("Password parameter is empty.")
|
131
119
|
try:
|
132
120
|
password_hasher.verify(account.password, password)
|
133
121
|
if password_hasher.check_needs_rehash(account.password):
|
@@ -253,7 +241,7 @@ def requires_authentication(arg=None):
|
|
253
241
|
def decorator(func):
|
254
242
|
@functools.wraps(func)
|
255
243
|
async def wrapper(request, *args, **kwargs):
|
256
|
-
|
244
|
+
await authenticate(request)
|
257
245
|
return await func(request, *args, **kwargs)
|
258
246
|
|
259
247
|
return wrapper
|
@@ -261,6 +249,22 @@ def requires_authentication(arg=None):
|
|
261
249
|
return decorator(arg) if callable(arg) else decorator
|
262
250
|
|
263
251
|
|
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
|
+
|
264
268
|
def create_initial_admin_account(app: Sanic) -> None:
|
265
269
|
"""
|
266
270
|
Creates the initial admin account that can be logged into and has complete authoritative access.
|
@@ -291,7 +295,7 @@ def create_initial_admin_account(app: Sanic) -> None:
|
|
291
295
|
account = await Account.create(
|
292
296
|
username="Head-Admin",
|
293
297
|
email=security_config.INITIAL_ADMIN_EMAIL,
|
294
|
-
password=
|
298
|
+
password=password_hasher.hash(security_config.INITIAL_ADMIN_PASSWORD),
|
295
299
|
verified=True,
|
296
300
|
)
|
297
301
|
await account.roles.add(role)
|
sanic_security/exceptions.py
CHANGED
sanic_security/models.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import base64
|
1
2
|
import datetime
|
2
3
|
from io import BytesIO
|
3
4
|
from typing import Union
|
@@ -195,6 +196,58 @@ class Account(BaseModel):
|
|
195
196
|
except DoesNotExist:
|
196
197
|
raise NotFoundError("Account with this username does not exist.")
|
197
198
|
|
199
|
+
@staticmethod
|
200
|
+
async def get_via_credential(credential: str):
|
201
|
+
"""
|
202
|
+
Retrieve an account with an email or username.
|
203
|
+
|
204
|
+
Args:
|
205
|
+
credential (str): Email or username associated to account being retrieved.
|
206
|
+
|
207
|
+
Returns:
|
208
|
+
account
|
209
|
+
|
210
|
+
Raises:
|
211
|
+
NotFoundError
|
212
|
+
"""
|
213
|
+
try:
|
214
|
+
account = await Account.get_via_email(credential)
|
215
|
+
except NotFoundError as e:
|
216
|
+
if security_config.ALLOW_LOGIN_WITH_USERNAME:
|
217
|
+
account = await Account.get_via_username(credential)
|
218
|
+
else:
|
219
|
+
raise e
|
220
|
+
return account
|
221
|
+
|
222
|
+
@staticmethod
|
223
|
+
async def get_via_header(request: Request):
|
224
|
+
"""
|
225
|
+
Retrieve the account the client is logging into and client's password attempt via the basic authorization header.
|
226
|
+
|
227
|
+
Args:
|
228
|
+
request (Request): Sanic request parameter.
|
229
|
+
|
230
|
+
Returns:
|
231
|
+
account, password
|
232
|
+
|
233
|
+
Raises:
|
234
|
+
NotFoundError
|
235
|
+
"""
|
236
|
+
if request.headers.get("Authorization"):
|
237
|
+
authorization_type, credentials = request.headers.get(
|
238
|
+
"Authorization"
|
239
|
+
).split()
|
240
|
+
if authorization_type == "Basic":
|
241
|
+
email_or_username, password = (
|
242
|
+
base64.b64decode(credentials).decode().split(":")
|
243
|
+
)
|
244
|
+
account = await Account.get_via_credential(email_or_username)
|
245
|
+
return account, password
|
246
|
+
else:
|
247
|
+
raise CredentialsError("Invalid authorization type.")
|
248
|
+
else:
|
249
|
+
raise CredentialsError("Authorization header not provided.")
|
250
|
+
|
198
251
|
@staticmethod
|
199
252
|
async def get_via_phone(phone: str):
|
200
253
|
"""
|
@@ -297,12 +350,15 @@ class Session(BaseModel):
|
|
297
350
|
secure=security_config.SESSION_SECURE,
|
298
351
|
)
|
299
352
|
if self.expiration_date: # Overrides refresh expiration.
|
300
|
-
|
301
|
-
|
302
|
-
|
353
|
+
response.cookies.get_cookie(cookie).expires = (
|
354
|
+
self.refresh_expiration_date
|
355
|
+
if (
|
356
|
+
hasattr(self, "refresh_expiration_date")
|
357
|
+
and self.refresh_expiration_date
|
303
358
|
)
|
304
|
-
|
305
|
-
|
359
|
+
else self.expiration_date
|
360
|
+
)
|
361
|
+
|
306
362
|
if security_config.SESSION_DOMAIN:
|
307
363
|
response.cookies.get_cookie(cookie).domain = security_config.SESSION_DOMAIN
|
308
364
|
|
sanic_security/test/server.py
CHANGED
@@ -12,6 +12,7 @@ from sanic_security.authentication import (
|
|
12
12
|
logout,
|
13
13
|
create_initial_admin_account,
|
14
14
|
fulfill_second_factor,
|
15
|
+
attach_refresh_encoder,
|
15
16
|
)
|
16
17
|
from sanic_security.authorization import (
|
17
18
|
assign_role,
|
@@ -173,14 +174,6 @@ async def on_authenticate(request):
|
|
173
174
|
return response
|
174
175
|
|
175
176
|
|
176
|
-
@app.on_response
|
177
|
-
async def authentication_refresh_encoder(request, response):
|
178
|
-
if hasattr(request.ctx, "authentication_session"):
|
179
|
-
authentication_session = request.ctx.authentication_session
|
180
|
-
if authentication_session.is_refresh:
|
181
|
-
authentication_session.encode(response)
|
182
|
-
|
183
|
-
|
184
177
|
@app.post("api/test/auth/expire")
|
185
178
|
@requires_authentication
|
186
179
|
async def on_authentication_expire(request):
|
@@ -351,6 +344,7 @@ register_tortoise(
|
|
351
344
|
modules={"models": ["sanic_security.models"]},
|
352
345
|
generate_schemas=True,
|
353
346
|
)
|
347
|
+
attach_refresh_encoder(app)
|
354
348
|
create_initial_admin_account(app)
|
355
349
|
if __name__ == "__main__":
|
356
350
|
app.run(host="127.0.0.1", port=8000, workers=1, debug=True)
|
sanic_security/test/tests.py
CHANGED
@@ -576,6 +576,4 @@ class MiscTest(TestCase):
|
|
576
576
|
authenticate_response = self.client.post(
|
577
577
|
"http://127.0.0.1:8000/api/test/auth",
|
578
578
|
) # Since session refresh handling is complete, it will be returned as a regular session now.
|
579
|
-
assert
|
580
|
-
json.loads(authenticate_response.text)["data"]["refresh"] is False
|
581
|
-
), authenticate_response.text
|
579
|
+
assert authenticate_response.status_code == 200, authenticate_response.text
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: sanic-security
|
3
|
-
Version: 1.12.
|
3
|
+
Version: 1.12.5
|
4
4
|
Summary: An async security library for the Sanic framework.
|
5
5
|
Author-email: Aidan Stewart <na.stewart365@gmail.com>
|
6
6
|
Project-URL: Documentation, https://security.na-stewart.com/
|
@@ -14,20 +14,20 @@ Classifier: Programming Language :: Python
|
|
14
14
|
Requires-Python: >=3.8
|
15
15
|
Description-Content-Type: text/markdown
|
16
16
|
License-File: LICENSE
|
17
|
-
Requires-Dist: tortoise-orm
|
18
|
-
Requires-Dist: pyjwt
|
19
|
-
Requires-Dist: captcha
|
20
|
-
Requires-Dist: pillow
|
21
|
-
Requires-Dist: argon2-cffi
|
22
|
-
Requires-Dist: sanic
|
17
|
+
Requires-Dist: tortoise-orm>=0.17.0
|
18
|
+
Requires-Dist: pyjwt>=1.7.0
|
19
|
+
Requires-Dist: captcha>=0.4
|
20
|
+
Requires-Dist: pillow>=9.5.0
|
21
|
+
Requires-Dist: argon2-cffi>=20.1.0
|
22
|
+
Requires-Dist: sanic>=21.3.0
|
23
23
|
Provides-Extra: crypto
|
24
|
-
Requires-Dist: cryptography
|
24
|
+
Requires-Dist: cryptography>=3.3.1; extra == "crypto"
|
25
25
|
Provides-Extra: dev
|
26
|
-
Requires-Dist: httpx
|
27
|
-
Requires-Dist: black
|
28
|
-
Requires-Dist: blacken-docs
|
29
|
-
Requires-Dist: pdoc3
|
30
|
-
Requires-Dist: cryptography
|
26
|
+
Requires-Dist: httpx; extra == "dev"
|
27
|
+
Requires-Dist: black; extra == "dev"
|
28
|
+
Requires-Dist: blacken-docs; extra == "dev"
|
29
|
+
Requires-Dist: pdoc3; extra == "dev"
|
30
|
+
Requires-Dist: cryptography; extra == "dev"
|
31
31
|
|
32
32
|
<!-- PROJECT SHIELDS -->
|
33
33
|
<!--
|
@@ -84,7 +84,7 @@ Sanic Security is an authentication, authorization, and verification library des
|
|
84
84
|
* Two-step verification
|
85
85
|
* Role based authorization with wildcard permissions
|
86
86
|
|
87
|
-
|
87
|
+
Visit [security.na-stewart.com](https://security.na-stewart.com) for documentation.
|
88
88
|
|
89
89
|
<!-- GETTING STARTED -->
|
90
90
|
## Getting Started
|
@@ -172,13 +172,13 @@ The tables in the below examples represent example [request form-data](https://s
|
|
172
172
|
|
173
173
|
* Initial Administrator Account
|
174
174
|
|
175
|
-
|
175
|
+
Creates initial admin account, you should modify its credentials in config!
|
176
176
|
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
177
|
+
```python
|
178
|
+
create_initial_admin_account(app)
|
179
|
+
if __name__ == "__main__":
|
180
|
+
app.run(host="127.0.0.1", port=8000)
|
181
|
+
```
|
182
182
|
|
183
183
|
* Registration (With two-step account verification)
|
184
184
|
|
@@ -224,11 +224,10 @@ async def on_verify(request):
|
|
224
224
|
|
225
225
|
* Login (With two-factor authentication)
|
226
226
|
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
You can use a username as well as an email for login if `ALLOW_LOGIN_WITH_USERNAME` is true in the config.
|
227
|
+
Credentials are retrieved via header are constructed by first combining the username and the password with a colon
|
228
|
+
(aladdin:opensesame), and then by encoding the resulting string in base64 (YWxhZGRpbjpvcGVuc2VzYW1l).
|
229
|
+
Here is an example authorization header: `Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l`. You can use a username
|
230
|
+
as well as an email for login if `ALLOW_LOGIN_WITH_USERNAME` is true in the config.
|
232
231
|
|
233
232
|
```python
|
234
233
|
@app.post("api/security/login")
|
@@ -249,6 +248,8 @@ async def on_login(request):
|
|
249
248
|
return response
|
250
249
|
```
|
251
250
|
|
251
|
+
If this isn't desired, you can pass an account and password attempt directly into the login method instead.
|
252
|
+
|
252
253
|
* Fulfill Second Factor
|
253
254
|
|
254
255
|
Fulfills client authentication session's second factor requirement via two-step session code.
|
@@ -320,19 +321,15 @@ async def on_authenticate(request):
|
|
320
321
|
return response
|
321
322
|
```
|
322
323
|
|
323
|
-
*
|
324
|
-
|
325
|
-
New/Refreshed session returned if client's session expired during authentication, requires encoding.
|
324
|
+
* Refresh Encoder
|
326
325
|
|
327
|
-
|
326
|
+
A new/refreshed session is returned during authentication if the client's session expired during authentication and
|
327
|
+
requires encoding. Rather than doing so manually, it can be done automatically via middleware.
|
328
328
|
|
329
329
|
```python
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
authentication_session = request.ctx.authentication_session
|
334
|
-
if authentication_session.is_refresh:
|
335
|
-
authentication_session.encode(response)
|
330
|
+
attach_refresh_encoder(app)
|
331
|
+
if __name__ == "__main__":
|
332
|
+
app.run(host="127.0.0.1", port=8000)
|
336
333
|
```
|
337
334
|
|
338
335
|
## Captcha
|
@@ -0,0 +1,16 @@
|
|
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=Zgde7W69ixwv_H8eTs7indO5_U2Jvq62YUpG6ipN768,2629
|
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.5.dist-info/LICENSE,sha256=sXlJs9_mG-dCkPfWsDnuzydJWagS82E2gYtkVH9enHA,1100
|
13
|
+
sanic_security-1.12.5.dist-info/METADATA,sha256=n8CLfkmnR8lcAn9ZO8tMQDC0H_TjWT1U0ITo3JCkWHs,23420
|
14
|
+
sanic_security-1.12.5.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
|
15
|
+
sanic_security-1.12.5.dist-info/top_level.txt,sha256=ZybkhHXSjfzhmv8XeqLvnNmLmv21Z0oPX6Ep4DJN8b0,15
|
16
|
+
sanic_security-1.12.5.dist-info/RECORD,,
|
@@ -1,16 +0,0 @@
|
|
1
|
-
sanic_security/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
sanic_security/authentication.py,sha256=E17jQg1gD06CTRk7l9q8EUzgeAEXn2J0E02Va-QYx9I,12573
|
3
|
-
sanic_security/authorization.py,sha256=aQztMiZG9LDctr_C6QEzO5qScwbxpiLk96XVxwdCChM,6921
|
4
|
-
sanic_security/configuration.py,sha256=p44nTSrBQQSJZYN6qJEod_Ettf90rRNlmPxmNzxqQ9A,5514
|
5
|
-
sanic_security/exceptions.py,sha256=8c3xoQSiIKfSiOQOtw49RG8Qdlc3vZDzqjrEnPad4Ds,5411
|
6
|
-
sanic_security/models.py,sha256=OEvO4xUh_7QCdwfaiKt51T3fmn3MJSrIcM1TszDfqgg,20776
|
7
|
-
sanic_security/utils.py,sha256=Zgde7W69ixwv_H8eTs7indO5_U2Jvq62YUpG6ipN768,2629
|
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=G5q7mzTUxOpKlhbzNbzTZYSWd6g8a0toOFX9qTA_nVg,12631
|
11
|
-
sanic_security/test/tests.py,sha256=Hg40wlZfC-CDZX6lIjeT6uXy-3BJMc4ChJsnCRCBIu8,22459
|
12
|
-
sanic_security-1.12.3.dist-info/LICENSE,sha256=sXlJs9_mG-dCkPfWsDnuzydJWagS82E2gYtkVH9enHA,1100
|
13
|
-
sanic_security-1.12.3.dist-info/METADATA,sha256=Xaqk6JqUV3Y7IafPDQ85k7VTaghTLTQGtbLpKiZ7gEo,23680
|
14
|
-
sanic_security-1.12.3.dist-info/WHEEL,sha256=Z4pYXqR_rTB7OWNDYFOm1qRk0RX6GFP2o8LgvP453Hk,91
|
15
|
-
sanic_security-1.12.3.dist-info/top_level.txt,sha256=ZybkhHXSjfzhmv8XeqLvnNmLmv21Z0oPX6Ep4DJN8b0,15
|
16
|
-
sanic_security-1.12.3.dist-info/RECORD,,
|
File without changes
|
File without changes
|