sanic-security 1.16.10__py3-none-any.whl → 1.16.12__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.
@@ -12,7 +12,6 @@ from sanic_security.configuration import config, DEFAULT_CONFIG
12
12
  from sanic_security.exceptions import (
13
13
  CredentialsError,
14
14
  DeactivatedError,
15
- SecondFactorFulfilledError,
16
15
  ExpiredError,
17
16
  AuditWarning,
18
17
  )
@@ -51,7 +50,7 @@ async def register(
51
50
  Args:
52
51
  request (Request): Sanic request parameter. Request body should contain form-data with the following argument(s): email, username, password, phone (including country code).
53
52
  verified (bool): Sets the verification requirement for the account being registered.
54
- disabled (bool): Renders the account being registered unusable.
53
+ disabled (bool): Renders the account being registered unusable until manual activation.
55
54
 
56
55
  Returns:
57
56
  account
@@ -88,8 +87,9 @@ async def register(
88
87
 
89
88
  async def login(
90
89
  request: Request,
91
- account: Account = None,
90
+ *,
92
91
  require_second_factor: bool = False,
92
+ email: str = None,
93
93
  password: str = None,
94
94
  ) -> AuthenticationSession:
95
95
  """
@@ -97,8 +97,8 @@ async def login(
97
97
 
98
98
  Args:
99
99
  request (Request): Sanic request parameter, login credentials are retrieved via the authorization header.
100
- account (Account): Account being logged into, overrides account retrieved via email or username.
101
100
  require_second_factor (bool): Determines authentication session second factor requirement on login.
101
+ email (str): Email (or username) of account being logged into, overrides account retrieved via authorization header.
102
102
  password (str): Overrides user's password attempt retrieved via the authorization header.
103
103
 
104
104
  Returns:
@@ -111,10 +111,12 @@ async def login(
111
111
  UnverifiedError
112
112
  DisabledError
113
113
  """
114
- if not account:
114
+ if not email:
115
115
  account, password = await Account.get_via_header(request)
116
116
  elif not password:
117
117
  raise CredentialsError("Password parameter is empty.")
118
+ else:
119
+ account = await Account.get_via_credential(email)
118
120
  try:
119
121
  password_hasher.verify(account.password, password)
120
122
  if password_hasher.check_needs_rehash(account.password):
@@ -176,14 +178,13 @@ async def fulfill_second_factor(request: Request) -> AuthenticationSession:
176
178
  DeactivatedError
177
179
  ChallengeError
178
180
  MaxedOutChallengeError
179
- SecondFactorFulfilledError
180
181
 
181
182
  Returns:
182
183
  authentication_session
183
184
  """
184
185
  authentication_session = await AuthenticationSession.decode(request)
185
186
  if not authentication_session.requires_second_factor:
186
- raise SecondFactorFulfilledError
187
+ raise DeactivatedError("Session second factor requirement already met.", 403)
187
188
  two_step_session = await TwoStepSession.decode(request)
188
189
  two_step_session.validate()
189
190
  await two_step_session.check_code(request.form.get("code"))
@@ -265,7 +266,7 @@ def requires_authentication(arg=None):
265
266
 
266
267
  def validate_password(password: str) -> str:
267
268
  """
268
- Validates password requirements.
269
+ Validates password formatting requirements.
269
270
 
270
271
  Args:
271
272
  password (str): Password being validated.
@@ -43,90 +43,63 @@ class SecurityError(SanicException):
43
43
 
44
44
 
45
45
  class NotFoundError(SecurityError):
46
- """
47
- Raised when a resource cannot be found.
48
- """
46
+ """Raised when a resource cannot be found on the database."""
49
47
 
50
48
  def __init__(self, message):
51
49
  super().__init__(message, 404)
52
50
 
53
51
 
54
52
  class DeletedError(SecurityError):
55
- """
56
- Raised when attempting to access a deleted resource.
57
- """
53
+ """Raised when attempting to access a resource marked as deleted."""
58
54
 
59
55
  def __init__(self, message):
60
56
  super().__init__(message, 404)
61
57
 
62
58
 
63
59
  class CredentialsError(SecurityError):
64
- """
65
- Raised when credentials are invalid.
66
- """
60
+ """Raised when credentials are invalid."""
67
61
 
68
62
  def __init__(self, message, code=400):
69
63
  super().__init__(message, code)
70
64
 
71
65
 
72
66
  class OAuthError(SecurityError):
73
- """
74
- Raised when an error occurs during OAuth flow.
75
- """
67
+ """Raised when an error occurs during OAuth flow."""
76
68
 
77
69
  def __init__(self, message, code=401):
78
70
  super().__init__(message, code)
79
71
 
80
72
 
81
73
  class AccountError(SecurityError):
82
- """
83
- Base account error that all other account errors derive from.
84
- """
74
+ """Base account error that all other account errors derive from."""
85
75
 
86
76
  def __init__(self, message, code):
87
77
  super().__init__(message, code)
88
78
 
89
79
 
90
80
  class DisabledError(AccountError):
91
- """
92
- Raised when account is disabled.
93
- """
81
+ """Raised when account is disabled."""
94
82
 
95
83
  def __init__(self, message: str = "Account is disabled.", code: int = 401):
96
84
  super().__init__(message, code)
97
85
 
98
86
 
99
87
  class UnverifiedError(AccountError):
100
- """
101
- Raised when account is unverified.
102
- """
88
+ """Raised when account is unverified."""
103
89
 
104
90
  def __init__(self):
105
91
  super().__init__("Account requires verification.", 401)
106
92
 
107
93
 
108
- class VerifiedError(AccountError):
109
- """
110
- Raised when account is already verified.
111
- """
112
-
113
- def __init__(self):
114
- super().__init__("Account already verified.", 403)
115
-
116
-
117
94
  class SessionError(SecurityError):
118
- """
119
- Base session error that all other session errors derive from.
120
- """
95
+ """Base session error that all other session errors derive from."""
121
96
 
122
97
  def __init__(self, message, code=401):
123
98
  super().__init__(message, code)
124
99
 
125
100
 
126
101
  class JWTDecodeError(SessionError):
127
- """
128
- Raised when client JWT is invalid.
129
- """
102
+ """Raised when client JWT is invalid."""
130
103
 
131
104
  def __init__(
132
105
  self, message="Session token invalid, not provided, or expired.", code=401
@@ -135,9 +108,7 @@ class JWTDecodeError(SessionError):
135
108
 
136
109
 
137
110
  class DeactivatedError(SessionError):
138
- """
139
- Raised when session is deactivated.
140
- """
111
+ """Raised when session is deactivated."""
141
112
 
142
113
  def __init__(
143
114
  self,
@@ -148,69 +119,46 @@ class DeactivatedError(SessionError):
148
119
 
149
120
 
150
121
  class ExpiredError(SessionError):
151
- """
152
- Raised when session has expired.
153
- """
122
+ """Raised when session has expired."""
154
123
 
155
124
  def __init__(self, message="Session has expired."):
156
125
  super().__init__(message)
157
126
 
158
127
 
159
128
  class SecondFactorRequiredError(SessionError):
160
- """
161
- Raised when authentication session two-factor requirement isn't met.
162
- """
129
+ """Raised when authentication session two-factor requirement isn't met."""
163
130
 
164
131
  def __init__(self):
165
132
  super().__init__("Session requires second factor for authentication.")
166
133
 
167
134
 
168
- class SecondFactorFulfilledError(SessionError):
169
- """
170
- Raised when authentication session two-factor requirement is already met.
171
- """
172
-
173
- def __init__(self):
174
- super().__init__("Session second factor requirement already met.", 403)
175
-
176
-
177
135
  class ChallengeError(SessionError):
178
- """
179
- Raised when a session challenge attempt is invalid.
180
- """
136
+ """Raised when a session challenge attempt is invalid."""
181
137
 
182
138
  def __init__(self, message):
183
139
  super().__init__(message)
184
140
 
185
141
 
186
142
  class MaxedOutChallengeError(ChallengeError):
187
- """
188
- Raised when a session's challenge attempt limit is reached.
189
- """
143
+ """Raised when a session's challenge attempt limit is reached."""
190
144
 
191
145
  def __init__(self):
192
146
  super().__init__("The maximum amount of attempts has been reached.")
193
147
 
194
148
 
195
149
  class AuthorizationError(SecurityError):
196
- """
197
- Raised when an account has insufficient permissions or roles for an action.
198
- """
150
+ """Raised when an account has insufficient permissions or roles for an action."""
199
151
 
200
152
  def __init__(self, message):
201
153
  super().__init__(message, 403)
202
154
 
203
155
 
204
156
  class AnonymousError(AuthorizationError):
205
- """
206
- Raised when attempting to authorize an anonymous user.
207
- """
157
+ """Raised when attempting to authorize an anonymous user."""
208
158
 
209
159
  def __init__(self):
210
160
  super().__init__("Session is anonymous.")
211
161
 
212
162
 
213
163
  class AuditWarning(Warning):
214
- """
215
- Raised when configuration may be dangerous.
216
- """
164
+ """Raised when configuration may be dangerous."""
sanic_security/oauth.py CHANGED
@@ -110,7 +110,7 @@ async def oauth_callback(
110
110
 
111
111
  def oauth_encode(response: HTTPResponse, token_info: dict) -> None:
112
112
  """
113
- Transforms access token into JWT and then is stored in a cookie.
113
+ Transforms token info into JWT and then is stored in a cookie.
114
114
 
115
115
  Args:
116
116
  response (HTTPResponse): Sanic response used to store JWT into a cookie on the client.
@@ -133,33 +133,9 @@ def oauth_encode(response: HTTPResponse, token_info: dict) -> None:
133
133
  )
134
134
 
135
135
 
136
- async def oauth_revoke(request: Request, client: BaseOAuth2) -> dict:
137
- """
138
- Revokes the client's access token.
139
-
140
- Args:
141
- request (Request): Sanic request parameter.
142
- client (BaseOAuth2): OAuth provider.
143
-
144
- Raises:
145
- OAuthError
146
- """
147
- if request.cookies.get(f"{config.SESSION_PREFIX}_oauth"):
148
- try:
149
- token_info = await oauth_decode(request, client, False)
150
- request.ctx.oauth["revoked"] = True
151
- with suppress(RevokeTokenNotSupportedError):
152
- await client.revoke_token(
153
- token_info.get("access_token"), "access_token"
154
- )
155
- return token_info
156
- except RevokeTokenError as e:
157
- raise OAuthError(f"Failed to revoke access token {e.response.text}")
158
-
159
-
160
136
  async def oauth_decode(request: Request, client: BaseOAuth2, refresh=True) -> dict:
161
137
  """
162
- Decodes JWT token from client cookie into an access token.
138
+ Decodes JWT from client cookie into token info.
163
139
 
164
140
  Args:
165
141
  request (Request): Sanic request parameter.
@@ -193,6 +169,30 @@ async def oauth_decode(request: Request, client: BaseOAuth2, refresh=True) -> di
193
169
  raise OAuthError(f"Access token invalid, not provided, or expired.", 400)
194
170
 
195
171
 
172
+ async def oauth_revoke(request: Request, client: BaseOAuth2) -> dict:
173
+ """
174
+ Revokes the client's access token.
175
+
176
+ Args:
177
+ request (Request): Sanic request parameter.
178
+ client (BaseOAuth2): OAuth provider.
179
+
180
+ Raises:
181
+ OAuthError
182
+ """
183
+ if request.cookies.get(f"{config.SESSION_PREFIX}_oauth"):
184
+ try:
185
+ token_info = await oauth_decode(request, client, False)
186
+ request.ctx.oauth["revoked"] = True
187
+ with suppress(RevokeTokenNotSupportedError):
188
+ await client.revoke_token(
189
+ token_info.get("access_token"), "access_token"
190
+ )
191
+ return token_info
192
+ except RevokeTokenError as e:
193
+ raise OAuthError(f"Failed to revoke access token {e.response.text}")
194
+
195
+
196
196
  def requires_oauth(client: BaseOAuth2):
197
197
  """
198
198
  Decodes JWT token from client cookie into an access token.
@@ -235,6 +235,7 @@ def initialize_oauth(app: Sanic) -> None:
235
235
  async def session_middleware(request, response):
236
236
  if hasattr(request.ctx, "oauth"):
237
237
  if request.ctx.oauth.get("is_refresh"):
238
+ del request.ctx.oauth["is_refresh"]
238
239
  oauth_encode(response, request.ctx.oauth)
239
240
  elif request.ctx.oauth.get("revoked"):
240
241
  response.delete_cookie(f"{config.SESSION_PREFIX}_oauth")
@@ -7,8 +7,8 @@ from sanic.request import Request
7
7
  from sanic_security.exceptions import (
8
8
  JWTDecodeError,
9
9
  NotFoundError,
10
- VerifiedError,
11
10
  MaxedOutChallengeError,
11
+ DeactivatedError,
12
12
  )
13
13
  from sanic_security.models import (
14
14
  Account,
@@ -157,14 +157,13 @@ async def verify_account(request: Request) -> TwoStepSession:
157
157
  DeactivatedError
158
158
  ChallengeError
159
159
  MaxedOutChallengeError
160
- VerifiedError
161
160
 
162
161
  Returns:
163
162
  two_step_session
164
163
  """
165
164
  two_step_session = await TwoStepSession.decode(request)
166
165
  if two_step_session.bearer.verified:
167
- raise VerifiedError
166
+ raise DeactivatedError("Account already verified.", 403)
168
167
  two_step_session.validate()
169
168
  try:
170
169
  await two_step_session.check_code(request.form.get("code"))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sanic-security
3
- Version: 1.16.10
3
+ Version: 1.16.12
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/
@@ -71,13 +71,12 @@ Dynamic: license-file
71
71
  ## About The Project
72
72
 
73
73
  Sanic Security is an authentication, authorization, and verification library designed for use with the
74
- [Sanic](https://github.com/huge-success/sanic) web app framework.
74
+ [Sanic](https://github.com/huge-success/sanic) web app framework. Designed to prioritize rapid prototyping with forgiving syntax and structure.
75
75
 
76
76
  * OAuth2 integration
77
77
  * Login, registration, and authentication with refresh mechanisms
78
78
  * Role based authorization with wildcard permissions
79
79
  * Image & audio CAPTCHA
80
- * Two-factor authentication
81
80
  * Two-step verification
82
81
  * Logging & auditing
83
82
 
@@ -254,7 +253,7 @@ async def on_oauth_token(request):
254
253
 
255
254
  ## Authentication
256
255
 
257
- * Registration (with two-step account verification)
256
+ * Registration (with two-step email verification)
258
257
 
259
258
  Phone can be null or empty.
260
259
 
@@ -295,12 +294,15 @@ async def on_verify(request):
295
294
  return json("You have verified your account and may login!", two_step_session.json)
296
295
  ```
297
296
 
298
- * Login (with two-factor authentication)
297
+ * Login (with two-step email verification)
299
298
 
300
299
  Credentials are retrieved via header are constructed by first combining the username and the password with a colon
301
300
  (aladdin:opensesame), and then by encoding the resulting string in base64 (YWxhZGRpbjpvcGVuc2VzYW1l).
302
- Here is an example authorization header: `Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l`. You can use a username
303
- as well as an email for login if `ALLOW_LOGIN_WITH_USERNAME` is true in the config.
301
+ Here is an example authorization header: `Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l`.
302
+
303
+ If this isn't desired, you can pass credentials into the login method instead.
304
+
305
+ You can use a username as well as an email for login if `ALLOW_LOGIN_WITH_USERNAME` is true in the config.
304
306
 
305
307
  ```python
306
308
  @app.post("api/security/login")
@@ -321,8 +323,6 @@ async def on_login(request):
321
323
  return response
322
324
  ```
323
325
 
324
- If this isn't desired, you can pass an account and password attempt directly into the login method instead.
325
-
326
326
  * Fulfill Second Factor
327
327
 
328
328
  Fulfills client authentication session's second factor requirement via two-step session code.
@@ -661,7 +661,7 @@ Distributed under the MIT License. See `LICENSE` for more information.
661
661
  <!-- Versioning -->
662
662
  ## Versioning
663
663
 
664
- **0.0.0**
664
+ **x.x.x**
665
665
 
666
666
  * MAJOR version when you make incompatible API changes.
667
667
 
@@ -1,17 +1,17 @@
1
1
  sanic_security/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- sanic_security/authentication.py,sha256=APs_YkwQCAEKyQo76ukKazQLGcm9fYrve6CUNxK2yKU,13201
2
+ sanic_security/authentication.py,sha256=Q-h63jFRmOMcvciPD2GGHrCSNkHptN1mvYNsrqopXJg,13302
3
3
  sanic_security/authorization.py,sha256=Hj1TXWppq7KDH-BQXFNihpZTbaimxnVCbif_Zb5W1bA,8232
4
4
  sanic_security/configuration.py,sha256=HxlKWe1vrQsxNtpoOx86RuHtkZz7Mjo88hMCMhTDvfo,6162
5
- sanic_security/exceptions.py,sha256=b_E6wbbtk9ziFfH3jZstp2E01hTm6V1yjTltANYAuMY,5582
5
+ sanic_security/exceptions.py,sha256=ZtXXo2HvpSlcoQiuNH4WZmnez1vVUX55ybGR0W5mOyY,4955
6
6
  sanic_security/models.py,sha256=nA4QqmYEMga4ufVTNggJiIuzZtTOYGOquOSlrKMeVdw,22076
7
- sanic_security/oauth.py,sha256=X1fx5KwvtWOa9ABGj7-MZ82ztlVeEuDz55yOh1Vtkes,8405
7
+ sanic_security/oauth.py,sha256=AKHUvz55OPUSZGwwLx-otUCoiXismEXdz-XXp577jD0,8445
8
8
  sanic_security/utils.py,sha256=WlPOEEQGcfZk-GbPNu6OiysNXAo9mw80TitDV7XxWMc,3762
9
- sanic_security/verification.py,sha256=vr_64HLC7TfOwhki7B4Xn3XQJ0V6OoVgh8fR4DISZ44,8085
9
+ sanic_security/verification.py,sha256=1DHdWCuSIsGCpFldoYAKrpOQcFGiyEW5OAXvzh71MKk,8102
10
10
  sanic_security/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  sanic_security/test/server.py,sha256=bVltV-AB_CEz9xrnVIft88FU6IYPgOOWuoSHDijeTDs,13717
12
12
  sanic_security/test/tests.py,sha256=YXyn9aJmYg7vCjUuAs8FcI_lGIgzhmMe4AYTzu47_18,22618
13
- sanic_security-1.16.10.dist-info/licenses/LICENSE,sha256=sXlJs9_mG-dCkPfWsDnuzydJWagS82E2gYtkVH9enHA,1100
14
- sanic_security-1.16.10.dist-info/METADATA,sha256=_3h-20wix8oJ_Aeyv7IFYJFh7Vjxh8YTkVwnpsOR-gY,25532
15
- sanic_security-1.16.10.dist-info/WHEEL,sha256=ck4Vq1_RXyvS4Jt6SI0Vz6fyVs4GWg7AINwpsaGEgPE,91
16
- sanic_security-1.16.10.dist-info/top_level.txt,sha256=ZybkhHXSjfzhmv8XeqLvnNmLmv21Z0oPX6Ep4DJN8b0,15
17
- sanic_security-1.16.10.dist-info/RECORD,,
13
+ sanic_security-1.16.12.dist-info/licenses/LICENSE,sha256=sXlJs9_mG-dCkPfWsDnuzydJWagS82E2gYtkVH9enHA,1100
14
+ sanic_security-1.16.12.dist-info/METADATA,sha256=kVoOjYuO42uMqbBwNmgFuJAWWadJc6VjRfPB_jv8Ra0,25554
15
+ sanic_security-1.16.12.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
16
+ sanic_security-1.16.12.dist-info/top_level.txt,sha256=ZybkhHXSjfzhmv8XeqLvnNmLmv21Z0oPX6Ep4DJN8b0,15
17
+ sanic_security-1.16.12.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.0.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5