sanic-security 1.12.6__py3-none-any.whl → 1.12.7__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 +30 -8
- sanic_security/authorization.py +2 -0
- sanic_security/configuration.py +0 -1
- sanic_security/models.py +8 -7
- sanic_security/test/server.py +18 -54
- sanic_security/test/tests.py +27 -76
- sanic_security/verification.py +6 -4
- {sanic_security-1.12.6.dist-info → sanic_security-1.12.7.dist-info}/METADATA +2 -2
- sanic_security-1.12.7.dist-info/RECORD +16 -0
- sanic_security-1.12.6.dist-info/RECORD +0 -16
- {sanic_security-1.12.6.dist-info → sanic_security-1.12.7.dist-info}/LICENSE +0 -0
- {sanic_security-1.12.6.dist-info → sanic_security-1.12.7.dist-info}/WHEEL +0 -0
- {sanic_security-1.12.6.dist-info → sanic_security-1.12.7.dist-info}/top_level.txt +0 -0
sanic_security/authentication.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
import functools
|
2
2
|
import re
|
3
|
+
import warnings
|
3
4
|
|
4
5
|
from argon2 import PasswordHasher
|
5
6
|
from argon2.exceptions import VerifyMismatchError
|
@@ -8,7 +9,7 @@ from sanic.log import logger
|
|
8
9
|
from sanic.request import Request
|
9
10
|
from tortoise.exceptions import DoesNotExist
|
10
11
|
|
11
|
-
from sanic_security.configuration import config as security_config
|
12
|
+
from sanic_security.configuration import config as security_config, DEFAULT_CONFIG
|
12
13
|
from sanic_security.exceptions import (
|
13
14
|
CredentialsError,
|
14
15
|
DeactivatedError,
|
@@ -16,6 +17,7 @@ from sanic_security.exceptions import (
|
|
16
17
|
ExpiredError,
|
17
18
|
)
|
18
19
|
from sanic_security.models import Account, AuthenticationSession, Role, TwoStepSession
|
20
|
+
from sanic_security.utils import get_ip
|
19
21
|
|
20
22
|
"""
|
21
23
|
Copyright (c) 2020-present Nicholas Aidan Stewart
|
@@ -75,15 +77,15 @@ async def register(
|
|
75
77
|
raise CredentialsError(
|
76
78
|
"An account with this phone number may already exist.", 409
|
77
79
|
)
|
78
|
-
validate_password(request.form.get("password"))
|
79
80
|
account = await Account.create(
|
80
81
|
email=email_lower,
|
81
82
|
username=request.form.get("username"),
|
82
|
-
password=password_hasher.hash(request.form.get("password")),
|
83
|
+
password=password_hasher.hash(validate_password(request.form.get("password"))),
|
83
84
|
phone=request.form.get("phone"),
|
84
85
|
verified=verified,
|
85
86
|
disabled=disabled,
|
86
87
|
)
|
88
|
+
logger.info(f"Client {get_ip(request)} has registered account {account.id}.")
|
87
89
|
return account
|
88
90
|
|
89
91
|
|
@@ -122,9 +124,13 @@ async def login(
|
|
122
124
|
account.password = password_hasher.hash(password)
|
123
125
|
await account.save(update_fields=["password"])
|
124
126
|
account.validate()
|
125
|
-
|
127
|
+
authentication_session = await AuthenticationSession.new(
|
126
128
|
request, account, requires_second_factor=require_second_factor
|
127
129
|
)
|
130
|
+
logger.info(
|
131
|
+
f"Client has logged into account {account.id} with authentication session {authentication_session.id}."
|
132
|
+
)
|
133
|
+
return authentication_session
|
128
134
|
except VerifyMismatchError:
|
129
135
|
raise CredentialsError("Incorrect password.", 401)
|
130
136
|
|
@@ -149,6 +155,10 @@ async def logout(request: Request) -> AuthenticationSession:
|
|
149
155
|
raise DeactivatedError("Already logged out.", 403)
|
150
156
|
authentication_session.active = False
|
151
157
|
await authentication_session.save(update_fields=["active"])
|
158
|
+
logger.info(
|
159
|
+
f"Client has logged out{" anonymously" if authentication_session.anonymous else
|
160
|
+
f" of account {authentication_session.bearer.id}"} with authentication session {authentication_session.id}."
|
161
|
+
)
|
152
162
|
return authentication_session
|
153
163
|
|
154
164
|
|
@@ -177,9 +187,12 @@ async def fulfill_second_factor(request: Request) -> AuthenticationSession:
|
|
177
187
|
raise SecondFactorFulfilledError()
|
178
188
|
two_step_session = await TwoStepSession.decode(request)
|
179
189
|
two_step_session.validate()
|
180
|
-
await two_step_session.check_code(request
|
190
|
+
await two_step_session.check_code(request.form.get("code"))
|
181
191
|
authentication_session.requires_second_factor = False
|
182
192
|
await authentication_session.save(update_fields=["requires_second_factor"])
|
193
|
+
logger.info(
|
194
|
+
f"Authentication session {authentication_session.id} second factor has been fulfilled."
|
195
|
+
)
|
183
196
|
return authentication_session
|
184
197
|
|
185
198
|
|
@@ -275,13 +288,22 @@ def create_initial_admin_account(app: Sanic) -> None:
|
|
275
288
|
|
276
289
|
@app.listener("before_server_start")
|
277
290
|
async def create(app, loop):
|
291
|
+
if security_config.SECRET == DEFAULT_CONFIG["SECRET"]:
|
292
|
+
warnings.warn("Secret should be changed from default.")
|
293
|
+
if security_config.INITIAL_ADMIN_EMAIL == DEFAULT_CONFIG["INITIAL_ADMIN_EMAIL"]:
|
294
|
+
warnings.warn("Initial admin email should be changed from default.")
|
295
|
+
if (
|
296
|
+
security_config.INITIAL_ADMIN_PASSWORD
|
297
|
+
== DEFAULT_CONFIG["INITIAL_ADMIN_PASSWORD"]
|
298
|
+
):
|
299
|
+
warnings.warn("Initial admin password should be changed from default.")
|
278
300
|
try:
|
279
|
-
role = await Role.filter(name="
|
301
|
+
role = await Role.filter(name="Admin").get()
|
280
302
|
except DoesNotExist:
|
281
303
|
role = await Role.create(
|
282
304
|
description="Has root abilities, assign sparingly.",
|
283
305
|
permissions="*:*",
|
284
|
-
name="
|
306
|
+
name="Admin",
|
285
307
|
)
|
286
308
|
try:
|
287
309
|
account = await Account.filter(
|
@@ -293,7 +315,7 @@ def create_initial_admin_account(app: Sanic) -> None:
|
|
293
315
|
logger.warning("Initial admin account role has been reinstated.")
|
294
316
|
except DoesNotExist:
|
295
317
|
account = await Account.create(
|
296
|
-
username="
|
318
|
+
username="Admin",
|
297
319
|
email=security_config.INITIAL_ADMIN_EMAIL,
|
298
320
|
password=password_hasher.hash(security_config.INITIAL_ADMIN_PASSWORD),
|
299
321
|
verified=True,
|
sanic_security/authorization.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
import functools
|
2
2
|
from fnmatch import fnmatch
|
3
3
|
|
4
|
+
from sanic.log import logger
|
4
5
|
from sanic.request import Request
|
5
6
|
from tortoise.exceptions import DoesNotExist
|
6
7
|
|
@@ -119,6 +120,7 @@ async def assign_role(
|
|
119
120
|
description=description, permissions=permissions, name=name
|
120
121
|
)
|
121
122
|
await account.roles.add(role)
|
123
|
+
logger.info(f"Role {role.id} has been assigned to account {account.id}.")
|
122
124
|
return role
|
123
125
|
|
124
126
|
|
sanic_security/configuration.py
CHANGED
sanic_security/models.py
CHANGED
@@ -142,6 +142,7 @@ class Account(BaseModel):
|
|
142
142
|
else:
|
143
143
|
self.disabled = True
|
144
144
|
await self.save(update_fields=["disabled"])
|
145
|
+
logger.info(f"Account {self.id} has been disabled.")
|
145
146
|
|
146
147
|
@property
|
147
148
|
def json(self) -> dict:
|
@@ -319,6 +320,7 @@ class Session(BaseModel):
|
|
319
320
|
if self.active:
|
320
321
|
self.active = False
|
321
322
|
await self.save(update_fields=["active"])
|
323
|
+
logger.info(f"Session {self.id} has been deactivated.")
|
322
324
|
else:
|
323
325
|
raise DeactivatedError("Session is already deactivated.", 403)
|
324
326
|
|
@@ -493,13 +495,12 @@ class VerificationSession(Session):
|
|
493
495
|
attempts: int = fields.IntField(default=0)
|
494
496
|
code: str = fields.CharField(max_length=10, default=get_code, null=True)
|
495
497
|
|
496
|
-
async def check_code(self,
|
498
|
+
async def check_code(self, code: str) -> None:
|
497
499
|
"""
|
498
500
|
Checks if code passed is equivalent to the session code.
|
499
501
|
|
500
502
|
Args:
|
501
503
|
code (str): Code being cross-checked with session code.
|
502
|
-
request (Request): Sanic request parameter.
|
503
504
|
|
504
505
|
Raises:
|
505
506
|
ChallengeError
|
@@ -513,13 +514,12 @@ class VerificationSession(Session):
|
|
513
514
|
"Your code does not match verification session code."
|
514
515
|
)
|
515
516
|
else:
|
516
|
-
logger.warning(
|
517
|
-
f"Client ({get_ip(request)}) has maxed out on session challenge attempts"
|
518
|
-
)
|
519
517
|
raise MaxedOutChallengeError()
|
520
518
|
else:
|
521
|
-
|
522
|
-
|
519
|
+
logger.info(
|
520
|
+
f"Client has completed verification session {self.id} challenge."
|
521
|
+
)
|
522
|
+
await self.deactivate()
|
523
523
|
|
524
524
|
@classmethod
|
525
525
|
async def new(cls, request: Request, account: Account, **kwargs):
|
@@ -633,6 +633,7 @@ class AuthenticationSession(Session):
|
|
633
633
|
):
|
634
634
|
self.active = False
|
635
635
|
await self.save(update_fields=["active"])
|
636
|
+
logger.info(f"Client has refreshed authentication session {self.id}.")
|
636
637
|
return await self.new(request, self.bearer, True)
|
637
638
|
else:
|
638
639
|
raise e
|
sanic_security/test/server.py
CHANGED
@@ -62,9 +62,7 @@ password_hasher = PasswordHasher()
|
|
62
62
|
|
63
63
|
@app.post("api/test/auth/register")
|
64
64
|
async def on_register(request):
|
65
|
-
"""
|
66
|
-
Register an account with email and password.
|
67
|
-
"""
|
65
|
+
"""Register an account with email and password."""
|
68
66
|
account = await register(
|
69
67
|
request,
|
70
68
|
verified=request.form.get("verified") == "true",
|
@@ -83,9 +81,7 @@ async def on_register(request):
|
|
83
81
|
|
84
82
|
@app.post("api/test/auth/verify")
|
85
83
|
async def on_verify(request):
|
86
|
-
"""
|
87
|
-
Verifies client account.
|
88
|
-
"""
|
84
|
+
"""Verifies client account."""
|
89
85
|
two_step_session = await verify_account(request)
|
90
86
|
return json(
|
91
87
|
"You have verified your account and may login!", two_step_session.bearer.json
|
@@ -94,9 +90,7 @@ async def on_verify(request):
|
|
94
90
|
|
95
91
|
@app.post("api/test/auth/login")
|
96
92
|
async def on_login(request):
|
97
|
-
"""
|
98
|
-
Login to an account with an email and password.
|
99
|
-
"""
|
93
|
+
"""Login to an account with an email and password."""
|
100
94
|
two_factor_authentication = request.args.get("two-factor-authentication") == "true"
|
101
95
|
authentication_session = await login(
|
102
96
|
request, require_second_factor=two_factor_authentication
|
@@ -118,9 +112,7 @@ async def on_login(request):
|
|
118
112
|
|
119
113
|
@app.post("api/test/auth/login/anon")
|
120
114
|
async def on_login_anonymous(request):
|
121
|
-
"""
|
122
|
-
Login as anonymous user.
|
123
|
-
"""
|
115
|
+
"""Login as anonymous user."""
|
124
116
|
authentication_session = await AuthenticationSession.new(request)
|
125
117
|
response = json(
|
126
118
|
"Anonymous user now associated with session!", authentication_session.json
|
@@ -131,9 +123,7 @@ async def on_login_anonymous(request):
|
|
131
123
|
|
132
124
|
@app.post("api/test/auth/validate-2fa")
|
133
125
|
async def on_two_factor_authentication(request):
|
134
|
-
"""
|
135
|
-
Fulfills client authentication session's second factor requirement.
|
136
|
-
"""
|
126
|
+
"""Fulfills client authentication session's second factor requirement."""
|
137
127
|
authentication_session = await fulfill_second_factor(request)
|
138
128
|
response = json(
|
139
129
|
"Authentication session second-factor fulfilled! You are now authenticated.",
|
@@ -145,9 +135,7 @@ async def on_two_factor_authentication(request):
|
|
145
135
|
|
146
136
|
@app.post("api/test/auth/logout")
|
147
137
|
async def on_logout(request):
|
148
|
-
"""
|
149
|
-
Logout of currently logged in account.
|
150
|
-
"""
|
138
|
+
"""Logout of currently logged in account."""
|
151
139
|
authentication_session = await logout(request)
|
152
140
|
response = json("Logout successful!", authentication_session.json)
|
153
141
|
return response
|
@@ -156,9 +144,7 @@ async def on_logout(request):
|
|
156
144
|
@app.post("api/test/auth")
|
157
145
|
@requires_authentication
|
158
146
|
async def on_authenticate(request):
|
159
|
-
"""
|
160
|
-
Authenticate client session and account.
|
161
|
-
"""
|
147
|
+
"""Authenticate client session and account."""
|
162
148
|
authentication_session = request.ctx.authentication_session
|
163
149
|
response = json(
|
164
150
|
"Authenticated!",
|
@@ -177,9 +163,7 @@ async def on_authenticate(request):
|
|
177
163
|
@app.post("api/test/auth/expire")
|
178
164
|
@requires_authentication
|
179
165
|
async def on_authentication_expire(request):
|
180
|
-
"""
|
181
|
-
Expire client's session.
|
182
|
-
"""
|
166
|
+
"""Expire client's session."""
|
183
167
|
authentication_session = request.ctx.authentication_session
|
184
168
|
authentication_session.expiration_date = datetime.datetime.now(datetime.UTC)
|
185
169
|
await authentication_session.save(update_fields=["expiration_date"])
|
@@ -189,9 +173,7 @@ async def on_authentication_expire(request):
|
|
189
173
|
@app.post("api/test/auth/associated")
|
190
174
|
@requires_authentication
|
191
175
|
async def on_get_associated_authentication_sessions(request):
|
192
|
-
"""
|
193
|
-
Retrieves authentication sessions associated with logged in account.
|
194
|
-
"""
|
176
|
+
"""Retrieves authentication sessions associated with logged in account."""
|
195
177
|
authentication_sessions = await AuthenticationSession.get_associated(
|
196
178
|
request.ctx.authentication_session.bearer
|
197
179
|
)
|
@@ -203,9 +185,7 @@ async def on_get_associated_authentication_sessions(request):
|
|
203
185
|
|
204
186
|
@app.get("api/test/capt/request")
|
205
187
|
async def on_captcha_request(request):
|
206
|
-
"""
|
207
|
-
Request captcha with solution in response.
|
208
|
-
"""
|
188
|
+
"""Request captcha with solution in response."""
|
209
189
|
captcha_session = await request_captcha(request)
|
210
190
|
response = json("Captcha request successful!", captcha_session.code)
|
211
191
|
captcha_session.encode(response)
|
@@ -214,9 +194,7 @@ async def on_captcha_request(request):
|
|
214
194
|
|
215
195
|
@app.get("api/test/capt/image")
|
216
196
|
async def on_captcha_image(request):
|
217
|
-
"""
|
218
|
-
Request captcha image.
|
219
|
-
"""
|
197
|
+
"""Request captcha image."""
|
220
198
|
captcha_session = await CaptchaSession.decode(request)
|
221
199
|
response = captcha_session.get_image()
|
222
200
|
captcha_session.encode(response)
|
@@ -226,17 +204,13 @@ async def on_captcha_image(request):
|
|
226
204
|
@app.post("api/test/capt")
|
227
205
|
@requires_captcha
|
228
206
|
async def on_captcha_attempt(request):
|
229
|
-
"""
|
230
|
-
Attempt captcha challenge.
|
231
|
-
"""
|
207
|
+
"""Attempt captcha challenge."""
|
232
208
|
return json("Captcha attempt successful!", request.ctx.captcha_session.json)
|
233
209
|
|
234
210
|
|
235
211
|
@app.post("api/test/two-step/request")
|
236
212
|
async def on_request_verification(request):
|
237
|
-
"""
|
238
|
-
Request two-step verification with code in the response.
|
239
|
-
"""
|
213
|
+
"""Request two-step verification with code in the response."""
|
240
214
|
two_step_session = await request_two_step_verification(request)
|
241
215
|
response = json("Verification request successful!", two_step_session.code)
|
242
216
|
two_step_session.encode(response)
|
@@ -246,9 +220,7 @@ async def on_request_verification(request):
|
|
246
220
|
@app.post("api/test/two-step")
|
247
221
|
@requires_two_step_verification
|
248
222
|
async def on_verification_attempt(request):
|
249
|
-
"""
|
250
|
-
Attempt two-step verification challenge.
|
251
|
-
"""
|
223
|
+
"""Attempt two-step verification challenge."""
|
252
224
|
return json(
|
253
225
|
"Two step verification attempt successful!", request.ctx.two_step_session.json
|
254
226
|
)
|
@@ -257,9 +229,7 @@ async def on_verification_attempt(request):
|
|
257
229
|
@app.post("api/test/auth/roles")
|
258
230
|
@requires_authentication
|
259
231
|
async def on_authorization(request):
|
260
|
-
"""
|
261
|
-
Check if client is authorized with sufficient roles and permissions.
|
262
|
-
"""
|
232
|
+
"""Check if client is authorized with sufficient roles and permissions."""
|
263
233
|
await check_roles(request, request.form.get("role"))
|
264
234
|
if request.form.get("permissions_required"):
|
265
235
|
await check_permissions(
|
@@ -271,9 +241,7 @@ async def on_authorization(request):
|
|
271
241
|
@app.post("api/test/auth/roles/assign")
|
272
242
|
@requires_authentication
|
273
243
|
async def on_role_assign(request):
|
274
|
-
"""
|
275
|
-
Assign authenticated account a role.
|
276
|
-
"""
|
244
|
+
"""Assign authenticated account a role."""
|
277
245
|
await assign_role(
|
278
246
|
request.form.get("name"),
|
279
247
|
request.ctx.authentication_session.bearer,
|
@@ -285,9 +253,7 @@ async def on_role_assign(request):
|
|
285
253
|
|
286
254
|
@app.post("api/test/account")
|
287
255
|
async def on_account_creation(request):
|
288
|
-
"""
|
289
|
-
Quick account creation.
|
290
|
-
"""
|
256
|
+
"""Quick account creation."""
|
291
257
|
account = await Account.create(
|
292
258
|
username=request.form.get("username"),
|
293
259
|
email=request.form.get("email").lower(),
|
@@ -301,9 +267,7 @@ async def on_account_creation(request):
|
|
301
267
|
|
302
268
|
@app.exception(SecurityError)
|
303
269
|
async def on_security_error(request, exception):
|
304
|
-
"""
|
305
|
-
Handles security errors with correct response.
|
306
|
-
"""
|
270
|
+
"""Handles security errors with correct response."""
|
307
271
|
traceback.print_exc()
|
308
272
|
return exception.json
|
309
273
|
|
sanic_security/test/tests.py
CHANGED
@@ -30,9 +30,7 @@ SOFTWARE.
|
|
30
30
|
|
31
31
|
|
32
32
|
class RegistrationTest(TestCase):
|
33
|
-
"""
|
34
|
-
Registration tests.
|
35
|
-
"""
|
33
|
+
"""Registration tests."""
|
36
34
|
|
37
35
|
def setUp(self):
|
38
36
|
self.client = httpx.Client()
|
@@ -62,9 +60,7 @@ class RegistrationTest(TestCase):
|
|
62
60
|
return registration_response
|
63
61
|
|
64
62
|
def test_registration(self):
|
65
|
-
"""
|
66
|
-
Account registration and login.
|
67
|
-
"""
|
63
|
+
"""Account registration and login."""
|
68
64
|
registration_response = self.register(
|
69
65
|
"account_registration@register.test",
|
70
66
|
"account_registration",
|
@@ -80,9 +76,7 @@ class RegistrationTest(TestCase):
|
|
80
76
|
assert login_response.status_code == 200, login_response.text
|
81
77
|
|
82
78
|
def test_invalid_registration(self):
|
83
|
-
"""
|
84
|
-
Registration with an intentionally invalid email, username, and phone.
|
85
|
-
"""
|
79
|
+
"""Registration with an intentionally invalid email, username, and phone."""
|
86
80
|
invalid_email_registration_response = self.register(
|
87
81
|
"invalid_register.test", "invalid_register", False, True
|
88
82
|
)
|
@@ -112,9 +106,7 @@ class RegistrationTest(TestCase):
|
|
112
106
|
), too_many_characters_registration_response.text
|
113
107
|
|
114
108
|
def test_registration_disabled(self):
|
115
|
-
"""
|
116
|
-
Registration and login with a disabled account.
|
117
|
-
"""
|
109
|
+
"""Registration and login with a disabled account."""
|
118
110
|
registration_response = self.register(
|
119
111
|
"disabled@register.test", "disabled", True, True
|
120
112
|
)
|
@@ -126,9 +118,7 @@ class RegistrationTest(TestCase):
|
|
126
118
|
assert "DisabledError" in login_response.text, login_response.text
|
127
119
|
|
128
120
|
def test_registration_unverified(self):
|
129
|
-
"""
|
130
|
-
Registration and login with an unverified account.
|
131
|
-
"""
|
121
|
+
"""Registration and login with an unverified account."""
|
132
122
|
registration_response = self.register(
|
133
123
|
"unverified@register.test", "unverified", False, False
|
134
124
|
)
|
@@ -140,9 +130,7 @@ class RegistrationTest(TestCase):
|
|
140
130
|
assert "UnverifiedError" in login_response.text, login_response.text
|
141
131
|
|
142
132
|
def test_registration_unverified_disabled(self):
|
143
|
-
"""
|
144
|
-
Registration and login with an unverified and disabled account.
|
145
|
-
"""
|
133
|
+
"""Registration and login with an unverified and disabled account."""
|
146
134
|
registration_response = self.register(
|
147
135
|
"unverified_disabled@register.test", "unverified_disabled", True, False
|
148
136
|
)
|
@@ -155,9 +143,7 @@ class RegistrationTest(TestCase):
|
|
155
143
|
|
156
144
|
|
157
145
|
class LoginTest(TestCase):
|
158
|
-
"""
|
159
|
-
Login tests.
|
160
|
-
"""
|
146
|
+
"""Login tests."""
|
161
147
|
|
162
148
|
def setUp(self):
|
163
149
|
self.client = httpx.Client()
|
@@ -166,9 +152,7 @@ class LoginTest(TestCase):
|
|
166
152
|
self.client.close()
|
167
153
|
|
168
154
|
def test_login(self):
|
169
|
-
"""
|
170
|
-
Login with an email and password.
|
171
|
-
"""
|
155
|
+
"""Login with an email and password."""
|
172
156
|
self.client.post(
|
173
157
|
"http://127.0.0.1:8000/api/test/account",
|
174
158
|
data={"email": "email_pass@login.test", "username": "email_pass"},
|
@@ -184,9 +168,7 @@ class LoginTest(TestCase):
|
|
184
168
|
assert authenticate_response.status_code == 200, authenticate_response.text
|
185
169
|
|
186
170
|
def test_login_with_username(self):
|
187
|
-
"""
|
188
|
-
Login with a username instead of an email and password.
|
189
|
-
"""
|
171
|
+
"""Login with a username instead of an email and password."""
|
190
172
|
self.client.post(
|
191
173
|
"http://127.0.0.1:8000/api/test/account",
|
192
174
|
data={"email": "user_pass@login.test", "username": "user_pass"},
|
@@ -202,9 +184,7 @@ class LoginTest(TestCase):
|
|
202
184
|
assert authenticate_response.status_code == 200, authenticate_response.text
|
203
185
|
|
204
186
|
def test_invalid_login(self):
|
205
|
-
"""
|
206
|
-
Login with an intentionally incorrect password and into a non existent account.
|
207
|
-
"""
|
187
|
+
"""Login with an intentionally incorrect password and into a non-existent account."""
|
208
188
|
self.client.post(
|
209
189
|
"http://127.0.0.1:8000/api/test/account",
|
210
190
|
data={"email": "incorrect_pass@login.test", "username": "incorrect_pass"},
|
@@ -225,9 +205,7 @@ class LoginTest(TestCase):
|
|
225
205
|
), unavailable_account_login_response
|
226
206
|
|
227
207
|
def test_logout(self):
|
228
|
-
"""
|
229
|
-
Logout of logged in account and attempt to authenticate.
|
230
|
-
"""
|
208
|
+
"""Logout of logged in account and attempt to authenticate."""
|
231
209
|
self.client.post(
|
232
210
|
"http://127.0.0.1:8000/api/test/account",
|
233
211
|
data={"email": "logout@login.test", "username": "logout"},
|
@@ -244,9 +222,7 @@ class LoginTest(TestCase):
|
|
244
222
|
assert authenticate_response.status_code == 401, authenticate_response.text
|
245
223
|
|
246
224
|
def test_initial_admin_login(self):
|
247
|
-
"""
|
248
|
-
Initial admin account login and authorization.
|
249
|
-
"""
|
225
|
+
"""Initial admin account login and authorization."""
|
250
226
|
login_response = self.client.post(
|
251
227
|
"http://127.0.0.1:8000/api/test/auth/login",
|
252
228
|
auth=("admin@login.test", "admin123"),
|
@@ -255,7 +231,7 @@ class LoginTest(TestCase):
|
|
255
231
|
permitted_authorization_response = self.client.post(
|
256
232
|
"http://127.0.0.1:8000/api/test/auth/roles",
|
257
233
|
data={
|
258
|
-
"role": "
|
234
|
+
"role": "Admin",
|
259
235
|
"permissions_required": "perm1:create,add, perm2:*",
|
260
236
|
},
|
261
237
|
)
|
@@ -264,9 +240,7 @@ class LoginTest(TestCase):
|
|
264
240
|
), permitted_authorization_response.text
|
265
241
|
|
266
242
|
def test_two_factor_login(self):
|
267
|
-
"""
|
268
|
-
Test login with two-factor authentication requirement.
|
269
|
-
"""
|
243
|
+
"""Test login with two-factor authentication requirement."""
|
270
244
|
self.client.post(
|
271
245
|
"http://127.0.0.1:8000/api/test/account",
|
272
246
|
data={"email": "two-factor@login.test", "username": "two-factor"},
|
@@ -295,9 +269,7 @@ class LoginTest(TestCase):
|
|
295
269
|
assert authenticate_response.status_code == 200, authenticate_response.text
|
296
270
|
|
297
271
|
def test_anonymous_login(self):
|
298
|
-
"""
|
299
|
-
Test login of anonymous user.
|
300
|
-
"""
|
272
|
+
"""Test login of anonymous user."""
|
301
273
|
anon_login_response = self.client.post(
|
302
274
|
"http://127.0.0.1:8000/api/test/auth/login/anon"
|
303
275
|
)
|
@@ -311,9 +283,7 @@ class LoginTest(TestCase):
|
|
311
283
|
|
312
284
|
|
313
285
|
class VerificationTest(TestCase):
|
314
|
-
"""
|
315
|
-
Two-step verification and captcha tests.
|
316
|
-
"""
|
286
|
+
"""Two-step verification and captcha tests."""
|
317
287
|
|
318
288
|
def setUp(self):
|
319
289
|
self.client = httpx.Client()
|
@@ -322,9 +292,7 @@ class VerificationTest(TestCase):
|
|
322
292
|
self.client.close()
|
323
293
|
|
324
294
|
def test_captcha(self):
|
325
|
-
"""
|
326
|
-
Captcha request and attempt.
|
327
|
-
"""
|
295
|
+
"""Captcha request and attempt."""
|
328
296
|
captcha_request_response = self.client.get(
|
329
297
|
"http://127.0.0.1:8000/api/test/capt/request"
|
330
298
|
)
|
@@ -344,9 +312,7 @@ class VerificationTest(TestCase):
|
|
344
312
|
), captcha_attempt_response.text
|
345
313
|
|
346
314
|
def test_two_step_verification(self):
|
347
|
-
"""
|
348
|
-
Two-step verification request and attempt.
|
349
|
-
"""
|
315
|
+
"""Two-step verification request and attempt."""
|
350
316
|
self.client.post(
|
351
317
|
"http://127.0.0.1:8000/api/test/account",
|
352
318
|
data={"email": "two_step@verification.test", "username": "two_step"},
|
@@ -382,9 +348,7 @@ class VerificationTest(TestCase):
|
|
382
348
|
), two_step_verification_no_email_request_response.text
|
383
349
|
|
384
350
|
def test_account_verification(self):
|
385
|
-
"""
|
386
|
-
Account registration and verification process with successful login.
|
387
|
-
"""
|
351
|
+
"""Account registration and verification process with successful login."""
|
388
352
|
registration_response = self.client.post(
|
389
353
|
"http://127.0.0.1:8000/api/test/auth/register",
|
390
354
|
data={
|
@@ -404,9 +368,7 @@ class VerificationTest(TestCase):
|
|
404
368
|
|
405
369
|
|
406
370
|
class AuthorizationTest(TestCase):
|
407
|
-
"""
|
408
|
-
Role and permissions based authorization tests.
|
409
|
-
"""
|
371
|
+
"""Role and permissions based authorization tests."""
|
410
372
|
|
411
373
|
def setUp(self):
|
412
374
|
self.client = httpx.Client()
|
@@ -415,9 +377,7 @@ class AuthorizationTest(TestCase):
|
|
415
377
|
self.client.close()
|
416
378
|
|
417
379
|
def test_permissions_authorization(self):
|
418
|
-
"""
|
419
|
-
Authorization with permissions.
|
420
|
-
"""
|
380
|
+
"""Authorization with permissions."""
|
421
381
|
self.client.post(
|
422
382
|
"http://127.0.0.1:8000/api/test/account",
|
423
383
|
data={"email": "permissions@authorization.test", "username": "permissions"},
|
@@ -455,9 +415,7 @@ class AuthorizationTest(TestCase):
|
|
455
415
|
), prohibited_authorization_response.text
|
456
416
|
|
457
417
|
def test_roles_authorization(self):
|
458
|
-
"""
|
459
|
-
Authorization with roles.
|
460
|
-
"""
|
418
|
+
"""Authorization with roles."""
|
461
419
|
self.client.post(
|
462
420
|
"http://127.0.0.1:8000/api/test/account",
|
463
421
|
data={"email": "roles@authorization.test", "username": "roles"},
|
@@ -488,6 +446,7 @@ class AuthorizationTest(TestCase):
|
|
488
446
|
), prohibited_authorization_response.text
|
489
447
|
|
490
448
|
def test_anonymous_authorization(self):
|
449
|
+
"""Authorization with anonymous client."""
|
491
450
|
anon_login_response = self.client.post(
|
492
451
|
"http://127.0.0.1:8000/api/test/auth/login/anon"
|
493
452
|
)
|
@@ -506,9 +465,7 @@ class AuthorizationTest(TestCase):
|
|
506
465
|
|
507
466
|
|
508
467
|
class MiscTest(TestCase):
|
509
|
-
"""
|
510
|
-
Miscellaneous tests that cannot be categorized.
|
511
|
-
"""
|
468
|
+
"""Miscellaneous tests that cannot be categorized."""
|
512
469
|
|
513
470
|
def setUp(self):
|
514
471
|
self.client = httpx.Client()
|
@@ -517,18 +474,14 @@ class MiscTest(TestCase):
|
|
517
474
|
self.client.close()
|
518
475
|
|
519
476
|
def test_environment_variable_load(self):
|
520
|
-
"""
|
521
|
-
Config loads environment variables.
|
522
|
-
"""
|
477
|
+
"""Config loads environment variables."""
|
523
478
|
os.environ["SANIC_SECURITY_SECRET"] = "test-secret"
|
524
479
|
security_config = Config()
|
525
480
|
security_config.load_environment_variables()
|
526
481
|
assert security_config.SECRET == "test-secret"
|
527
482
|
|
528
483
|
def test_get_associated_sessions(self):
|
529
|
-
"""
|
530
|
-
Retrieve sessions associated to logged in account.
|
531
|
-
"""
|
484
|
+
"""Retrieve sessions associated to logged in account."""
|
532
485
|
self.client.post(
|
533
486
|
"http://127.0.0.1:8000/api/test/account",
|
534
487
|
data={
|
@@ -549,9 +502,7 @@ class MiscTest(TestCase):
|
|
549
502
|
), retrieve_associated_response.text
|
550
503
|
|
551
504
|
def test_authentication_refresh(self):
|
552
|
-
"""
|
553
|
-
Test automatic authentication refresh.
|
554
|
-
"""
|
505
|
+
"""Test automatic authentication refresh."""
|
555
506
|
self.client.post(
|
556
507
|
"http://127.0.0.1:8000/api/test/account",
|
557
508
|
data={
|
sanic_security/verification.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
import functools
|
2
2
|
from contextlib import suppress
|
3
3
|
|
4
|
+
from sanic.log import logger
|
4
5
|
from sanic.request import Request
|
5
6
|
|
6
7
|
from sanic_security.exceptions import (
|
@@ -84,12 +85,12 @@ async def two_step_verification(request: Request) -> TwoStepSession:
|
|
84
85
|
MaxedOutChallengeError
|
85
86
|
|
86
87
|
Returns:
|
87
|
-
|
88
|
+
two_step_session
|
88
89
|
"""
|
89
90
|
two_step_session = await TwoStepSession.decode(request)
|
90
91
|
two_step_session.validate()
|
91
92
|
two_step_session.bearer.validate()
|
92
|
-
await two_step_session.check_code(request
|
93
|
+
await two_step_session.check_code(request.form.get("code"))
|
93
94
|
return two_step_session
|
94
95
|
|
95
96
|
|
@@ -153,9 +154,10 @@ async def verify_account(request: Request) -> TwoStepSession:
|
|
153
154
|
if two_step_session.bearer.verified:
|
154
155
|
raise VerifiedError()
|
155
156
|
two_step_session.validate()
|
156
|
-
await two_step_session.check_code(request
|
157
|
+
await two_step_session.check_code(request.form.get("code"))
|
157
158
|
two_step_session.bearer.verified = True
|
158
159
|
await two_step_session.bearer.save(update_fields=["verified"])
|
160
|
+
logger.info(f"Account {two_step_session.bearer.id} has been verified.")
|
159
161
|
return two_step_session
|
160
162
|
|
161
163
|
|
@@ -197,7 +199,7 @@ async def captcha(request: Request) -> CaptchaSession:
|
|
197
199
|
"""
|
198
200
|
captcha_session = await CaptchaSession.decode(request)
|
199
201
|
captcha_session.validate()
|
200
|
-
await captcha_session.check_code(request
|
202
|
+
await captcha_session.check_code(request.form.get("captcha"))
|
201
203
|
return captcha_session
|
202
204
|
|
203
205
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: sanic-security
|
3
|
-
Version: 1.12.
|
3
|
+
Version: 1.12.7
|
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/
|
@@ -172,7 +172,7 @@ The tables in the below examples represent example [request form-data](https://s
|
|
172
172
|
|
173
173
|
* Initial Administrator Account
|
174
174
|
|
175
|
-
Creates
|
175
|
+
Creates root account if it doesn't exist, you should modify its credentials in config!
|
176
176
|
|
177
177
|
```python
|
178
178
|
create_initial_admin_account(app)
|
@@ -0,0 +1,16 @@
|
|
1
|
+
sanic_security/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
sanic_security/authentication.py,sha256=CzjbVh7uDjBs22oQCklcbmp5gTDW17AoYaa3lmyPnGk,13783
|
3
|
+
sanic_security/authorization.py,sha256=80vuM_2oNgfhOMuct89R6UBhDYmSWQzIgocKxhKzReE,7030
|
4
|
+
sanic_security/configuration.py,sha256=MKxYjq1q9RBRX2cMJkIe87ke0mLKa69RWoQ5MhVciho,5512
|
5
|
+
sanic_security/exceptions.py,sha256=MTPF4tm_68Nmf_z06RHH_6DTiC_CNiLER1jzEoW1dFk,5398
|
6
|
+
sanic_security/models.py,sha256=zWn9zXl-vwP8qJ-DzBuNNG7MCl1ggZX6yb6Zgh3Jfcs,22601
|
7
|
+
sanic_security/utils.py,sha256=XAUNalcTi53qTz0D8xiDyDyRlq7Z7ffNBzUONJZqe90,2705
|
8
|
+
sanic_security/verification.py,sha256=rNyZk_J53dOzk8qy5iG3yiMRuRcGaYeHwIwnFDH9TWw,7582
|
9
|
+
sanic_security/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
+
sanic_security/test/server.py,sha256=-dQAB75C-l9kfN7PnaYNDOLiLXaqg0euZAWF5oXMxeE,12164
|
11
|
+
sanic_security/test/tests.py,sha256=jUZ5kgQF4rFx1bImKqHYR7UeoYAfjtNNXSQYTMfzlg4,21994
|
12
|
+
sanic_security-1.12.7.dist-info/LICENSE,sha256=sXlJs9_mG-dCkPfWsDnuzydJWagS82E2gYtkVH9enHA,1100
|
13
|
+
sanic_security-1.12.7.dist-info/METADATA,sha256=ZGVTjtv-OVMrs_3Ac__jKE1JgdhtIAyhMCW2n3VUW3Y,23393
|
14
|
+
sanic_security-1.12.7.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
|
15
|
+
sanic_security-1.12.7.dist-info/top_level.txt,sha256=ZybkhHXSjfzhmv8XeqLvnNmLmv21Z0oPX6Ep4DJN8b0,15
|
16
|
+
sanic_security-1.12.7.dist-info/RECORD,,
|
@@ -1,16 +0,0 @@
|
|
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=XAUNalcTi53qTz0D8xiDyDyRlq7Z7ffNBzUONJZqe90,2705
|
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.6.dist-info/LICENSE,sha256=sXlJs9_mG-dCkPfWsDnuzydJWagS82E2gYtkVH9enHA,1100
|
13
|
-
sanic_security-1.12.6.dist-info/METADATA,sha256=aiKkOtkYiexSjoB4uysSQwxAVqRGAQnultZKvx5srAs,23382
|
14
|
-
sanic_security-1.12.6.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
|
15
|
-
sanic_security-1.12.6.dist-info/top_level.txt,sha256=ZybkhHXSjfzhmv8XeqLvnNmLmv21Z0oPX6Ep4DJN8b0,15
|
16
|
-
sanic_security-1.12.6.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|