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.
- sanic_security/authentication.py +9 -8
- sanic_security/exceptions.py +17 -69
- sanic_security/oauth.py +27 -26
- sanic_security/verification.py +2 -3
- {sanic_security-1.16.10.dist-info → sanic_security-1.16.12.dist-info}/METADATA +10 -10
- {sanic_security-1.16.10.dist-info → sanic_security-1.16.12.dist-info}/RECORD +9 -9
- {sanic_security-1.16.10.dist-info → sanic_security-1.16.12.dist-info}/WHEEL +1 -1
- {sanic_security-1.16.10.dist-info → sanic_security-1.16.12.dist-info}/licenses/LICENSE +0 -0
- {sanic_security-1.16.10.dist-info → sanic_security-1.16.12.dist-info}/top_level.txt +0 -0
sanic_security/authentication.py
CHANGED
@@ -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
|
-
|
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
|
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
|
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.
|
sanic_security/exceptions.py
CHANGED
@@ -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
|
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
|
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")
|
sanic_security/verification.py
CHANGED
@@ -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
|
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.
|
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
|
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-
|
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`.
|
303
|
-
|
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
|
-
**
|
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=
|
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=
|
5
|
+
sanic_security/exceptions.py,sha256=ZtXXo2HvpSlcoQiuNH4WZmnez1vVUX55ybGR0W5mOyY,4955
|
6
6
|
sanic_security/models.py,sha256=nA4QqmYEMga4ufVTNggJiIuzZtTOYGOquOSlrKMeVdw,22076
|
7
|
-
sanic_security/oauth.py,sha256=
|
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=
|
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.
|
14
|
-
sanic_security-1.16.
|
15
|
-
sanic_security-1.16.
|
16
|
-
sanic_security-1.16.
|
17
|
-
sanic_security-1.16.
|
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,,
|
File without changes
|
File without changes
|