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.
@@ -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, account: Account = None, require_second_factor: bool = False
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. Login credentials are retrieved via the authorization header.
100
- account (Account): Account being logged into, overrides retrieving account via email or username in form-data.
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
- try:
125
- account = await Account.get_via_email(email_or_username.lower())
126
- except NotFoundError as e:
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
- request.ctx.authentication_session = await authenticate(request)
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=PasswordHasher().hash(security_config.INITIAL_ADMIN_PASSWORD),
298
+ password=password_hasher.hash(security_config.INITIAL_ADMIN_PASSWORD),
295
299
  verified=True,
296
300
  )
297
301
  await account.roles.add(role)
@@ -130,7 +130,7 @@ class DeactivatedError(SessionError):
130
130
 
131
131
  def __init__(
132
132
  self,
133
- message: str = "Session has been deactivated or refreshed.",
133
+ message: str = "Session has been deactivated.",
134
134
  code: int = 401,
135
135
  ):
136
136
  super().__init__(message, code)
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
- if hasattr(self, "refresh_expiration_date"):
301
- response.cookies.get_cookie(cookie).expires = (
302
- self.refresh_expiration_date
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
- else:
305
- response.cookies.get_cookie(cookie).expires = self.expiration_date
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
 
@@ -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)
@@ -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
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 >=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
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 >=3.3.1 ; extra == 'crypto'
24
+ Requires-Dist: cryptography>=3.3.1; extra == "crypto"
25
25
  Provides-Extra: dev
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'
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
- Please visit [security.na-stewart.com](https://security.na-stewart.com) for documentation and [here for an implementation guide](https://blog.na-stewart.com/entry?id=3).
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
- This account can be logged into and has complete authoritative access. Login credentials should be modified in config!
175
+ Creates initial admin account, you should modify its credentials in config!
176
176
 
177
- ```python
178
- create_initial_admin_account(app)
179
- if __name__ == "__main__":
180
- app.run(host="127.0.0.1", port=8000)
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
- Login credentials are retrieved via the Authorization header. Credentials are constructed by first combining the
228
- username and the password with a colon (aladdin:opensesame), and then by encoding the resulting string in base64
229
- (YWxhZGRpbjpvcGVuc2VzYW1l). Here is an example authorization header: `Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l`.
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
- * Authentication Middleware
324
-
325
- New/Refreshed session returned if client's session expired during authentication, requires encoding.
324
+ * Refresh Encoder
326
325
 
327
- Middleware is recommended to automatically encode the refreshed session.
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
- @app.on_response
331
- async def authentication_refresh_encoder(request, response):
332
- if hasattr(request.ctx, "authentication_session"):
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,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (70.3.0)
2
+ Generator: setuptools (74.1.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,16 +0,0 @@
1
- sanic_security/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- sanic_security/authentication.py,sha256=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,,