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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,591 +1,685 @@
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.2
2
+ Name: sanic-security
3
+ Version: 1.16.7
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/)