sanic-security 1.11.7__py3-none-any.whl → 1.12.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,591 +1,622 @@
1
- Metadata-Version: 2.1
2
- Name: sanic-security
3
- Version: 1.11.7
4
- Summary: An effective, simple, and async security library for the Sanic framework.
5
- Author: Aidan Stewart
6
- Author-email: na.stewart365@gmail.com
7
- Requires-Python: >=3.6
8
- Classifier: Programming Language :: Python :: 3
9
- Classifier: Programming Language :: Python :: 3.6
10
- Classifier: Programming Language :: Python :: 3.7
11
- Classifier: Programming Language :: Python :: 3.8
12
- Classifier: Programming Language :: Python :: 3.9
13
- Classifier: Programming Language :: Python :: 3.10
14
- Classifier: Programming Language :: Python :: 3.11
15
- Provides-Extra: crypto
16
- Provides-Extra: dev
17
- Requires-Dist: argon2-cffi (>=20.1.0)
18
- Requires-Dist: black ; extra == "dev"
19
- Requires-Dist: blacken-docs ; extra == "dev"
20
- Requires-Dist: captcha (==0.4)
21
- Requires-Dist: cryptography (>=3.3.1) ; extra == "dev" or extra == "crypto"
22
- Requires-Dist: httpx (>=0.13.0) ; extra == "dev"
23
- Requires-Dist: pdoc3 ; extra == "dev"
24
- Requires-Dist: pillow (==9.5.0)
25
- Requires-Dist: pyjwt (>=1.7.0)
26
- Requires-Dist: sanic (>=21.3.0)
27
- Requires-Dist: tortoise-orm (>=0.17.0)
28
- Description-Content-Type: text/markdown
29
-
30
- <!-- PROJECT SHIELDS -->
31
- <!--
32
- *** I'm using markdown "reference style" links for readability.
33
- *** Reference links are enclosed in brackets [ ] instead of parentheses ( ).
34
- *** See the bottom of this document for the declaration of the reference variables
35
- *** for contributors-url, forks-url, etc. This is an optional, concise syntax you may use.
36
- *** https://www.markdownguide.org/basic-syntax/#reference-style-links
37
- -->
38
-
39
- [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
40
- [![Downloads](https://pepy.tech/badge/sanic-security)](https://pepy.tech/project/sanic-security)
41
- [![Conda Downloads](https://img.shields.io/conda/dn/conda-forge/sanic-security.svg)](https://anaconda.org/conda-forge/sanic-security)
42
-
43
-
44
- <!-- PROJECT LOGO -->
45
- <br />
46
- <p align="center">
47
- <h3 align="center">Sanic Security</h3>
48
- <p align="center">
49
- An effective, simple, and async security library for the Sanic framework.
50
- </p>
51
- </p>
52
-
53
-
54
- <!-- TABLE OF CONTENTS -->
55
- ## Table of Contents
56
-
57
- * [About the Project](#about-the-project)
58
- * [Getting Started](#getting-started)
59
- * [Prerequisites](#prerequisites)
60
- * [Installation](#installation)
61
- * [Configuration](#configuration)
62
- * [Usage](#usage)
63
- * [Authentication](#authentication)
64
- * [Captcha](#captcha)
65
- * [Two Step Verification](#two-step-verification)
66
- * [Authorization](#authorization)
67
- * [Testing](#testing)
68
- * [Tortoise](#tortoise)
69
- * [Contributing](#contributing)
70
- * [License](#license)
71
- * [Versioning](#versioning)
72
- * [Support](https://discord.gg/JHpZkMfKTJ)
73
-
74
- <!-- ABOUT THE PROJECT -->
75
- ## About The Project
76
-
77
- Sanic Security is an authentication, authorization, and verification library designed for use with [Sanic](https://github.com/huge-success/sanic).
78
- This library contains a variety of features including:
79
-
80
- * Login, registration, and authentication
81
- * Two-factor authentication
82
- * Captcha
83
- * Two-step verification
84
- * Role based authorization with wildcard permissions
85
-
86
- Please visit [security.na-stewart.com](https://security.na-stewart.com) for documentation,
87
-
88
- and check out [blog.na-stewart.com](https://github.com/na-stewart/Aidans-Page) for an example implementation.
89
-
90
- <!-- GETTING STARTED -->
91
- ## Getting Started
92
-
93
- In order to get started, please install [Pip](https://pypi.org/).
94
-
95
- ### Installation
96
-
97
- * Install the Sanic Security pip package.
98
- ```shell
99
- pip3 install sanic-security
100
- ````
101
-
102
- * Install the Sanic Security pip package with the `cryptography` dependency included.
103
-
104
- If you are planning on encoding or decoding JWTs using certain digital signature algorithms (like RSA or ECDSA which use
105
- the public secret and private secret), you will need to install the `cryptography` library. This can be installed explicitly, or
106
- as a required extra in the `sanic-security` requirement.
107
-
108
- ```shell
109
- pip3 install sanic-security[crypto]
110
- ````
111
-
112
- * For developers, fork Sanic Security and install development dependencies.
113
- ```shell
114
- pip3 install -e ".[dev]"
115
- ````
116
-
117
- * Update sanic-security if already installed.
118
- ```shell
119
- pip3 install --upgrade sanic-security
120
- ```
121
-
122
- ### Configuration
123
-
124
- Sanic Security configuration is merely an object that can be modified either using dot-notation or like a
125
- dictionary.
126
-
127
- For example:
128
-
129
- ```python
130
- from sanic_security.configuration import config
131
-
132
- config.SECRET = "This is a big secret. Shhhhh"
133
- config["CAPTCHA_FONT"] = "./resources/captcha-font.ttf"
134
- ```
135
-
136
- You can also use the update() method like on regular dictionaries.
137
-
138
- Any environment variables defined with the SANIC_SECURITY_ prefix will be applied to the config. For example, setting
139
- SANIC_SECURITY_SECRET will be loaded by the application automatically and fed into the SECRET config variable.
140
-
141
- You can load environment variables with a different prefix via calling the `config.load_environment_variables("NEW_PREFIX_")` method.
142
-
143
- * Default configuration values:
144
-
145
- | Key | Value | Description |
146
- |---------------------------------------|------------------------------|----------------------------------------------------------------------------------------------------------------------------------|
147
- | **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. |
148
- | **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. |
149
- | **SESSION_SAMESITE** | strict | The SameSite attribute of session cookies. |
150
- | **SESSION_SECURE** | True | The Secure attribute of session cookies. |
151
- | **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. |
152
- | **SESSION_DOMAIN** | None | The Domain attribute of session cookies. |
153
- | **SESSION_ENCODING_ALGORITHM** | HS256 | The algorithm used to encode and decode session JWT's. |
154
- | **SESSION_PREFIX** | token | Prefix attached to the beginning of session cookies. |
155
- | **MAX_CHALLENGE_ATTEMPTS** | 5 | The maximum amount of session challenge attempts allowed. |
156
- | **CAPTCHA_SESSION_EXPIRATION** | 60 | The amount of seconds till captcha session expiration on creation. Setting to 0 will disable expiration. |
157
- | **CAPTCHA_FONT** | captcha-font.ttf | The file path to the font being used for captcha generation. |
158
- | **TWO_STEP_SESSION_EXPIRATION** | 200 | The amount of seconds till two step session expiration on creation. Setting to 0 will disable expiration. |
159
- | **AUTHENTICATION_SESSION_EXPIRATION** | 2692000 | The amount of seconds till authentication session expiration on creation. Setting to 0 will disable expiration. |
160
- | **ALLOW_LOGIN_WITH_USERNAME** | False | Allows login via username and email. |
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 would be sent
167
- 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.
168
-
169
- The tables in the below examples represent example [request form-data](https://sanicframework.org/en/guide/basics/request.html#form).
170
-
171
- ## Authentication
172
-
173
- * Initial Administrator Account
174
-
175
- This account can be logged into and has complete authoritative access. Login credentials should be modified in config!
176
-
177
- ```python
178
- create_initial_admin_account(app)
179
- if __name__ == "__main__":
180
- app.run(host="127.0.0.1", port=8000)
181
- ```
182
-
183
- * Registration (With Two-step Verification)
184
-
185
- Phone can be null or empty.
186
-
187
- | Key | Value |
188
- |--------------|---------------------|
189
- | **username** | example |
190
- | **email** | example@example.com |
191
- | **phone** | 19811354186 |
192
- | **password** | examplepass |
193
-
194
- ```python
195
- @app.post("api/security/register")
196
- async def on_register(request):
197
- account = await register(request)
198
- two_step_session = await request_two_step_verification(request, account)
199
- await email_code(
200
- account.email, two_step_session.code # Code = AJ8HGD
201
- ) # Custom method for emailing verification code.
202
- response = json(
203
- "Registration successful! Email verification required.",
204
- two_step_session.bearer.json,
205
- )
206
- two_step_session.encode(response)
207
- return response
208
- ```
209
-
210
- * Verify Account
211
-
212
- Verifies the client's account via two-step session code.
213
-
214
- | Key | Value |
215
- |----------|--------|
216
- | **code** | AJ8HGD |
217
-
218
- ```python
219
- @app.post("api/security/verify")
220
- async def on_verify(request):
221
- two_step_session = await verify_account(request)
222
- return json(
223
- "You have verified your account and may login!", two_step_session.bearer.json
224
- )
225
- ```
226
-
227
- * Login (With Two-factor Authentication)
228
-
229
- Login credentials are retrieved via the Authorization header. Credentials are constructed by first combining the
230
- username and the password with a colon (aladdin:opensesame), and then by encoding the resulting string in base64
231
- (YWxhZGRpbjpvcGVuc2VzYW1l). Here is an example authorization header: `Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l`.
232
-
233
- You can use a username as well as an email for login if `ALLOW_LOGIN_WITH_USERNAME` is true in the config.
234
-
235
- ```python
236
- @app.post("api/security/login")
237
- async def on_login(request):
238
- authentication_session = await login(request, require_second_factor=True)
239
- two_step_session = await request_two_step_verification(
240
- request, authentication_session.bearer
241
- )
242
- await email_code(
243
- authentication_session.bearer.email, two_step_session.code # Code = BG5KLP
244
- ) # Custom method for emailing verification code.
245
- response = json(
246
- "Login successful! Two-factor authentication required.",
247
- authentication_session.bearer.json,
248
- )
249
- authentication_session.encode(response)
250
- two_step_session.encode(response)
251
- return response
252
- ```
253
-
254
- * Fulfill Second Factor
255
-
256
- Fulfills client authentication session's second factor requirement via two-step session code.
257
-
258
- | Key | Value |
259
- |----------|--------|
260
- | **code** | BG5KLP |
261
-
262
- ```python
263
- @app.post("api/security/validate-2fa")
264
- async def on_two_factor_authentication(request):
265
- authentication_session = await fulfill_second_factor(request)
266
- response = json(
267
- "Authentication session second-factor fulfilled! You are now authenticated.",
268
- authentication_session.bearer.json,
269
- )
270
- authentication_session.encode(response)
271
- return response
272
- ```
273
-
274
- * Logout
275
-
276
- ```python
277
- @app.post("api/security/logout")
278
- async def on_logout(request):
279
- authentication_session = await logout(request)
280
- response = json("Logout successful!", authentication_session.bearer.json)
281
- return response
282
- ```
283
-
284
- * Authenticate
285
-
286
- ```python
287
- @app.post("api/security/auth")
288
- async def on_authenticate(request):
289
- authentication_session = await authenticate(request)
290
- return json(
291
- "You have been authenticated.",
292
- authentication_session.bearer.json,
293
- )
294
- ```
295
-
296
- * Requires Authentication (This method is not called directly and instead used as a decorator.)
297
-
298
- ```python
299
- @app.post("api/security/auth")
300
- @requires_authentication
301
- async def on_authenticate(request):
302
- return json(
303
- "You have been authenticated.",
304
- request.ctx.authentication_session.bearer.json,
305
- )
306
- ```
307
-
308
- ## Captcha
309
-
310
- A pre-existing font for captcha challenges is included in the Sanic Security repository. You may set your own font by
311
- downloading a .ttf font and defining the file's path in the configuration.
312
-
313
- [1001 Free Fonts](https://www.1001fonts.com/)
314
-
315
- [Recommended Font](https://www.1001fonts.com/source-sans-pro-font.html)
316
-
317
- Captcha challenge example:
318
-
319
- [![Captcha image.](https://github.com/na-stewart/sanic-security/blob/main/images/captcha.png?raw=true)](https://github.com/na-stewart/sanic-security/blob/main/images/captcha.png?raw=true)
320
-
321
- * Request Captcha
322
-
323
- ```python
324
- @app.get("api/security/captcha")
325
- async def on_captcha_img_request(request):
326
- captcha_session = await request_captcha(request)
327
- response = captcha_session.get_image() # Captcha: FV9NMQ
328
- captcha_session.encode(response)
329
- return response
330
- ```
331
-
332
- * Captcha
333
-
334
- | Key | Value |
335
- |-------------|--------|
336
- | **captcha** | FV9NMQ |
337
-
338
- ```python
339
- @app.post("api/security/captcha")
340
- async def on_captcha(request):
341
- captcha_session = await captcha(request)
342
- return json("Captcha attempt successful!", captcha_session.json)
343
- ```
344
-
345
- * Requires Captcha (This method is not called directly and instead used as a decorator.)
346
-
347
- | Key | Value |
348
- |-------------|--------|
349
- | **captcha** | FV9NMQ |
350
-
351
- ```python
352
- @app.post("ap/security/captcha")
353
- @requires_captcha
354
- async def on_captcha(request):
355
- return json("Captcha attempt successful!", request.ctx.captcha_session.json)
356
- ```
357
-
358
- ## Two-step Verification
359
-
360
- Two-step verification should be integrated with other custom functionality. For example, account verification during registration.
361
-
362
- * Request Two-step Verification
363
-
364
- | Key | Value |
365
- |-------------|---------------------|
366
- | **email** | example@example.com |
367
-
368
- ```python
369
- @app.post("api/security/two-step/request")
370
- async def on_two_step_request(request):
371
- two_step_session = await request_two_step_verification(request)
372
- await email_code(
373
- account.email, two_step_session.code # Code = DT6JZX
374
- ) # Custom method for emailing verification code.
375
- response = json("Verification request successful!", two_step_session.bearer.json)
376
- two_step_session.encode(response)
377
- return response
378
- ```
379
-
380
- * Resend Two-step Verification Code
381
-
382
- ```python
383
- @app.post("api/security/two-step/resend")
384
- async def on_two_step_resend(request):
385
- two_step_session = await TwoStepSession.decode(request)
386
- await email_code(
387
- account.email, two_step_session.code # Code = DT6JZX
388
- ) # Custom method for emailing verification code.
389
- return json("Verification code resend successful!", two_step_session.bearer.json)
390
- ```
391
-
392
- * Two-step Verification
393
-
394
- | Key | Value |
395
- |----------|--------|
396
- | **code** | DT6JZX |
397
-
398
- ```python
399
- @app.post("api/security/two-step")
400
- async def on_two_step_verification(request):
401
- two_step_session = await two_step_verification(request)
402
- response = json(
403
- "Two-step verification attempt successful!", two_step_session.bearer.json
404
- )
405
- return response
406
- ```
407
-
408
- * Requires Two-step Verification (This method is not called directly and instead used as a decorator.)
409
-
410
- | Key | Value |
411
- |----------|--------|
412
- | **code** | DT6JZX |
413
-
414
- ```python
415
- @app.post("api/security/two-step")
416
- @requires_two_step_verification
417
- async def on_two_step_verification(request):
418
- response = json(
419
- "Two-step verification attempt successful!",
420
- request.ctx.two_step_session.bearer.json,
421
- )
422
- return response
423
- ```
424
-
425
- ## Authorization
426
-
427
- Sanic Security uses role based authorization with wildcard permissions.
428
-
429
- Roles are created for various job functions. The permissions to perform certain operations are assigned to specific roles.
430
- Users are assigned particular roles, and through those role assignments acquire the permissions needed to perform
431
- particular system functions. Since users are not assigned permissions directly, but only acquire them through their
432
- role (or roles), management of individual user rights becomes a matter of simply assigning appropriate roles to the
433
- user's account; this simplifies common operations, such as adding a user, or changing a user's department.
434
-
435
- Wildcard permissions support the concept of multiple levels or parts. For example, you could grant a user the permission
436
- `printer:query`, `printer:query,delete`, or `printer:*`.
437
- * Assign Role
438
-
439
- ```python
440
- await assign_role(
441
- "Chat Room Moderator",
442
- account,
443
- "channels:view,delete, account:suspend,mute, voice:*",
444
- "Can read and delete messages in all chat rooms, suspend and mute accounts, and control voice chat.",
445
- )
446
- ```
447
-
448
- * Check Permissions
449
-
450
- ```python
451
- @app.post("api/security/perms")
452
- async def on_check_perms(request):
453
- authentication_session = await check_permissions(
454
- request, "channels:view", "voice:*"
455
- )
456
- return text("Account is authorized.")
457
- ```
458
-
459
- * Require Permissions (This method is not called directly and instead used as a decorator.)
460
-
461
- ```python
462
- @app.post("api/security/perms")
463
- @require_permissions("channels:view", "voice:*")
464
- async def on_check_perms(request):
465
- return text("Account is authorized.")
466
- ```
467
-
468
- * Check Roles
469
-
470
- ```python
471
- @app.post("api/security/roles")
472
- async def on_check_roles(request):
473
- authentication_session = await check_roles(request, "Chat Room Moderator")
474
- return text("Account is authorized.")
475
- ```
476
-
477
- * Require Roles (This method is not called directly and instead used as a decorator.)
478
-
479
- ```python
480
- @app.post("api/security/roles")
481
- @require_roles("Chat Room Moderator")
482
- async def on_check_roles(request):
483
- return text("Account is authorized.")
484
- ```
485
-
486
- ## Testing
487
-
488
- * Set the `TEST_DATABASE_URL` configuration value.
489
-
490
- * Make sure the test Sanic instance (`test/server.py`) is running on your machine.
491
-
492
- * Run the unit test client (`test/tests.py`) for results.
493
-
494
- ## Tortoise
495
-
496
- Sanic Security uses [Tortoise ORM](https://tortoise-orm.readthedocs.io/en/latest/index.html) for database operations.
497
-
498
- Tortoise ORM is an easy-to-use asyncio ORM (Object Relational Mapper).
499
-
500
- * Initialise your models and database like so:
501
-
502
- ```python
503
- async def init():
504
- await Tortoise.init(
505
- db_url="sqlite://db.sqlite3",
506
- modules={"models": ["sanic_security.models", "app.models"]},
507
- )
508
- await Tortoise.generate_schemas()
509
- ```
510
-
511
- or
512
-
513
- ```python
514
- register_tortoise(
515
- app,
516
- db_url="sqlite://db.sqlite3",
517
- modules={"models": ["sanic_security.models", "app.models"]},
518
- generate_schemas=True,
519
- )
520
- ```
521
-
522
- * Define your models like so:
523
-
524
- ```python
525
- from tortoise.models import Model
526
- from tortoise import fields
527
-
528
-
529
- class Tournament(Model):
530
- id = fields.IntField(pk=True)
531
- name = fields.TextField()
532
- ```
533
-
534
- * Use it like so:
535
-
536
- ```python
537
- # Create instance by save
538
- tournament = Tournament(name="New Tournament")
539
- await tournament.save()
540
-
541
- # Or by .create()
542
- await Tournament.create(name="Another Tournament")
543
-
544
- # Now search for a record
545
- tour = await Tournament.filter(name__contains="Another").first()
546
- print(tour.name)
547
- ```
548
-
549
- <!-- CONTRIBUTING -->
550
- ## Contributing
551
-
552
- 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**.
553
-
554
- 1. Fork the Project
555
- 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
556
- 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
557
- 4. Push to the Branch (`git push origin feature/AmazingFeature`)
558
- 5. Open a Pull Request
559
-
560
-
561
- <!-- LICENSE -->
562
- ## License
563
-
564
- Distributed under the GNU Affero General Public License v3.0. See `LICENSE` for more information.
565
-
566
- <!-- Versioning -->
567
- ## Versioning
568
-
569
- **0.0.0**
570
-
571
- * MAJOR version when you make incompatible API changes.
572
-
573
- * MINOR version when you add functionality in a backwards compatible manner.
574
-
575
- * PATCH version when you make backwards compatible bug fixes.
576
-
577
- [https://semver.org/](https://semver.org/)
578
-
579
- <!-- MARKDOWN LINKS & IMAGES -->
580
- <!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
581
- [contributors-shield]: https://img.shields.io/github/contributors/sunset-developer/sanic-security.svg?style=flat-square
582
- [contributors-url]: https://github.com/sunset-developer/sanic-security/graphs/contributors
583
- [forks-shield]: https://img.shields.io/github/forks/sunset-developer/sanic-security.svg?style=flat-square
584
- [forks-url]: https://github.com/sunset-developer/sanic-security/network/members
585
- [stars-shield]: https://img.shields.io/github/stars/sunset-developer/sanic-security.svg?style=flat-square
586
- [stars-url]: https://github.com/sunset-developer/sanic-security/stargazers
587
- [issues-shield]: https://img.shields.io/github/issues/sunset-developer/sanic-security.svg?style=flat-square
588
- [issues-url]: https://github.com/sunset-developer/sanic-security/issues
589
- [license-shield]: https://img.shields.io/github/license/sunset-developer/sanic-security.svg?style=flat-square
590
- [license-url]: https://github.com/sunset-developer/sanic-security/blob/master/LICENSE
591
-
1
+ Metadata-Version: 2.1
2
+ Name: sanic-security
3
+ Version: 1.12.1
4
+ Summary: An async security library for the Sanic framework.
5
+ Author-email: Aidan Stewart <na.stewart365@gmail.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: pillow >=9.5.0
21
+ Requires-Dist: argon2-cffi >=20.1.0
22
+ Requires-Dist: sanic >=21.3.0
23
+ Provides-Extra: crypto
24
+ Requires-Dist: cryptography >=3.3.1 ; extra == 'crypto'
25
+ Provides-Extra: dev
26
+ Requires-Dist: httpx ; extra == 'dev'
27
+ Requires-Dist: black ; extra == 'dev'
28
+ Requires-Dist: blacken-docs ; extra == 'dev'
29
+ Requires-Dist: pdoc3 ; extra == 'dev'
30
+ Requires-Dist: cryptography ; extra == 'dev'
31
+
32
+ <!-- PROJECT SHIELDS -->
33
+ <!--
34
+ *** I'm using markdown "reference style" links for readability.
35
+ *** Reference links are enclosed in brackets [ ] instead of parentheses ( ).
36
+ *** See the bottom of this document for the declaration of the reference variables
37
+ *** for contributors-url, forks-url, etc. This is an optional, concise syntax you may use.
38
+ *** https://www.markdownguide.org/basic-syntax/#reference-style-links
39
+ -->
40
+
41
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
42
+ [![Downloads](https://static.pepy.tech/badge/sanic-security)](https://pepy.tech/project/sanic-security)
43
+ [![Conda Downloads](https://img.shields.io/conda/dn/conda-forge/sanic-security.svg)](https://anaconda.org/conda-forge/sanic-security)
44
+
45
+
46
+ <!-- PROJECT LOGO -->
47
+ <br />
48
+ <p align="center">
49
+ <h3 align="center">Sanic Security</h3>
50
+ <p align="center">
51
+ An async security library for the Sanic framework.
52
+ </p>
53
+ </p>
54
+
55
+
56
+ <!-- TABLE OF CONTENTS -->
57
+ ## Table of Contents
58
+
59
+ * [About the Project](#about-the-project)
60
+ * [Getting Started](#getting-started)
61
+ * [Prerequisites](#prerequisites)
62
+ * [Installation](#installation)
63
+ * [Configuration](#configuration)
64
+ * [Usage](#usage)
65
+ * [Authentication](#authentication)
66
+ * [Captcha](#captcha)
67
+ * [Two Step Verification](#two-step-verification)
68
+ * [Authorization](#authorization)
69
+ * [Testing](#testing)
70
+ * [Tortoise](#tortoise)
71
+ * [Contributing](#contributing)
72
+ * [License](#license)
73
+ * [Versioning](#versioning)
74
+ * [Support](https://discord.gg/JHpZkMfKTJ)
75
+
76
+ <!-- ABOUT THE PROJECT -->
77
+ ## About The Project
78
+
79
+ Sanic Security is an authentication, authorization, and verification library designed for use with [Sanic](https://github.com/huge-success/sanic).
80
+
81
+ * Login, registration, and authentication with refresh mechanisms
82
+ * Two-factor authentication
83
+ * Captcha
84
+ * Two-step verification
85
+ * Role based authorization with wildcard permissions
86
+
87
+ Please visit [security.na-stewart.com](https://security.na-stewart.com) for documentation and [here for an implementation guide](https://blog.na-stewart.com/entry?id=3).
88
+
89
+ <!-- GETTING STARTED -->
90
+ ## Getting Started
91
+
92
+ In order to get started, please install [Pip](https://pypi.org/).
93
+
94
+ ### Installation
95
+
96
+ * Install the Sanic Security pip package.
97
+ ```shell
98
+ pip3 install sanic-security
99
+ ````
100
+
101
+ * Install the Sanic Security pip package with the `cryptography` dependency included.
102
+
103
+ If you are planning on encoding or decoding JWTs using certain digital signature algorithms (like RSA or ECDSA which use
104
+ the public secret and private secret), you will need to install the `cryptography` library. This can be installed explicitly, or
105
+ as an extra requirement.
106
+
107
+ ```shell
108
+ pip3 install sanic-security[crypto]
109
+ ````
110
+
111
+ * For developers, fork Sanic Security and install development dependencies.
112
+ ```shell
113
+ pip3 install -e ".[dev]"
114
+ ````
115
+
116
+ * Update sanic-security if already installed.
117
+ ```shell
118
+ pip3 install --upgrade sanic-security
119
+ ```
120
+
121
+ ### Configuration
122
+
123
+ Sanic Security configuration is merely an object that can be modified either using dot-notation or like a
124
+ dictionary.
125
+
126
+ For example:
127
+
128
+ ```python
129
+ from sanic_security.configuration import config
130
+
131
+ config.SECRET = "This is a big secret. Shhhhh"
132
+ config["CAPTCHA_FONT"] = "./resources/captcha-font.ttf"
133
+ ```
134
+
135
+ You can also use the update() method like on regular dictionaries.
136
+
137
+ Any environment variables defined with the SANIC_SECURITY_ prefix will be applied to the config. For example, setting
138
+ SANIC_SECURITY_SECRET will be loaded by the application automatically and fed into the SECRET config variable.
139
+
140
+ You can load environment variables with a different prefix via `config.load_environment_variables("NEW_PREFIX_")` method.
141
+
142
+ * Default configuration values:
143
+
144
+ | Key | Value | Description |
145
+ |---------------------------------------|------------------------------|----------------------------------------------------------------------------------------------------------------------------------|
146
+ | **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. |
147
+ | **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. |
148
+ | **SESSION_SAMESITE** | strict | The SameSite attribute of session cookies. |
149
+ | **SESSION_SECURE** | True | The Secure attribute of session cookies. |
150
+ | **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. |
151
+ | **SESSION_DOMAIN** | None | The Domain attribute of session cookies. |
152
+ | **SESSION_ENCODING_ALGORITHM** | HS256 | The algorithm used to encode and decode session JWT's. |
153
+ | **SESSION_PREFIX** | token | Prefix attached to the beginning of session cookies. |
154
+ | **MAX_CHALLENGE_ATTEMPTS** | 5 | The maximum amount of session challenge attempts allowed. |
155
+ | **CAPTCHA_SESSION_EXPIRATION** | 60 | The amount of seconds till captcha session expiration on creation. Setting to 0 will disable expiration. |
156
+ | **CAPTCHA_FONT** | captcha-font.ttf | The file path to the font being used for captcha generation. |
157
+ | **TWO_STEP_SESSION_EXPIRATION** | 200 | 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** | 2592000 | The amount of seconds till authentication refresh expiration. |
160
+ | **ALLOW_LOGIN_WITH_USERNAME** | False | Allows login via username and email. |
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
167
+ 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.
168
+
169
+ The tables in the below examples represent example [request form-data](https://sanicframework.org/en/guide/basics/request.html#form).
170
+
171
+ ## Authentication
172
+
173
+ * Initial Administrator Account
174
+
175
+ This account can be logged into and has complete authoritative access. Login credentials should be modified in config!
176
+
177
+ ```python
178
+ create_initial_admin_account(app)
179
+ if __name__ == "__main__":
180
+ app.run(host="127.0.0.1", port=8000)
181
+ ```
182
+
183
+ * Registration (With two-step account verification)
184
+
185
+ Phone can be null or empty.
186
+
187
+ | Key | Value |
188
+ |--------------|---------------------|
189
+ | **username** | example |
190
+ | **email** | example@example.com |
191
+ | **phone** | 19811354186 |
192
+ | **password** | examplepass |
193
+
194
+ ```python
195
+ @app.post("api/security/register")
196
+ async def on_register(request):
197
+ account = await register(request)
198
+ two_step_session = await request_two_step_verification(request, account)
199
+ await email_code(
200
+ account.email, two_step_session.code # Code = 197251
201
+ ) # Custom method for emailing verification code.
202
+ response = json(
203
+ "Registration successful! Email verification required.",
204
+ two_step_session.json,
205
+ )
206
+ two_step_session.encode(response)
207
+ return response
208
+ ```
209
+
210
+ * Verify Account
211
+
212
+ Verifies the client's account via two-step session code.
213
+
214
+ | Key | Value |
215
+ |----------|--------|
216
+ | **code** | 197251 |
217
+
218
+ ```python
219
+ @app.post("api/security/verify")
220
+ async def on_verify(request):
221
+ two_step_session = await verify_account(request)
222
+ return json("You have verified your account and may login!", two_step_session.json)
223
+ ```
224
+
225
+ * Login (With two-factor authentication)
226
+
227
+ Login credentials are retrieved via the Authorization header. Credentials are constructed by first combining the
228
+ username and the password with a colon (aladdin:opensesame), and then by encoding the resulting string in base64
229
+ (YWxhZGRpbjpvcGVuc2VzYW1l). Here is an example authorization header: `Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l`.
230
+
231
+ You can use a username as well as an email for login if `ALLOW_LOGIN_WITH_USERNAME` is true in the config.
232
+
233
+ ```python
234
+ @app.post("api/security/login")
235
+ async def on_login(request):
236
+ authentication_session = await login(request, require_second_factor=True)
237
+ two_step_session = await request_two_step_verification(
238
+ request, authentication_session.bearer
239
+ )
240
+ await email_code(
241
+ authentication_session.bearer.email, two_step_session.code # Code = 197251
242
+ ) # Custom method for emailing verification code.
243
+ response = json(
244
+ "Login successful! Two-factor authentication required.",
245
+ authentication_session.json,
246
+ )
247
+ authentication_session.encode(response)
248
+ two_step_session.encode(response)
249
+ return response
250
+ ```
251
+
252
+ * Fulfill Second Factor
253
+
254
+ Fulfills client authentication session's second factor requirement via two-step session code.
255
+
256
+ | Key | Value |
257
+ |----------|--------|
258
+ | **code** | 197251 |
259
+
260
+ ```python
261
+ @app.post("api/security/fulfill-2fa")
262
+ async def on_two_factor_authentication(request):
263
+ authentication_session = await fulfill_second_factor(request)
264
+ response = json(
265
+ "Authentication session second-factor fulfilled! You are now authenticated.",
266
+ authentication_session.json,
267
+ )
268
+ authentication_session.encode(response)
269
+ return response
270
+ ```
271
+
272
+ * Anonymous Login
273
+
274
+ Simply create a new session and encode it.
275
+
276
+ ```python
277
+ @app.post("api/security/login/anon")
278
+ async def on_anonymous_login(request):
279
+ authentication_session = await AuthenticationSession.new(request)
280
+ response = json(
281
+ "Anonymous client now associated with session!", authentication_session.json
282
+ )
283
+ authentication_session.encode(response)
284
+ return response
285
+ ```
286
+
287
+ * Logout
288
+
289
+ ```python
290
+ @app.post("api/security/logout")
291
+ async def on_logout(request):
292
+ authentication_session = await logout(request)
293
+ return json("Logout successful!", authentication_session.json)
294
+ ```
295
+
296
+ * Authenticate
297
+
298
+ New/Refreshed session will be returned if expired, requires encoding.
299
+
300
+ ```python
301
+ @app.post("api/security/auth")
302
+ async def on_authenticate(request):
303
+ authentication_session = await authenticate(request)
304
+ response = json(
305
+ "You have been authenticated.",
306
+ authentication_session.json,
307
+ )
308
+ if authentication_session.is_refresh:
309
+ authentication_session.encode(response)
310
+ return response
311
+ ```
312
+
313
+ * Requires Authentication (This method is not called directly and instead used as a decorator)
314
+
315
+ New/Refreshed session will be returned if expired, requires encoding.
316
+
317
+ ```python
318
+ @app.post("api/security/auth")
319
+ @requires_authentication
320
+ async def on_authenticate(request):
321
+ authentication_session = request.ctx.authentication_session
322
+ response = json(
323
+ "You have been authenticated.",
324
+ authentication_session.json,
325
+ )
326
+ if authentication_session.is_refresh:
327
+ authentication_session.encode(response)
328
+ return response
329
+ ```
330
+
331
+ * Authentication Refresh Middleware
332
+
333
+ If it's inconvenient to encode the refreshed session during authentication, it can also be done automatically via middleware.
334
+
335
+ ```python
336
+ @app.on_response
337
+ async def authentication_refresh_encoder(request, response):
338
+ try:
339
+ authentication_session = request.ctx.authentication_session
340
+ if authentication_session.is_refresh:
341
+ authentication_session.encode(response)
342
+ except AttributeError:
343
+ pass
344
+ ```
345
+
346
+ ## Captcha
347
+
348
+ A pre-existing font for captcha challenges is included in the Sanic Security repository. You may set your own font by
349
+ downloading a .ttf font and defining the file's path in the configuration.
350
+
351
+ [1001 Free Fonts](https://www.1001fonts.com/)
352
+
353
+ [Recommended Font](https://www.1001fonts.com/source-sans-pro-font.html)
354
+
355
+ * Request Captcha
356
+
357
+ ```python
358
+ @app.get("api/security/captcha")
359
+ async def on_captcha_img_request(request):
360
+ captcha_session = await request_captcha(request)
361
+ response = captcha_session.get_image() # Captcha: 192731
362
+ captcha_session.encode(response)
363
+ return response
364
+ ```
365
+
366
+ * Captcha
367
+
368
+ | Key | Value |
369
+ |-------------|--------|
370
+ | **captcha** | 192731 |
371
+
372
+ ```python
373
+ @app.post("api/security/captcha")
374
+ async def on_captcha(request):
375
+ captcha_session = await captcha(request)
376
+ return json("Captcha attempt successful!", captcha_session.json)
377
+ ```
378
+
379
+ * Requires Captcha (This method is not called directly and instead used as a decorator)
380
+
381
+ | Key | Value |
382
+ |-------------|--------|
383
+ | **captcha** | 192731 |
384
+
385
+ ```python
386
+ @app.post("api/security/captcha")
387
+ @requires_captcha
388
+ async def on_captcha(request):
389
+ return json("Captcha attempt successful!", request.ctx.captcha_session.json)
390
+ ```
391
+
392
+ ## Two-step Verification
393
+
394
+ Two-step verification should be integrated with other custom functionality. For example, account verification during registration.
395
+
396
+ * Request Two-step Verification
397
+
398
+ | Key | Value |
399
+ |-------------|---------------------|
400
+ | **email** | example@example.com |
401
+
402
+ ```python
403
+ @app.post("api/security/two-step/request")
404
+ async def on_two_step_request(request):
405
+ two_step_session = await request_two_step_verification(request) # Code = 197251
406
+ await email_code(
407
+ two_step_session.bearer.email, two_step_session.code
408
+ ) # Custom method for emailing verification code.
409
+ response = json("Verification request successful!", two_step_session.json)
410
+ two_step_session.encode(response)
411
+ return response
412
+ ```
413
+
414
+ * Resend Two-step Verification Code
415
+
416
+ ```python
417
+ @app.post("api/security/two-step/resend")
418
+ async def on_two_step_resend(request):
419
+ two_step_session = await TwoStepSession.decode(request) # Code = 197251
420
+ await email_code(
421
+ two_step_session.bearer.email, two_step_session.code
422
+ ) # Custom method for emailing verification code.
423
+ return json("Verification code resend successful!", two_step_session.json)
424
+ ```
425
+
426
+ * Two-step Verification
427
+
428
+ | Key | Value |
429
+ |----------|--------|
430
+ | **code** | 197251 |
431
+
432
+ ```python
433
+ @app.post("api/security/two-step")
434
+ async def on_two_step_verification(request):
435
+ two_step_session = await two_step_verification(request)
436
+ response = json("Two-step verification attempt successful!", two_step_session.json)
437
+ return response
438
+ ```
439
+
440
+ * Requires Two-step Verification (This method is not called directly and instead used as a decorator)
441
+
442
+ | Key | Value |
443
+ |----------|--------|
444
+ | **code** | 197251 |
445
+
446
+ ```python
447
+ @app.post("api/security/two-step")
448
+ @requires_two_step_verification
449
+ async def on_two_step_verification(request):
450
+ response = json(
451
+ "Two-step verification attempt successful!",
452
+ request.ctx.two_step_session.json,
453
+ )
454
+ return response
455
+ ```
456
+
457
+ ## Authorization
458
+
459
+ Sanic Security uses role based authorization with wildcard permissions.
460
+
461
+ Roles are created for various job functions. The permissions to perform certain operations are assigned to specific roles.
462
+ Users are assigned particular roles, and through those role assignments acquire the permissions needed to perform
463
+ particular system functions. Since users are not assigned permissions directly, but only acquire them through their
464
+ role (or roles), management of individual user rights becomes a matter of simply assigning appropriate roles to the
465
+ user's account; this simplifies common operations, such as adding a user, or changing a user's department.
466
+
467
+ Wildcard permissions support the concept of multiple levels or parts. For example, you could grant a user the permission
468
+ `printer:query`, `printer:query,delete`, or `printer:*`.
469
+ * Assign Role
470
+
471
+ ```python
472
+ await assign_role(
473
+ "Chat Room Moderator",
474
+ account,
475
+ "channels:view,delete, account:suspend,mute, voice:*",
476
+ "Can read and delete messages in all chat rooms, suspend and mute accounts, and control voice chat.",
477
+ )
478
+ ```
479
+
480
+ * Check Permissions
481
+
482
+ ```python
483
+ @app.post("api/security/perms")
484
+ async def on_check_perms(request):
485
+ authentication_session = await check_permissions(
486
+ request, "channels:view", "voice:*"
487
+ )
488
+ return text("Account is authorized.")
489
+ ```
490
+
491
+ * Require Permissions (This method is not called directly and instead used as a decorator.)
492
+
493
+ ```python
494
+ @app.post("api/security/perms")
495
+ @require_permissions("channels:view", "voice:*")
496
+ async def on_check_perms(request):
497
+ return text("Account is authorized.")
498
+ ```
499
+
500
+ * Check Roles
501
+
502
+ ```python
503
+ @app.post("api/security/roles")
504
+ async def on_check_roles(request):
505
+ authentication_session = await check_roles(request, "Chat Room Moderator")
506
+ return text("Account is authorized.")
507
+ ```
508
+
509
+ * Require Roles (This method is not called directly and instead used as a decorator)
510
+
511
+ ```python
512
+ @app.post("api/security/roles")
513
+ @require_roles("Chat Room Moderator")
514
+ async def on_check_roles(request):
515
+ return text("Account is authorized.")
516
+ ```
517
+
518
+ ## Testing
519
+
520
+ * Set the `TEST_DATABASE_URL` configuration value.
521
+
522
+ * Make sure the test Sanic instance (`test/server.py`) is running on your machine.
523
+
524
+ * Run the unit test client (`test/tests.py`) for results.
525
+
526
+ ## Tortoise
527
+
528
+ Sanic Security uses [Tortoise ORM](https://tortoise-orm.readthedocs.io/en/latest/index.html) for database operations.
529
+
530
+ Tortoise ORM is an easy-to-use asyncio ORM (Object Relational Mapper).
531
+
532
+ * Initialise your models and database like so:
533
+
534
+ ```python
535
+ async def init():
536
+ await Tortoise.init(
537
+ db_url="sqlite://db.sqlite3",
538
+ modules={"models": ["sanic_security.models", "app.models"]},
539
+ )
540
+ await Tortoise.generate_schemas()
541
+ ```
542
+
543
+ or
544
+
545
+ ```python
546
+ register_tortoise(
547
+ app,
548
+ db_url="sqlite://db.sqlite3",
549
+ modules={"models": ["sanic_security.models", "app.models"]},
550
+ generate_schemas=True,
551
+ )
552
+ ```
553
+
554
+ * Define your models like so:
555
+
556
+ ```python
557
+ from tortoise.models import Model
558
+ from tortoise import fields
559
+
560
+
561
+ class Tournament(Model):
562
+ id = fields.IntField(pk=True)
563
+ name = fields.TextField()
564
+ ```
565
+
566
+ * Use it like so:
567
+
568
+ ```python
569
+ # Create instance by save
570
+ tournament = Tournament(name="New Tournament")
571
+ await tournament.save()
572
+
573
+ # Or by .create()
574
+ await Tournament.create(name="Another Tournament")
575
+
576
+ # Now search for a record
577
+ tour = await Tournament.filter(name__contains="Another").first()
578
+ print(tour.name)
579
+ ```
580
+
581
+ <!-- CONTRIBUTING -->
582
+ ## Contributing
583
+
584
+ 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**.
585
+
586
+ 1. Fork the Project
587
+ 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
588
+ 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
589
+ 4. Push to the Branch (`git push origin feature/AmazingFeature`)
590
+ 5. Open a Pull Request
591
+
592
+
593
+ <!-- LICENSE -->
594
+ ## License
595
+
596
+ Distributed under the MIT License. See `LICENSE` for more information.
597
+
598
+ <!-- Versioning -->
599
+ ## Versioning
600
+
601
+ **0.0.0**
602
+
603
+ * MAJOR version when you make incompatible API changes.
604
+
605
+ * MINOR version when you add functionality in a backwards compatible manner.
606
+
607
+ * PATCH version when you make backwards compatible bug fixes.
608
+
609
+ [https://semver.org/](https://semver.org/)
610
+
611
+ <!-- MARKDOWN LINKS & IMAGES -->
612
+ <!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
613
+ [contributors-shield]: https://img.shields.io/github/contributors/sunset-developer/sanic-security.svg?style=flat-square
614
+ [contributors-url]: https://github.com/sunset-developer/sanic-security/graphs/contributors
615
+ [forks-shield]: https://img.shields.io/github/forks/sunset-developer/sanic-security.svg?style=flat-square
616
+ [forks-url]: https://github.com/sunset-developer/sanic-security/network/members
617
+ [stars-shield]: https://img.shields.io/github/stars/sunset-developer/sanic-security.svg?style=flat-square
618
+ [stars-url]: https://github.com/sunset-developer/sanic-security/stargazers
619
+ [issues-shield]: https://img.shields.io/github/issues/sunset-developer/sanic-security.svg?style=flat-square
620
+ [issues-url]: https://github.com/sunset-developer/sanic-security/issues
621
+ [license-shield]: https://img.shields.io/github/license/sunset-developer/sanic-security.svg?style=flat-square
622
+ [license-url]: https://github.com/sunset-developer/sanic-security/blob/master/LICENSE