sanic-security 1.11.7__py3-none-any.whl → 1.16.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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)