sanic-security 1.11.7__py3-none-any.whl → 1.16.7__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- 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)
|