├── .python-version ├── AUTHORS.rst ├── requirements.txt ├── setup.cfg ├── .editorconfig ├── pyproject.toml ├── src ├── example.py └── sanic_token_auth │ └── __init__.py ├── .gitignore ├── README.md └── setup.py /.python-version: -------------------------------------------------------------------------------- 1 | 3.6.3 2 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | 2 | Authors 3 | ======= 4 | 5 | * Sergei Beilin 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | sanic 2 | sanic-testing 3 | pytest 4 | mypy 5 | isort 6 | black 7 | flake8 8 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.2.1 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | 8 | [bdist_wheel] 9 | universal = 1 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 4 10 | charset = utf-8 11 | 12 | [*.{bat,cmd,ps1}] 13 | end_of_line = crlf 14 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.pytest.ini_options] 2 | minversion = "7.0" 3 | addopts = "-v --ff" 4 | testpaths = [ 5 | "tests", 6 | ] 7 | 8 | [tool.black] 9 | target-version = ['py311'] 10 | 11 | [tool.isort] 12 | profile = "black" 13 | src_paths = ["src", "tests"] 14 | py_version = 311 15 | 16 | [tool.mypy] 17 | show_error_context = true 18 | ignore_missing_imports = true 19 | disallow_untyped_calls = true 20 | disallow_untyped_defs = true 21 | check_untyped_defs = true 22 | disallow_incomplete_defs = true 23 | python_version = "3.11" 24 | -------------------------------------------------------------------------------- /src/example.py: -------------------------------------------------------------------------------- 1 | import sanic.response 2 | from sanic import Sanic 3 | from sanic.response import text 4 | 5 | from sanic_token_auth import SanicTokenAuth 6 | 7 | app = Sanic("SanicTokenAuthExample") 8 | auth = SanicTokenAuth(app, secret_key="utee3Quaaxohh1Oo", header="X-My-App-Auth-Token") 9 | 10 | 11 | @app.route("/") 12 | async def index(request: sanic.Request) -> sanic.response.HTTPResponse: 13 | return text("Go to /protected") 14 | 15 | 16 | @app.route("/protected") 17 | @auth.auth_required 18 | async def protected(request: sanic.Request) -> sanic.response.HTTPResponse: 19 | return text("Welcome!") 20 | 21 | 22 | if __name__ == "__main__": 23 | app.run(host="0.0.0.0", port=8000, debug=True) 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | .eggs 13 | parts 14 | bin 15 | var 16 | sdist 17 | wheelhouse 18 | develop-eggs 19 | .installed.cfg 20 | lib 21 | lib64 22 | venv*/ 23 | pyvenv*/ 24 | 25 | # Installer logs 26 | pip-log.txt 27 | 28 | # Unit test / coverage reports 29 | .coverage 30 | .tox 31 | .coverage.* 32 | nosetests.xml 33 | coverage.xml 34 | htmlcov 35 | 36 | # Translations 37 | *.mo 38 | 39 | # Mr Developer 40 | .mr.developer.cfg 41 | .project 42 | .pydevproject 43 | .idea 44 | *.iml 45 | *.komodoproject 46 | 47 | # Complexity 48 | output/*.html 49 | output/*/index.html 50 | 51 | # Sphinx 52 | docs/_build 53 | 54 | .DS_Store 55 | *~ 56 | .*.sw[po] 57 | .build 58 | .ve 59 | .env 60 | .cache 61 | .pytest 62 | .bootstrap 63 | .appveyor.token 64 | *.bak 65 | -------------------------------------------------------------------------------- /src/sanic_token_auth/__init__.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from functools import wraps 3 | 4 | from sanic import Sanic, exceptions 5 | from sanic.request import Request 6 | 7 | 8 | class SanicTokenAuth: 9 | def __init__( 10 | self, 11 | app: typing.Optional[Sanic] = None, 12 | header: typing.Optional[str] = None, 13 | token_verifier: typing.Optional[typing.Callable] = None, 14 | secret_key: typing.Optional[str] = None, 15 | ): 16 | self.secret_key = secret_key 17 | self.header = header 18 | self.token_verifier = token_verifier 19 | 20 | async def _is_authenticated(self, request: Request) -> bool: 21 | token = request.headers.get(self.header, None) if self.header else request.token 22 | if self.token_verifier: 23 | return await self.token_verifier(token) 24 | return token == self.secret_key 25 | 26 | def auth_required(self, handler=None): 27 | @wraps(handler) 28 | async def wrapper(request, *args, **kwargs): 29 | if not await self._is_authenticated(request): 30 | raise exceptions.Unauthorized("Auth required.") 31 | 32 | return await handler(request, *args, **kwargs) 33 | 34 | return wrapper 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Gotta get your API protected! 2 | ============================= 3 | 4 | Token-based authentication is the most common way of protecting your APIs from unwanted folk. 5 | 6 | Sometimes you need to do things _fast_ (you know, get it to prod _yesterday_) 7 | and you do not really have time to implement a proper authentication layer. 8 | 9 | Okay, are you fine with a temporary solution? There's nothing more permanent than temporary, right. 10 | 11 | If you can: 12 | 13 | * provide a simple async token verifier (say, checking it in memcached or Redis) 14 | 15 | or 16 | * hard-code a token in your app prototype, 17 | 18 | and also 19 | 20 | * sent the token in a request header 21 | 22 | - then we are ready to go. 23 | 24 | 25 | ## Usage example 26 | 27 | 28 | ```python 29 | from sanic import Sanic 30 | from sanic.response import text 31 | 32 | from sanic_token_auth import SanicTokenAuth 33 | 34 | app = Sanic() 35 | auth = SanicTokenAuth(app, secret_key='utee3Quaaxohh1Oo', header='X-My-App-Auth-Token') 36 | 37 | 38 | @app.route("/") 39 | async def index(request): 40 | return text("Go to /protected") 41 | 42 | 43 | @app.route("/protected") 44 | @auth.auth_required 45 | async def protected(request): 46 | return text("Welcome!") 47 | 48 | 49 | if __name__ == "__main__": 50 | app.run(host="0.0.0.0", port=8000, debug=True) 51 | ``` 52 | 53 | And let's try it: 54 | 55 | ```bash 56 | $ curl http://localhost:8000/protected -H "X-My-App-Auth-Token: utee3Quaaxohh1Oo" 57 | 58 | Welcome! 59 | ``` 60 | 61 | 62 | If you omit the `header` argument, you can instead send a token in either 63 | `Authorization: Bearer ` or `Authorization: Token ` 64 | header. 65 | 66 | 67 | ----- 68 | 69 | TODO: 70 | 71 | [ ] Document `token_verifier` and implement examples of using of 72 | 73 | [ ] Implement "protect all" behaviour 74 | 75 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | from __future__ import absolute_import 4 | from __future__ import print_function 5 | 6 | import io 7 | from glob import glob 8 | from os.path import basename 9 | from os.path import dirname 10 | from os.path import join 11 | from os.path import splitext 12 | 13 | import m2r 14 | from setuptools import setup 15 | 16 | 17 | def read(*names, **kwargs): 18 | return io.open( 19 | join(dirname(__file__), *names), encoding=kwargs.get("encoding", "utf8") 20 | ).read() 21 | 22 | 23 | setup( 24 | name="sanic_token_auth", 25 | version="0.2.0", 26 | license="MIT license", 27 | description="A simple token-based auth plugin for Sanic", 28 | # long_description='%s\n%s' % ( 29 | # re.compile('^.. start-badges.*^.. end-badges', re.M | re.S).sub('', read('README.rst')), 30 | # re.sub(':[a-z]+:`~?(.*?)`', r'``\1``', read('CHANGELOG.rst')) 31 | # ), 32 | long_description=m2r.convert(read("README.md")), 33 | author="Sergei Beilin", 34 | author_email="saabeilin@gmail.com", 35 | url="https://github.com/saabeilin/sanic-token-auth", 36 | # packages=find_packages('src'), 37 | packages=["sanic_token_auth"], 38 | package_dir={"": "src"}, 39 | py_modules=[splitext(basename(path))[0] for path in glob("src/*.py")], 40 | include_package_data=True, 41 | zip_safe=False, 42 | classifiers=[ 43 | # complete classifier list: http://pypi.python.org/pypi?%3Aaction=list_classifiers 44 | "Development Status :: 3 - Alpha", 45 | "Intended Audience :: Developers", 46 | "License :: OSI Approved :: MIT License", 47 | "Operating System :: Unix", 48 | "Operating System :: POSIX", 49 | "Operating System :: Microsoft :: Windows", 50 | "Programming Language :: Python", 51 | "Programming Language :: Python :: 3.9", 52 | "Programming Language :: Python :: 3.10", 53 | "Programming Language :: Python :: 3.11", 54 | "Programming Language :: Python :: Implementation :: CPython", 55 | "Programming Language :: Python :: Implementation :: PyPy", 56 | "Topic :: Utilities", 57 | ], 58 | keywords=["sanic", "authentication"], 59 | install_requires=["sanic"], 60 | ) 61 | --------------------------------------------------------------------------------