├── 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 |
--------------------------------------------------------------------------------