sanic-security 1.11.6__py3-none-any.whl → 1.16.6__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,8 @@
1
- from argon2 import PasswordHasher
2
- from sanic import Sanic, text
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 as security_config
19
- from sanic_security.exceptions import SecurityError, CredentialsError
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.utils import json
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
- An effective, simple, and async security library for the Sanic framework.
32
- Copyright (C) 2020-present Aidan Stewart
33
-
34
- This program is free software: you can redistribute it and/or modify
35
- it under the terms of the GNU Affero General Public License as published
36
- by the Free Software Foundation, either version 3 of the License, or
37
- (at your option) any later version.
38
-
39
- This program is distributed in the hope that it will be useful,
40
- but WITHOUT ANY WARRANTY; without even the implied warranty of
41
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
42
- GNU Affero General Public License for more details.
43
-
44
- You should have received a copy of the GNU Affero General Public License
45
- along with this program. If not, see <https://www.gnu.org/licenses/>.
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("sanic-security-test")
49
- password_hasher = PasswordHasher()
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") == "true",
60
- disabled=request.form.get("disabled") == "true",
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, require_second_factor=two_factor_authentication
97
+ request,
98
+ require_second_factor=str_to_bool(
99
+ request.args.get("two-factor-authentication")
100
+ ),
92
101
  )
93
- if two_factor_authentication:
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.bearer.json)
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
- response = json("Logout successful!", authentication_session.bearer.json)
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
- Authenticate client session and account.
137
- """
138
- response = json("Authenticated!", request.ctx.authentication_session.bearer.json)
139
- request.ctx.authentication_session.encode(response)
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/associated")
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.authentication_session.bearer
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
- Request captcha with solution in response.
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
- response = captcha_session.get_image()
176
- captcha_session.encode(response)
177
- return response
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
- Attempt captcha challenge.
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
- Attempt two-step verification challenge.
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.authentication_session.bearer,
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.b")
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").lower(),
267
+ email=request.form.get("email"),
252
268
  password=password_hasher.hash("password"),
253
269
  verified=True,
254
- dbisabled=False,
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
- Handles security errors with correct response.
264
- """
324
+ """Handles security errors with correct response."""
325
+ traceback.print_exc()
265
326
  return exception.json
266
327
 
267
328
 
268
- security_config.SECRET = """
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
- security_config.PUBLIC_SECRET = """
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
- security_config.INITIAL_ADMIN_EMAIL = "admin@login.test"
295
- security_config.SESSION_ENCODING_ALGORITHM = "RS256"
296
- security_config.ALLOW_LOGIN_WITH_USERNAME = True
297
- security_config.SESSION_SECURE = False
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=security_config.TEST_DATABASE_URL,
361
+ db_url=config.TEST_DATABASE_URL,
301
362
  modules={"models": ["sanic_security.models"]},
302
363
  generate_schemas=True,
303
364
  )
304
- create_initial_admin_account(app)
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)