├── docs ├── changelog.rst ├── _static │ ├── flask-mail.jpg │ └── flask-mail.png ├── Makefile ├── make.bat ├── index.rst └── conf.py ├── requirements.txt ├── .gitmodules ├── requirements-dev.txt ├── MANIFEST.in ├── tox.ini ├── setup.cfg ├── .gitignore ├── .travis.yml ├── LICENSE ├── CHANGES ├── README.rst ├── example └── app.py ├── tests ├── conftest.py └── test_jwt.py ├── setup.py ├── scripts └── release.py └── flask_jwt └── __init__.py /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGES -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask>=0.9 2 | PyJWT>=1.4.0,<1.5.0 3 | -------------------------------------------------------------------------------- /docs/_static/flask-mail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pallets-eco/flask-jwt/HEAD/docs/_static/flask-mail.jpg -------------------------------------------------------------------------------- /docs/_static/flask-mail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pallets-eco/flask-jwt/HEAD/docs/_static/flask-mail.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "docs/_themes"] 2 | path = docs/_themes 3 | url = git://github.com/mitsuhiko/flask-sphinx-themes.git 4 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest>=2.5.2 2 | pytest-cache>=1.0 3 | pytest-cov>=1.6 4 | pytest-flakes>=0.2 5 | pytest-pep8>=1.0.5 6 | tox>=1.7.0 7 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include CHANGES 3 | include README.rst 4 | include requirements.txt 5 | include requirements-dev.txt 6 | include tox.ini 7 | recursive-include tests * 8 | recursive-exclude tests/__pycache__ * 9 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27, py33, py34, pypy 3 | 4 | [testenv] 5 | deps = 6 | -r{toxinidir}/requirements.txt 7 | -r{toxinidir}/requirements-dev.txt 8 | 9 | commands = 10 | py.test --clearcache {posargs} ./tests 11 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [pytest] 2 | pep8maxlinelength = 99 3 | pep8ignore = 4 | docs/* ALL 5 | example/* ALL 6 | 7 | [build_sphinx] 8 | source-dir = docs/ 9 | build-dir = docs/_build 10 | 11 | [upload_sphinx] 12 | upload-dir = docs/_build/html 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # Packages 4 | .cache 5 | *.egg 6 | *.egg-info 7 | dist 8 | *build 9 | eggs 10 | parts 11 | bin 12 | var 13 | sdist 14 | develop-eggs 15 | .installed.cfg 16 | 17 | # Installer logs 18 | pip-log.txt 19 | 20 | # Unit test / coverage reports 21 | .coverage 22 | .tox 23 | 24 | #Translations 25 | *.mo 26 | 27 | 28 | #Virtualenv 29 | env/ 30 | 31 | #Editor temporaries 32 | *~ 33 | 34 | *.db 35 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: python 4 | python: 5 | - "2.7" 6 | - "3.3" 7 | - "3.4" 8 | - "pypy" 9 | 10 | install: 11 | - travis_retry pip install -r requirements.txt -r requirements-dev.txt -e . 12 | - travis_retry pip install coverage coveralls 13 | 14 | script: py.test --cov flask_jwt --cov-report term-missing --pep8 --flakes 15 | 16 | after_script: 17 | - coveralls 18 | 19 | branches: 20 | only: 21 | - master 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (C) 2014 by Matthew Wright 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | Flask-JWT Changelog 2 | =================== 3 | 4 | Here you can see the full list of changes between each Flask-JWT release. 5 | 6 | Version 0.3.2 7 | ------------- 8 | 9 | Released November 3rd 2015 10 | 11 | - Fixed an Authorization header conditional bug 12 | 13 | Version 0.3.1 14 | ------------- 15 | 16 | Released October 26th 2015 17 | 18 | - Fix a bug with `auth_request_handler` 19 | - Deprecate `auth_request_handler` 20 | 21 | Version 0.3.0 22 | ------------- 23 | 24 | Released October 15th 2015 25 | 26 | .. note:: This release includes many breaking changes 27 | 28 | - Fix major implementation issue with encoding/decoding tokens 29 | - Changed new configuration options to align with PyJWT 30 | - Changed `current_user` to `current_identity` 31 | 32 | Version 0.2.0 33 | ------------- 34 | 35 | Released June 10th 2014 36 | 37 | - Fixed an issue where `current_user` was not None 38 | - Added a response handler hook to be able to adjust auth response(s) 39 | - Removed the configurable handlers in favor of decorators 40 | - Removed pyjwt dependency 41 | 42 | 43 | Version 0.1.0 44 | ------------- 45 | 46 | Released March 5th 2014 47 | 48 | - Initial release 49 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Flask-JWT 2 | ============== 3 | 4 | .. image:: https://img.shields.io/travis/mattupstate/flask-jwt.svg 5 | :target: https://travis-ci.org/mattupstate/flask-jwt 6 | :alt: Latest Build 7 | 8 | .. image:: https://img.shields.io/pypi/v/flask-jwt.svg 9 | :target: https://pypi.python.org/pypi/Flask-JWT/ 10 | :alt: Latest Version 11 | 12 | .. image:: https://img.shields.io/coveralls/mattupstate/flask-jwt.svg 13 | :target: https://coveralls.io/r/mattupstate/flask-jwt 14 | 15 | .. image:: https://img.shields.io/pypi/dm/flask-jwt.svg 16 | :target: https://pypi.python.org/pypi//Flask-JWT/ 17 | :alt: Downloads 18 | 19 | .. image:: https://img.shields.io/pypi/l/flask-jwt.svg 20 | :target: https://pypi.python.org/pypi/Flask-JWT/ 21 | :alt: License 22 | 23 | JWT (JSON Web Tokens) for Flask applications 24 | 25 | 26 | Resources 27 | --------- 28 | 29 | - `Documentation `_ 30 | - `Issue Tracker `_ 31 | - `Code `_ 32 | - `Development Version 33 | `_ 34 | -------------------------------------------------------------------------------- /example/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_jwt import JWT, jwt_required, current_identity 3 | from werkzeug.security import safe_str_cmp 4 | 5 | class User(object): 6 | def __init__(self, id, username, password): 7 | self.id = id 8 | self.username = username 9 | self.password = password 10 | 11 | def __str__(self): 12 | return "User(id='%s')" % self.id 13 | 14 | users = [ 15 | User(1, 'user1', 'abcxyz'), 16 | User(2, 'user2', 'abcxyz'), 17 | ] 18 | 19 | username_table = {u.username: u for u in users} 20 | userid_table = {u.id: u for u in users} 21 | 22 | def authenticate(username, password): 23 | user = username_table.get(username, None) 24 | if user and safe_str_cmp(user.password.encode('utf-8'), password.encode('utf-8')): 25 | return user 26 | 27 | def identity(payload): 28 | user_id = payload['identity'] 29 | return userid_table.get(user_id, None) 30 | 31 | app = Flask(__name__) 32 | app.debug = True 33 | app.config['SECRET_KEY'] = 'super-secret' 34 | 35 | jwt = JWT(app, authenticate, identity) 36 | 37 | @app.route('/protected') 38 | @jwt_required() 39 | def protected(): 40 | return '%s' % current_identity 41 | 42 | if __name__ == '__main__': 43 | app.run() 44 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tests.conftest 4 | ~~~~~~~~~~~~~~ 5 | 6 | Test fixtures and what not 7 | """ 8 | 9 | import logging 10 | 11 | import pytest 12 | 13 | from flask import Flask 14 | 15 | import flask_jwt 16 | 17 | logging.basicConfig(level=logging.DEBUG) 18 | 19 | 20 | class User(object): 21 | def __init__(self, id, username, password): 22 | self.id = id 23 | self.username = username 24 | self.password = password 25 | 26 | def __str__(self): 27 | return "User(id='%s')" % self.id 28 | 29 | 30 | @pytest.fixture(scope='function') 31 | def jwt(): 32 | return flask_jwt.JWT() 33 | 34 | 35 | @pytest.fixture(scope='function') 36 | def user(): 37 | return User(id=1, username='joe', password='pass') 38 | 39 | 40 | @pytest.fixture(scope='function') 41 | def app(jwt, user): 42 | app = Flask(__name__) 43 | app.debug = True 44 | app.config['SECRET_KEY'] = 'super-secret' 45 | 46 | @jwt.authentication_handler 47 | def authenticate(username, password): 48 | if username == user.username and password == user.password: 49 | return user 50 | return None 51 | 52 | @jwt.identity_handler 53 | def load_user(payload): 54 | if payload['identity'] == user.id: 55 | return user 56 | 57 | jwt.init_app(app) 58 | 59 | @app.route('/protected') 60 | @flask_jwt.jwt_required() 61 | def protected(): 62 | return 'success' 63 | 64 | return app 65 | 66 | 67 | @pytest.fixture(scope='function') 68 | def client(app): 69 | return app.test_client() 70 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Flask-JWT 3 | ========= 4 | 5 | Flask-JWT is a Flask extension that adds basic Json Web Token features to any application. 6 | 7 | Resources 8 | --------- 9 | 10 | * `Documentation `_ 11 | * `Issue Tracker `_ 12 | * `Source `_ 13 | * `Development Version 14 | `_ 15 | 16 | """ 17 | 18 | import sys 19 | 20 | from setuptools import setup, find_packages 21 | from setuptools.command.test import test as TestCommand 22 | 23 | 24 | def get_requirements(suffix=''): 25 | with open('requirements%s.txt' % suffix) as f: 26 | rv = f.read().splitlines() 27 | return rv 28 | 29 | 30 | def get_long_description(): 31 | with open('README.rst') as f: 32 | rv = f.read() 33 | return rv 34 | 35 | 36 | class PyTest(TestCommand): 37 | def finalize_options(self): 38 | TestCommand.finalize_options(self) 39 | self.test_args = [ 40 | '-xrs', 41 | '--cov', 'flask_jwt', 42 | '--cov-report', 'term-missing', 43 | '--pep8', 44 | '--flakes', 45 | '--clearcache', 46 | 'tests' 47 | ] 48 | self.test_suite = True 49 | 50 | def run_tests(self): 51 | import pytest 52 | errno = pytest.main(self.test_args) 53 | sys.exit(errno) 54 | 55 | setup( 56 | name='Flask-JWT', 57 | version='0.3.2', 58 | url='https://github.com/mattupstate/flask-jwt', 59 | license='MIT', 60 | author='Matt Wright', 61 | author_email='matt@nobien.net', 62 | description='JWT token authentication for Flask apps', 63 | long_description=__doc__, 64 | packages=find_packages(), 65 | zip_safe=False, 66 | include_package_data=True, 67 | platforms='any', 68 | install_requires=get_requirements(), 69 | tests_require=get_requirements('-dev'), 70 | cmdclass={'test': PyTest}, 71 | classifiers=[ 72 | 'Development Status :: 4 - Beta', 73 | 'Environment :: Web Environment', 74 | 'Intended Audience :: Developers', 75 | 'License :: OSI Approved :: MIT License', 76 | 'Operating System :: OS Independent', 77 | 'Programming Language :: Python', 78 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 79 | 'Topic :: Software Development :: Libraries :: Python Modules' 80 | ] 81 | ) 82 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " pickle to make pickle files" 22 | @echo " json to make JSON files" 23 | @echo " htmlhelp to make HTML files and a HTML help project" 24 | @echo " qthelp to make HTML files and a qthelp project" 25 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 26 | @echo " changes to make an overview of all changed/added/deprecated items" 27 | @echo " linkcheck to check all external links for integrity" 28 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 29 | 30 | clean: 31 | -rm -rf $(BUILDDIR)/* 32 | 33 | html: 34 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 35 | @echo 36 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 37 | 38 | dirhtml: 39 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 40 | @echo 41 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 42 | 43 | pickle: 44 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 45 | @echo 46 | @echo "Build finished; now you can process the pickle files." 47 | 48 | json: 49 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 50 | @echo 51 | @echo "Build finished; now you can process the JSON files." 52 | 53 | htmlhelp: 54 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 55 | @echo 56 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 57 | ".hhp project file in $(BUILDDIR)/htmlhelp." 58 | 59 | qthelp: 60 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 61 | @echo 62 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 63 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 64 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/flask-mail.qhcp" 65 | @echo "To view the help file:" 66 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/flask-mail.qhc" 67 | 68 | latex: 69 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 70 | @echo 71 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 72 | @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ 73 | "run these through (pdf)latex." 74 | 75 | changes: 76 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 77 | @echo 78 | @echo "The overview file is in $(BUILDDIR)/changes." 79 | 80 | linkcheck: 81 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 82 | @echo 83 | @echo "Link check complete; look for any errors in the above output " \ 84 | "or in $(BUILDDIR)/linkcheck/output.txt." 85 | 86 | doctest: 87 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 88 | @echo "Testing of doctests in the sources finished, look at the " \ 89 | "results in $(BUILDDIR)/doctest/output.txt." 90 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | set SPHINXBUILD=sphinx-build 6 | set BUILDDIR=_build 7 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 8 | if NOT "%PAPER%" == "" ( 9 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 10 | ) 11 | 12 | if "%1" == "" goto help 13 | 14 | if "%1" == "help" ( 15 | :help 16 | echo.Please use `make ^` where ^ is one of 17 | echo. html to make standalone HTML files 18 | echo. dirhtml to make HTML files named index.html in directories 19 | echo. pickle to make pickle files 20 | echo. json to make JSON files 21 | echo. htmlhelp to make HTML files and a HTML help project 22 | echo. qthelp to make HTML files and a qthelp project 23 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 24 | echo. changes to make an overview over all changed/added/deprecated items 25 | echo. linkcheck to check all external links for integrity 26 | echo. doctest to run all doctests embedded in the documentation if enabled 27 | goto end 28 | ) 29 | 30 | if "%1" == "clean" ( 31 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 32 | del /q /s %BUILDDIR%\* 33 | goto end 34 | ) 35 | 36 | if "%1" == "html" ( 37 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 38 | echo. 39 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 40 | goto end 41 | ) 42 | 43 | if "%1" == "dirhtml" ( 44 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 45 | echo. 46 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 47 | goto end 48 | ) 49 | 50 | if "%1" == "pickle" ( 51 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 52 | echo. 53 | echo.Build finished; now you can process the pickle files. 54 | goto end 55 | ) 56 | 57 | if "%1" == "json" ( 58 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 59 | echo. 60 | echo.Build finished; now you can process the JSON files. 61 | goto end 62 | ) 63 | 64 | if "%1" == "htmlhelp" ( 65 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 66 | echo. 67 | echo.Build finished; now you can run HTML Help Workshop with the ^ 68 | .hhp project file in %BUILDDIR%/htmlhelp. 69 | goto end 70 | ) 71 | 72 | if "%1" == "qthelp" ( 73 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 74 | echo. 75 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 76 | .qhcp project file in %BUILDDIR%/qthelp, like this: 77 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\flask-mail.qhcp 78 | echo.To view the help file: 79 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\flask-mail.ghc 80 | goto end 81 | ) 82 | 83 | if "%1" == "latex" ( 84 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 85 | echo. 86 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 87 | goto end 88 | ) 89 | 90 | if "%1" == "changes" ( 91 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 92 | echo. 93 | echo.The overview file is in %BUILDDIR%/changes. 94 | goto end 95 | ) 96 | 97 | if "%1" == "linkcheck" ( 98 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 99 | echo. 100 | echo.Link check complete; look for any errors in the above output ^ 101 | or in %BUILDDIR%/linkcheck/output.txt. 102 | goto end 103 | ) 104 | 105 | if "%1" == "doctest" ( 106 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 107 | echo. 108 | echo.Testing of doctests in the sources finished, look at the ^ 109 | results in %BUILDDIR%/doctest/output.txt. 110 | goto end 111 | ) 112 | 113 | :end 114 | -------------------------------------------------------------------------------- /scripts/release.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | make-release 5 | ~~~~~~~~~~~~ 6 | 7 | Helper script that performs a release. Does pretty much everything 8 | automatically for us. 9 | 10 | :copyright: (c) 2011 by Armin Ronacher. 11 | :license: BSD, see LICENSE for more details. 12 | """ 13 | import sys 14 | import os 15 | import re 16 | from datetime import datetime, date 17 | from subprocess import Popen, PIPE 18 | 19 | _date_clean_re = re.compile(r'(\d+)(st|nd|rd|th)') 20 | 21 | 22 | def installed_libraries(): 23 | return Popen(['pip', 'freeze'], stdout=PIPE).communicate()[0] 24 | 25 | 26 | def has_library_installed(library): 27 | return library + '==' in installed_libraries() 28 | 29 | 30 | def parse_changelog(): 31 | with open('CHANGES') as f: 32 | lineiter = iter(f) 33 | for line in lineiter: 34 | match = re.search('^Version\s+(.*)', line.strip()) 35 | 36 | if match is None: 37 | continue 38 | 39 | version = match.group(1).strip() 40 | 41 | if lineiter.next().count('-') != len(line.strip()): 42 | fail('Invalid hyphen count below version line: %s', line.strip()) 43 | 44 | while 1: 45 | released = lineiter.next().strip() 46 | if released: 47 | break 48 | 49 | match = re.search(r'Released (\w+\s+\d+\w+\s+\d+)', released) 50 | 51 | if match is None: 52 | fail('Could not find release date in version %s' % version) 53 | 54 | datestr = parse_date(match.group(1).strip()) 55 | 56 | return version, datestr 57 | 58 | 59 | def bump_version(version): 60 | try: 61 | parts = map(int, version.split('.')) 62 | except ValueError: 63 | fail('Current version is not numeric') 64 | parts[-1] += 1 65 | return '.'.join(map(str, parts)) 66 | 67 | 68 | def parse_date(string): 69 | string = _date_clean_re.sub(r'\1', string) 70 | return datetime.strptime(string, '%B %d %Y') 71 | 72 | 73 | def set_filename_version(filename, version_number, pattern): 74 | changed = [] 75 | 76 | def inject_version(match): 77 | before, old, after = match.groups() 78 | changed.append(True) 79 | return before + version_number + after 80 | 81 | with open(filename) as f: 82 | contents = re.sub(r"^(\s*%s\s*=\s*')(.+?)(')(?sm)" % pattern, 83 | inject_version, f.read()) 84 | 85 | if not changed: 86 | fail('Could not find %s in %s', pattern, filename) 87 | 88 | with open(filename, 'w') as f: 89 | f.write(contents) 90 | 91 | 92 | def set_init_version(version): 93 | info('Setting __init__.py version to %s', version) 94 | set_filename_version('flask_jwt/__init__.py', version, '__version__') 95 | 96 | 97 | def set_setup_version(version): 98 | info('Setting setup.py version to %s', version) 99 | set_filename_version('setup.py', version, 'version') 100 | 101 | 102 | def set_docs_version(version): 103 | info('Setting docs/conf.py version to %s', version) 104 | set_filename_version('docs/conf.py', version, 'version') 105 | 106 | 107 | def build_and_upload(): 108 | Popen([sys.executable, 'setup.py', 'sdist', 'build_sphinx', 'upload', 'upload_sphinx']).wait() 109 | 110 | 111 | def fail(message, *args): 112 | print >> sys.stderr, 'Error:', message % args 113 | sys.exit(1) 114 | 115 | 116 | def info(message, *args): 117 | print >> sys.stderr, message % args 118 | 119 | 120 | def get_git_tags(): 121 | return set(Popen(['git', 'tag'], stdout=PIPE).communicate()[0].splitlines()) 122 | 123 | 124 | def git_is_clean(): 125 | return Popen(['git', 'diff', '--quiet']).wait() == 0 126 | 127 | 128 | def make_git_commit(message, *args): 129 | message = message % args 130 | Popen(['git', 'commit', '-am', message]).wait() 131 | 132 | 133 | def make_git_tag(tag): 134 | info('Tagging "%s"', tag) 135 | Popen(['git', 'tag', '-a', tag, '-m', '%s release' % tag]).wait() 136 | Popen(['git', 'push', '--tags']).wait() 137 | 138 | 139 | def update_version(version): 140 | for f in [set_init_version, set_setup_version, set_docs_version]: 141 | f(version) 142 | 143 | 144 | def get_branches(): 145 | return set(Popen(['git', 'branch'], stdout=PIPE).communicate()[0].splitlines()) 146 | 147 | 148 | def branch_is(branch): 149 | return '* ' + branch in get_branches() 150 | 151 | 152 | def main(): 153 | os.chdir(os.path.join(os.path.dirname(__file__), '..')) 154 | 155 | rv = parse_changelog() 156 | 157 | if rv is None: 158 | fail('Could not parse changelog') 159 | 160 | version, release_date = rv 161 | 162 | tags = get_git_tags() 163 | 164 | for lib in ['Sphinx', 'Sphinx-PyPI-upload']: 165 | if not has_library_installed(lib): 166 | fail('Build requires that %s be installed', lib) 167 | 168 | if version in tags: 169 | fail('Version "%s" is already tagged', version) 170 | if release_date.date() != date.today(): 171 | fail('Release date is not today') 172 | 173 | if not branch_is('master'): 174 | fail('You are not on the master branch') 175 | 176 | if not git_is_clean(): 177 | fail('You have uncommitted changes in git') 178 | 179 | info('Releasing %s (release date %s)', 180 | version, release_date.strftime('%d/%m/%Y')) 181 | 182 | update_version(version) 183 | make_git_commit('Bump version number to %s', version) 184 | make_git_tag(version) 185 | build_and_upload() 186 | 187 | 188 | if __name__ == '__main__': 189 | main() 190 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Flask-JWT 2 | ========= 3 | 4 | .. currentmodule:: flask_jwt 5 | 6 | Add basic JWT features to your `Flask`_ application. 7 | 8 | 9 | Links 10 | ----- 11 | 12 | * `documentation `_ 13 | * `source `_ 14 | * :doc:`changelog ` 15 | 16 | 17 | Installation 18 | ------------ 19 | 20 | Install with **pip** or **easy_install**:: 21 | 22 | pip install Flask-JWT 23 | 24 | or download the latest version from version control:: 25 | 26 | git clone https://github.com/mattupstate/flask-jwt.git ./flask-jwt 27 | pip install ./flask-jwt 28 | 29 | 30 | Quickstart 31 | ---------- 32 | 33 | Minimum viable application configuration: 34 | 35 | .. code-block:: python 36 | 37 | from flask import Flask 38 | from flask_jwt import JWT, jwt_required, current_identity 39 | from werkzeug.security import safe_str_cmp 40 | 41 | class User(object): 42 | def __init__(self, id, username, password): 43 | self.id = id 44 | self.username = username 45 | self.password = password 46 | 47 | def __str__(self): 48 | return "User(id='%s')" % self.id 49 | 50 | users = [ 51 | User(1, 'user1', 'abcxyz'), 52 | User(2, 'user2', 'abcxyz'), 53 | ] 54 | 55 | username_table = {u.username: u for u in users} 56 | userid_table = {u.id: u for u in users} 57 | 58 | def authenticate(username, password): 59 | user = username_table.get(username, None) 60 | if user and safe_str_cmp(user.password.encode('utf-8'), password.encode('utf-8')): 61 | return user 62 | 63 | def identity(payload): 64 | user_id = payload['identity'] 65 | return userid_table.get(user_id, None) 66 | 67 | app = Flask(__name__) 68 | app.debug = True 69 | app.config['SECRET_KEY'] = 'super-secret' 70 | 71 | jwt = JWT(app, authenticate, identity) 72 | 73 | @app.route('/protected') 74 | @jwt_required() 75 | def protected(): 76 | return '%s' % current_identity 77 | 78 | if __name__ == '__main__': 79 | app.run() 80 | 81 | 82 | 83 | To get a token make a request to the auth resource:: 84 | 85 | POST /auth HTTP/1.1 86 | Host: localhost:5000 87 | Content-Type: application/json 88 | 89 | { 90 | "username": "joe", 91 | "password": "pass" 92 | } 93 | 94 | The response should look similar to:: 95 | 96 | HTTP/1.1 200 OK 97 | Content-Type: application/json 98 | 99 | { 100 | "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eSI6MSwiaWF0IjoxNDQ0OTE3NjQwLCJuYmYiOjE0NDQ5MTc2NDAsImV4cCI6MTQ0NDkxNzk0MH0.KPmI6WSjRjlpzecPvs3q_T3cJQvAgJvaQAPtk1abC_E" 101 | } 102 | 103 | This token can then be used to make requests against protected endpoints:: 104 | 105 | GET /protected HTTP/1.1 106 | Authorization: JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eSI6MSwiaWF0IjoxNDQ0OTE3NjQwLCJuYmYiOjE0NDQ5MTc2NDAsImV4cCI6MTQ0NDkxNzk0MH0.KPmI6WSjRjlpzecPvs3q_T3cJQvAgJvaQAPtk1abC_E 107 | 108 | 109 | Within a function decorated by `jwt_required()`, you can use the 110 | `current_identity` proxy to access the user whose token was passed into this 111 | request context. 112 | 113 | 114 | Configuration Options 115 | --------------------- 116 | 117 | .. tabularcolumns:: |p{6.5cm}|p{8.5cm}| 118 | 119 | ========================== ===================================================== 120 | ``JWT_DEFAULT_REALM`` The default realm. Defaults to ``Login Required`` 121 | ``JWT_AUTH_URL_RULE`` The authentication endpoint URL. Defaults to 122 | ``/auth``. 123 | ``JWT_AUTH_ENDPOINT`` The authentication endpoint name. Defaults to 124 | ``jwt``. 125 | ``JWT_AUTH_USERNAME_KEY`` The username key in the authentication request 126 | payload. Defaults to ``username``. 127 | ``JWT_AUTH_PASSWORD_KEY`` The password key in the authentication request 128 | payload. Defaults to ``password``. 129 | ``JWT_ALGORITHM`` The token algorithm. Defaults to ``HS256`` 130 | ``JWT_LEEWAY`` The amount of leeway given when decoding access 131 | tokens specified as an integer of seconds or a 132 | ``datetime.timedelta`` instance. Defaults to 133 | ``timedelta(seconds=10)``. 134 | ``JWT_VERIFY`` Flag indicating if all tokens should be verified. 135 | Defaults to ``True``. It is not recommended to 136 | change this value. 137 | ``JWT_AUTH_HEADER_PREFIX`` The Authorization header value prefix. Defaults to 138 | ``JWT`` as to not conflict with OAuth2 Bearer 139 | tokens. This is not a case sensitive value. 140 | ``JWT_VERIFY_EXPIRATION`` Flag indicating if all tokens should verify their 141 | expiration time. Defaults to ``True``. It is not 142 | recommended to change this value. 143 | ``JWT_LEEWAY`` A token expiration leeway value. Defaults to ``0``. 144 | ``JWT_EXPIRATION_DELTA`` A ``datetime.timedelta`` value indicating how long 145 | tokens are valid for. This value is added to the 146 | ``iat`` (issued at) claim. Defaults to 147 | ``timedelta(seconds=300)`` 148 | ``JWT_NOT_BEFORE_DELTA`` A ``datetime.timedelta`` value indicating a relative 149 | time from the ``iat`` (issued at) claim that the 150 | token can begin to be used. This value is added to 151 | the ``iat`` (issued at) claim. Defaults to 152 | ``timedelta(seconds=0)`` 153 | ``JWT_VERIFY_CLAIMS`` A list of claims to verify when decoding tokens. 154 | Defaults to ``['signature', 'exp', 'nbf', 'iat']``. 155 | ``JWT_REQUIRED_CLAIMS`` A list of claims that are required in a token to be 156 | considered valid. Defaults to 157 | ``['exp', 'iat', 'nbf']`` 158 | ========================== ===================================================== 159 | 160 | API 161 | --- 162 | 163 | .. data:: current_identity 164 | 165 | A proxy for the current identity. It will only be set in the context of 166 | function decorated by `jwt_required()`. 167 | 168 | .. module:: flask_jwt 169 | 170 | .. autoclass:: JWT 171 | :members: 172 | 173 | .. autofunction:: jwt_required 174 | 175 | 176 | Changelog 177 | --------- 178 | .. toctree:: 179 | :maxdepth: 2 180 | 181 | changelog 182 | 183 | .. _Flask: http://flask.pocoo.org 184 | .. _GitHub: http://github.com/mattupstate/flask-jwt 185 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # flask-jwt documentation build configuration file, created by 4 | # sphinx-quickstart on Fri May 28 11:39:14 2010. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | sys.path.insert(0, os.path.abspath('..')) 20 | sys.path.append(os.path.abspath('_themes')) 21 | 22 | # -- General configuration ----------------------------------------------------- 23 | 24 | # Add any Sphinx extension module names here, as strings. They can be extensions 25 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 26 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode'] 27 | 28 | # Add any paths that contain templates here, relative to this directory. 29 | templates_path = ['_templates'] 30 | 31 | # The suffix of source filenames. 32 | source_suffix = '.rst' 33 | 34 | # The encoding of source files. 35 | #source_encoding = 'utf-8' 36 | 37 | # The master toctree document. 38 | master_doc = 'index' 39 | 40 | # General information about the project. 41 | project = u'Flask-JWT' 42 | copyright = u'2014, Matt Wright' 43 | 44 | # The version info for the project you're documenting, acts as replacement for 45 | # |version| and |release|, also used in various other places throughout the 46 | # built documents. 47 | # 48 | # The short X.Y version. 49 | version = '0.3.2' 50 | # The full version, including alpha/beta/rc tags. 51 | release = version 52 | 53 | # The language for content autogenerated by Sphinx. Refer to documentation 54 | # for a list of supported languages. 55 | #language = None 56 | 57 | # There are two options for replacing |today|: either, you set today to some 58 | # non-false value, then it is used: 59 | #today = '' 60 | # Else, today_fmt is used as the format for a strftime call. 61 | #today_fmt = '%B %d, %Y' 62 | 63 | # List of documents that shouldn't be included in the build. 64 | #unused_docs = [] 65 | 66 | # List of directories, relative to source directory, that shouldn't be searched 67 | # for source files. 68 | exclude_trees = ['_build'] 69 | 70 | # The reST default role (used for this markup: `text`) to use for all documents. 71 | default_role = 'obj' 72 | 73 | # If true, '()' will be appended to :func: etc. cross-reference text. 74 | #add_function_parentheses = True 75 | 76 | # If true, the current module name will be prepended to all description 77 | # unit titles (such as .. function::). 78 | #add_module_names = True 79 | 80 | # If true, sectionauthor and moduleauthor directives will be shown in the 81 | # output. They are ignored by default. 82 | #show_authors = False 83 | 84 | # The name of the Pygments (syntax highlighting) style to use. 85 | #pygments_style = 'sphinx' 86 | 87 | # A list of ignored prefixes for module index sorting. 88 | #modindex_common_prefix = [] 89 | 90 | 91 | # -- Options for HTML output --------------------------------------------------- 92 | 93 | # The theme to use for HTML and HTML Help pages. Major themes that come with 94 | # Sphinx are currently 'default' and 'sphinxdoc'. 95 | html_theme = 'flask_small' 96 | #html_theme = 'default' 97 | html_theme_options = { 98 | 'index_logo': False, 99 | 'github_fork': 'mattupstate/flask-jwt' 100 | } 101 | 102 | # Theme options are theme-specific and customize the look and feel of a theme 103 | # further. For a list of options available for each theme, see the 104 | # documentation. 105 | #html_theme_options = {} 106 | 107 | # Add any paths that contain custom themes here, relative to this directory. 108 | html_theme_path = ['_themes'] 109 | 110 | # The name for this set of Sphinx documents. If None, it defaults to 111 | # " v documentation". 112 | #html_title = None 113 | 114 | # A shorter title for the navigation bar. Default is the same as html_title. 115 | #html_short_title = None 116 | 117 | # The name of an image file (relative to this directory) to place at the top 118 | # of the sidebar. 119 | #html_logo = None 120 | 121 | # The name of an image file (within the static path) to use as favicon of the 122 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 123 | # pixels large. 124 | #html_favicon = None 125 | 126 | # Add any paths that contain custom static files (such as style sheets) here, 127 | # relative to this directory. They are copied after the builtin static files, 128 | # so a file named "default.css" will overwrite the builtin "default.css". 129 | html_static_path = ['_static'] 130 | 131 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 132 | # using the given strftime format. 133 | #html_last_updated_fmt = '%b %d, %Y' 134 | 135 | # If true, SmartyPants will be used to convert quotes and dashes to 136 | # typographically correct entities. 137 | #html_use_smartypants = True 138 | 139 | # Custom sidebar templates, maps document names to template names. 140 | #html_sidebars = {} 141 | 142 | # Additional templates that should be rendered to pages, maps page names to 143 | # template names. 144 | #html_additional_pages = {} 145 | 146 | # If false, no module index is generated. 147 | #html_use_modindex = True 148 | 149 | # If false, no index is generated. 150 | #html_use_index = True 151 | 152 | # If true, the index is split into individual pages for each letter. 153 | #html_split_index = False 154 | 155 | # If true, links to the reST sources are added to the pages. 156 | #html_show_sourcelink = True 157 | 158 | # If true, an OpenSearch description file will be output, and all pages will 159 | # contain a tag referring to it. The value of this option must be the 160 | # base URL from which the finished HTML is served. 161 | #html_use_opensearch = '' 162 | 163 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). 164 | #html_file_suffix = '' 165 | 166 | # Output file base name for HTML help builder. 167 | htmlhelp_basename = 'flask-jwtdoc' 168 | 169 | 170 | # -- Options for LaTeX output -------------------------------------------------- 171 | 172 | # The paper size ('letter' or 'a4'). 173 | #latex_paper_size = 'letter' 174 | 175 | # The font size ('10pt', '11pt' or '12pt'). 176 | #latex_font_size = '10pt' 177 | 178 | # Grouping the document tree into LaTeX files. List of tuples 179 | # (source start file, target name, title, author, documentclass [howto/manual]). 180 | latex_documents = [ 181 | ('index', 'flask-jwt.tex', u'flask-jwt Documentation', 182 | u'Dan Jacob', 'manual'), 183 | ] 184 | 185 | # The name of an image file (relative to this directory) to place at the top of 186 | # the title page. 187 | #latex_logo = None 188 | 189 | # For "manual" documents, if this is true, then toplevel headings are parts, 190 | # not chapters. 191 | #latex_use_parts = False 192 | 193 | # Additional stuff for the LaTeX preamble. 194 | #latex_preamble = '' 195 | 196 | # Documents to append as an appendix to all manuals. 197 | #latex_appendices = [] 198 | 199 | # If false, no module index is generated. 200 | #latex_use_modindex = True 201 | 202 | intersphinx_mapping = {'http://docs.python.org/': None, 203 | 'http://flask.pocoo.org/docs/': None} 204 | -------------------------------------------------------------------------------- /tests/test_jwt.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tests.test_jwt 4 | ~~~~~~~~~~~~~~ 5 | 6 | Flask-JWT tests 7 | """ 8 | import time 9 | 10 | from datetime import datetime, timedelta 11 | 12 | import jwt as _jwt 13 | import pytest 14 | 15 | from flask import Flask, json, jsonify 16 | 17 | import flask_jwt 18 | 19 | 20 | def post_json(client, url, data): 21 | data = json.dumps(data) 22 | resp = client.post(url, headers={'Content-Type': 'application/json'}, data=data) 23 | return resp, json.loads(resp.data) 24 | 25 | 26 | def assert_error_response(r, code, msg, desc): 27 | assert r.status_code == code 28 | jdata = json.loads(r.data) 29 | assert jdata['status_code'] == code 30 | assert jdata['error'] == msg 31 | assert jdata['description'] == desc 32 | 33 | 34 | def test_initialize(): 35 | app = Flask(__name__) 36 | app.config['SECRET_KEY'] = 'super-secret' 37 | jwt = flask_jwt.JWT(app, lambda: None, lambda: None) 38 | assert isinstance(jwt, flask_jwt.JWT) 39 | assert len(app.url_map._rules) == 2 40 | 41 | 42 | def test_adds_auth_endpoint(): 43 | app = Flask(__name__) 44 | app.config['SECRET_KEY'] = 'super-secret' 45 | app.config['JWT_AUTH_URL_RULE'] = '/auth' 46 | app.config['JWT_AUTH_ENDPOINT'] = 'jwt_auth' 47 | flask_jwt.JWT(app, lambda: None, lambda: None) 48 | rules = [str(r) for r in app.url_map._rules] 49 | assert '/auth' in rules 50 | 51 | 52 | def test_auth_endpoint_with_valid_request(client, user): 53 | resp, jdata = post_json( 54 | client, '/auth', {'username': user.username, 'password': user.password}) 55 | assert resp.status_code == 200 56 | assert 'access_token' in jdata 57 | 58 | 59 | def test_custom_auth_endpoint_with_valid_request(app, client, user): 60 | app.config['JWT_AUTH_USERNAME_KEY'] = 'email' 61 | app.config['JWT_AUTH_PASSWORD_KEY'] = 'pass' 62 | resp, jdata = post_json( 63 | client, 64 | '/auth', 65 | {'email': user.username, 'pass': user.password} 66 | ) 67 | assert resp.status_code == 200 68 | assert 'access_token' in jdata 69 | 70 | 71 | def test_auth_endpoint_with_invalid_request(client, user): 72 | # Invalid request (no password) 73 | resp, jdata = post_json(client, '/auth', {'username': user.username}) 74 | assert resp.status_code == 401 75 | assert 'error' in jdata 76 | assert jdata['error'] == 'Bad Request' 77 | assert 'description' in jdata 78 | assert jdata['description'] == 'Invalid credentials' 79 | assert 'status_code' in jdata 80 | assert jdata['status_code'] == 401 81 | 82 | 83 | def test_auth_endpoint_with_invalid_credentials(client): 84 | resp, jdata = post_json( 85 | client, '/auth', {'username': 'bogus', 'password': 'bogus'}) 86 | 87 | assert resp.status_code == 401 88 | assert 'error' in jdata 89 | assert jdata['error'] == 'Bad Request' 90 | assert 'description' in jdata 91 | assert jdata['description'] == 'Invalid credentials' 92 | assert 'status_code' in jdata 93 | assert jdata['status_code'] == 401 94 | 95 | 96 | def test_jwt_required_decorator_with_valid_token(app, client, user): 97 | resp, jdata = post_json( 98 | client, '/auth', {'username': user.username, 'password': user.password}) 99 | 100 | token = jdata['access_token'] 101 | resp = client.get('/protected', headers={'Authorization': 'JWT ' + token}) 102 | 103 | assert resp.status_code == 200 104 | assert resp.data == b'success' 105 | 106 | 107 | def test_jwt_required_decorator_with_valid_request_current_identity(app, client, user): 108 | with client as c: 109 | resp, jdata = post_json( 110 | client, '/auth', {'username': user.username, 'password': user.password}) 111 | token = jdata['access_token'] 112 | 113 | c.get( 114 | '/protected', 115 | headers={'authorization': 'JWT ' + token}) 116 | assert flask_jwt.current_identity 117 | 118 | 119 | def test_jwt_required_decorator_with_invalid_request_current_identity(app, client): 120 | with client as c: 121 | c.get('/protected', headers={'authorization': 'JWT bogus'}) 122 | assert flask_jwt.current_identity._get_current_object() is None 123 | 124 | 125 | def test_jwt_required_decorator_with_invalid_authorization_headers(app, client): 126 | # Missing authorization header 127 | r = client.get('/protected') 128 | 129 | assert_error_response( 130 | r, 401, 'Authorization Required', 'Request does not contain an access token') 131 | 132 | assert r.headers['WWW-Authenticate'] == 'JWT realm="Login Required"' 133 | 134 | # Not a JWT auth header prefix 135 | r = client.get('/protected', headers={'authorization': 'Bogus xxx'}) 136 | 137 | assert_error_response( 138 | r, 401, 'Invalid JWT header', 'Unsupported authorization type') 139 | 140 | # Missing token 141 | r = client.get('/protected', headers={'authorization': 'JWT'}) 142 | 143 | assert_error_response( 144 | r, 401, 'Invalid JWT header', 'Token missing') 145 | 146 | # Token with spaces 147 | r = client.get('/protected', headers={'authorization': 'JWT xxx xxx'}) 148 | 149 | assert_error_response( 150 | r, 401, 'Invalid JWT header', 'Token contains spaces') 151 | 152 | 153 | def test_jwt_required_decorator_with_invalid_jwt_tokens(client, user, app): 154 | app.config['JWT_LEEWAY'] = timedelta(seconds=0) 155 | app.config['JWT_EXPIRATION_DELTA'] = timedelta(milliseconds=200) 156 | 157 | resp, jdata = post_json( 158 | client, '/auth', {'username': user.username, 'password': user.password}) 159 | token = jdata['access_token'] 160 | 161 | # Undecipherable 162 | r = client.get('/protected', headers={'authorization': 'JWT %sX' % token}) 163 | assert_error_response(r, 401, 'Invalid token', 'Signature verification failed') 164 | 165 | # Expired 166 | time.sleep(1.5) 167 | r = client.get('/protected', headers={'authorization': 'JWT ' + token}) 168 | assert_error_response(r, 401, 'Invalid token', 'Signature has expired') 169 | 170 | 171 | def test_jwt_required_decorator_with_missing_user(client, jwt, user): 172 | resp, jdata = post_json( 173 | client, '/auth', {'username': user.username, 'password': user.password}) 174 | token = jdata['access_token'] 175 | 176 | @jwt.identity_handler 177 | def load_user(payload): 178 | return None 179 | 180 | r = client.get('/protected', headers={'authorization': 'JWT %s' % token}) 181 | assert_error_response(r, 401, 'Invalid JWT', 'User does not exist') 182 | 183 | 184 | def test_custom_error_handler(client, jwt): 185 | @jwt.jwt_error_handler 186 | def error_handler(e): 187 | return "custom" 188 | 189 | r = client.get('/protected') 190 | assert r.data == b'custom' 191 | 192 | 193 | def test_custom_response_handler(client, jwt, user): 194 | @jwt.auth_response_handler 195 | def resp_handler(access_token, identity): 196 | return jsonify({'mytoken': access_token.decode('utf-8')}) 197 | 198 | resp, jdata = post_json( 199 | client, '/auth', {'username': user.username, 'password': user.password}) 200 | 201 | assert 'mytoken' in jdata 202 | 203 | 204 | def test_custom_encode_handler(client, jwt, user, app): 205 | secret = app.config['JWT_SECRET_KEY'] 206 | alg = 'HS256' 207 | 208 | @jwt.jwt_encode_handler 209 | def encode_data(identity): 210 | return _jwt.encode({'hello': 'world'}, secret, algorithm=alg) 211 | 212 | resp, jdata = post_json( 213 | client, '/auth', {'username': user.username, 'password': user.password}) 214 | 215 | decoded = _jwt.decode(jdata['access_token'], secret, algorithms=[alg]) 216 | 217 | assert decoded == {'hello': 'world'} 218 | 219 | 220 | def test_custom_decode_handler(client, user, jwt): 221 | # The following function should receive the decode return value 222 | @jwt.identity_handler 223 | def load_user(payload): 224 | assert payload == {'user_id': user.id} 225 | 226 | @jwt.jwt_decode_handler 227 | def decode_data(token): 228 | return {'user_id': user.id} 229 | 230 | with client as c: 231 | resp, jdata = post_json( 232 | client, '/auth', {'username': user.username, 'password': user.password}) 233 | 234 | token = jdata['access_token'] 235 | 236 | c.get('/protected', headers={'authorization': 'JWT ' + token}) 237 | 238 | 239 | def test_custom_payload_handler(client, jwt, user): 240 | @jwt.identity_handler 241 | def load_user(payload): 242 | if payload['id'] == user.id: 243 | return user 244 | 245 | @jwt.jwt_payload_handler 246 | def make_payload(u): 247 | iat = datetime.utcnow() 248 | exp = iat + timedelta(seconds=60) 249 | nbf = iat + timedelta(seconds=0) 250 | return {'iat': iat, 'exp': exp, 'nbf': nbf, 'id': u.id} 251 | 252 | with client as c: 253 | resp, jdata = post_json( 254 | client, '/auth', {'username': user.username, 'password': user.password}) 255 | 256 | token = jdata['access_token'] 257 | 258 | c.get('/protected', headers={'authorization': 'JWT ' + token}) 259 | assert flask_jwt.current_identity == user 260 | 261 | 262 | def test_custom_auth_header(app, client, user): 263 | app.config['JWT_AUTH_HEADER_PREFIX'] = 'Bearer' 264 | 265 | with client as c: 266 | resp, jdata = post_json( 267 | client, '/auth', {'username': user.username, 'password': user.password}) 268 | 269 | token = jdata['access_token'] 270 | 271 | # Custom Bearer auth header prefix 272 | resp = c.get('/protected', headers={'authorization': 'Bearer ' + token}) 273 | assert resp.status_code == 200 274 | assert resp.data == b'success' 275 | 276 | # Not custom Bearer auth header prefix 277 | resp = c.get('/protected', headers={'authorization': 'JWT ' + token}) 278 | assert_error_response(resp, 401, 'Invalid JWT header', 'Unsupported authorization type') 279 | 280 | 281 | def test_custom_auth_handler(): 282 | def custom_auth_request_handler(): 283 | return jsonify({'hello': 'world'}) 284 | 285 | jwt = flask_jwt.JWT() 286 | pytest.deprecated_call(jwt.auth_request_handler, custom_auth_request_handler) 287 | 288 | app = Flask(__name__) 289 | jwt.init_app(app) 290 | 291 | with app.test_client() as c: 292 | resp, jdata = post_json(c, '/auth', {}) 293 | assert jdata == {'hello': 'world'} 294 | -------------------------------------------------------------------------------- /flask_jwt/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | flask_jwt 4 | ~~~~~~~~~ 5 | 6 | Flask-JWT module 7 | """ 8 | 9 | import logging 10 | import warnings 11 | 12 | from collections import OrderedDict 13 | from datetime import datetime, timedelta 14 | from functools import wraps 15 | 16 | import jwt 17 | 18 | from flask import current_app, request, jsonify, _request_ctx_stack 19 | from werkzeug.local import LocalProxy 20 | 21 | __version__ = '0.3.2' 22 | 23 | logger = logging.getLogger(__name__) 24 | 25 | current_identity = LocalProxy(lambda: getattr(_request_ctx_stack.top, 'current_identity', None)) 26 | 27 | _jwt = LocalProxy(lambda: current_app.extensions['jwt']) 28 | 29 | CONFIG_DEFAULTS = { 30 | 'JWT_DEFAULT_REALM': 'Login Required', 31 | 'JWT_AUTH_URL_RULE': '/auth', 32 | 'JWT_AUTH_ENDPOINT': 'jwt', 33 | 'JWT_AUTH_USERNAME_KEY': 'username', 34 | 'JWT_AUTH_PASSWORD_KEY': 'password', 35 | 'JWT_ALGORITHM': 'HS256', 36 | 'JWT_LEEWAY': timedelta(seconds=10), 37 | 'JWT_AUTH_HEADER_PREFIX': 'JWT', 38 | 'JWT_EXPIRATION_DELTA': timedelta(seconds=300), 39 | 'JWT_NOT_BEFORE_DELTA': timedelta(seconds=0), 40 | 'JWT_VERIFY_CLAIMS': ['signature', 'exp', 'nbf', 'iat'], 41 | 'JWT_REQUIRED_CLAIMS': ['exp', 'iat', 'nbf'] 42 | } 43 | 44 | 45 | def _default_jwt_headers_handler(identity): 46 | return None 47 | 48 | 49 | def _default_jwt_payload_handler(identity): 50 | iat = datetime.utcnow() 51 | exp = iat + current_app.config.get('JWT_EXPIRATION_DELTA') 52 | nbf = iat + current_app.config.get('JWT_NOT_BEFORE_DELTA') 53 | identity = getattr(identity, 'id') or identity['id'] 54 | return {'exp': exp, 'iat': iat, 'nbf': nbf, 'identity': identity} 55 | 56 | 57 | def _default_jwt_encode_handler(identity): 58 | secret = current_app.config['JWT_SECRET_KEY'] 59 | algorithm = current_app.config['JWT_ALGORITHM'] 60 | required_claims = current_app.config['JWT_REQUIRED_CLAIMS'] 61 | 62 | payload = _jwt.jwt_payload_callback(identity) 63 | missing_claims = list(set(required_claims) - set(payload.keys())) 64 | 65 | if missing_claims: 66 | raise RuntimeError('Payload is missing required claims: %s' % ', '.join(missing_claims)) 67 | 68 | headers = _jwt.jwt_headers_callback(identity) 69 | 70 | return jwt.encode(payload, secret, algorithm=algorithm, headers=headers) 71 | 72 | 73 | def _default_jwt_decode_handler(token): 74 | secret = current_app.config['JWT_SECRET_KEY'] 75 | algorithm = current_app.config['JWT_ALGORITHM'] 76 | leeway = current_app.config['JWT_LEEWAY'] 77 | 78 | verify_claims = current_app.config['JWT_VERIFY_CLAIMS'] 79 | required_claims = current_app.config['JWT_REQUIRED_CLAIMS'] 80 | 81 | options = { 82 | 'verify_' + claim: True 83 | for claim in verify_claims 84 | } 85 | 86 | options.update({ 87 | 'require_' + claim: True 88 | for claim in required_claims 89 | }) 90 | 91 | return jwt.decode(token, secret, options=options, algorithms=[algorithm], leeway=leeway) 92 | 93 | 94 | def _default_request_handler(): 95 | auth_header_value = request.headers.get('Authorization', None) 96 | auth_header_prefix = current_app.config['JWT_AUTH_HEADER_PREFIX'] 97 | 98 | if not auth_header_value: 99 | return 100 | 101 | parts = auth_header_value.split() 102 | 103 | if parts[0].lower() != auth_header_prefix.lower(): 104 | raise JWTError('Invalid JWT header', 'Unsupported authorization type') 105 | elif len(parts) == 1: 106 | raise JWTError('Invalid JWT header', 'Token missing') 107 | elif len(parts) > 2: 108 | raise JWTError('Invalid JWT header', 'Token contains spaces') 109 | 110 | return parts[1] 111 | 112 | 113 | def _default_auth_request_handler(): 114 | data = request.get_json() 115 | username = data.get(current_app.config.get('JWT_AUTH_USERNAME_KEY'), None) 116 | password = data.get(current_app.config.get('JWT_AUTH_PASSWORD_KEY'), None) 117 | criterion = [username, password, len(data) == 2] 118 | 119 | if not all(criterion): 120 | raise JWTError('Bad Request', 'Invalid credentials') 121 | 122 | identity = _jwt.authentication_callback(username, password) 123 | 124 | if identity: 125 | access_token = _jwt.jwt_encode_callback(identity) 126 | return _jwt.auth_response_callback(access_token, identity) 127 | else: 128 | raise JWTError('Bad Request', 'Invalid credentials') 129 | 130 | 131 | def _default_auth_response_handler(access_token, identity): 132 | return jsonify({'access_token': access_token.decode('utf-8')}) 133 | 134 | 135 | def _default_jwt_error_handler(error): 136 | logger.error(error) 137 | return jsonify(OrderedDict([ 138 | ('status_code', error.status_code), 139 | ('error', error.error), 140 | ('description', error.description), 141 | ])), error.status_code, error.headers 142 | 143 | 144 | def _jwt_required(realm): 145 | """Does the actual work of verifying the JWT data in the current request. 146 | This is done automatically for you by `jwt_required()` but you could call it manually. 147 | Doing so would be useful in the context of optional JWT access in your APIs. 148 | 149 | :param realm: an optional realm 150 | """ 151 | token = _jwt.request_callback() 152 | 153 | if token is None: 154 | raise JWTError('Authorization Required', 'Request does not contain an access token', 155 | headers={'WWW-Authenticate': 'JWT realm="%s"' % realm}) 156 | 157 | try: 158 | payload = _jwt.jwt_decode_callback(token) 159 | except jwt.InvalidTokenError as e: 160 | raise JWTError('Invalid token', str(e)) 161 | 162 | _request_ctx_stack.top.current_identity = identity = _jwt.identity_callback(payload) 163 | 164 | if identity is None: 165 | raise JWTError('Invalid JWT', 'User does not exist') 166 | 167 | 168 | def jwt_required(realm=None): 169 | """View decorator that requires a valid JWT token to be present in the request 170 | 171 | :param realm: an optional realm 172 | """ 173 | def wrapper(fn): 174 | @wraps(fn) 175 | def decorator(*args, **kwargs): 176 | _jwt_required(realm or current_app.config['JWT_DEFAULT_REALM']) 177 | return fn(*args, **kwargs) 178 | return decorator 179 | return wrapper 180 | 181 | 182 | class JWTError(Exception): 183 | def __init__(self, error, description, status_code=401, headers=None): 184 | self.error = error 185 | self.description = description 186 | self.status_code = status_code 187 | self.headers = headers 188 | 189 | def __repr__(self): 190 | return 'JWTError: %s' % self.error 191 | 192 | def __str__(self): 193 | return '%s. %s' % (self.error, self.description) 194 | 195 | 196 | def encode_token(): 197 | return _jwt.encode_callback(_jwt.header_callback(), _jwt.payload_callback()) 198 | 199 | 200 | class JWT(object): 201 | 202 | def __init__(self, app=None, authentication_handler=None, identity_handler=None): 203 | self.authentication_callback = authentication_handler 204 | self.identity_callback = identity_handler 205 | 206 | self.auth_response_callback = _default_auth_response_handler 207 | self.auth_request_callback = _default_auth_request_handler 208 | self.jwt_encode_callback = _default_jwt_encode_handler 209 | self.jwt_decode_callback = _default_jwt_decode_handler 210 | self.jwt_headers_callback = _default_jwt_headers_handler 211 | self.jwt_payload_callback = _default_jwt_payload_handler 212 | self.jwt_error_callback = _default_jwt_error_handler 213 | self.request_callback = _default_request_handler 214 | 215 | if app is not None: 216 | self.init_app(app) 217 | 218 | def init_app(self, app): 219 | for k, v in CONFIG_DEFAULTS.items(): 220 | app.config.setdefault(k, v) 221 | app.config.setdefault('JWT_SECRET_KEY', app.config['SECRET_KEY']) 222 | 223 | auth_url_rule = app.config.get('JWT_AUTH_URL_RULE', None) 224 | 225 | if auth_url_rule: 226 | if self.auth_request_callback == _default_auth_request_handler: 227 | assert self.authentication_callback is not None, ( 228 | 'an authentication_handler function must be defined when using the built in ' 229 | 'authentication resource') 230 | 231 | auth_url_options = app.config.get('JWT_AUTH_URL_OPTIONS', {'methods': ['POST']}) 232 | auth_url_options.setdefault('view_func', self.auth_request_callback) 233 | app.add_url_rule(auth_url_rule, **auth_url_options) 234 | 235 | app.errorhandler(JWTError)(self._jwt_error_callback) 236 | 237 | if not hasattr(app, 'extensions'): # pragma: no cover 238 | app.extensions = {} 239 | 240 | app.extensions['jwt'] = self 241 | 242 | def _jwt_error_callback(self, error): 243 | return self.jwt_error_callback(error) 244 | 245 | def authentication_handler(self, callback): 246 | """Specifies the identity handler function. This function receives two positional 247 | arguments. The first being the username the second being the password. It should return an 248 | object representing an authenticated identity. Example:: 249 | 250 | @jwt.authentication_handler 251 | def authenticate(username, password): 252 | user = User.query.filter(User.username == username).scalar() 253 | if bcrypt.check_password_hash(user.password, password): 254 | return user 255 | 256 | :param callback: the identity handler function 257 | """ 258 | self.authentication_callback = callback 259 | return callback 260 | 261 | def identity_handler(self, callback): 262 | """Specifies the identity handler function. This function receives one positional argument 263 | being the JWT payload. For example:: 264 | 265 | @jwt.identity_handler 266 | def identify(payload): 267 | return User.query.filter(User.id == payload['identity']).scalar() 268 | 269 | :param callback: the identity handler function 270 | """ 271 | self.identity_callback = callback 272 | return callback 273 | 274 | def jwt_error_handler(self, callback): 275 | """Specifies the error handler function. Example:: 276 | 277 | @jwt.error_handler 278 | def error_handler(e): 279 | return "Something bad happened", 400 280 | 281 | :param callback: the error handler function 282 | """ 283 | self.jwt_error_callback = callback 284 | return callback 285 | 286 | def auth_response_handler(self, callback): 287 | """Specifies the authentication response handler function. 288 | 289 | :param callable callback: the auth response handler function 290 | """ 291 | self.auth_response_callback = callback 292 | return callback 293 | 294 | def auth_request_handler(self, callback): 295 | """Specifies the authentication response handler function. 296 | 297 | :param callable callback: the auth request handler function 298 | 299 | .. deprecated 300 | """ 301 | warnings.warn("This handler is deprecated. The recommended approach to have control over " 302 | "the authentication resource is to disable the built-in resource by " 303 | "setting JWT_AUTH_URL_RULE=None and registering your own authentication " 304 | "resource directly on your application.", DeprecationWarning, stacklevel=2) 305 | self.auth_request_callback = callback 306 | return callback 307 | 308 | def request_handler(self, callback): 309 | """Specifieds the request handler function. This function returns a JWT from the current 310 | request. 311 | 312 | :param callable callback: the request handler function 313 | """ 314 | self.request_callback = callback 315 | return callback 316 | 317 | def jwt_encode_handler(self, callback): 318 | """Specifies the encoding handler function. This function receives a payload and signs it. 319 | 320 | :param callable callback: the encoding handler function 321 | """ 322 | self.jwt_encode_callback = callback 323 | return callback 324 | 325 | def jwt_decode_handler(self, callback): 326 | """Specifies the decoding handler function. This function receives a 327 | signed payload and decodes it. 328 | 329 | :param callable callback: the decoding handler function 330 | """ 331 | self.jwt_decode_callback = callback 332 | return callback 333 | 334 | def jwt_payload_handler(self, callback): 335 | """Specifies the JWT payload handler function. This function receives the return value from 336 | the ``identity_handler`` function 337 | 338 | Example:: 339 | 340 | @jwt.payload_handler 341 | def make_payload(identity): 342 | return {'user_id': identity.id} 343 | 344 | :param callable callback: the payload handler function 345 | """ 346 | self.jwt_payload_callback = callback 347 | return callback 348 | 349 | def jwt_headers_handler(self, callback): 350 | """Specifies the JWT header handler function. This function receives the return value from 351 | the ``identity_handler`` function. 352 | 353 | Example:: 354 | 355 | @jwt.payload_handler 356 | def make_payload(identity): 357 | return {'user_id': identity.id} 358 | 359 | :param callable callback: the payload handler function 360 | """ 361 | self.jwt_headers_callback = callback 362 | return callback 363 | --------------------------------------------------------------------------------