sanic-security 1.11.7__py3-none-any.whl → 1.16.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 +192 -203
- sanic_security/authorization.py +110 -64
- sanic_security/configuration.py +42 -25
- sanic_security/exceptions.py +58 -24
- sanic_security/models.py +287 -161
- sanic_security/oauth.py +238 -0
- sanic_security/test/server.py +174 -112
- sanic_security/test/tests.py +137 -103
- sanic_security/utils.py +67 -28
- sanic_security/verification.py +59 -46
- sanic_security-1.16.7.dist-info/LICENSE +21 -0
- {sanic_security-1.11.7.dist-info → sanic_security-1.16.7.dist-info}/METADATA +685 -591
- sanic_security-1.16.7.dist-info/RECORD +17 -0
- {sanic_security-1.11.7.dist-info → sanic_security-1.16.7.dist-info}/WHEEL +2 -1
- sanic_security-1.16.7.dist-info/top_level.txt +1 -0
- sanic_security-1.11.7.dist-info/LICENSE +0 -661
- sanic_security-1.11.7.dist-info/RECORD +0 -15
sanic_security/test/server.py
CHANGED
@@ -1,5 +1,8 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
import datetime
|
2
|
+
import traceback
|
3
|
+
|
4
|
+
from httpx_oauth.clients.google import GoogleOAuth2
|
5
|
+
from sanic import Sanic, text, raw, redirect
|
3
6
|
from tortoise.contrib.sanic import register_tortoise
|
4
7
|
|
5
8
|
from sanic_security.authentication import (
|
@@ -7,57 +10,65 @@ from sanic_security.authentication import (
|
|
7
10
|
register,
|
8
11
|
requires_authentication,
|
9
12
|
logout,
|
10
|
-
create_initial_admin_account,
|
11
13
|
fulfill_second_factor,
|
14
|
+
initialize_security,
|
12
15
|
)
|
13
16
|
from sanic_security.authorization import (
|
14
17
|
assign_role,
|
15
18
|
check_permissions,
|
16
19
|
check_roles,
|
17
20
|
)
|
18
|
-
from sanic_security.configuration import config
|
19
|
-
from sanic_security.exceptions import SecurityError
|
21
|
+
from sanic_security.configuration import config
|
22
|
+
from sanic_security.exceptions import SecurityError
|
20
23
|
from sanic_security.models import Account, CaptchaSession, AuthenticationSession
|
21
|
-
from sanic_security.
|
24
|
+
from sanic_security.oauth import (
|
25
|
+
oauth_encode,
|
26
|
+
initialize_oauth,
|
27
|
+
oauth_callback,
|
28
|
+
oauth_decode,
|
29
|
+
oauth_revoke,
|
30
|
+
)
|
31
|
+
from sanic_security.utils import json, str_to_bool, password_hasher
|
22
32
|
from sanic_security.verification import (
|
23
33
|
request_two_step_verification,
|
24
34
|
requires_two_step_verification,
|
25
35
|
verify_account,
|
26
|
-
request_captcha,
|
27
36
|
requires_captcha,
|
28
37
|
)
|
29
38
|
|
30
39
|
"""
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
40
|
+
Copyright (c) 2020-present Nicholas Aidan Stewart
|
41
|
+
|
42
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
43
|
+
of this software and associated documentation files (the "Software"), to deal
|
44
|
+
in the Software without restriction, including without limitation the rights
|
45
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
46
|
+
copies of the Software, and to permit persons to whom the Software is
|
47
|
+
furnished to do so, subject to the following conditions:
|
48
|
+
|
49
|
+
The above copyright notice and this permission notice shall be included in all
|
50
|
+
copies or substantial portions of the Software.
|
51
|
+
|
52
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
53
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
54
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
55
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
56
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
57
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
58
|
+
SOFTWARE.
|
46
59
|
"""
|
47
60
|
|
48
|
-
app = Sanic("
|
49
|
-
|
61
|
+
app = Sanic("tests")
|
62
|
+
google_oauth = GoogleOAuth2(config.OAUTH_CLIENT, config.OAUTH_SECRET)
|
50
63
|
|
51
64
|
|
52
65
|
@app.post("api/test/auth/register")
|
53
66
|
async def on_register(request):
|
54
|
-
"""
|
55
|
-
Register an account with email and password.
|
56
|
-
"""
|
67
|
+
"""Register an account with email and password."""
|
57
68
|
account = await register(
|
58
69
|
request,
|
59
|
-
verified=request.form.get("verified")
|
60
|
-
disabled=request.form.get("disabled")
|
70
|
+
verified=str_to_bool(request.form.get("verified")),
|
71
|
+
disabled=str_to_bool(request.form.get("disabled")),
|
61
72
|
)
|
62
73
|
if not account.verified:
|
63
74
|
two_step_session = await request_two_step_verification(request, account)
|
@@ -72,9 +83,7 @@ async def on_register(request):
|
|
72
83
|
|
73
84
|
@app.post("api/test/auth/verify")
|
74
85
|
async def on_verify(request):
|
75
|
-
"""
|
76
|
-
Verifies client account.
|
77
|
-
"""
|
86
|
+
"""Verifies client account."""
|
78
87
|
two_step_session = await verify_account(request)
|
79
88
|
return json(
|
80
89
|
"You have verified your account and may login!", two_step_session.bearer.json
|
@@ -83,14 +92,14 @@ async def on_verify(request):
|
|
83
92
|
|
84
93
|
@app.post("api/test/auth/login")
|
85
94
|
async def on_login(request):
|
86
|
-
"""
|
87
|
-
Login to an account with an email and password.
|
88
|
-
"""
|
89
|
-
two_factor_authentication = request.args.get("two-factor-authentication") == "true"
|
95
|
+
"""Login to an account with an email and password."""
|
90
96
|
authentication_session = await login(
|
91
|
-
request,
|
97
|
+
request,
|
98
|
+
require_second_factor=str_to_bool(
|
99
|
+
request.args.get("two-factor-authentication")
|
100
|
+
),
|
92
101
|
)
|
93
|
-
if
|
102
|
+
if str_to_bool(request.args.get("two-factor-authentication")):
|
94
103
|
two_step_session = await request_two_step_verification(
|
95
104
|
request, authentication_session.bearer
|
96
105
|
)
|
@@ -100,54 +109,75 @@ async def on_login(request):
|
|
100
109
|
)
|
101
110
|
two_step_session.encode(response)
|
102
111
|
else:
|
103
|
-
response = json("Login successful!", authentication_session.
|
112
|
+
response = json("Login successful!", authentication_session.json)
|
113
|
+
authentication_session.encode(response)
|
114
|
+
return response
|
115
|
+
|
116
|
+
|
117
|
+
@app.post("api/test/auth/login/anon")
|
118
|
+
async def on_login_anonymous(request):
|
119
|
+
"""Login as anonymous user."""
|
120
|
+
authentication_session = await AuthenticationSession.new(request)
|
121
|
+
response = json(
|
122
|
+
"Anonymous user now associated with session!", authentication_session.json
|
123
|
+
)
|
104
124
|
authentication_session.encode(response)
|
105
125
|
return response
|
106
126
|
|
107
127
|
|
108
128
|
@app.post("api/test/auth/validate-2fa")
|
109
129
|
async def on_two_factor_authentication(request):
|
110
|
-
"""
|
111
|
-
Fulfills client authentication session's second factor requirement.
|
112
|
-
"""
|
130
|
+
"""Fulfills client authentication session's second factor requirement."""
|
113
131
|
authentication_session = await fulfill_second_factor(request)
|
114
132
|
response = json(
|
115
133
|
"Authentication session second-factor fulfilled! You are now authenticated.",
|
116
134
|
authentication_session.bearer.json,
|
117
135
|
)
|
118
|
-
authentication_session.encode(response)
|
119
136
|
return response
|
120
137
|
|
121
138
|
|
122
139
|
@app.post("api/test/auth/logout")
|
123
140
|
async def on_logout(request):
|
124
|
-
"""
|
125
|
-
Logout of currently logged in account.
|
126
|
-
"""
|
141
|
+
"""Logout of currently logged in account."""
|
127
142
|
authentication_session = await logout(request)
|
128
|
-
|
143
|
+
await oauth_revoke(request, google_oauth)
|
144
|
+
response = json("Logout successful!", authentication_session.json)
|
129
145
|
return response
|
130
146
|
|
131
147
|
|
132
148
|
@app.post("api/test/auth")
|
133
|
-
@requires_authentication
|
149
|
+
@requires_authentication
|
134
150
|
async def on_authenticate(request):
|
135
|
-
"""
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
151
|
+
"""Authenticate client session and account."""
|
152
|
+
response = json(
|
153
|
+
"Authenticated!",
|
154
|
+
{
|
155
|
+
"bearer": (
|
156
|
+
request.ctx.session.bearer.json
|
157
|
+
if not request.ctx.session.anonymous
|
158
|
+
else None
|
159
|
+
),
|
160
|
+
"refresh": request.ctx.session.is_refresh,
|
161
|
+
},
|
162
|
+
)
|
140
163
|
return response
|
141
164
|
|
142
165
|
|
143
|
-
@app.post("api/test/auth/
|
166
|
+
@app.post("api/test/auth/expire")
|
167
|
+
@requires_authentication
|
168
|
+
async def on_authentication_expire(request):
|
169
|
+
"""Expire client's session."""
|
170
|
+
request.ctx.session.expiration_date = datetime.datetime.now(datetime.UTC)
|
171
|
+
await request.ctx.session.save(update_fields=["expiration_date"])
|
172
|
+
return json("Authentication expired!", request.ctx.session.json)
|
173
|
+
|
174
|
+
|
175
|
+
@app.get("api/test/auth/associated")
|
144
176
|
@requires_authentication
|
145
177
|
async def on_get_associated_authentication_sessions(request):
|
146
|
-
"""
|
147
|
-
Retrieves authentication sessions associated with logged in account.
|
148
|
-
"""
|
178
|
+
"""Retrieves authentication sessions associated with logged in account."""
|
149
179
|
authentication_sessions = await AuthenticationSession.get_associated(
|
150
|
-
request.ctx.
|
180
|
+
request.ctx.session.bearer
|
151
181
|
)
|
152
182
|
return json(
|
153
183
|
"Associated authentication sessions retrieved!",
|
@@ -157,10 +187,8 @@ async def on_get_associated_authentication_sessions(request):
|
|
157
187
|
|
158
188
|
@app.get("api/test/capt/request")
|
159
189
|
async def on_captcha_request(request):
|
160
|
-
"""
|
161
|
-
|
162
|
-
"""
|
163
|
-
captcha_session = await request_captcha(request)
|
190
|
+
"""Request captcha with solution in response."""
|
191
|
+
captcha_session = await CaptchaSession.new(request)
|
164
192
|
response = json("Captcha request successful!", captcha_session.code)
|
165
193
|
captcha_session.encode(response)
|
166
194
|
return response
|
@@ -168,29 +196,28 @@ async def on_captcha_request(request):
|
|
168
196
|
|
169
197
|
@app.get("api/test/capt/image")
|
170
198
|
async def on_captcha_image(request):
|
171
|
-
"""
|
172
|
-
Request captcha image.
|
173
|
-
"""
|
199
|
+
"""Request captcha image."""
|
174
200
|
captcha_session = await CaptchaSession.decode(request)
|
175
|
-
|
176
|
-
|
177
|
-
|
201
|
+
return raw(captcha_session.get_image(), content_type="image/jpeg")
|
202
|
+
|
203
|
+
|
204
|
+
@app.get("api/test/capt/audio")
|
205
|
+
async def on_captcha_audio(request):
|
206
|
+
"""Request captcha audio."""
|
207
|
+
captcha_session = await CaptchaSession.decode(request)
|
208
|
+
return raw(captcha_session.get_audio(), content_type="audio/mpeg")
|
178
209
|
|
179
210
|
|
180
211
|
@app.post("api/test/capt")
|
181
212
|
@requires_captcha
|
182
213
|
async def on_captcha_attempt(request):
|
183
|
-
"""
|
184
|
-
|
185
|
-
"""
|
186
|
-
return json("Captcha attempt successful!", request.ctx.captcha_session.json)
|
214
|
+
"""Attempt captcha challenge."""
|
215
|
+
return json("Captcha attempt successful!", request.ctx.session.json)
|
187
216
|
|
188
217
|
|
189
218
|
@app.post("api/test/two-step/request")
|
190
219
|
async def on_request_verification(request):
|
191
|
-
"""
|
192
|
-
Request two-step verification with code in the response.
|
193
|
-
"""
|
220
|
+
"""Request two-step verification with code in the response."""
|
194
221
|
two_step_session = await request_two_step_verification(request)
|
195
222
|
response = json("Verification request successful!", two_step_session.code)
|
196
223
|
two_step_session.encode(response)
|
@@ -200,72 +227,106 @@ async def on_request_verification(request):
|
|
200
227
|
@app.post("api/test/two-step")
|
201
228
|
@requires_two_step_verification
|
202
229
|
async def on_verification_attempt(request):
|
203
|
-
"""
|
204
|
-
|
205
|
-
"""
|
206
|
-
return json(
|
207
|
-
"Two step verification attempt successful!", request.ctx.two_step_session.json
|
208
|
-
)
|
230
|
+
"""Attempt two-step verification challenge."""
|
231
|
+
return json("Two step verification attempt successful!", request.ctx.session.json)
|
209
232
|
|
210
233
|
|
211
234
|
@app.post("api/test/auth/roles")
|
212
|
-
@requires_authentication
|
213
235
|
async def on_authorization(request):
|
214
|
-
"""
|
215
|
-
Check if client is authorized with sufficient roles and permissions.
|
216
|
-
"""
|
236
|
+
"""Check if client is authorized with sufficient roles and permissions."""
|
217
237
|
await check_roles(request, request.form.get("role"))
|
218
238
|
if request.form.get("permissions_required"):
|
219
239
|
await check_permissions(
|
220
240
|
request, *request.form.get("permissions_required").split(", ")
|
221
241
|
)
|
222
|
-
return text("Account permitted
|
242
|
+
return text("Account permitted!")
|
223
243
|
|
224
244
|
|
225
245
|
@app.post("api/test/auth/roles/assign")
|
226
246
|
@requires_authentication
|
227
247
|
async def on_role_assign(request):
|
228
|
-
"""
|
229
|
-
Assign authenticated account a role.
|
230
|
-
"""
|
248
|
+
"""Assign authenticated account a role."""
|
231
249
|
await assign_role(
|
232
250
|
request.form.get("name"),
|
233
|
-
request.ctx.
|
234
|
-
request.form.get("permissions"),
|
251
|
+
request.ctx.session.bearer,
|
235
252
|
"Role used for testing.",
|
253
|
+
*(
|
254
|
+
request.form.get("permissions").split(", ")
|
255
|
+
if request.form.get("permissions")
|
256
|
+
else []
|
257
|
+
),
|
236
258
|
)
|
237
|
-
return text("Role assigned
|
259
|
+
return text("Role assigned!")
|
238
260
|
|
239
261
|
|
240
262
|
@app.post("api/test/account")
|
241
263
|
async def on_account_creation(request):
|
242
|
-
"""
|
243
|
-
Quick account creation.
|
244
|
-
"""
|
245
|
-
if await Account.filter(email=request.form.get("email").lower()).exists():
|
246
|
-
raise CredentialsError("An account with this email already exists.", 409)
|
247
|
-
elif await Account.filter(username=request.form.get("username")).exists():
|
248
|
-
raise CredentialsError("An account with this username already exists.", 409)
|
264
|
+
"""Quick account creation."""
|
249
265
|
account = await Account.create(
|
250
266
|
username=request.form.get("username"),
|
251
|
-
email=request.form.get("email")
|
267
|
+
email=request.form.get("email"),
|
252
268
|
password=password_hasher.hash("password"),
|
253
269
|
verified=True,
|
254
|
-
|
270
|
+
disabled=False,
|
255
271
|
)
|
256
272
|
response = json("Account creation successful!", account.json)
|
257
273
|
return response
|
258
274
|
|
259
275
|
|
276
|
+
@app.route("api/test/oauth", methods=["GET", "POST"])
|
277
|
+
async def on_oauth_request(request):
|
278
|
+
"""OAuth request."""
|
279
|
+
return redirect(
|
280
|
+
await google_oauth.get_authorization_url(
|
281
|
+
"http://localhost:8000/api/test/oauth/callback",
|
282
|
+
scope=google_oauth.base_scopes,
|
283
|
+
)
|
284
|
+
)
|
285
|
+
|
286
|
+
|
287
|
+
@app.get("api/test/oauth/callback")
|
288
|
+
async def on_oauth_callback(request):
|
289
|
+
"""OAuth callback."""
|
290
|
+
token_info, authentication_session = await oauth_callback(
|
291
|
+
request,
|
292
|
+
google_oauth,
|
293
|
+
"http://localhost:8000/api/test/oauth/callback",
|
294
|
+
)
|
295
|
+
response = json(
|
296
|
+
"OAuth successful.",
|
297
|
+
{"token_info": token_info, "auth_session": authentication_session.json},
|
298
|
+
)
|
299
|
+
oauth_encode(response, token_info)
|
300
|
+
authentication_session.encode(response)
|
301
|
+
return response
|
302
|
+
|
303
|
+
|
304
|
+
@app.get("api/test/oauth/token")
|
305
|
+
@requires_authentication
|
306
|
+
async def on_oauth_token(request):
|
307
|
+
"""OAuth token retrieval."""
|
308
|
+
token_info = await oauth_decode(request, google_oauth)
|
309
|
+
return json(
|
310
|
+
"Access token retrieved!",
|
311
|
+
{"token_info": token_info, "auth_session": request.ctx.session.json},
|
312
|
+
)
|
313
|
+
|
314
|
+
|
315
|
+
@app.route("api/test/oauth/revoke", methods=["GET", "POST"])
|
316
|
+
async def on_oauth_revoke(request):
|
317
|
+
"""OAuth token revocation."""
|
318
|
+
token_info = await oauth_revoke(request, google_oauth)
|
319
|
+
return json("Access token revoked!", token_info)
|
320
|
+
|
321
|
+
|
260
322
|
@app.exception(SecurityError)
|
261
323
|
async def on_security_error(request, exception):
|
262
|
-
"""
|
263
|
-
|
264
|
-
"""
|
324
|
+
"""Handles security errors with correct response."""
|
325
|
+
traceback.print_exc()
|
265
326
|
return exception.json
|
266
327
|
|
267
328
|
|
268
|
-
|
329
|
+
config.SECRET = """
|
269
330
|
-----BEGIN RSA PRIVATE KEY-----
|
270
331
|
MIIEpAIBAAKCAQEAww3pEiUx6wMFawJNAHCI80Qj3eyrP6Yx3LNNluQZXMyZkd+6ugBN9e1hw7v2z2PwmJENhYrqbBHU4vHCHEEZjdZIQRqwriFpeeoqMA1
|
271
332
|
ecgwJz3fOuYo6WrUbS6pEyJ9vtjh5TaeZLzER+KIK2uvsjsQnFVt41hh3Xd+tR9p+QXT8aRep9hp4XLF87QlDVDrZIStfVn25+ZfSfKH+WYBUglZBmz/K6uW
|
@@ -283,7 +344,7 @@ mQ4BjbU1slel/eXlhomQpxoBCH3J/Ba9qd+uBql29QZMQXtKFg/mryjprapq8sUcbgazr9u1x+zJz9w+
|
|
283
344
|
1G1CHHo/vq8zPNkVWmhciIUeHR3YJbw==
|
284
345
|
-----END RSA PRIVATE KEY-----
|
285
346
|
"""
|
286
|
-
|
347
|
+
config.PUBLIC_SECRET = """
|
287
348
|
-----BEGIN PUBLIC KEY-----
|
288
349
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAww3pEiUx6wMFawJNAHCI80Qj3eyrP6Yx3LNNluQZXMyZkd+6ugBN9e1hw7v2z2PwmJENhYrqbBHU
|
289
350
|
4vHCHEEZjdZIQRqwriFpeeoqMA1ecgwJz3fOuYo6WrUbS6pEyJ9vtjh5TaeZLzER+KIK2uvsjsQnFVt41hh3Xd+tR9p+QXT8aRep9hp4XLF87QlDVDrZIStf
|
@@ -291,16 +352,17 @@ Vn25+ZfSfKH+WYBUglZBmz/K6uW41mSRuuH3Pu/lnPgGvsxtT7KE8dkbyrI+Tyg0pniOYdxBxgpu06S6
|
|
291
352
|
MHlkstd6FFYu5lJQcuppOm79iQIDAQAB
|
292
353
|
-----END PUBLIC KEY-----
|
293
354
|
"""
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
355
|
+
config.INITIAL_ADMIN_EMAIL = "admin@login.test"
|
356
|
+
config.SESSION_ENCODING_ALGORITHM = "RS256"
|
357
|
+
config.ALLOW_LOGIN_WITH_USERNAME = True
|
358
|
+
config.SESSION_SECURE = False
|
298
359
|
register_tortoise(
|
299
360
|
app,
|
300
|
-
db_url=
|
361
|
+
db_url=config.TEST_DATABASE_URL,
|
301
362
|
modules={"models": ["sanic_security.models"]},
|
302
363
|
generate_schemas=True,
|
303
364
|
)
|
304
|
-
|
365
|
+
initialize_security(app, True)
|
366
|
+
initialize_oauth(app)
|
305
367
|
if __name__ == "__main__":
|
306
368
|
app.run(host="127.0.0.1", port=8000, workers=1, debug=True)
|