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,672 +1,672 @@
1
- Metadata-Version: 2.4
2
- Name: sanic-security
3
- Version: 1.16.12
4
- Summary: An async security library for the Sanic framework.
5
- Author-email: Aidan Stewart <me@na-stewart.com>
6
- Project-URL: Documentation, https://security.na-stewart.com/
7
- Project-URL: Repository, https://github.com/na-stewart/sanic-security
8
- Keywords: security,authentication,authorization,verification,async,sanic
9
- Classifier: Development Status :: 5 - Production/Stable
10
- Classifier: Intended Audience :: Developers
11
- Classifier: Topic :: Security
12
- Classifier: License :: OSI Approved :: MIT License
13
- Classifier: Programming Language :: Python
14
- Requires-Python: >=3.8
15
- Description-Content-Type: text/markdown
16
- License-File: LICENSE
17
- Requires-Dist: tortoise-orm>=0.17.0
18
- Requires-Dist: pyjwt>=1.7.0
19
- Requires-Dist: captcha>=0.4
20
- Requires-Dist: argon2-cffi>=20.1.0
21
- Requires-Dist: sanic>=21.3.0
22
- Provides-Extra: oauth
23
- Requires-Dist: httpx-oauth>=0.16.1; extra == "oauth"
24
- Provides-Extra: dev
25
- Requires-Dist: httpx-oauth; extra == "dev"
26
- Requires-Dist: black; extra == "dev"
27
- Requires-Dist: blacken-docs; extra == "dev"
28
- Requires-Dist: pdoc3; extra == "dev"
29
- Requires-Dist: cryptography; extra == "dev"
30
- Provides-Extra: crypto
31
- Requires-Dist: cryptography>=3.3.1; extra == "crypto"
32
- Dynamic: license-file
33
-
34
- [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
35
- [![Downloads](https://static.pepy.tech/badge/sanic-security)](https://pepy.tech/project/sanic-security)
36
- [![Conda Downloads](https://img.shields.io/conda/dn/conda-forge/sanic-security.svg)](https://anaconda.org/conda-forge/sanic-security)
37
-
38
-
39
- <!-- PROJECT LOGO -->
40
- <br />
41
- <p align="center">
42
- <h3 align="center">Sanic Security</h3>
43
- <p align="center">
44
- An async security library for the Sanic framework.
45
- </p>
46
- </p>
47
-
48
-
49
- <!-- TABLE OF CONTENTS -->
50
- ## Table of Contents
51
-
52
- * [About the Project](#about-the-project)
53
- * [Getting Started](#getting-started)
54
- * [Prerequisites](#prerequisites)
55
- * [Installation](#installation)
56
- * [Configuration](#configuration)
57
- * [Usage](#usage)
58
- * [OAuth](#oauth)
59
- * [Authentication](#authentication)
60
- * [CAPTCHA](#captcha)
61
- * [Two-step Verification](#two-step-verification)
62
- * [Authorization](#authorization)
63
- * [Testing](#testing)
64
- * [Tortoise](#tortoise)
65
- * [Contributing](#contributing)
66
- * [License](#license)
67
- * [Versioning](#versioning)
68
- * [Support](https://discord.gg/JHpZkMfKTJ)
69
-
70
- <!-- ABOUT THE PROJECT -->
71
- ## About The Project
72
-
73
- Sanic Security is an authentication, authorization, and verification library designed for use with the
74
- [Sanic](https://github.com/huge-success/sanic) web app framework. Designed to prioritize rapid prototyping with forgiving syntax and structure.
75
-
76
- * OAuth2 integration
77
- * Login, registration, and authentication with refresh mechanisms
78
- * Role based authorization with wildcard permissions
79
- * Image & audio CAPTCHA
80
- * Two-step verification
81
- * Logging & auditing
82
-
83
- Visit [security.na-stewart.com](https://security.na-stewart.com) for documentation.
84
-
85
- <!-- GETTING STARTED -->
86
- ## Getting Started
87
-
88
- In order to get started, please install [PyPI](https://pypi.org/) (likely included with your Python build).
89
-
90
- ### Installation
91
-
92
- * Install the Sanic Security package.
93
- ```shell
94
- pip3 install sanic-security
95
- ````
96
-
97
- * Install the Sanic Security package with the [cryptography](https://github.com/pyca/cryptography) dependency included.
98
-
99
- If you're planning on encoding or decoding JWTs using certain digital signature algorithms (like RSA or ECDSA which use
100
- the public secret and private secret), you will need to install the `cryptography` library. This can be installed explicitly, or
101
- as an extra requirement.
102
-
103
- ```shell
104
- pip3 install sanic-security[crypto]
105
- ````
106
-
107
- * Install the Sanic Security package with the [httpx-oauth](https://github.com/frankie567/httpx-oauth) dependency included.
108
-
109
- If you're planning on utilizing OAuth, you will need to install the `httpx-oauth` library. This can be installed explicitly, or
110
- as an extra requirement.
111
-
112
- ```shell
113
- pip3 install sanic-security[oauth]
114
- ````
115
-
116
- * Update Sanic Security if already installed.
117
-
118
- ```shell
119
- pip3 install sanic-security --upgrade
120
- ```
121
-
122
- ### Configuration
123
-
124
- Sanic Security configuration is merely a `SimpleNamespace` that can be modified using dot-notation.
125
- For example:
126
-
127
- ```python
128
- from sanic_security.configuration import config as security_config
129
-
130
- security_config.SECRET = "This is a big secret. Shhhhh"
131
- security_config.CAPTCHA_FONT = "resources/captcha-font.ttf"
132
- ```
133
-
134
- Any environment variables defined with the SANIC_SECURITY_ prefix will be applied to the config. For example, setting
135
- SANIC_SECURITY_SECRET will be loaded by the application automatically and fed into the SECRET config variable.
136
-
137
- You can load environment variables with a different prefix via `security_config.load_environment_variables("NEW_PREFIX_")` method.
138
-
139
- * Default configuration values:
140
-
141
- | Key | Value | Description |
142
- |---------------------------------------|------------------------------|-----------------------------------------------------------------------------------------------------------------------------------|
143
- | **SECRET** | This is a big secret. Shhhhh | The secret used for generating and signing JWTs. This should be a string unique to your application. Keep it safe. |
144
- | **PUBLIC_SECRET** | None | The secret used for verifying and decoding JWTs and can be publicly shared. This should be a string unique to your application. |
145
- | **OAUTH_CLIENT** | None | The client ID provided by the OAuth provider, this is used to identify the application making the OAuth request. |
146
- | **OAUTH_SECRET** | None | The client secret provided by the OAuth provider, this is used in conjunction with the client ID to authenticate the application. |
147
- | **SESSION_SAMESITE** | Strict | The SameSite attribute of session cookies. |
148
- | **SESSION_SECURE** | True | The Secure attribute of session cookies. |
149
- | **SESSION_HTTPONLY** | True | The HttpOnly attribute of session cookies. HIGHLY recommended that you do not turn this off, unless you know what you are doing. |
150
- | **SESSION_DOMAIN** | None | The Domain attribute of session cookies. |
151
- | **SESSION_ENCODING_ALGORITHM** | HS256 | The algorithm used to encode and decode session JWT's. |
152
- | **SESSION_PREFIX** | tkn | Prefix attached to the beginning of session cookies. |
153
- | **MAX_CHALLENGE_ATTEMPTS** | 3 | The maximum amount of session challenge attempts allowed. |
154
- | **CAPTCHA_SESSION_EXPIRATION** | 180 | The amount of seconds till captcha session expiration on creation. Setting to 0 will disable expiration. |
155
- | **CAPTCHA_FONT** | captcha-font.ttf | The file path to the font being used for captcha generation. Several fonts can be used by separating them via comma. |
156
- | **CAPTCHA_VOICE** | captcha-voice/ | The directory of the voice library being used for audio captcha generation. |
157
- | **TWO_STEP_SESSION_EXPIRATION** | 300 | The amount of seconds till two-step session expiration on creation. Setting to 0 will disable expiration. |
158
- | **AUTHENTICATION_SESSION_EXPIRATION** | 86400 | The amount of seconds till authentication session expiration on creation. Setting to 0 will disable expiration. |
159
- | **AUTHENTICATION_REFRESH_EXPIRATION** | 604800 | The amount of seconds till authentication refresh expiration. Setting to 0 will disable refresh mechanism. |
160
- | **ALLOW_LOGIN_WITH_USERNAME** | False | Allows login via username; unique constraint is disabled when set to false. |
161
- | **INITIAL_ADMIN_EMAIL** | admin@example.com | Email used when creating the initial admin account. |
162
- | **INITIAL_ADMIN_PASSWORD** | admin123 | Password used when creating the initial admin account. |
163
-
164
- ## Usage
165
-
166
- Sanic Security's authentication and verification functionality is session based. A new session will be created for the user after the user logs in or requests some form of verification (two-step, captcha). The session data is then encoded into a JWT and stored on a cookie on the user’s browser. The session cookie is then sent along with every subsequent request. The server can then compare the session stored on the cookie against the session information stored in the database to verify user’s identity and send a response with the corresponding state.
167
-
168
- * Initialize Sanic Security as follows:
169
- ```python
170
- initialize_security(app)
171
- initialize_oauth(app) # Remove if not utilizing OAuth
172
- if __name__ == "__main__":
173
- app.run(host="127.0.0.1", port=8000, workers=1, debug=True)
174
- ```
175
-
176
- The tables in the below examples represent example [request form-data](https://sanicframework.org/en/guide/basics/request.html#form).
177
-
178
- ## OAuth
179
-
180
- Provides users with a familiar experience by having them register/login using their existing credentials from other trusted services (such as Google, Discord, etc.).
181
-
182
- This feature is designed to complement existing protocols by linking a Sanic Security account with the user's OAuth credentials. As a result, developers can leverage all of Sanic Security's capabilities including robust session handling and account management.
183
-
184
- * Define OAuth clients
185
-
186
- You can [utilize various OAuth clients](https://frankie567.github.io/httpx-oauth/reference/httpx_oauth.clients/) based on your needs or [customize one](https://frankie567.github.io/httpx-oauth/usage/).
187
- ID and secret should be stored and referenced via configuration.
188
-
189
- ```python
190
- discord_oauth = DiscordOAuth2(
191
- "1325594509043830895",
192
- "WNMYbkDJjGlC0ej60qM-50tC9mMy0EXa",
193
- )
194
- google_oauth = GoogleOAuth2(
195
- "480512993828-e2e9tqtl2b8or62hc4l7hpoh478s3ni1.apps.googleusercontent.com",
196
- "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",
197
- )
198
- ```
199
-
200
- * Redirect to authorization URL
201
-
202
- ```python
203
- @app.route("api/security/oauth", methods=["GET", "POST"])
204
- async def on_oauth_request(request):
205
- return redirect(
206
- await google_oauth.get_authorization_url(
207
- "http://localhost:8000/api/security/oauth/callback",
208
- scope=google_oauth.base_scopes,
209
- )
210
- )
211
- ```
212
-
213
- * Handle OAuth callback
214
-
215
- ```python
216
- @app.get("api/security/oauth/callback")
217
- async def on_oauth_callback(request):
218
- token_info, authentication_session = await oauth_callback(
219
- request, google_oauth, "http://localhost:8000/api/security/oauth/callback"
220
- )
221
- response = json(
222
- "Authorization successful.",
223
- {"token_info": token_info, "auth_session": authentication_session.json},
224
- )
225
- oauth_encode(response, token_info)
226
- authentication_session.encode(response)
227
- return response
228
- ```
229
-
230
- * Get access token
231
-
232
- ```python
233
- @app.get("api/security/oauth/token")
234
- async def on_oauth_token(request):
235
- token_info = await decode_oauth(request, google_oauth)
236
- return json(
237
- "Access token retrieved.",
238
- token_info,
239
- )
240
- ```
241
-
242
- * Requires access token (This method is not called directly and instead used as a decorator)
243
-
244
- ```python
245
- @app.get("api/security/oauth/token")
246
- @requires_oauth(google_oauth)
247
- async def on_oauth_token(request):
248
- return json(
249
- "Access token retrieved.",
250
- request.ctx.oauth,
251
- )
252
- ```
253
-
254
- ## Authentication
255
-
256
- * Registration (with two-step email verification)
257
-
258
- Phone can be null or empty.
259
-
260
- | Key | Value |
261
- |--------------|---------------------|
262
- | **username** | example |
263
- | **email** | example@example.com |
264
- | **phone** | 19811354186 |
265
- | **password** | examplepass |
266
-
267
- ```python
268
- @app.post("api/security/register")
269
- async def on_register(request):
270
- account = await register(request)
271
- two_step_session = await request_two_step_verification(request, account)
272
- await email_code(
273
- account.email, two_step_session.code # Code = 24KF19
274
- ) # Custom method for emailing verification code.
275
- response = json(
276
- "Registration successful! Email verification required.", account.json
277
- )
278
- two_step_session.encode(response)
279
- return response
280
- ```
281
-
282
- * Verify Account
283
-
284
- Verifies the client's account via two-step session code.
285
-
286
- | Key | Value |
287
- |----------|--------|
288
- | **code** | 24KF19 |
289
-
290
- ```python
291
- @app.put("api/security/verify")
292
- async def on_verify(request):
293
- two_step_session = await verify_account(request)
294
- return json("You have verified your account and may login!", two_step_session.json)
295
- ```
296
-
297
- * Login (with two-step email verification)
298
-
299
- Credentials are retrieved via header are constructed by first combining the username and the password with a colon
300
- (aladdin:opensesame), and then by encoding the resulting string in base64 (YWxhZGRpbjpvcGVuc2VzYW1l).
301
- Here is an example authorization header: `Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l`.
302
-
303
- If this isn't desired, you can pass credentials into the login method instead.
304
-
305
- You can use a username as well as an email for login if `ALLOW_LOGIN_WITH_USERNAME` is true in the config.
306
-
307
- ```python
308
- @app.post("api/security/login")
309
- async def on_login(request):
310
- authentication_session = await login(request, require_second_factor=True)
311
- two_step_session = await request_two_step_verification(
312
- request, authentication_session.bearer
313
- )
314
- await email_code(
315
- authentication_session.bearer.email, two_step_session.code # Code = XGED2U
316
- ) # Custom method for emailing verification code.
317
- response = json(
318
- "Login successful! Two-factor authentication required.",
319
- authentication_session.json,
320
- )
321
- authentication_session.encode(response)
322
- two_step_session.encode(response)
323
- return response
324
- ```
325
-
326
- * Fulfill Second Factor
327
-
328
- Fulfills client authentication session's second factor requirement via two-step session code.
329
-
330
- | Key | Value |
331
- |----------|--------|
332
- | **code** | XGED2U |
333
-
334
- ```python
335
- @app.put("api/security/fulfill-2fa")
336
- async def on_two_factor_authentication(request):
337
- authentication_session = await fulfill_second_factor(request)
338
- response = json(
339
- "Authentication session second-factor fulfilled! You are now authenticated.",
340
- authentication_session.json,
341
- )
342
- return response
343
- ```
344
-
345
- * Anonymous Login
346
-
347
- Simply create a new session and encode it.
348
-
349
- ```python
350
- @app.post("api/security/login/anon")
351
- async def on_anonymous_login(request):
352
- authentication_session = await AuthenticationSession.new(request)
353
- response = json(
354
- "Anonymous client now associated with session!", authentication_session.json
355
- )
356
- authentication_session.encode(response)
357
- return response
358
- ```
359
-
360
- * Logout
361
-
362
- ```python
363
- @app.post("api/security/logout")
364
- async def on_logout(request):
365
- authentication_session = await logout(request)
366
- token_info = await oauth_revoke(
367
- request, google_oauth
368
- ) # Remove if not utilizing OAuth
369
- response = json(
370
- "Logout successful!",
371
- {"token_info": token_info, "auth_session": authentication_session.json},
372
- )
373
- return response
374
- ```
375
-
376
- * Authenticate
377
-
378
- ```python
379
- @app.post("api/security/auth")
380
- async def on_authenticate(request):
381
- authentication_session = await authenticate(request)
382
- response = json(
383
- "You have been authenticated.",
384
- authentication_session.json,
385
- )
386
- return response
387
- ```
388
-
389
- * Requires Authentication (this method is not called directly and instead used as a decorator)
390
-
391
- ```python
392
- @app.post("api/security/auth")
393
- @requires_authentication
394
- async def on_authenticate(request):
395
- response = json("You have been authenticated.", request.ctx.session.json)
396
- return response
397
- ```
398
-
399
- ## CAPTCHA
400
-
401
- Protects against spam and malicious activities by ensuring that only real humans can complete certain actions like
402
- submitting a form or creating an account. A font and voice library for CAPTCHA challenges is included in the repository,
403
- or you can download/create your own and specify its path in the configuration.
404
-
405
- * Request CAPTCHA
406
-
407
- ```python
408
- @app.get("api/security/captcha")
409
- async def on_captcha_img_request(request):
410
- captcha_session = await CaptchaSession.new(request)
411
- response = raw(
412
- captcha_session.get_image(), content_type="image/jpeg"
413
- ) # Captcha: LJ0F3U
414
- captcha_session.encode(response)
415
- return response
416
- ```
417
-
418
- * Request CAPTCHA Audio
419
-
420
- ```python
421
- @app.get("api/security/captcha/audio")
422
- async def on_captcha_audio_request(request):
423
- captcha_session = await CaptchaSession.decode(request)
424
- return raw(captcha_session.get_audio(), content_type="audio/mpeg")
425
- ```
426
-
427
- * Attempt CAPTCHA
428
-
429
- | Key | Value |
430
- |-------------|--------|
431
- | **captcha** | LJ0F3U |
432
-
433
- ```python
434
- @app.post("api/security/captcha")
435
- async def on_captcha(request):
436
- captcha_session = await captcha(request)
437
- return json("Captcha attempt successful!", captcha_session.json)
438
- ```
439
-
440
- * Requires CAPTCHA (this method is not called directly and instead used as a decorator)
441
-
442
- | Key | Value |
443
- |-------------|--------|
444
- | **captcha** | LJ0F3U |
445
-
446
- ```python
447
- @app.post("api/security/captcha")
448
- @requires_captcha
449
- async def on_captcha(request):
450
- return json("Captcha attempt successful!", request.ctx.session.json)
451
- ```
452
-
453
- ## Two-step Verification
454
-
455
- Two-step verification should be integrated with other custom functionalities, such as forgot password recovery.
456
-
457
- * Request Two-step Verification
458
-
459
- | Key | Value |
460
- |-------------|---------------------|
461
- | **email** | example@example.com |
462
-
463
- ```python
464
- @app.post("api/security/two-step/request")
465
- async def on_two_step_request(request):
466
- two_step_session = await request_two_step_verification(request) # Code = T2I58I
467
- await email_code(
468
- two_step_session.bearer.email, two_step_session.code
469
- ) # Custom method for emailing verification code.
470
- response = json("Verification request successful!", two_step_session.json)
471
- two_step_session.encode(response)
472
- return response
473
- ```
474
-
475
- * Resend Two-step Verification Code
476
-
477
- ```python
478
- @app.post("api/security/two-step/resend")
479
- async def on_two_step_resend(request):
480
- two_step_session = await TwoStepSession.decode(request) # Code = T2I58I
481
- await email_code(
482
- two_step_session.bearer.email, two_step_session.code
483
- ) # Custom method for emailing verification code.
484
- return json("Verification code resend successful!", two_step_session.json)
485
- ```
486
-
487
- * Attempt Two-step Verification
488
-
489
- | Key | Value |
490
- |----------|--------|
491
- | **code** | T2I58I |
492
-
493
- ```python
494
- @app.post("api/security/two-step")
495
- async def on_two_step_verification(request):
496
- two_step_session = await two_step_verification(request)
497
- response = json("Two-step verification attempt successful!", two_step_session.json)
498
- return response
499
- ```
500
-
501
- * Requires Two-step Verification (this method is not called directly and instead used as a decorator)
502
-
503
- | Key | Value |
504
- |----------|--------|
505
- | **code** | T2I58I |
506
-
507
- ```python
508
- @app.post("api/security/two-step")
509
- @requires_two_step_verification
510
- async def on_two_step_verification(request):
511
- response = json(
512
- "Two-step verification attempt successful!", request.ctx.session.json
513
- )
514
- return response
515
- ```
516
-
517
- ## Authorization
518
-
519
- Sanic Security uses role based authorization with wildcard permissions.
520
-
521
- Roles are created for various job functions. The permissions to perform certain operations are assigned to specific roles.
522
- Users are assigned particular roles, and through those role assignments acquire the permissions needed to perform
523
- particular system functions. Since users are not assigned permissions directly, but only acquire them through their
524
- role (or roles), management of individual user rights becomes a matter of simply assigning appropriate roles to the
525
- user's account; this simplifies common operations, such as adding a user, or changing a user's department.
526
-
527
- Wildcard permissions support the concept of multiple levels or parts. For example, you could grant a user the permission
528
- `printer:query`, `printer:query,delete`, or `printer:*`.
529
-
530
- * Assign Role
531
-
532
- ```python
533
- await assign_role(
534
- "Chat Room Moderator",
535
- account,
536
- "Can read and delete messages in all chat rooms, suspend and mute accounts, and control voice chat.",
537
- "channels:view,delete",
538
- "voice:*",
539
- "account:suspend,mute",
540
- )
541
- ```
542
-
543
- * Check Permissions
544
-
545
- ```python
546
- @app.post("api/security/perms")
547
- async def on_check_perms(request):
548
- authentication_session = await check_permissions(
549
- request, "channels:view", "voice:*"
550
- )
551
- return json("Account is authorized.", authentication_session.json)
552
- ```
553
-
554
- * Require Permissions (this method is not called directly and instead used as a decorator.)
555
-
556
- ```python
557
- @app.post("api/security/perms")
558
- @requires_permission("channels:view", "voice:*")
559
- async def on_check_perms(request):
560
- return json("Account is authorized.", request.ctx.session.json)
561
- ```
562
-
563
- * Check Roles
564
-
565
- ```python
566
- @app.post("api/security/roles")
567
- async def on_check_roles(request):
568
- authentication_session = await check_roles(request, "Chat Room Moderator")
569
- return json("Account is authorized.", authentication_session.json)
570
- ```
571
-
572
- * Require Roles (This method is not called directly and instead used as a decorator)
573
-
574
- ```python
575
- @app.post("api/security/roles")
576
- @requires_role("Chat Room Moderator")
577
- async def on_check_roles(request):
578
- return json("Account is authorized.", request.ctx.session.json)
579
- ```
580
-
581
- ## Testing
582
-
583
- * Set the `TEST_DATABASE_URL` configuration value.
584
-
585
- * Make sure the test Sanic instance (`test/server.py`) is running on your machine.
586
-
587
- * Run the test client (`test/tests.py`) for results.
588
-
589
- ## Tortoise
590
-
591
- Sanic Security uses [Tortoise ORM](https://tortoise-orm.readthedocs.io/en/latest/index.html) for database operations.
592
-
593
- Tortoise ORM is an easy-to-use asyncio ORM (Object Relational Mapper).
594
-
595
- * Initialise your models and database like so:
596
-
597
- ```python
598
- async def init():
599
- await Tortoise.init(
600
- db_url="sqlite://db.sqlite3",
601
- modules={"models": ["sanic_security.models", "app.models"]},
602
- )
603
- await Tortoise.generate_schemas()
604
- ```
605
-
606
- or
607
-
608
- ```python
609
- register_tortoise(
610
- app,
611
- db_url="sqlite://db.sqlite3",
612
- modules={"models": ["sanic_security.models", "app.models"]},
613
- generate_schemas=True,
614
- )
615
- ```
616
-
617
- * Define your models like so:
618
-
619
- ```python
620
- from tortoise.models import Model
621
- from tortoise import fields
622
-
623
-
624
- class Tournament(Model):
625
- id = fields.IntField(pk=True)
626
- name = fields.TextField()
627
- ```
628
-
629
- * Use it like so:
630
-
631
- ```python
632
- # Create instance by save
633
- tournament = Tournament(name="New Tournament")
634
- await tournament.save()
635
-
636
- # Or by .create()
637
- await Tournament.create(name="Another Tournament")
638
-
639
- # Now search for a record
640
- tour = await Tournament.filter(name__contains="Another").first()
641
- print(tour.name)
642
- ```
643
-
644
- <!-- CONTRIBUTING -->
645
- ## Contributing
646
-
647
- Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**.
648
-
649
- 1. Fork the Project
650
- 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
651
- 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
652
- 4. Push to the Branch (`git push origin feature/AmazingFeature`)
653
- 5. Open a Pull Request
654
-
655
-
656
- <!-- LICENSE -->
657
- ## License
658
-
659
- Distributed under the MIT License. See `LICENSE` for more information.
660
-
661
- <!-- Versioning -->
662
- ## Versioning
663
-
664
- **x.x.x**
665
-
666
- * MAJOR version when you make incompatible API changes.
667
-
668
- * MINOR version when you add functionality in a backwards compatible manner.
669
-
670
- * PATCH version when you make backwards compatible bug fixes.
671
-
672
- [https://semver.org/](https://semver.org/)
1
+ Metadata-Version: 2.4
2
+ Name: sanic-security
3
+ Version: 1.17.1
4
+ Summary: An async security library for the Sanic framework.
5
+ Author-email: Aidan Stewart <me@na-stewart.com>
6
+ Project-URL: Documentation, https://security.na-stewart.com/
7
+ Project-URL: Repository, https://github.com/na-stewart/sanic-security
8
+ Keywords: security,authentication,authorization,verification,async,sanic
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Topic :: Security
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python
14
+ Requires-Python: >=3.8
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: tortoise-orm>=0.17.0
18
+ Requires-Dist: pyjwt>=1.7.0
19
+ Requires-Dist: captcha>=0.4
20
+ Requires-Dist: argon2-cffi>=20.1.0
21
+ Requires-Dist: sanic>=21.3.0
22
+ Provides-Extra: oauth
23
+ Requires-Dist: httpx-oauth>=0.16.1; extra == "oauth"
24
+ Provides-Extra: dev
25
+ Requires-Dist: httpx-oauth; extra == "dev"
26
+ Requires-Dist: black; extra == "dev"
27
+ Requires-Dist: blacken-docs; extra == "dev"
28
+ Requires-Dist: pdoc3; extra == "dev"
29
+ Requires-Dist: cryptography; extra == "dev"
30
+ Provides-Extra: crypto
31
+ Requires-Dist: cryptography>=3.3.1; extra == "crypto"
32
+ Dynamic: license-file
33
+
34
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
35
+ [![Downloads](https://static.pepy.tech/badge/sanic-security)](https://pepy.tech/project/sanic-security)
36
+ [![Conda Downloads](https://img.shields.io/conda/dn/conda-forge/sanic-security.svg)](https://anaconda.org/conda-forge/sanic-security)
37
+
38
+
39
+ <!-- PROJECT LOGO -->
40
+ <br />
41
+ <p align="center">
42
+ <h3 align="center">Sanic Security</h3>
43
+ <p align="center">
44
+ An async security library for the Sanic framework.
45
+ </p>
46
+ </p>
47
+
48
+
49
+ <!-- TABLE OF CONTENTS -->
50
+ ## Table of Contents
51
+
52
+ * [About the Project](#about-the-project)
53
+ * [Getting Started](#getting-started)
54
+ * [Prerequisites](#prerequisites)
55
+ * [Installation](#installation)
56
+ * [Configuration](#configuration)
57
+ * [Usage](#usage)
58
+ * [OAuth](#oauth)
59
+ * [Authentication](#authentication)
60
+ * [CAPTCHA](#captcha)
61
+ * [Two-step Verification](#two-step-verification)
62
+ * [Authorization](#authorization)
63
+ * [Testing](#testing)
64
+ * [Tortoise](#tortoise)
65
+ * [Contributing](#contributing)
66
+ * [License](#license)
67
+ * [Versioning](#versioning)
68
+ * [Support](https://discord.gg/JHpZkMfKTJ)
69
+
70
+ <!-- ABOUT THE PROJECT -->
71
+ ## About The Project
72
+
73
+ Sanic Security is an authentication, authorization, and verification library designed for use with the
74
+ [Sanic](https://github.com/huge-success/sanic) web app framework.
75
+
76
+ * OAuth2 integration
77
+ * Login, registration, and authentication with refresh mechanisms
78
+ * Role based authorization with wildcard permissions
79
+ * Image & audio CAPTCHA
80
+ * Two-step verification
81
+ * Logging & auditing
82
+
83
+ Visit [security.na-stewart.com](https://security.na-stewart.com) for documentation.
84
+
85
+ <!-- GETTING STARTED -->
86
+ ## Getting Started
87
+
88
+ In order to get started, please install [PyPI](https://pypi.org/) (likely included with your Python build).
89
+
90
+ ### Installation
91
+
92
+ * Install the Sanic Security package.
93
+ ```shell
94
+ pip3 install sanic-security
95
+ ````
96
+
97
+ * Install the Sanic Security package with the [cryptography](https://github.com/pyca/cryptography) dependency included.
98
+
99
+ If you're planning on encoding or decoding JWTs using certain digital signature algorithms (like RSA or ECDSA which use
100
+ the public secret and private secret), you will need to install the `cryptography` library. This can be installed explicitly, or
101
+ as an extra requirement.
102
+
103
+ ```shell
104
+ pip3 install sanic-security[crypto]
105
+ ````
106
+
107
+ * Install the Sanic Security package with the [httpx-oauth](https://github.com/frankie567/httpx-oauth) dependency included.
108
+
109
+ If you're planning on utilizing OAuth, you will need to install the `httpx-oauth` library. This can be installed explicitly, or
110
+ as an extra requirement.
111
+
112
+ ```shell
113
+ pip3 install sanic-security[oauth]
114
+ ````
115
+
116
+ * Update Sanic Security if already installed.
117
+
118
+ ```shell
119
+ pip3 install sanic-security --upgrade
120
+ ```
121
+
122
+ ### Configuration
123
+
124
+ Sanic Security configuration is merely a `SimpleNamespace` that can be modified using dot-notation.
125
+ For example:
126
+
127
+ ```python
128
+ from sanic_security.configuration import config as security_config
129
+
130
+ security_config.SECRET = "This is a big secret. Shhhhh"
131
+ security_config.CAPTCHA_FONT = "resources/captcha-font.ttf"
132
+ ```
133
+
134
+ Any environment variables defined with the SANIC_SECURITY_ prefix will be applied to the config. For example, setting
135
+ SANIC_SECURITY_SECRET will be loaded by the application automatically and fed into the SECRET config variable.
136
+
137
+ You can load environment variables with a different prefix via `security_config.load_environment_variables("NEW_PREFIX_")` method.
138
+
139
+ * Default configuration values:
140
+
141
+ | Key | Value | Description |
142
+ |---------------------------------------|------------------------------|-----------------------------------------------------------------------------------------------------------------------------------|
143
+ | **SECRET** | This is a big secret. Shhhhh | The secret used for generating and signing JWTs. This should be a string unique to your application. Keep it safe. |
144
+ | **PUBLIC_SECRET** | None | The secret used for verifying and decoding JWTs and can be publicly shared. This should be a string unique to your application. |
145
+ | **OAUTH_CLIENT** | None | The client ID provided by the OAuth provider, this is used to identify the application making the OAuth request. |
146
+ | **OAUTH_SECRET** | None | The client secret provided by the OAuth provider, this is used in conjunction with the client ID to authenticate the application. |
147
+ | **SESSION_SAMESITE** | Strict | The SameSite attribute of session cookies. |
148
+ | **SESSION_SECURE** | True | The Secure attribute of session cookies. |
149
+ | **SESSION_HTTPONLY** | True | The HttpOnly attribute of session cookies. HIGHLY recommended that you do not turn this off, unless you know what you are doing. |
150
+ | **SESSION_DOMAIN** | None | The Domain attribute of session cookies. |
151
+ | **SESSION_ENCODING_ALGORITHM** | HS256 | The algorithm used to encode and decode session JWT's. |
152
+ | **SESSION_PREFIX** | tkn | Prefix attached to the beginning of session cookies. |
153
+ | **MAX_CHALLENGE_ATTEMPTS** | 3 | The maximum amount of session challenge attempts allowed. |
154
+ | **CAPTCHA_SESSION_EXPIRATION** | 180 | The amount of seconds till captcha session expiration on creation. Setting to 0 will disable expiration. |
155
+ | **CAPTCHA_FONT** | captcha-font.ttf | The file path to the font being used for captcha generation. Several fonts can be used by separating them via comma. |
156
+ | **CAPTCHA_VOICE** | captcha-voice/ | The directory of the voice library being used for audio captcha generation. |
157
+ | **TWO_STEP_SESSION_EXPIRATION** | 300 | The amount of seconds till two-step session expiration on creation. Setting to 0 will disable expiration. |
158
+ | **AUTHENTICATION_SESSION_EXPIRATION** | 86400 | The amount of seconds till authentication session expiration on creation. Setting to 0 will disable expiration. |
159
+ | **AUTHENTICATION_REFRESH_EXPIRATION** | 604800 | The amount of seconds till authentication refresh expiration. Setting to 0 will disable refresh mechanism. |
160
+ | **ALLOW_LOGIN_WITH_USERNAME** | False | Allows login via username; unique constraint is disabled when set to false. |
161
+ | **INITIAL_ADMIN_EMAIL** | admin@example.com | Email used when creating the initial admin account. |
162
+ | **INITIAL_ADMIN_PASSWORD** | admin123 | Password used when creating the initial admin account. |
163
+
164
+ ## Usage
165
+
166
+ Sanic Security's authentication and verification functionality is session based. A new session will be created for the user after the user logs in or requests some form of verification (two-step, captcha). The session data is then encoded into a JWT and stored on a cookie on the user’s browser. The session cookie is then sent along with every subsequent request. The server can then compare the session stored on the cookie against the session information stored in the database to verify user’s identity and send a response with the corresponding state.
167
+
168
+ * Initialize Sanic Security as follows:
169
+ ```python
170
+ initialize_security(app)
171
+ initialize_oauth(app) # Remove if not utilizing OAuth
172
+ if __name__ == "__main__":
173
+ app.run(host="127.0.0.1", port=8000, workers=1, debug=True)
174
+ ```
175
+
176
+ The tables in the below examples represent example [request form-data](https://sanicframework.org/en/guide/basics/request.html#form).
177
+
178
+ ## OAuth
179
+
180
+ Provides users with a familiar experience by having them register/login using their existing credentials from other trusted services (such as Google, Discord, etc.).
181
+
182
+ This feature is designed to complement existing protocols by linking Sanic Security with the user's OAuth credentials. As a result, developers can leverage robust session handling and account management.
183
+
184
+ * Define OAuth clients
185
+
186
+ You can [utilize various OAuth clients](https://frankie567.github.io/httpx-oauth/reference/httpx_oauth.clients/) based on your needs or [customize one](https://frankie567.github.io/httpx-oauth/usage/).
187
+ ID and secret should be stored and referenced via configuration.
188
+
189
+ ```python
190
+ discord_oauth = DiscordOAuth2(
191
+ "1325594509043830895",
192
+ "WNMYbkDJjGlC0ej60qM-50tC9mMy0EXa",
193
+ )
194
+ google_oauth = GoogleOAuth2(
195
+ "480512993828-e2e9tqtl2b8or62hc4l7hpoh478s3ni1.apps.googleusercontent.com",
196
+ "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",
197
+ )
198
+ ```
199
+
200
+ * Redirect to authorization URL
201
+
202
+ ```python
203
+ @app.route("api/security/oauth", methods=["GET", "POST"])
204
+ async def on_oauth_request(request):
205
+ return redirect(
206
+ await google_oauth.get_authorization_url(
207
+ "http://localhost:8000/api/security/oauth/callback",
208
+ scope=google_oauth.base_scopes,
209
+ )
210
+ )
211
+ ```
212
+
213
+ * Handle OAuth callback
214
+
215
+ ```python
216
+ @app.get("api/security/oauth/callback")
217
+ async def on_oauth_callback(request):
218
+ token_info, authentication_session = await oauth_callback(
219
+ request, google_oauth, "http://localhost:8000/api/security/oauth/callback"
220
+ )
221
+ response = json(
222
+ "Authorization successful.",
223
+ {"token_info": token_info, "auth_session": authentication_session.json},
224
+ )
225
+ oauth_encode(response, token_info)
226
+ authentication_session.encode(response)
227
+ return response
228
+ ```
229
+
230
+ * Get access token
231
+
232
+ ```python
233
+ @app.get("api/security/oauth/token")
234
+ async def on_oauth_token(request):
235
+ token_info = await decode_oauth(request, google_oauth)
236
+ return json(
237
+ "Access token retrieved.",
238
+ token_info,
239
+ )
240
+ ```
241
+
242
+ * Requires access token (This method is not called directly and instead used as a decorator)
243
+
244
+ ```python
245
+ @app.get("api/security/oauth/token")
246
+ @requires_oauth(google_oauth)
247
+ async def on_oauth_token(request):
248
+ return json(
249
+ "Access token retrieved.",
250
+ request.ctx.oauth,
251
+ )
252
+ ```
253
+
254
+ ## Authentication
255
+
256
+ * Registration (with two-step email verification)
257
+
258
+ Phone can be null or empty.
259
+
260
+ | Key | Value |
261
+ |--------------|---------------------|
262
+ | **username** | example |
263
+ | **email** | example@example.com |
264
+ | **phone** | 19811354186 |
265
+ | **password** | examplepass |
266
+
267
+ ```python
268
+ @app.post("api/security/register")
269
+ async def on_register(request):
270
+ account = await register(request)
271
+ two_step_session = await request_two_step_verification(request, account)
272
+ await email_code(
273
+ account.email, two_step_session.code # Code = 24KF19
274
+ ) # Custom method for emailing verification code.
275
+ response = json(
276
+ "Registration successful! Email verification required.", account.json
277
+ )
278
+ two_step_session.encode(response)
279
+ return response
280
+ ```
281
+
282
+ * Verify Account
283
+
284
+ Verifies the client's account via two-step session code.
285
+
286
+ | Key | Value |
287
+ |----------|--------|
288
+ | **code** | 24KF19 |
289
+
290
+ ```python
291
+ @app.put("api/security/verify")
292
+ async def on_verify(request):
293
+ two_step_session = await verify_account(request)
294
+ return json("You have verified your account and may login!", two_step_session.json)
295
+ ```
296
+
297
+ * Login (with two-step email verification)
298
+
299
+ Credentials are retrieved via header are constructed by first combining the username and the password with a colon
300
+ (aladdin:opensesame), and then by encoding the resulting string in base64 (YWxhZGRpbjpvcGVuc2VzYW1l).
301
+ Here is an example authorization header: `Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l`.
302
+
303
+ If this isn't desired, you can pass credentials into the login method instead.
304
+
305
+ You can use a username as well as an email for login if `ALLOW_LOGIN_WITH_USERNAME` is true in the config.
306
+
307
+ ```python
308
+ @app.post("api/security/login")
309
+ async def on_login(request):
310
+ authentication_session = await login(request, require_second_factor=True)
311
+ two_step_session = await request_two_step_verification(
312
+ request, authentication_session.bearer, "2fa"
313
+ )
314
+ await email_code(
315
+ authentication_session.bearer.email, two_step_session.code # Code = XGED2U
316
+ ) # Custom method for emailing verification code.
317
+ response = json(
318
+ "Login successful! Two-factor authentication required.",
319
+ authentication_session.json,
320
+ )
321
+ authentication_session.encode(response)
322
+ two_step_session.encode(response)
323
+ return response
324
+ ```
325
+
326
+ * Fulfill Second Factor
327
+
328
+ Fulfills client authentication session's second factor requirement via two-step session code.
329
+
330
+ | Key | Value |
331
+ |----------|--------|
332
+ | **code** | XGED2U |
333
+
334
+ ```python
335
+ @app.put("api/security/fulfill-2fa")
336
+ async def on_two_factor_authentication(request):
337
+ authentication_session = await fulfill_second_factor(request)
338
+ response = json(
339
+ "Authentication session second-factor fulfilled! You are now authenticated.",
340
+ authentication_session.json,
341
+ )
342
+ return response
343
+ ```
344
+
345
+ * Anonymous Login
346
+
347
+ Simply create a new session and encode it.
348
+
349
+ ```python
350
+ @app.post("api/security/login/anon")
351
+ async def on_anonymous_login(request):
352
+ authentication_session = await AuthenticationSession.new(request)
353
+ response = json(
354
+ "Anonymous client now associated with session!", authentication_session.json
355
+ )
356
+ authentication_session.encode(response)
357
+ return response
358
+ ```
359
+
360
+ * Logout
361
+
362
+ ```python
363
+ @app.post("api/security/logout")
364
+ async def on_logout(request):
365
+ authentication_session = await logout(request)
366
+ token_info = await oauth_revoke(
367
+ request, google_oauth
368
+ ) # Remove if not utilizing OAuth
369
+ response = json(
370
+ "Logout successful!",
371
+ {"token_info": token_info, "auth_session": authentication_session.json},
372
+ )
373
+ return response
374
+ ```
375
+
376
+ * Authenticate
377
+
378
+ ```python
379
+ @app.post("api/security/auth")
380
+ async def on_authenticate(request):
381
+ authentication_session = await authenticate(request)
382
+ response = json(
383
+ "You have been authenticated.",
384
+ authentication_session.json,
385
+ )
386
+ return response
387
+ ```
388
+
389
+ * Requires Authentication (this method is not called directly and instead used as a decorator)
390
+
391
+ ```python
392
+ @app.post("api/security/auth")
393
+ @requires_authentication
394
+ async def on_authenticate(request):
395
+ response = json("You have been authenticated.", request.ctx.session.json)
396
+ return response
397
+ ```
398
+
399
+ ## CAPTCHA
400
+
401
+ Protects against spam and malicious activities by ensuring that only real humans can complete certain actions like
402
+ submitting a form or creating an account. A font and voice library for CAPTCHA challenges is included in the repository,
403
+ or you can download/create your own and specify its path in the configuration.
404
+
405
+ * Request CAPTCHA
406
+
407
+ ```python
408
+ @app.get("api/security/captcha")
409
+ async def on_captcha_img_request(request):
410
+ captcha_session = await CaptchaSession.new(request)
411
+ response = raw(
412
+ captcha_session.get_image(), content_type="image/jpeg"
413
+ ) # Captcha: LJ0F3U
414
+ captcha_session.encode(response)
415
+ return response
416
+ ```
417
+
418
+ * Request CAPTCHA Audio
419
+
420
+ ```python
421
+ @app.get("api/security/captcha/audio")
422
+ async def on_captcha_audio_request(request):
423
+ captcha_session = await CaptchaSession.decode(request)
424
+ return raw(captcha_session.get_audio(), content_type="audio/mpeg")
425
+ ```
426
+
427
+ * Attempt CAPTCHA
428
+
429
+ | Key | Value |
430
+ |-------------|--------|
431
+ | **captcha** | LJ0F3U |
432
+
433
+ ```python
434
+ @app.post("api/security/captcha")
435
+ async def on_captcha(request):
436
+ captcha_session = await captcha(request)
437
+ return json("Captcha attempt successful!", captcha_session.json)
438
+ ```
439
+
440
+ * Requires CAPTCHA (this method is not called directly and instead used as a decorator)
441
+
442
+ | Key | Value |
443
+ |-------------|--------|
444
+ | **captcha** | LJ0F3U |
445
+
446
+ ```python
447
+ @app.post("api/security/captcha")
448
+ @requires_captcha
449
+ async def on_captcha(request):
450
+ return json("Captcha attempt successful!", request.ctx.session.json)
451
+ ```
452
+
453
+ ## Two-step Verification
454
+
455
+ Two-step verification should be integrated with other custom functionalities, such as forgot password recovery.
456
+
457
+ * Request Two-step Verification
458
+
459
+ | Key | Value |
460
+ |-------------|---------------------|
461
+ | **email** | example@example.com |
462
+
463
+ ```python
464
+ @app.post("api/security/two-step/request")
465
+ async def on_two_step_request(request):
466
+ two_step_session = await request_two_step_verification(request) # Code = T2I58I
467
+ await email_code(
468
+ two_step_session.bearer.email, two_step_session.code
469
+ ) # Custom method for emailing verification code.
470
+ response = json("Verification request successful!", two_step_session.json)
471
+ two_step_session.encode(response)
472
+ return response
473
+ ```
474
+
475
+ * Resend Two-step Verification Code
476
+
477
+ ```python
478
+ @app.post("api/security/two-step/resend")
479
+ async def on_two_step_resend(request):
480
+ two_step_session = await TwoStepSession.decode(request) # Code = T2I58I
481
+ await email_code(
482
+ two_step_session.bearer.email, two_step_session.code
483
+ ) # Custom method for emailing verification code.
484
+ return json("Verification code resend successful!", two_step_session.json)
485
+ ```
486
+
487
+ * Attempt Two-step Verification
488
+
489
+ | Key | Value |
490
+ |----------|--------|
491
+ | **code** | T2I58I |
492
+
493
+ ```python
494
+ @app.post("api/security/two-step")
495
+ async def on_two_step_verification(request):
496
+ two_step_session = await two_step_verification(request)
497
+ response = json("Two-step verification attempt successful!", two_step_session.json)
498
+ return response
499
+ ```
500
+
501
+ * Requires Two-step Verification (this method is not called directly and instead used as a decorator)
502
+
503
+ | Key | Value |
504
+ |----------|--------|
505
+ | **code** | T2I58I |
506
+
507
+ ```python
508
+ @app.post("api/security/two-step")
509
+ @requires_two_step_verification
510
+ async def on_two_step_verification(request):
511
+ response = json(
512
+ "Two-step verification attempt successful!", request.ctx.session.json
513
+ )
514
+ return response
515
+ ```
516
+
517
+ ## Authorization
518
+
519
+ Sanic Security uses role based authorization with wildcard permissions.
520
+
521
+ Roles are created for various job functions. The permissions to perform certain operations are assigned to specific roles.
522
+ Users are assigned particular roles, and through those role assignments acquire the permissions needed to perform
523
+ particular system functions. Since users are not assigned permissions directly, but only acquire them through their
524
+ role (or roles), management of individual user rights becomes a matter of simply assigning appropriate roles to the
525
+ user's account; this simplifies common operations, such as adding a user, or changing a user's department.
526
+
527
+ Wildcard permissions support the concept of multiple levels or parts. For example, you could grant a user the permission
528
+ `printer:query`, `printer:query,delete`, or `printer:*`.
529
+
530
+ * Assign Role
531
+
532
+ ```python
533
+ await assign_role(
534
+ "Chat Room Moderator",
535
+ account,
536
+ "Can read and delete messages in all chat rooms, suspend and mute accounts, and control voice chat.",
537
+ "channels:view,delete",
538
+ "voice:*",
539
+ "account:suspend,mute",
540
+ )
541
+ ```
542
+
543
+ * Check Permissions
544
+
545
+ ```python
546
+ @app.post("api/security/perms")
547
+ async def on_check_perms(request):
548
+ authentication_session = await check_permissions(
549
+ request, "channels:view", "voice:*"
550
+ )
551
+ return json("Account is authorized.", authentication_session.json)
552
+ ```
553
+
554
+ * Require Permissions (this method is not called directly and instead used as a decorator.)
555
+
556
+ ```python
557
+ @app.post("api/security/perms")
558
+ @requires_permission("channels:view", "voice:*")
559
+ async def on_check_perms(request):
560
+ return json("Account is authorized.", request.ctx.session.json)
561
+ ```
562
+
563
+ * Check Roles
564
+
565
+ ```python
566
+ @app.post("api/security/roles")
567
+ async def on_check_roles(request):
568
+ authentication_session = await check_roles(request, "Chat Room Moderator")
569
+ return json("Account is authorized.", authentication_session.json)
570
+ ```
571
+
572
+ * Require Roles (This method is not called directly and instead used as a decorator)
573
+
574
+ ```python
575
+ @app.post("api/security/roles")
576
+ @requires_role("Chat Room Moderator")
577
+ async def on_check_roles(request):
578
+ return json("Account is authorized.", request.ctx.session.json)
579
+ ```
580
+
581
+ ## Testing
582
+
583
+ * Set the `TEST_DATABASE_URL` configuration value.
584
+
585
+ * Make sure the test Sanic instance (`test/server.py`) is running on your machine.
586
+
587
+ * Run the test client (`test/tests.py`) for results.
588
+
589
+ ## Tortoise
590
+
591
+ Sanic Security uses [Tortoise ORM](https://tortoise-orm.readthedocs.io/en/latest/index.html) for database operations.
592
+
593
+ Tortoise ORM is an easy-to-use asyncio ORM (Object Relational Mapper).
594
+
595
+ * Initialise your models and database like so:
596
+
597
+ ```python
598
+ async def init():
599
+ await Tortoise.init(
600
+ db_url="sqlite://db.sqlite3",
601
+ modules={"models": ["sanic_security.models", "app.models"]},
602
+ )
603
+ await Tortoise.generate_schemas()
604
+ ```
605
+
606
+ or
607
+
608
+ ```python
609
+ register_tortoise(
610
+ app,
611
+ db_url="sqlite://db.sqlite3",
612
+ modules={"models": ["sanic_security.models", "app.models"]},
613
+ generate_schemas=True,
614
+ )
615
+ ```
616
+
617
+ * Define your models like so:
618
+
619
+ ```python
620
+ from tortoise.models import Model
621
+ from tortoise import fields
622
+
623
+
624
+ class Tournament(Model):
625
+ id = fields.IntField(pk=True)
626
+ name = fields.TextField()
627
+ ```
628
+
629
+ * Use it like so:
630
+
631
+ ```python
632
+ # Create instance by save
633
+ tournament = Tournament(name="New Tournament")
634
+ await tournament.save()
635
+
636
+ # Or by .create()
637
+ await Tournament.create(name="Another Tournament")
638
+
639
+ # Now search for a record
640
+ tour = await Tournament.filter(name__contains="Another").first()
641
+ print(tour.name)
642
+ ```
643
+
644
+ <!-- CONTRIBUTING -->
645
+ ## Contributing
646
+
647
+ Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**.
648
+
649
+ 1. Fork the Project
650
+ 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
651
+ 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
652
+ 4. Push to the Branch (`git push origin feature/AmazingFeature`)
653
+ 5. Open a Pull Request
654
+
655
+
656
+ <!-- LICENSE -->
657
+ ## License
658
+
659
+ Distributed under the MIT License. See `LICENSE` for more information.
660
+
661
+ <!-- Versioning -->
662
+ ## Versioning
663
+
664
+ **x.x.x**
665
+
666
+ * MAJOR version when you make incompatible API changes.
667
+
668
+ * MINOR version when you add functionality in a backwards compatible manner.
669
+
670
+ * PATCH version when you make backwards compatible bug fixes.
671
+
672
+ [https://semver.org/](https://semver.org/)