sanic-security 1.16.12__py3-none-any.whl → 1.17.1__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.
@@ -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, "2fa"
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)