sanic-security 1.16.11__py3-none-any.whl → 1.17.0__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 +379 -362
- sanic_security/authorization.py +240 -240
- sanic_security/configuration.py +125 -125
- sanic_security/exceptions.py +164 -216
- sanic_security/models.py +721 -701
- sanic_security/oauth.py +242 -241
- sanic_security/test/server.py +368 -368
- sanic_security/test/tests.py +547 -547
- sanic_security/utils.py +121 -121
- sanic_security/verification.py +253 -249
- {sanic_security-1.16.11.dist-info → sanic_security-1.17.0.dist-info}/METADATA +672 -672
- sanic_security-1.17.0.dist-info/RECORD +17 -0
- {sanic_security-1.16.11.dist-info → sanic_security-1.17.0.dist-info}/WHEEL +1 -1
- {sanic_security-1.16.11.dist-info → sanic_security-1.17.0.dist-info}/licenses/LICENSE +21 -21
- sanic_security-1.16.11.dist-info/RECORD +0 -17
- {sanic_security-1.16.11.dist-info → sanic_security-1.17.0.dist-info}/top_level.txt +0 -0
sanic_security/test/server.py
CHANGED
@@ -1,368 +1,368 @@
|
|
1
|
-
import datetime
|
2
|
-
import traceback
|
3
|
-
|
4
|
-
from httpx_oauth.clients.google import GoogleOAuth2
|
5
|
-
from sanic import Sanic, text, raw, redirect
|
6
|
-
from tortoise.contrib.sanic import register_tortoise
|
7
|
-
|
8
|
-
from sanic_security.authentication import (
|
9
|
-
login,
|
10
|
-
register,
|
11
|
-
requires_authentication,
|
12
|
-
logout,
|
13
|
-
fulfill_second_factor,
|
14
|
-
initialize_security,
|
15
|
-
)
|
16
|
-
from sanic_security.authorization import (
|
17
|
-
assign_role,
|
18
|
-
check_permissions,
|
19
|
-
check_roles,
|
20
|
-
)
|
21
|
-
from sanic_security.configuration import config
|
22
|
-
from sanic_security.exceptions import SecurityError
|
23
|
-
from sanic_security.models import Account, CaptchaSession, AuthenticationSession
|
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
|
32
|
-
from sanic_security.verification import (
|
33
|
-
request_two_step_verification,
|
34
|
-
requires_two_step_verification,
|
35
|
-
verify_account,
|
36
|
-
requires_captcha,
|
37
|
-
)
|
38
|
-
|
39
|
-
"""
|
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.
|
59
|
-
"""
|
60
|
-
|
61
|
-
app = Sanic("tests")
|
62
|
-
google_oauth = GoogleOAuth2(config.OAUTH_CLIENT, config.OAUTH_SECRET)
|
63
|
-
|
64
|
-
|
65
|
-
@app.post("api/test/auth/register")
|
66
|
-
async def on_register(request):
|
67
|
-
"""Register an account with email and password."""
|
68
|
-
account = await register(
|
69
|
-
request,
|
70
|
-
verified=str_to_bool(request.form.get("verified")),
|
71
|
-
disabled=str_to_bool(request.form.get("disabled")),
|
72
|
-
)
|
73
|
-
if not account.verified:
|
74
|
-
two_step_session = await request_two_step_verification(request, account)
|
75
|
-
response = json(
|
76
|
-
"Registration successful! Verification required.", two_step_session.code
|
77
|
-
)
|
78
|
-
two_step_session.encode(response)
|
79
|
-
else:
|
80
|
-
response = json("Registration successful!", account.json)
|
81
|
-
return response
|
82
|
-
|
83
|
-
|
84
|
-
@app.post("api/test/auth/verify")
|
85
|
-
async def on_verify(request):
|
86
|
-
"""Verifies client account."""
|
87
|
-
two_step_session = await verify_account(request)
|
88
|
-
return json(
|
89
|
-
"You have verified your account and may login!", two_step_session.bearer.json
|
90
|
-
)
|
91
|
-
|
92
|
-
|
93
|
-
@app.post("api/test/auth/login")
|
94
|
-
async def on_login(request):
|
95
|
-
"""Login to an account with an email and password."""
|
96
|
-
authentication_session = await login(
|
97
|
-
request,
|
98
|
-
require_second_factor=str_to_bool(
|
99
|
-
request.args.get("two-factor-authentication")
|
100
|
-
),
|
101
|
-
)
|
102
|
-
if str_to_bool(request.args.get("two-factor-authentication")):
|
103
|
-
two_step_session = await request_two_step_verification(
|
104
|
-
request, authentication_session.bearer
|
105
|
-
)
|
106
|
-
response = json(
|
107
|
-
"Login successful! Two-factor authentication required.",
|
108
|
-
two_step_session.code,
|
109
|
-
)
|
110
|
-
two_step_session.encode(response)
|
111
|
-
else:
|
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
|
-
)
|
124
|
-
authentication_session.encode(response)
|
125
|
-
return response
|
126
|
-
|
127
|
-
|
128
|
-
@app.post("api/test/auth/validate-2fa")
|
129
|
-
async def on_two_factor_authentication(request):
|
130
|
-
"""Fulfills client authentication session's second factor requirement."""
|
131
|
-
authentication_session = await fulfill_second_factor(request)
|
132
|
-
response = json(
|
133
|
-
"Authentication session second-factor fulfilled! You are now authenticated.",
|
134
|
-
authentication_session.bearer.json,
|
135
|
-
)
|
136
|
-
return response
|
137
|
-
|
138
|
-
|
139
|
-
@app.post("api/test/auth/logout")
|
140
|
-
async def on_logout(request):
|
141
|
-
"""Logout of currently logged in account."""
|
142
|
-
authentication_session = await logout(request)
|
143
|
-
await oauth_revoke(request, google_oauth)
|
144
|
-
response = json("Logout successful!", authentication_session.json)
|
145
|
-
return response
|
146
|
-
|
147
|
-
|
148
|
-
@app.post("api/test/auth")
|
149
|
-
@requires_authentication
|
150
|
-
async def on_authenticate(request):
|
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
|
-
)
|
163
|
-
return response
|
164
|
-
|
165
|
-
|
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")
|
176
|
-
@requires_authentication
|
177
|
-
async def on_get_associated_authentication_sessions(request):
|
178
|
-
"""Retrieves authentication sessions associated with logged in account."""
|
179
|
-
authentication_sessions = await AuthenticationSession.get_associated(
|
180
|
-
request.ctx.session.bearer
|
181
|
-
)
|
182
|
-
return json(
|
183
|
-
"Associated authentication sessions retrieved!",
|
184
|
-
[auth_session.json for auth_session in authentication_sessions],
|
185
|
-
)
|
186
|
-
|
187
|
-
|
188
|
-
@app.get("api/test/capt/request")
|
189
|
-
async def on_captcha_request(request):
|
190
|
-
"""Request captcha with solution in response."""
|
191
|
-
captcha_session = await CaptchaSession.new(request)
|
192
|
-
response = json("Captcha request successful!", captcha_session.code)
|
193
|
-
captcha_session.encode(response)
|
194
|
-
return response
|
195
|
-
|
196
|
-
|
197
|
-
@app.get("api/test/capt/image")
|
198
|
-
async def on_captcha_image(request):
|
199
|
-
"""Request captcha image."""
|
200
|
-
captcha_session = await CaptchaSession.decode(request)
|
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")
|
209
|
-
|
210
|
-
|
211
|
-
@app.post("api/test/capt")
|
212
|
-
@requires_captcha
|
213
|
-
async def on_captcha_attempt(request):
|
214
|
-
"""Attempt captcha challenge."""
|
215
|
-
return json("Captcha attempt successful!", request.ctx.session.json)
|
216
|
-
|
217
|
-
|
218
|
-
@app.post("api/test/two-step/request")
|
219
|
-
async def on_request_verification(request):
|
220
|
-
"""Request two-step verification with code in the response."""
|
221
|
-
two_step_session = await request_two_step_verification(request)
|
222
|
-
response = json("Verification request successful!", two_step_session.code)
|
223
|
-
two_step_session.encode(response)
|
224
|
-
return response
|
225
|
-
|
226
|
-
|
227
|
-
@app.post("api/test/two-step")
|
228
|
-
@requires_two_step_verification
|
229
|
-
async def on_verification_attempt(request):
|
230
|
-
"""Attempt two-step verification challenge."""
|
231
|
-
return json("Two step verification attempt successful!", request.ctx.session.json)
|
232
|
-
|
233
|
-
|
234
|
-
@app.post("api/test/auth/roles")
|
235
|
-
async def on_authorization(request):
|
236
|
-
"""Check if client is authorized with sufficient roles and permissions."""
|
237
|
-
await check_roles(request, request.form.get("role"))
|
238
|
-
if request.form.get("permissions_required"):
|
239
|
-
await check_permissions(
|
240
|
-
request, *request.form.get("permissions_required").split(", ")
|
241
|
-
)
|
242
|
-
return text("Account permitted!")
|
243
|
-
|
244
|
-
|
245
|
-
@app.post("api/test/auth/roles/assign")
|
246
|
-
@requires_authentication
|
247
|
-
async def on_role_assign(request):
|
248
|
-
"""Assign authenticated account a role."""
|
249
|
-
await assign_role(
|
250
|
-
request.form.get("name"),
|
251
|
-
request.ctx.session.bearer,
|
252
|
-
"Role used for testing.",
|
253
|
-
*(
|
254
|
-
request.form.get("permissions").split(", ")
|
255
|
-
if request.form.get("permissions")
|
256
|
-
else []
|
257
|
-
),
|
258
|
-
)
|
259
|
-
return text("Role assigned!")
|
260
|
-
|
261
|
-
|
262
|
-
@app.post("api/test/account")
|
263
|
-
async def on_account_creation(request):
|
264
|
-
"""Quick account creation."""
|
265
|
-
account = await Account.create(
|
266
|
-
username=request.form.get("username"),
|
267
|
-
email=request.form.get("email"),
|
268
|
-
password=password_hasher.hash("password"),
|
269
|
-
verified=True,
|
270
|
-
disabled=False,
|
271
|
-
)
|
272
|
-
response = json("Account creation successful!", account.json)
|
273
|
-
return response
|
274
|
-
|
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
|
-
|
322
|
-
@app.exception(SecurityError)
|
323
|
-
async def on_security_error(request, exception):
|
324
|
-
"""Handles security errors with correct response."""
|
325
|
-
traceback.print_exc()
|
326
|
-
return exception.json
|
327
|
-
|
328
|
-
|
329
|
-
config.SECRET = """
|
330
|
-
-----BEGIN RSA PRIVATE KEY-----
|
331
|
-
MIIEpAIBAAKCAQEAww3pEiUx6wMFawJNAHCI80Qj3eyrP6Yx3LNNluQZXMyZkd+6ugBN9e1hw7v2z2PwmJENhYrqbBHU4vHCHEEZjdZIQRqwriFpeeoqMA1
|
332
|
-
ecgwJz3fOuYo6WrUbS6pEyJ9vtjh5TaeZLzER+KIK2uvsjsQnFVt41hh3Xd+tR9p+QXT8aRep9hp4XLF87QlDVDrZIStfVn25+ZfSfKH+WYBUglZBmz/K6uW
|
333
|
-
41mSRuuH3Pu/lnPgGvsxtT7KE8dkbyrI+Tyg0pniOYdxBxgpu06S6LTC8Zou0U0SGd6uOMUHT86H8uxbDTa8CNiGI251QMHlkstd6FFYu5lJQcuppOm79iQI
|
334
|
-
DAQABAoIBAACRz1RBMmV9ruIFWtcNu24u1SBw8FAniW4SGuPBbxeg1KcmOlegx3IdkBhG7j9hBF5+S/3ZhGTGhYdglYcS2aSMK0Q6ofd4NDMk+bzlIdEZNTV
|
335
|
-
bTnlle1vBjVjxOoIP7aL6mC/HFO7T+SYqjIGkjsxYFHf1DFu0nHS5OA/rOoEt1SZA5DO0dCd1IjuPvKsvJIRErjnFuW6bs9K7XNpE2gHKvtvzVFRQC2F7AY7
|
336
|
-
b45cx6QZ08yCbToITRI59RzGgrpqIsJI0N5yT96DUALQDkAJz4XzhS8+bHoCDGeTPfJLq4xXcLrtFSk5Mhp4eIOPCI/fv3IO8JnSopgeP+y+NeFMCgYEA/rq
|
337
|
-
0R5v9JuxtcbXsFXua5KWoDojOvHkeP93F5eGSDu8iRo/4zhyHWGhZuMIuMARAOJ7tAyWxDTzoSILhC4+fF6WQJKiBIlLLGXFyJ9qgq2eN+Z/b9+k6PotQV9z
|
338
|
-
unmIN8vuCrtPBlVbOMrofGHG85zSDyDDDUXZoh7ko8tJ3nosCgYEAxAb/8E/fmEADxJZSFoqwlElXm6h7sfThrhjf12ENwBv7AvH8XsiNVQsIGnoVxeHQJ7U
|
339
|
-
0pROucD/iykf8I9+ou9ZBQyfoRJiOkzExeMWEyhmGyGmcNCZ1kKK/RZu6Bks/EoqnpVH9bUjjAwSXeFRZE3zfsAclQr3BYjqFjQzuSrsCgYEA7RhLBPwkPT6
|
340
|
-
C//wcqkJKgdfO/PhJtRPnG/sIYFf84vmiJZuMMgxLzfYSzO2wn/DU9d63LN7AVVoDurpXTbN4mUH5UKWmzJPThvMZFg9gzSmt9FLfI3lqRRzWw3FYiQMriKa
|
341
|
-
hlKh03tPVSVID73SuJ2Wx43u/0OstkGa/voQ34tECgYA+G2mjnerdtgp7kpTXh4GCueoD61GlhEyseD0TZDCTGUpiGIE5FpmQxDoBCYU0eOMWcZcIZj/yWIt
|
342
|
-
mQ4BjbU1slel/eXlhomQpxoBCH3J/Ba9qd+uBql29QZMQXtKFg/mryjprapq8sUcbgazr9u1x+zJz9w+bIbvPf3MoyVwGWQKBgQDXKMG9fV+/61imgsOZTyd
|
343
|
-
2ld8MnIWAeUGgk5e6P+niAOPGFSPue3FgGvLURiJtuu05dM9U9pQhtGVrCwHcT9Yixiwpnyw31DQp3uU91DhrtHyRIf3H/ywrWLwY4Z+TsktW6UPoe2cyGbN
|
344
|
-
1G1CHHo/vq8zPNkVWmhciIUeHR3YJbw==
|
345
|
-
-----END RSA PRIVATE KEY-----
|
346
|
-
"""
|
347
|
-
config.PUBLIC_SECRET = """
|
348
|
-
-----BEGIN PUBLIC KEY-----
|
349
|
-
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAww3pEiUx6wMFawJNAHCI80Qj3eyrP6Yx3LNNluQZXMyZkd+6ugBN9e1hw7v2z2PwmJENhYrqbBHU
|
350
|
-
4vHCHEEZjdZIQRqwriFpeeoqMA1ecgwJz3fOuYo6WrUbS6pEyJ9vtjh5TaeZLzER+KIK2uvsjsQnFVt41hh3Xd+tR9p+QXT8aRep9hp4XLF87QlDVDrZIStf
|
351
|
-
Vn25+ZfSfKH+WYBUglZBmz/K6uW41mSRuuH3Pu/lnPgGvsxtT7KE8dkbyrI+Tyg0pniOYdxBxgpu06S6LTC8Zou0U0SGd6uOMUHT86H8uxbDTa8CNiGI251Q
|
352
|
-
MHlkstd6FFYu5lJQcuppOm79iQIDAQAB
|
353
|
-
-----END PUBLIC KEY-----
|
354
|
-
"""
|
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
|
359
|
-
register_tortoise(
|
360
|
-
app,
|
361
|
-
db_url=config.TEST_DATABASE_URL,
|
362
|
-
modules={"models": ["sanic_security.models"]},
|
363
|
-
generate_schemas=True,
|
364
|
-
)
|
365
|
-
initialize_security(app, True)
|
366
|
-
initialize_oauth(app)
|
367
|
-
if __name__ == "__main__":
|
368
|
-
app.run(host="127.0.0.1", port=8000, workers=1, debug=True)
|
1
|
+
import datetime
|
2
|
+
import traceback
|
3
|
+
|
4
|
+
from httpx_oauth.clients.google import GoogleOAuth2
|
5
|
+
from sanic import Sanic, text, raw, redirect
|
6
|
+
from tortoise.contrib.sanic import register_tortoise
|
7
|
+
|
8
|
+
from sanic_security.authentication import (
|
9
|
+
login,
|
10
|
+
register,
|
11
|
+
requires_authentication,
|
12
|
+
logout,
|
13
|
+
fulfill_second_factor,
|
14
|
+
initialize_security,
|
15
|
+
)
|
16
|
+
from sanic_security.authorization import (
|
17
|
+
assign_role,
|
18
|
+
check_permissions,
|
19
|
+
check_roles,
|
20
|
+
)
|
21
|
+
from sanic_security.configuration import config
|
22
|
+
from sanic_security.exceptions import SecurityError
|
23
|
+
from sanic_security.models import Account, CaptchaSession, AuthenticationSession
|
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
|
32
|
+
from sanic_security.verification import (
|
33
|
+
request_two_step_verification,
|
34
|
+
requires_two_step_verification,
|
35
|
+
verify_account,
|
36
|
+
requires_captcha,
|
37
|
+
)
|
38
|
+
|
39
|
+
"""
|
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.
|
59
|
+
"""
|
60
|
+
|
61
|
+
app = Sanic("tests")
|
62
|
+
google_oauth = GoogleOAuth2(config.OAUTH_CLIENT, config.OAUTH_SECRET)
|
63
|
+
|
64
|
+
|
65
|
+
@app.post("api/test/auth/register")
|
66
|
+
async def on_register(request):
|
67
|
+
"""Register an account with email and password."""
|
68
|
+
account = await register(
|
69
|
+
request,
|
70
|
+
verified=str_to_bool(request.form.get("verified")),
|
71
|
+
disabled=str_to_bool(request.form.get("disabled")),
|
72
|
+
)
|
73
|
+
if not account.verified:
|
74
|
+
two_step_session = await request_two_step_verification(request, account, "2fa")
|
75
|
+
response = json(
|
76
|
+
"Registration successful! Verification required.", two_step_session.code
|
77
|
+
)
|
78
|
+
two_step_session.encode(response)
|
79
|
+
else:
|
80
|
+
response = json("Registration successful!", account.json)
|
81
|
+
return response
|
82
|
+
|
83
|
+
|
84
|
+
@app.post("api/test/auth/verify")
|
85
|
+
async def on_verify(request):
|
86
|
+
"""Verifies client account."""
|
87
|
+
two_step_session = await verify_account(request)
|
88
|
+
return json(
|
89
|
+
"You have verified your account and may login!", two_step_session.bearer.json
|
90
|
+
)
|
91
|
+
|
92
|
+
|
93
|
+
@app.post("api/test/auth/login")
|
94
|
+
async def on_login(request):
|
95
|
+
"""Login to an account with an email and password."""
|
96
|
+
authentication_session = await login(
|
97
|
+
request,
|
98
|
+
require_second_factor=str_to_bool(
|
99
|
+
request.args.get("two-factor-authentication")
|
100
|
+
),
|
101
|
+
)
|
102
|
+
if str_to_bool(request.args.get("two-factor-authentication")):
|
103
|
+
two_step_session = await request_two_step_verification(
|
104
|
+
request, authentication_session.bearer
|
105
|
+
)
|
106
|
+
response = json(
|
107
|
+
"Login successful! Two-factor authentication required.",
|
108
|
+
two_step_session.code,
|
109
|
+
)
|
110
|
+
two_step_session.encode(response)
|
111
|
+
else:
|
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
|
+
)
|
124
|
+
authentication_session.encode(response)
|
125
|
+
return response
|
126
|
+
|
127
|
+
|
128
|
+
@app.post("api/test/auth/validate-2fa")
|
129
|
+
async def on_two_factor_authentication(request):
|
130
|
+
"""Fulfills client authentication session's second factor requirement."""
|
131
|
+
authentication_session = await fulfill_second_factor(request)
|
132
|
+
response = json(
|
133
|
+
"Authentication session second-factor fulfilled! You are now authenticated.",
|
134
|
+
authentication_session.bearer.json,
|
135
|
+
)
|
136
|
+
return response
|
137
|
+
|
138
|
+
|
139
|
+
@app.post("api/test/auth/logout")
|
140
|
+
async def on_logout(request):
|
141
|
+
"""Logout of currently logged in account."""
|
142
|
+
authentication_session = await logout(request)
|
143
|
+
await oauth_revoke(request, google_oauth)
|
144
|
+
response = json("Logout successful!", authentication_session.json)
|
145
|
+
return response
|
146
|
+
|
147
|
+
|
148
|
+
@app.post("api/test/auth")
|
149
|
+
@requires_authentication
|
150
|
+
async def on_authenticate(request):
|
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
|
+
)
|
163
|
+
return response
|
164
|
+
|
165
|
+
|
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")
|
176
|
+
@requires_authentication
|
177
|
+
async def on_get_associated_authentication_sessions(request):
|
178
|
+
"""Retrieves authentication sessions associated with logged in account."""
|
179
|
+
authentication_sessions = await AuthenticationSession.get_associated(
|
180
|
+
request.ctx.session.bearer
|
181
|
+
)
|
182
|
+
return json(
|
183
|
+
"Associated authentication sessions retrieved!",
|
184
|
+
[auth_session.json for auth_session in authentication_sessions],
|
185
|
+
)
|
186
|
+
|
187
|
+
|
188
|
+
@app.get("api/test/capt/request")
|
189
|
+
async def on_captcha_request(request):
|
190
|
+
"""Request captcha with solution in response."""
|
191
|
+
captcha_session = await CaptchaSession.new(request)
|
192
|
+
response = json("Captcha request successful!", captcha_session.code)
|
193
|
+
captcha_session.encode(response)
|
194
|
+
return response
|
195
|
+
|
196
|
+
|
197
|
+
@app.get("api/test/capt/image")
|
198
|
+
async def on_captcha_image(request):
|
199
|
+
"""Request captcha image."""
|
200
|
+
captcha_session = await CaptchaSession.decode(request)
|
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")
|
209
|
+
|
210
|
+
|
211
|
+
@app.post("api/test/capt")
|
212
|
+
@requires_captcha
|
213
|
+
async def on_captcha_attempt(request):
|
214
|
+
"""Attempt captcha challenge."""
|
215
|
+
return json("Captcha attempt successful!", request.ctx.session.json)
|
216
|
+
|
217
|
+
|
218
|
+
@app.post("api/test/two-step/request")
|
219
|
+
async def on_request_verification(request):
|
220
|
+
"""Request two-step verification with code in the response."""
|
221
|
+
two_step_session = await request_two_step_verification(request)
|
222
|
+
response = json("Verification request successful!", two_step_session.code)
|
223
|
+
two_step_session.encode(response)
|
224
|
+
return response
|
225
|
+
|
226
|
+
|
227
|
+
@app.post("api/test/two-step")
|
228
|
+
@requires_two_step_verification
|
229
|
+
async def on_verification_attempt(request):
|
230
|
+
"""Attempt two-step verification challenge."""
|
231
|
+
return json("Two step verification attempt successful!", request.ctx.session.json)
|
232
|
+
|
233
|
+
|
234
|
+
@app.post("api/test/auth/roles")
|
235
|
+
async def on_authorization(request):
|
236
|
+
"""Check if client is authorized with sufficient roles and permissions."""
|
237
|
+
await check_roles(request, request.form.get("role"))
|
238
|
+
if request.form.get("permissions_required"):
|
239
|
+
await check_permissions(
|
240
|
+
request, *request.form.get("permissions_required").split(", ")
|
241
|
+
)
|
242
|
+
return text("Account permitted!")
|
243
|
+
|
244
|
+
|
245
|
+
@app.post("api/test/auth/roles/assign")
|
246
|
+
@requires_authentication
|
247
|
+
async def on_role_assign(request):
|
248
|
+
"""Assign authenticated account a role."""
|
249
|
+
await assign_role(
|
250
|
+
request.form.get("name"),
|
251
|
+
request.ctx.session.bearer,
|
252
|
+
"Role used for testing.",
|
253
|
+
*(
|
254
|
+
request.form.get("permissions").split(", ")
|
255
|
+
if request.form.get("permissions")
|
256
|
+
else []
|
257
|
+
),
|
258
|
+
)
|
259
|
+
return text("Role assigned!")
|
260
|
+
|
261
|
+
|
262
|
+
@app.post("api/test/account")
|
263
|
+
async def on_account_creation(request):
|
264
|
+
"""Quick account creation."""
|
265
|
+
account = await Account.create(
|
266
|
+
username=request.form.get("username"),
|
267
|
+
email=request.form.get("email"),
|
268
|
+
password=password_hasher.hash("password"),
|
269
|
+
verified=True,
|
270
|
+
disabled=False,
|
271
|
+
)
|
272
|
+
response = json("Account creation successful!", account.json)
|
273
|
+
return response
|
274
|
+
|
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
|
+
|
322
|
+
@app.exception(SecurityError)
|
323
|
+
async def on_security_error(request, exception):
|
324
|
+
"""Handles security errors with correct response."""
|
325
|
+
traceback.print_exc()
|
326
|
+
return exception.json
|
327
|
+
|
328
|
+
|
329
|
+
config.SECRET = """
|
330
|
+
-----BEGIN RSA PRIVATE KEY-----
|
331
|
+
MIIEpAIBAAKCAQEAww3pEiUx6wMFawJNAHCI80Qj3eyrP6Yx3LNNluQZXMyZkd+6ugBN9e1hw7v2z2PwmJENhYrqbBHU4vHCHEEZjdZIQRqwriFpeeoqMA1
|
332
|
+
ecgwJz3fOuYo6WrUbS6pEyJ9vtjh5TaeZLzER+KIK2uvsjsQnFVt41hh3Xd+tR9p+QXT8aRep9hp4XLF87QlDVDrZIStfVn25+ZfSfKH+WYBUglZBmz/K6uW
|
333
|
+
41mSRuuH3Pu/lnPgGvsxtT7KE8dkbyrI+Tyg0pniOYdxBxgpu06S6LTC8Zou0U0SGd6uOMUHT86H8uxbDTa8CNiGI251QMHlkstd6FFYu5lJQcuppOm79iQI
|
334
|
+
DAQABAoIBAACRz1RBMmV9ruIFWtcNu24u1SBw8FAniW4SGuPBbxeg1KcmOlegx3IdkBhG7j9hBF5+S/3ZhGTGhYdglYcS2aSMK0Q6ofd4NDMk+bzlIdEZNTV
|
335
|
+
bTnlle1vBjVjxOoIP7aL6mC/HFO7T+SYqjIGkjsxYFHf1DFu0nHS5OA/rOoEt1SZA5DO0dCd1IjuPvKsvJIRErjnFuW6bs9K7XNpE2gHKvtvzVFRQC2F7AY7
|
336
|
+
b45cx6QZ08yCbToITRI59RzGgrpqIsJI0N5yT96DUALQDkAJz4XzhS8+bHoCDGeTPfJLq4xXcLrtFSk5Mhp4eIOPCI/fv3IO8JnSopgeP+y+NeFMCgYEA/rq
|
337
|
+
0R5v9JuxtcbXsFXua5KWoDojOvHkeP93F5eGSDu8iRo/4zhyHWGhZuMIuMARAOJ7tAyWxDTzoSILhC4+fF6WQJKiBIlLLGXFyJ9qgq2eN+Z/b9+k6PotQV9z
|
338
|
+
unmIN8vuCrtPBlVbOMrofGHG85zSDyDDDUXZoh7ko8tJ3nosCgYEAxAb/8E/fmEADxJZSFoqwlElXm6h7sfThrhjf12ENwBv7AvH8XsiNVQsIGnoVxeHQJ7U
|
339
|
+
0pROucD/iykf8I9+ou9ZBQyfoRJiOkzExeMWEyhmGyGmcNCZ1kKK/RZu6Bks/EoqnpVH9bUjjAwSXeFRZE3zfsAclQr3BYjqFjQzuSrsCgYEA7RhLBPwkPT6
|
340
|
+
C//wcqkJKgdfO/PhJtRPnG/sIYFf84vmiJZuMMgxLzfYSzO2wn/DU9d63LN7AVVoDurpXTbN4mUH5UKWmzJPThvMZFg9gzSmt9FLfI3lqRRzWw3FYiQMriKa
|
341
|
+
hlKh03tPVSVID73SuJ2Wx43u/0OstkGa/voQ34tECgYA+G2mjnerdtgp7kpTXh4GCueoD61GlhEyseD0TZDCTGUpiGIE5FpmQxDoBCYU0eOMWcZcIZj/yWIt
|
342
|
+
mQ4BjbU1slel/eXlhomQpxoBCH3J/Ba9qd+uBql29QZMQXtKFg/mryjprapq8sUcbgazr9u1x+zJz9w+bIbvPf3MoyVwGWQKBgQDXKMG9fV+/61imgsOZTyd
|
343
|
+
2ld8MnIWAeUGgk5e6P+niAOPGFSPue3FgGvLURiJtuu05dM9U9pQhtGVrCwHcT9Yixiwpnyw31DQp3uU91DhrtHyRIf3H/ywrWLwY4Z+TsktW6UPoe2cyGbN
|
344
|
+
1G1CHHo/vq8zPNkVWmhciIUeHR3YJbw==
|
345
|
+
-----END RSA PRIVATE KEY-----
|
346
|
+
"""
|
347
|
+
config.PUBLIC_SECRET = """
|
348
|
+
-----BEGIN PUBLIC KEY-----
|
349
|
+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAww3pEiUx6wMFawJNAHCI80Qj3eyrP6Yx3LNNluQZXMyZkd+6ugBN9e1hw7v2z2PwmJENhYrqbBHU
|
350
|
+
4vHCHEEZjdZIQRqwriFpeeoqMA1ecgwJz3fOuYo6WrUbS6pEyJ9vtjh5TaeZLzER+KIK2uvsjsQnFVt41hh3Xd+tR9p+QXT8aRep9hp4XLF87QlDVDrZIStf
|
351
|
+
Vn25+ZfSfKH+WYBUglZBmz/K6uW41mSRuuH3Pu/lnPgGvsxtT7KE8dkbyrI+Tyg0pniOYdxBxgpu06S6LTC8Zou0U0SGd6uOMUHT86H8uxbDTa8CNiGI251Q
|
352
|
+
MHlkstd6FFYu5lJQcuppOm79iQIDAQAB
|
353
|
+
-----END PUBLIC KEY-----
|
354
|
+
"""
|
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
|
359
|
+
register_tortoise(
|
360
|
+
app,
|
361
|
+
db_url=config.TEST_DATABASE_URL,
|
362
|
+
modules={"models": ["sanic_security.models"]},
|
363
|
+
generate_schemas=True,
|
364
|
+
)
|
365
|
+
initialize_security(app, True)
|
366
|
+
initialize_oauth(app)
|
367
|
+
if __name__ == "__main__":
|
368
|
+
app.run(host="127.0.0.1", port=8000, workers=1, debug=True)
|