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

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