pax_global_header00006660000000000000000000000064135240174260014516gustar00rootroot0000000000000052 comment=01106a6c3e669f05df1a1e61d9a6489a7e8f4f21 python-flask-jwt-extended-3.21.0/000077500000000000000000000000001352401742600166205ustar00rootroot00000000000000python-flask-jwt-extended-3.21.0/.gitignore000066400000000000000000000021261352401742600206110ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg .pytest_cache # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # IPython Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # dotenv .env # virtualenv venv/ ENV/ # Spyder project settings .spyderproject # Rope project settings .ropeproject # pycharm .idea/ # MacOS specific crap .DS_Store python-flask-jwt-extended-3.21.0/.pep8speaks.yml000066400000000000000000000000771352401742600215100ustar00rootroot00000000000000# File : .pep8speaks.yml pycodestyle: max-line-length: 90 python-flask-jwt-extended-3.21.0/.travis.yml000066400000000000000000000005631352401742600207350ustar00rootroot00000000000000language: python matrix: include: - python: 3.6 env: TOXENV=py - python: 3.5 env: TOXENV=py - python: 3.4 env: TOXENV=py - python: 2.7 env: TOXENV=py - python: pypy env: TOXENV=py sudo: false install: - pip install -U pip - pip install -U tox coverage coveralls script: - tox after_success: - coveralls python-flask-jwt-extended-3.21.0/LICENSE000066400000000000000000000020471352401742600176300ustar00rootroot00000000000000MIT License Copyright (c) 2016 Landon Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. python-flask-jwt-extended-3.21.0/MANIFEST.in000066400000000000000000000001201352401742600203470ustar00rootroot00000000000000include LICENSE README.md requirements.txt tox.ini recursive-include tests *.py python-flask-jwt-extended-3.21.0/README.md000066400000000000000000000044441352401742600201050ustar00rootroot00000000000000# Flask-JWT-Extended [![Build Status](https://travis-ci.org/vimalloc/flask-jwt-extended.svg?branch=master)](https://travis-ci.org/vimalloc/flask-jwt-extended) [![Coverage Status](https://coveralls.io/repos/github/vimalloc/flask-jwt-extended/badge.svg?branch=master)](https://coveralls.io/github/vimalloc/flask-jwt-extended?branch=master) [![PyPI version](https://badge.fury.io/py/Flask-JWT-Extended.svg)](https://badge.fury.io/py/Flask-JWT-Extended) [![Documentation Status](https://readthedocs.org/projects/flask-jwt-extended/badge/)](http://flask-jwt-extended.readthedocs.io/en/latest/) [![Join the chat at https://gitter.im/flask-jwt-extended/Lobby](https://badges.gitter.im/flask-jwt-extended/Lobby.svg)](https://gitter.im/flask-jwt-extended/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ### Features Flask-JWT-Extended not only adds support for using JSON Web Tokens (JWT) to Flask for protecting views, but also many helpful (and **optional**) features built in to make working with JSON Web Tokens easier. These include: * Support for adding custom claims to JSON Web Tokens * Custom claims validation on received tokens * Creating tokens from complex objects or complex object from received tokens * [Refresh tokens](https://auth0.com/blog/refresh-tokens-what-are-they-and-when-to-use-them/) * Token freshness and separate view decorators to only allow fresh tokens * Token revoking/blacklisting * Storing tokens in cookies and CSRF protection ### Usage [View the documentation online](http://flask-jwt-extended.readthedocs.io/en/latest/) ### Changelog You can view the changelog [here](https://github.com/vimalloc/flask-jwt-extended/releases). This project follows [semantic versioning](https://semver.org/). ### Chatting Come chat with the community or ask questions at https://gitter.im/flask-jwt-extended/Lobby. ### Local Development We require 100% code coverage in our unit tests. You can run the tests locally with `tox` which will print out a code coverage report. Creating a pull request will run the tests against python 2.7, 3.4, 3.5, 3.6, and PyPy. ``` $ tox ``` We also require features to be well documented. After installing the requirements, you can generate a local copy of documentation by going to the `docs` directory and running: ``` $ make clean && make html ``` python-flask-jwt-extended-3.21.0/docs/000077500000000000000000000000001352401742600175505ustar00rootroot00000000000000python-flask-jwt-extended-3.21.0/docs/Makefile000066400000000000000000000167461352401742600212260ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " epub3 to make an epub3" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" @echo " dummy to check syntax errors of document sources" .PHONY: clean clean: rm -rf $(BUILDDIR)/* .PHONY: html html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." .PHONY: dirhtml dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." .PHONY: singlehtml singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." .PHONY: pickle pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." .PHONY: json json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." .PHONY: htmlhelp htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." .PHONY: qthelp qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/flask-jwt-extended.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/flask-jwt-extended.qhc" .PHONY: applehelp applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." .PHONY: devhelp devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/flask-jwt-extended" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/flask-jwt-extended" @echo "# devhelp" .PHONY: epub epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." .PHONY: epub3 epub3: $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 @echo @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." .PHONY: latex latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." .PHONY: latexpdf latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: latexpdfja latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: text text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." .PHONY: man man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." .PHONY: texinfo texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." .PHONY: info info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." .PHONY: gettext gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." .PHONY: changes changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." .PHONY: linkcheck linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." .PHONY: doctest doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." .PHONY: coverage coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." .PHONY: xml xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." .PHONY: pseudoxml pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." .PHONY: dummy dummy: $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy @echo @echo "Build finished. Dummy builder generates no files." python-flask-jwt-extended-3.21.0/docs/__init__.py000066400000000000000000000000001352401742600216470ustar00rootroot00000000000000python-flask-jwt-extended-3.21.0/docs/add_custom_data_claims.rst000066400000000000000000000015211352401742600247440ustar00rootroot00000000000000Storing Data in Access Tokens ============================= You may want to store additional information in the access token which you could later access in the protected views. This can be done with the :meth:`~flask_jwt_extended.JWTManager.user_claims_loader` decorator, and the data can be accessed later in a protected endpoint with the :func:`~flask_jwt_extended.get_jwt_claims` function. Storing data in an access token can be good for performance. If you store data in the token, you wont need to look it up from disk next time you need it in a protected endpoint. However, you should take care what data you put in the token. Any data in the access token can be trivially viewed by anyone who can read the token. **Do not** store sensitive information in access tokens! .. literalinclude:: ../examples/additional_data_in_access_token.py python-flask-jwt-extended-3.21.0/docs/api.rst000066400000000000000000000043531352401742600210600ustar00rootroot00000000000000API Documentation ================= In here you will find the API for everything exposed in this extension. Configuring Flask-JWT-Extended ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. currentmodule:: flask_jwt_extended .. module:: flask_jwt_extended .. autoclass:: JWTManager .. automethod:: __init__ .. automethod:: init_app .. automethod:: claims_verification_loader .. automethod:: claims_verification_failed_loader .. automethod:: decode_key_loader .. automethod:: encode_key_loader .. automethod:: expired_token_loader .. automethod:: invalid_token_loader .. automethod:: needs_fresh_token_loader .. automethod:: revoked_token_loader .. automethod:: token_in_blacklist_loader .. automethod:: unauthorized_loader .. automethod:: user_claims_loader .. automethod:: user_identity_loader .. automethod:: user_loader_callback_loader .. automethod:: user_loader_error_loader Protected endpoint decorators ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. autofunction:: jwt_required .. autofunction:: jwt_refresh_token_required .. autofunction:: fresh_jwt_required .. autofunction:: jwt_optional .. _Verify Tokens in Request: Verify Tokens in Request ~~~~~~~~~~~~~~~~~~~~~~~~ These perform the same actions as the protected endpoint decorators, without actually decorating a function. These are very useful if you want to create your own decorators on top of flask jwt extended (such as role_required), or if you want to hook some of this extensions functionality into a flask before_request handler. .. autofunction:: verify_jwt_in_request .. autofunction:: verify_jwt_in_request_optional .. autofunction:: verify_fresh_jwt_in_request .. autofunction:: verify_jwt_refresh_token_in_request Utilities ~~~~~~~~~ .. autofunction:: create_access_token .. autofunction:: create_refresh_token .. attribute:: current_user A LocalProxy for accessing the current user. Roughly equilivant to :func:`~flask_jwt_extended.get_current_user` .. autofunction:: decode_token .. autofunction:: get_current_user .. autofunction:: get_csrf_token .. autofunction:: get_jti .. autofunction:: get_jwt_claims .. autofunction:: get_jwt_identity .. autofunction:: get_raw_jwt .. autofunction:: set_access_cookies .. autofunction:: set_refresh_cookies .. autofunction:: unset_jwt_cookies python-flask-jwt-extended-3.21.0/docs/basic_usage.rst000066400000000000000000000036151352401742600225540ustar00rootroot00000000000000Basic Usage =========== In its simplest form, there is not much to using flask_jwt_extended. You use :func:`~flask_jwt_extended.create_access_token` to make new access JWTs, the :func:`~flask_jwt_extended.jwt_required` decorator to protect endpoints, and :func:`~flask_jwt_extended.get_jwt_identity` function to get the identity of a JWT in a protected endpoint. .. literalinclude:: ../examples/simple.py To access a jwt_required protected view, all we have to do is send in the JWT with the request. By default, this is done with an authorization header that looks like: .. code-block :: bash Authorization: Bearer We can see this in action using CURL: .. code-block :: bash $ curl http://localhost:5000/protected { "msg": "Missing Authorization Header" } $ curl -H "Content-Type: application/json" -X POST \ -d '{"username":"test","password":"test"}' http://localhost:5000/login { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6dHJ1ZSwianRpIjoiZjhmNDlmMjUtNTQ4OS00NmRjLTkyOWUtZTU2Y2QxOGZhNzRlIiwidXNlcl9jbGFpbXMiOnt9LCJuYmYiOjE0NzQ0NzQ3OTEsImlhdCI6MTQ3NDQ3NDc5MSwiaWRlbnRpdHkiOiJ0ZXN0IiwiZXhwIjoxNDc0NDc1NjkxLCJ0eXBlIjoiYWNjZXNzIn0.vCy0Sec61i9prcGIRRCbG8e9NV6_wFH2ICFgUGCLKpc" } $ export ACCESS="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6dHJ1ZSwianRpIjoiZjhmNDlmMjUtNTQ4OS00NmRjLTkyOWUtZTU2Y2QxOGZhNzRlIiwidXNlcl9jbGFpbXMiOnt9LCJuYmYiOjE0NzQ0NzQ3OTEsImlhdCI6MTQ3NDQ3NDc5MSwiaWRlbnRpdHkiOiJ0ZXN0IiwiZXhwIjoxNDc0NDc1NjkxLCJ0eXBlIjoiYWNjZXNzIn0.vCy0Sec61i9prcGIRRCbG8e9NV6_wFH2ICFgUGCLKpc" $ curl -H "Authorization: Bearer $ACCESS" http://localhost:5000/protected { "logged_in_as": "test" } NOTE: Remember to change the secret key of your application, and ensure that no one is able to view it. The JSON Web Tokens are signed with the secret key, so if someone gets that, they can create arbitrary tokens, and in essence log in as any user. python-flask-jwt-extended-3.21.0/docs/blacklist_and_token_revoking.rst000066400000000000000000000042471352401742600262070ustar00rootroot00000000000000.. _Blacklist and Token Revoking: Blacklist and Token Revoking ============================ This extension supports optional token revoking out of the box. This will allow you to revoke a specific token so that it can no longer access your endpoints. You will have to choose what tokens you want to check against the blacklist. In most cases, you will probably want to check both refresh and access tokens, which is the default behavior. However, if the extra overhead of checking tokens is a concern you could instead only check the refresh tokens, and set the access tokens to have a short expires time so any damage a compromised token could cause is minimal. Blacklisting works by is providing a callback function to this extension, using the :meth:`~flask_jwt_extended.JWTManager.token_in_blacklist_loader` decorator. This method will be called whenever the specified tokens (`access` and/or `refresh`) are used to access a protected endpoint. If the callback function says that the token is revoked, we will not allow the call to continue, otherwise we will allow the call to access the endpoint as normal. Here is a basic example of this in action. .. literalinclude:: ../examples/blacklist.py In production, you will likely want to use either a database or in memory store (such as redis) to store your tokens. In memory stores are great if you are wanting to revoke a token when the users logs out, as they are blazing fast. A downside to using redis is that in the case of a power outage or other such event, it's possible that you might 'forget' that some tokens have been revoked, depending on if the redis data was synced to disk. In contrast to that, databases are great if the data persistance is of the highest importance (for example, if you have very long lived tokens that other developers use to access your api), or if you want to add some addition features like showing users all of their active tokens, and letting them revoke and unrevoke those tokens. For more in depth examples of these, check out: - https://github.com/vimalloc/flask-jwt-extended/blob/master/examples/redis_blacklist.py - https://github.com/vimalloc/flask-jwt-extended/tree/master/examples/database_blacklist python-flask-jwt-extended-3.21.0/docs/changing_default_behavior.rst000066400000000000000000000102331352401742600254420ustar00rootroot00000000000000Changing Default Behaviors ========================== Changing callback functions ~~~~~~~~~~~~~~~~~~~~~~~~~~~ We provide what we think are sensible behaviors when attempting to access a protected endpoint. If the access token is not valid for any reason (missing, expired, tampered with, etc) we will return json in the format of {'msg': 'why accessing endpoint failed'} along with an appropriate http status code (generally 401 or 422). However, you may want to customize what you return in some situations. We can do that with the jwt_manager loader functions. .. literalinclude:: ../examples/loaders.py Here are the possible loader functions. Click on the links for a more more details about what arguments your callback functions should expect and what the return values of your callback functions need to be. .. list-table:: :header-rows: 1 * - Loader Decorator - Description * - :meth:`~flask_jwt_extended.JWTManager.claims_verification_loader` - Function that is called to verify the user_claims data. Must return True or False * - :meth:`~flask_jwt_extended.JWTManager.claims_verification_failed_loader` - Function that is called when the user claims verification callback returns False * - :meth:`~flask_jwt_extended.JWTManager.decode_key_loader` - Function that is called to get the decode key before verifying a token * - :meth:`~flask_jwt_extended.JWTManager.encode_key_loader` - Function that is called to get the encode key before creating a token * - :meth:`~flask_jwt_extended.JWTManager.expired_token_loader` - Function to call when an expired token accesses a protected endpoint * - :meth:`~flask_jwt_extended.JWTManager.invalid_token_loader` - Function to call when an invalid token accesses a protected endpoint * - :meth:`~flask_jwt_extended.JWTManager.needs_fresh_token_loader` - Function to call when a non-fresh token accesses a :func:`~flask_jwt_extended.fresh_jwt_required` endpoint * - :meth:`~flask_jwt_extended.JWTManager.revoked_token_loader` - Function to call when a revoked token accesses a protected endpoint * - :meth:`~flask_jwt_extended.JWTManager.token_in_blacklist_loader` - Function that is called to check if a token has been revoked * - :meth:`~flask_jwt_extended.JWTManager.unauthorized_loader` - Function to call when a request with no JWT accesses a protected endpoint * - :meth:`~flask_jwt_extended.JWTManager.user_loader_callback_loader` - Function to call to load a user object when token accesses a protected endpoint * - :meth:`~flask_jwt_extended.JWTManager.user_loader_error_loader` - Function that is called when the user_loader callback function returns `None` Dynamic token expires time ~~~~~~~~~~~~~~~~~~~~~~~~~~ You can also change the expires time for a token via the `expires_delta` kwarg in the :func:`~flask_jwt_extended.create_refresh_token` and :func:`~flask_jwt_extended.create_access_token` functions. This takes a `datetime.timedelta` and overrides the `JWT_REFRESH_TOKEN_EXPIRES` and `JWT_ACCESS_TOKEN_EXPIRES` settings (see :ref:`Configuration Options`). This can be useful if you have different use cases for different tokens. For example, you might use short lived access tokens used in your web application, but you allow the creation of long lived access tokens that other developers can generate and use to interact with your api in their programs. You could accomplish this like such: .. code-block:: python @app.route('/create-dev-token', methods=['POST']) @jwt_required def create_dev_token(): username = get_jwt_identity() expires = datetime.timedelta(days=365) token = create_access_token(username, expires_delta=expires) return jsonify({'token': token}), 201 You can even disable expiration by setting `expires_delta` to `False`: .. code-block:: python @app.route('/create-api-token', methods=['POST']) @jwt_required def create_api_token(): username = get_jwt_identity() token = create_access_token(username, expires_delta=False) return jsonify({'token': token}), 201 Note that in this case, you should enable token revoking (see :ref:`Blacklist and Token Revoking`). python-flask-jwt-extended-3.21.0/docs/complex_objects_from_token.rst000066400000000000000000000022001352401742600256770ustar00rootroot00000000000000Complex Objects from Tokens =========================== We can also do the inverse of creating tokens from complex objects like we did in the last section. In this case, we can take a token and every time a protected endpoint is accessed, automatically use the token to load a complex object (such as a SQLAlchemy instance). This is done through the :meth:`~flask_jwt_extended.JWTManager.user_loader_callback_loader` decorator. The resulting object can be accessed in your protected endpoints by using the :func:`~flask_jwt_extended.get_current_user` function, or directly with the :attr:`~flask_jwt_extended.current_user` LocalProxy. One thing to note is if you access a database in the :meth:`~flask_jwt_extended.JWTManager.user_loader_callback_loader`, you will incur the cost of that database lookup on every call, regardless of if you need the additional data from the database or not. In most cases this likely isn't something to be worried about, but do be aware that it could slow your application if it handles high traffic. Here's an example of how this feature might look: .. literalinclude:: ../examples/complex_objects_from_tokens.py python-flask-jwt-extended-3.21.0/docs/conf.py000066400000000000000000000241061352401742600210520ustar00rootroot00000000000000# -*- coding: utf-8 -*- # import sys, os, io, re # flask-jwt-extended documentation build configuration file, created by # sphinx-quickstart on Thu Oct 6 13:07:36 2016. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('../../..')) sys.path.insert(0, os.path.abspath('..')) sys.path.insert(0, os.path.abspath('../flask_jwt_extended/')) sys.path.append(os.path.join(os.path.dirname(__file__), '_themes')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'pallets_sphinx_themes', 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode', 'sphinx.ext.autosummary' ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The encoding of source files. # # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'flask-jwt-extended' author = u'Landon Gilbert-Bland' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # with io.open('../flask_jwt_extended/__init__.py', encoding='utf-8') as f: package_version = re.search(r"__version__ = '(.+)'", f.read()).group(1) version = package_version release = package_version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # # today = '' # # Else, today_fmt is used as the format for a strftime call. # # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all # documents. # # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # # show_authors = False # The name of the Pygments (syntax highlighting) style to use. # pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # #html_theme = 'sphinx_rtd_theme' html_theme = 'flask' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. html_theme_path = ["_themes", ] # The name for this set of Sphinx documents. # " v documentation" by default. # # html_title = u'flask-jwt-extended v0.0.2' # A shorter title for the navigation bar. Default is the same as html_title. # # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # # html_logo = None # The name of an image file (relative to this directory) to use as a favicon of # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". #html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # # html_extra_path = [] # If not None, a 'Last updated on:' timestamp is inserted at every page # bottom, using the given strftime format. # The empty string is equivalent to '%b %d, %Y'. # # html_last_updated_fmt = None # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # # html_additional_pages = {} # If false, no module index is generated. # # html_domain_indices = True # If false, no index is generated. # # html_use_index = True # If true, the index is split into individual pages for each letter. # # html_split_index = False # If true, links to the reST sources are added to the pages. # # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' # # html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # 'ja' uses this config value. # 'zh' user can custom change `jieba` dictionary path. # # html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. # # html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'flask-jwt-extendeddoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'flask-jwt-extended.tex', u'flask-jwt-extended Documentation', u'vimalloc rlam3', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # # latex_use_parts = False # If true, show page references after internal links. # # latex_show_pagerefs = False # If true, show URL addresses after external links. # # latex_show_urls = False # Documents to append as an appendix to all manuals. # # latex_appendices = [] # It false, will not define \strong, \code, itleref, \crossref ... but only # \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added # packages. # # latex_keep_old_macro_names = True # If false, no module index is generated. # # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'flask-jwt-extended', u'flask-jwt-extended Documentation', [author], 1) ] # If true, show URL addresses after external links. # # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'flask-jwt-extended', u'flask-jwt-extended Documentation', author, 'flask-jwt-extended', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. # # texinfo_appendices = [] # If false, no module index is generated. # # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # # texinfo_no_detailmenu = False python-flask-jwt-extended-3.21.0/docs/custom_decorators.rst000066400000000000000000000011561352401742600240440ustar00rootroot00000000000000Custom Decorators ================= You can create your own decorators that extend the functionality of the decorators provided by this extension. For example, you may want to create your own decorator that verifies a JWT is present as well as verifying that this token has sufficient permissions/roles to access an endpoint. :ref:`Verify Tokens in Request` is a list of functions that can be used to build your own decorators (these are also what all the default decorators provided by this extension use internally). Here is an example of how this might look. .. literalinclude:: ../examples/custom_decorators.py python-flask-jwt-extended-3.21.0/docs/index.rst000066400000000000000000000012451352401742600214130ustar00rootroot00000000000000.. flask-jwt-extended documentation master file, created by sphinx-quickstart on Fri Oct 7 10:31:53 2016. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Flask-JWT-Extended's Documentation ================================== .. toctree:: :maxdepth: 2 installation basic_usage optional_endpoints add_custom_data_claims tokens_from_complex_object complex_objects_from_token custom_decorators refresh_tokens token_freshness changing_default_behavior options blacklist_and_token_revoking tokens_in_cookies tokens_in_query_string tokens_in_json_body api python-flask-jwt-extended-3.21.0/docs/installation.rst000066400000000000000000000012231352401742600230010ustar00rootroot00000000000000Installation ============== The easiest way to start working with this extension with pip: .. code-block:: bash $ pip install flask-jwt-extended If you want to use asymmetric (public/private) key signing algorithms, include the ``asymmetric_crypto`` extra requirements. .. code-block:: bash $ pip install flask-jwt-extended[asymmetric_crypto] Note that if you are using ZSH (probably other shells as well), you will need to escape the brackets .. code-block:: bash $ pip install flask-jwt-extended\[asymmetric_crypto\] If you prefer to install from source, you can clone this repo and run .. code-block:: bash $ python setup.py install python-flask-jwt-extended-3.21.0/docs/optional_endpoints.rst000066400000000000000000000011141352401742600242070ustar00rootroot00000000000000Partially protecting routes =========================== There may be cases where you want to use one endpoint for both protected and unprotected data. In these situations, you can use the :func:`~flask_jwt_extended.jwt_optional` decorator. This will allow the endpoint to be accessed regardless of if a JWT is sent in with the request. If a JWT that is expired or badly constructed is sent in with the request, an error will be returned instead of calling the protected endpoint as if no token was present in the request. .. literalinclude:: ../examples/optional_protected_endpoints.py python-flask-jwt-extended-3.21.0/docs/options.rst000066400000000000000000000261311352401742600220000ustar00rootroot00000000000000.. _Configuration Options: Configuration Options ===================== You can change many options for how this extension works via .. code-block:: python app.config[OPTION_NAME] = new_options General Options: ~~~~~~~~~~~~~~~~ .. tabularcolumns:: |p{6.5cm}|p{8.5cm}| ================================= ========================================= ``JWT_TOKEN_LOCATION`` Where to look for a JWT when processing a request. The options are ``'headers'``, ``'cookies'``, ``'query_string'``, or ``'json'``. You can pass in a sequence or a set to check more then one location, such as: ``('headers', 'cookies')``. Defaults to ``['headers']``. The order sets the precedence, so that if a valid token is found in an earlier location in this list, the request is authenticated. ``JWT_ACCESS_TOKEN_EXPIRES`` How long an access token should live before it expires. This takes any value that can be safely added to a ``datetime.datetime`` object, including ``datetime.timedelta``, `dateutil.relativedelta `_, or an ``int`` (seconds), and defaults to 15 minutes. Can be set to ``False`` to disable expiration. ``JWT_REFRESH_TOKEN_EXPIRES`` How long a refresh token should live before it expires. This takes any value that can be safely added to a ``datetime.datetime`` object, including ``datetime.timedelta``, `dateutil.relativedelta `_, or an ``int`` (seconds), and defaults to 30 days. Can be set to ``False`` to disable expiration. ``JWT_ALGORITHM`` Which algorithm to sign the JWT with. `See here `_ for the options. Defaults to ``'HS256'``. ``JWT_DECODE_ALGORITHMS`` Which algorithms are allowed to decode a JWT. Defaults to a list with only the algorithm set in ``JWT_ALGORITHM``. ``JWT_SECRET_KEY`` The secret key needed for symmetric based signing algorithms, such as ``HS*``. If this is not set, we use the flask ``SECRET_KEY`` value instead. ``JWT_PUBLIC_KEY`` The public key needed for asymmetric based signing algorithms, such as ``RS*`` or ``ES*``. PEM format expected. ``JWT_PRIVATE_KEY`` The private key needed for asymmetric based signing algorithms, such as ``RS*`` or ``ES*``. PEM format expected. ``JWT_IDENTITY_CLAIM`` Claim in the tokens that is used as source of identity. For interoperability, the JWT RFC recommends using ``'sub'``. Defaults to ``'identity'`` for legacy reasons. ``JWT_USER_CLAIMS`` Claim in the tokens that is used to store user claims. Defaults to ``'user_claims'``. ``JWT_CLAIMS_IN_REFRESH_TOKEN`` If user claims should be included in refresh tokens. Defaults to ``False``. ``JWT_ERROR_MESSAGE_KEY`` The key of the error message in a JSON error response when using the default error handlers. Defaults to ``'msg'``. ``JWT_DECODE_AUDIENCE`` The audience or list of audiences you expect in a JWT when decoding it. The ``'invalid_token_callback'`` is invoked when a JWTs audience is invalid. Defaults to ``'None'``. ``JWT_DECODE_LEEWAY`` Define the leeway part of the expiration time definition, which means you can validate an expiration time which is in the past but not very far. This leeway is used for `nbf` (“not before”) and `exp` (“expiration time”). Defaults to ``0`` ================================= ========================================= Header Options: ~~~~~~~~~~~~~~~ These are only applicable if ``JWT_TOKEN_LOCATION`` is set to use headers. .. tabularcolumns:: |p{6.5cm}|p{8.5cm}| ================================= ========================================= ``JWT_HEADER_NAME`` What header to look for the JWT in a request. Defaults to ``'Authorization'`` ``JWT_HEADER_TYPE`` What type of header the JWT is in. Defaults to ``'Bearer'``. This can be an empty string, in which case the header contains only the JWT (insead of something like ``HeaderName: Bearer ``) ================================= ========================================= Query String Options: ~~~~~~~~~~~~~~~~~~~~~ These are only applicable if ``JWT_TOKEN_LOCATION`` is set to use query strings. .. tabularcolumns:: |p{6.5cm}|p{8.5cm}| ================================= ========================================= ``JWT_QUERY_STRING_NAME`` What query paramater name to look for a JWT in a request. Defaults to ``'jwt'`` ================================= ========================================= Cookie Options: ~~~~~~~~~~~~~~~ These are only applicable if ``JWT_TOKEN_LOCATION`` is set to use cookies. .. tabularcolumns:: |p{6.5cm}|p{8.5cm}| ================================= ========================================= ``JWT_ACCESS_COOKIE_NAME`` The name of the cookie that holds the access token. Defaults to ``access_token_cookie`` ``JWT_REFRESH_COOKIE_NAME`` The name of the cookie that holds the refresh token. Defaults to ``refresh_token_cookie`` ``JWT_ACCESS_COOKIE_PATH`` What ``path`` should be set for the access cookie. Defaults to ``'/'``, which will cause this access cookie to be sent in with every request. Should be modified for only the paths that need the access cookie ``JWT_REFRESH_COOKIE_PATH`` What ``path`` should be set for the refresh cookie. Defaults to ``'/'``, which will cause this refresh cookie to be sent in with every request. Should be modified for only the paths that need the refresh cookie ``JWT_COOKIE_SECURE`` If the secure flag should be set on your JWT cookies. This will only allow the cookies to be sent over https. Defaults to ``False``, but in production this should likely be set to ``True``. ``JWT_COOKIE_DOMAIN`` Value to use for cross domain cookies. Defaults to ``None`` which sets this cookie to only be readable by the domain that set it. ``JWT_SESSION_COOKIE`` If the cookies should be session cookies (deleted when the browser is closed) or persistent cookies (never expire). Defaults to ``True`` (session cookies). ``JWT_COOKIE_SAMESITE`` If the cookies should be sent in a cross-site browsing context. Defaults to ``None``, which means cookies are always sent. ``JWT_COOKIE_CSRF_PROTECT`` Enable/disable CSRF protection when using cookies. Defaults to ``True``. ================================= ========================================= Json Body Options: ~~~~~~~~~~~~~~~~~~~~~ These are only applicable if ``JWT_TOKEN_LOCATION`` is set to use json data. .. tabularcolumns:: |p{6.5cm}|p{8.5cm}| ================================= ========================================= ``JWT_JSON_KEY`` Key to look for in the body of an `application/json` request. Defaults to ``'access_token'`` ``JWT_REFRESH_JSON_KEY`` Key to look for the refresh token in an `application/json` request. Defaults to ``'refresh_token'`` ================================= ========================================= Cross Site Request Forgery Options: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ These are only applicable if ``JWT_TOKEN_LOCATION`` is set to use cookies and ``JWT_COOKIE_CSRF_PROTECT`` is True. .. tabularcolumns:: |p{6.5cm}|p{8.5cm}| ================================= ========================================= ``JWT_CSRF_METHODS`` The request types that will use CSRF protection. Defaults to ``['POST', 'PUT', 'PATCH', 'DELETE']`` ``JWT_ACCESS_CSRF_HEADER_NAME`` Name of the header that should contain the CSRF double submit value for access tokens. Defaults to ``X-CSRF-TOKEN``. ``JWT_REFRESH_CSRF_HEADER_NAME`` Name of the header that should contains the CSRF double submit value for refresh tokens. Defaults to ``X-CSRF-TOKEN``. ``JWT_CSRF_IN_COOKIES`` If we should store the CSRF double submit value in another cookies when using ``set_access_cookies()`` and ``set_refresh_cookies()``. Defaults to ``True``. If this is False, you are responsible for getting the CSRF value to the callers (see: ``get_csrf_token(encoded_token)``). ``JWT_ACCESS_CSRF_COOKIE_NAME`` Name of the CSRF access cookie. Defaults to ``'csrf_access_token'``. Only applicable if ``JWT_CSRF_IN_COOKIES`` is ``True`` ``JWT_REFRESH_CSRF_COOKIE_NAME`` Name of the CSRF refresh cookie. Defaults to ``'csrf_refresh_token'``. Only applicable if ``JWT_CSRF_IN_COOKIES`` is ``True`` ``JWT_ACCESS_CSRF_COOKIE_PATH`` Path for the CSRF access cookie. Defaults to ``'/'``. Only applicable if ``JWT_CSRF_IN_COOKIES`` is ``True`` ``JWT_REFRESH_CSRF_COOKIE_PATH`` Path of the CSRF refresh cookie. Defaults to ``'/'``. Only applicable if ``JWT_CSRF_IN_COOKIES`` is ``True`` ================================= ========================================= Blacklist Options: ~~~~~~~~~~~~~~~~~~ .. tabularcolumns:: |p{6.5cm}|p{8.5cm}| ================================= ========================================= ``JWT_BLACKLIST_ENABLED`` Enable/disable token revoking. Defaults to ``False`` ``JWT_BLACKLIST_TOKEN_CHECKS`` What token types to check against the blacklist. The options are ``'refresh'`` or ``'access'``. You can pass in a sequence or a set to check more then one type. Defaults to ``('access', 'refresh')``. Only used if blacklisting is enabled. ================================= ========================================= python-flask-jwt-extended-3.21.0/docs/refresh_tokens.rst000066400000000000000000000017361352401742600233320ustar00rootroot00000000000000Refresh Tokens ============== Flask-JWT-Extended supports refresh tokens out of the box. These are long lived tokens which can be used to create new access tokens once an old access token has expired. Refresh tokens cannot access an endpoint that is protected with :func:`~flask_jwt_extended.jwt_required` and access tokens cannot access and endpoint that is protected with :func:`~flask_jwt_extended.jwt_refresh_token_required`. By setting the access tokens to a shorter lifetime (see :ref:`Configuration Options`), and utilizing refresh tokens we can help reduce the damage that can be done if an access token is stolen. However, if an attacker gets their hands on the refresh token, they can keep generating new access tokens and accessing protected endpoints as though he was that user. We can help combat this by using the fresh token pattern, discussed in the next section. Here is an example of using access and refresh tokens: .. literalinclude:: ../examples/refresh_tokens.py python-flask-jwt-extended-3.21.0/docs/token_freshness.rst000066400000000000000000000014601352401742600235030ustar00rootroot00000000000000Token Freshness =============== The fresh token pattern is built into this extension. This pattern is very simple, you can choose to mark some access tokens as fresh and others as non-fresh, and use the :func:`~flask_jwt_extended.fresh_jwt_required` decorator to only allow fresh tokens to access certain endpoints. This is useful for allowing fresh tokens to do some critical things (such as update an email address or complete an online purchase), but to deny those features to non-fresh tokens. Utilizing Fresh tokens in conjunction with refresh tokens can lead to a more secure site, without creating a bad user experience by making users constantly re-authenticate. Here is an example of how you could utilize refresh tokens with the fresh token pattern: .. literalinclude:: ../examples/token_freshness.py python-flask-jwt-extended-3.21.0/docs/tokens_from_complex_object.rst000066400000000000000000000033411352401742600257060ustar00rootroot00000000000000Tokens from Complex Objects =========================== A very common setup is to have your users information (usernames, passwords, roles, etc) stored in a database. Now, lets pretend that we want to create an access tokens where the tokens identity is a username, and we also want to store a users roles as an additional claim in the token. We can do this with the :meth:`~flask_jwt_extended.JWTManager.user_claims_loader` decorator, discussed in the previous section. However, if we pass the username to the :meth:`~flask_jwt_extended.JWTManager.user_claims_loader`, we would end up needing to query this user from the database two times. The first time would be when login endpoint is hit and we need to verify a username and password. The second time would be in the :meth:`~flask_jwt_extended.JWTManager.user_claims_loader` function, because we need to query the roles for this user. This isn't a huge deal, but obviously it could be more efficient. This extension provides the ability to pass any object to the :func:`~flask_jwt_extended.create_access_token` function, which will then be passed as is to the :meth:`~flask_jwt_extended.JWTManager.user_claims_loader`. This allows us access the database only once, but introduces a new issue that needs to be addressed. We still need to pull the username out of the object, so that we can have the username be the identity for the new token. We have a second decorator we can use for this, :meth:`~flask_jwt_extended.JWTManager.user_identity_loader`, which lets you take any object passed in to :func:`~flask_jwt_extended.create_access_token` and return a json serializable identity from that object. Here is an example of this in action: .. literalinclude:: ../examples/tokens_from_complex_objects.py python-flask-jwt-extended-3.21.0/docs/tokens_in_cookies.rst000066400000000000000000000074341352401742600240170ustar00rootroot00000000000000JWT in Cookies ============== If the frontend that is consuming this backend is a website, you may be storing JWTs in the browser localStorage or sessionStorage. There is nothing wrong with this, but if you have any sort of XSS vulnerability on your site, an attacker will be able to trivially steal your refresh and access tokens. If you want some additional security on your site, you can save your JWTs in a httponly cookie instead, which keeps javascript from being able to access the cookie. See this great blog for a more in depth analysis between these options: http://www.redotheweb.com/2015/11/09/api-security.html Here is a basic example of how to store JWTs in cookies: .. literalinclude:: ../examples/jwt_in_cookie.py This isn't the full story however. We can now keep our cookie from being stolen via XSS attacks, but have traded that for a vulnerability to CSRF attacks. To combat CSRF, we are going to use a technique called double submit verification. When we create a JWT, we will also create a random string and store it in the JWT. This token is saved in a cookie with httponly set to True, so it cannot be accessed via javascript. We will then create a secondary cookie that contains only the random string, but has httponly set to False, so that it can be accessed via javascript running on your website. Now in order to access a protected endpoint, you will need to add a custom header that contains the the random string in it, and if that header doesn't exist or it doesn't match the string that is stored in the JWT, the request will be kicked out as unauthorized. To break this down, if an attacker attempts to perform a CSRF attack they will send the JWT (via the cookie) to a protected endpoint, but without the random string in the requests header, they wont be able to access the endpoint. They cannot access the random string, unless they can run javascript on your website (likely via an XSS attack), and if they are able to perform an XSS attack, they will not be able to steal the actual access and refresh JWTs, as javascript is still not able to access those httponly cookies. This obviously isn't a golden bullet. If an attacker can perform an XSS attack they can still access protected endpoints from people who visit your site. However, it is better then if they were able to steal the access and refresh tokens tokens from local/session storage, and use them whenever they wanted. If this additional security is worth the added complexity of using cookies and double submit CSRF protection is a choice you will have to make. Here is an example of using cookies with CSRF protection: .. literalinclude:: ../examples/csrf_protection_with_cookies.py By default, the CSRF double submit values are sent back as additional cookies to the caller. If you prefer, you can disable that, and send them back directly to the caller, like such: .. code-block:: python app.config['JWT_CSRF_IN_COOKIES'] = False #... #... #... @app.route('/token/auth', methods=['POST']) def login(): username = request.json.get('username', None) password = request.json.get('password', None) if username != 'test' or password != 'test': return jsonify({'login': False}), 401 # Create the tokens we will be sending back to the user access_token = create_access_token(identity=username) refresh_token = create_refresh_token(identity=username) # Return the double submit values in the resulting JSON # instead of in additional cookies resp = jsonify({ 'access_csrf': get_csrf_token(access_token), 'refresh_csrf': get_csrf_token(refresh_token) }) # We still need to call these functions to set the # JWTs in the cookies set_access_cookies(resp, access_token) set_refresh_cookies(resp, refresh_token) return resp, 200 python-flask-jwt-extended-3.21.0/docs/tokens_in_json_body.rst000066400000000000000000000010461352401742600243420ustar00rootroot00000000000000JWT in JSON Body ================ You can also pass the token as an attribute in the body of an `application/json` request. However, since the body is meaningless in a `GET` request, this is mostly useful for protecting routes that only accept `POST`, `PATCH`, or `DELETE` methods. That is to say, the `GET` method will become essentially unauthorized in any protected route if you only use this lookup method. If you decide to use JWTs in the request body, here is an example of how it might look: .. literalinclude:: ../examples/jwt_in_json.py python-flask-jwt-extended-3.21.0/docs/tokens_in_query_string.rst000066400000000000000000000013331352401742600251060ustar00rootroot00000000000000JWT in Query String =================== You can also pass the token in as a paramater in the query string instead of as a header or a cookie (ex: /protected?jwt=). However, in almost all cases it is recomended that you do not do this, as it comes with some security issues. If you perform a GET request with a JWT in the query param, it is possible that the browser will save the URL, which could lead to a leaked token. It is also very likely that your backend (such as nginx or uwsgi) could log the full url paths, which is obviously not ideal from a security standpoint. If you do decide to use JWTs in query paramaters, here is an example of how it might look: .. literalinclude:: ../examples/jwt_in_query_string.py python-flask-jwt-extended-3.21.0/examples/000077500000000000000000000000001352401742600204365ustar00rootroot00000000000000python-flask-jwt-extended-3.21.0/examples/additional_data_in_access_token.py000066400000000000000000000025331352401742600273230ustar00rootroot00000000000000from flask import Flask, jsonify, request from flask_jwt_extended import ( JWTManager, jwt_required, create_access_token, get_jwt_claims ) app = Flask(__name__) app.config['JWT_SECRET_KEY'] = 'super-secret' # Change this! jwt = JWTManager(app) # Using the user_claims_loader, we can specify a method that will be # called when creating access tokens, and add these claims to the said # token. This method is passed the identity of who the token is being # created for, and must return data that is json serializable @jwt.user_claims_loader def add_claims_to_access_token(identity): return { 'hello': identity, 'foo': ['bar', 'baz'] } @app.route('/login', methods=['POST']) def login(): username = request.json.get('username', None) password = request.json.get('password', None) if username != 'test' or password != 'test': return jsonify({"msg": "Bad username or password"}), 401 ret = {'access_token': create_access_token(username)} return jsonify(ret), 200 # In a protected view, get the claims you added to the jwt with the # get_jwt_claims() method @app.route('/protected', methods=['GET']) @jwt_required def protected(): claims = get_jwt_claims() return jsonify({ 'hello_is': claims['hello'], 'foo_is': claims['foo'] }), 200 if __name__ == '__main__': app.run() python-flask-jwt-extended-3.21.0/examples/blacklist.py000066400000000000000000000063371352401742600227710ustar00rootroot00000000000000from flask import Flask, request, jsonify from flask_jwt_extended import ( JWTManager, jwt_required, get_jwt_identity, create_access_token, create_refresh_token, jwt_refresh_token_required, get_raw_jwt ) # Setup flask app = Flask(__name__) # Enable blacklisting and specify what kind of tokens to check # against the blacklist app.config['JWT_SECRET_KEY'] = 'super-secret' # Change this! app.config['JWT_BLACKLIST_ENABLED'] = True app.config['JWT_BLACKLIST_TOKEN_CHECKS'] = ['access', 'refresh'] jwt = JWTManager(app) # A storage engine to save revoked tokens. In production if # speed is the primary concern, redis is a good bet. If data # persistence is more important for you, postgres is another # great option. In this example, we will be using an in memory # store, just to show you how this might work. For more # complete examples, check out these: # https://github.com/vimalloc/flask-jwt-extended/blob/master/examples/redis_blacklist.py # https://github.com/vimalloc/flask-jwt-extended/tree/master/examples/database_blacklist blacklist = set() # For this example, we are just checking if the tokens jti # (unique identifier) is in the blacklist set. This could # be made more complex, for example storing all tokens # into the blacklist with a revoked status when created, # and returning the revoked status in this call. This # would allow you to have a list of all created tokens, # and to consider tokens that aren't in the blacklist # (aka tokens you didn't create) as revoked. These are # just two options, and this can be tailored to whatever # your application needs. @jwt.token_in_blacklist_loader def check_if_token_in_blacklist(decrypted_token): jti = decrypted_token['jti'] return jti in blacklist # Standard login endpoint @app.route('/login', methods=['POST']) def login(): username = request.json.get('username', None) password = request.json.get('password', None) if username != 'test' or password != 'test': return jsonify({"msg": "Bad username or password"}), 401 ret = { 'access_token': create_access_token(identity=username), 'refresh_token': create_refresh_token(identity=username) } return jsonify(ret), 200 # Standard refresh endpoint. A blacklisted refresh token # will not be able to access this endpoint @app.route('/refresh', methods=['POST']) @jwt_refresh_token_required def refresh(): current_user = get_jwt_identity() ret = { 'access_token': create_access_token(identity=current_user) } return jsonify(ret), 200 # Endpoint for revoking the current users access token @app.route('/logout', methods=['DELETE']) @jwt_required def logout(): jti = get_raw_jwt()['jti'] blacklist.add(jti) return jsonify({"msg": "Successfully logged out"}), 200 # Endpoint for revoking the current users refresh token @app.route('/logout2', methods=['DELETE']) @jwt_refresh_token_required def logout2(): jti = get_raw_jwt()['jti'] blacklist.add(jti) return jsonify({"msg": "Successfully logged out"}), 200 # This will now prevent users with blacklisted tokens from # accessing this endpoint @app.route('/protected', methods=['GET']) @jwt_required def protected(): return jsonify({'hello': 'world'}) if __name__ == '__main__': app.run() python-flask-jwt-extended-3.21.0/examples/complex_objects_from_tokens.py000066400000000000000000000046631352401742600266070ustar00rootroot00000000000000from flask import Flask, jsonify, request from flask_jwt_extended import ( JWTManager, jwt_required, create_access_token, current_user ) app = Flask(__name__) app.config['JWT_SECRET_KEY'] = 'super-secret' # Change this! jwt = JWTManager(app) # A demo user object that we will use in this example. class UserObject: def __init__(self, username, roles): self.username = username self.roles = roles # An example store of users. In production, this would likely # be a sqlalchemy instance or something similar. users_to_roles = { 'foo': ['admin'], 'bar': ['peasant'], 'baz': ['peasant'] } # This function is called whenever a protected endpoint is accessed, # and must return an object based on the tokens identity. # This is called after the token is verified, so you can use # get_jwt_claims() in here if desired. Note that this needs to # return None if the user could not be loaded for any reason, # such as not being found in the underlying data store @jwt.user_loader_callback_loader def user_loader_callback(identity): if identity not in users_to_roles: return None return UserObject( username=identity, roles=users_to_roles[identity] ) # You can override the error returned to the user if the # user_loader_callback returns None. If you don't override # this, # it will return a 401 status code with the JSON: # {"msg": "Error loading the user "}. # You can use # get_jwt_claims() here too if desired @jwt.user_loader_error_loader def custom_user_loader_error(identity): ret = { "msg": "User {} not found".format(identity) } return jsonify(ret), 404 # Create a token for any user, so this can be tested out @app.route('/login', methods=['POST']) def login(): username = request.get_json().get('username', None) access_token = create_access_token(identity=username) ret = {'access_token': access_token} return jsonify(ret), 200 # If the user_loader_callback returns None, this method will # not be run, even if the access token is valid. You can # access the loaded user via the ``current_user``` LocalProxy, # or with the ```get_current_user()``` method @app.route('/admin-only', methods=['GET']) @jwt_required def protected(): if 'admin' not in current_user.roles: return jsonify({"msg": "Forbidden"}), 403 else: return jsonify({"msg": "don't forget to drink your ovaltine"}) if __name__ == '__main__': app.run() python-flask-jwt-extended-3.21.0/examples/csrf_protection_with_cookies.py000066400000000000000000000065111352401742600267650ustar00rootroot00000000000000from flask import Flask, jsonify, request from flask_jwt_extended import ( JWTManager, jwt_required, create_access_token, jwt_refresh_token_required, create_refresh_token, get_jwt_identity, set_access_cookies, set_refresh_cookies, unset_jwt_cookies ) app = Flask(__name__) # Configure application to store JWTs in cookies app.config['JWT_TOKEN_LOCATION'] = ['cookies'] # Only allow JWT cookies to be sent over https. In production, this # should likely be True app.config['JWT_COOKIE_SECURE'] = False # Set the cookie paths, so that you are only sending your access token # cookie to the access endpoints, and only sending your refresh token # to the refresh endpoint. Technically this is optional, but it is in # your best interest to not send additional cookies in the request if # they aren't needed. app.config['JWT_ACCESS_COOKIE_PATH'] = '/api/' app.config['JWT_REFRESH_COOKIE_PATH'] = '/token/refresh' # Enable csrf double submit protection. See this for a thorough # explanation: http://www.redotheweb.com/2015/11/09/api-security.html app.config['JWT_COOKIE_CSRF_PROTECT'] = True # Set the secret key to sign the JWTs with app.config['JWT_SECRET_KEY'] = 'super-secret' # Change this! jwt = JWTManager(app) # By default, the CRSF cookies will be called csrf_access_token and # csrf_refresh_token, and in protected endpoints we will look for the # CSRF token in the 'X-CSRF-TOKEN' header. You can modify all of these # with various app.config options. Check the options page for details. # With JWT_COOKIE_CSRF_PROTECT set to True, set_access_cookies() and # set_refresh_cookies() will now also set the non-httponly CSRF cookies # as well @app.route('/token/auth', methods=['POST']) def login(): username = request.json.get('username', None) password = request.json.get('password', None) if username != 'test' or password != 'test': return jsonify({'login': False}), 401 # Create the tokens we will be sending back to the user access_token = create_access_token(identity=username) refresh_token = create_refresh_token(identity=username) # Set the JWTs and the CSRF double submit protection cookies # in this response resp = jsonify({'login': True}) set_access_cookies(resp, access_token) set_refresh_cookies(resp, refresh_token) return resp, 200 @app.route('/token/refresh', methods=['POST']) @jwt_refresh_token_required def refresh(): # Create the new access token current_user = get_jwt_identity() access_token = create_access_token(identity=current_user) # Set the access JWT and CSRF double submit protection cookies # in this response resp = jsonify({'refresh': True}) set_access_cookies(resp, access_token) return resp, 200 # Because the JWTs are stored in an httponly cookie now, we cannot # log the user out by simply deleting the cookie in the frontend. # We need the backend to send us a response to delete the cookies # in order to logout. unset_jwt_cookies is a helper function to # do just that. @app.route('/token/remove', methods=['POST']) def logout(): resp = jsonify({'logout': True}) unset_jwt_cookies(resp) return resp, 200 @app.route('/api/example', methods=['GET']) @jwt_required def protected(): username = get_jwt_identity() return jsonify({'hello': 'from {}'.format(username)}), 200 if __name__ == '__main__': app.run() python-flask-jwt-extended-3.21.0/examples/custom_decorators.py000066400000000000000000000024241352401742600245510ustar00rootroot00000000000000from functools import wraps from flask import Flask, jsonify, request from flask_jwt_extended import ( JWTManager, verify_jwt_in_request, create_access_token, get_jwt_claims ) app = Flask(__name__) app.config['JWT_SECRET_KEY'] = 'super-secret' # Change this! jwt = JWTManager(app) # Here is a custom decorator that verifies the JWT is present in # the request, as well as insuring that this user has a role of # `admin` in the access token def admin_required(fn): @wraps(fn) def wrapper(*args, **kwargs): verify_jwt_in_request() claims = get_jwt_claims() if claims['roles'] != 'admin': return jsonify(msg='Admins only!'), 403 else: return fn(*args, **kwargs) return wrapper @jwt.user_claims_loader def add_claims_to_access_token(identity): if identity == 'admin': return {'roles': 'admin'} else: return {'roles': 'peasant'} @app.route('/login', methods=['POST']) def login(): username = request.json.get('username', None) access_token = create_access_token(username) return jsonify(access_token=access_token) @app.route('/protected', methods=['GET']) @admin_required def protected(): return jsonify(secret_message="go banana!") if __name__ == '__main__': app.run() python-flask-jwt-extended-3.21.0/examples/database_blacklist/000077500000000000000000000000001352401742600242325ustar00rootroot00000000000000python-flask-jwt-extended-3.21.0/examples/database_blacklist/README.md000066400000000000000000000026241352401742600255150ustar00rootroot00000000000000# Blacklist with a database A database is a common choice for storing blacklisted tokens. It has many benefits over an in memory store, like redis. The most obvious benefit of using a database is data consistency. If you add something to the database, you don't need to worry about it vanishing in an event like a power outage. This is huge if you need to revoke long lived keys (for example, keys that you give to another developer so they can access your API). Another advantage of using a database is that you have easy access to all of the relational data stored in there. You can easily and efficiently get a list of all tokens that belong to a given user, and revoke or unrevoke those tokens with ease. This is very handy if you want to provide a user with a way to see all the active tokens they have with your service. Databases also have some cons compared to an in memory store, namely that they are potentially slower, and they may grow huge over time and need to be manually pruned back down. This project contains example code for you to implement a blacklist using a database, with some more complex features that might benefit your application. For ease of use, we will use flask-sqlalchemy with an in memory data store, but in production I would highly recommend using postgres. Please note that this code is only an example, and although I do my best to ensure its quality, it has not been thoroughly tested. python-flask-jwt-extended-3.21.0/examples/database_blacklist/__init__.py000066400000000000000000000000001352401742600263310ustar00rootroot00000000000000python-flask-jwt-extended-3.21.0/examples/database_blacklist/app.py000066400000000000000000000102031352401742600253600ustar00rootroot00000000000000from flask import Flask, request, jsonify from extensions import jwt, db from exceptions import TokenNotFound from flask_jwt_extended import ( jwt_refresh_token_required, get_jwt_identity, create_access_token, create_refresh_token, jwt_required ) from blacklist_helpers import ( is_token_revoked, add_token_to_database, get_user_tokens, revoke_token, unrevoke_token, prune_database ) # We will use an in memory sqlite database for this example. In production, # I would recommend postgres. def create_app(): app = Flask(__name__) app.secret_key = 'ChangeMe!' app.config['JWT_BLACKLIST_ENABLED'] = True app.config['JWT_BLACKLIST_TOKEN_CHECKS'] = ['access', 'refresh'] app.config['SQLALCHEMY_DATABASE_URI'] = "sqlite://" app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db.init_app(app) jwt.init_app(app) # In a real application, these would likely be blueprints register_endpoints(app) return app def register_endpoints(app): # Make sure the sqlalchemy database is created @app.before_first_request def setup_sqlalchemy(): db.create_all() # Define our callback function to check if a token has been revoked or not @jwt.token_in_blacklist_loader def check_if_token_revoked(decoded_token): return is_token_revoked(decoded_token) @app.route('/auth/login', methods=['POST']) def login(): username = request.json.get('username', None) password = request.json.get('password', None) if username != 'test' or password != 'test': return jsonify({"msg": "Bad username or password"}), 401 # Create our JWTs access_token = create_access_token(identity=username) refresh_token = create_refresh_token(identity=username) # Store the tokens in our store with a status of not currently revoked. add_token_to_database(access_token, app.config['JWT_IDENTITY_CLAIM']) add_token_to_database(refresh_token, app.config['JWT_IDENTITY_CLAIM']) ret = { 'access_token': access_token, 'refresh_token': refresh_token } return jsonify(ret), 201 # A revoked refresh tokens will not be able to access this endpoint @app.route('/auth/refresh', methods=['POST']) @jwt_refresh_token_required def refresh(): # Do the same thing that we did in the login endpoint here current_user = get_jwt_identity() access_token = create_access_token(identity=current_user) add_token_to_database(access_token, app.config['JWT_IDENTITY_CLAIM']) return jsonify({'access_token': access_token}), 201 # Provide a way for a user to look at their tokens @app.route('/auth/token', methods=['GET']) @jwt_required def get_tokens(): user_identity = get_jwt_identity() all_tokens = get_user_tokens(user_identity) ret = [token.to_dict() for token in all_tokens] return jsonify(ret), 200 # Provide a way for a user to revoke/unrevoke their tokens @app.route('/auth/token/', methods=['PUT']) @jwt_required def modify_token(token_id): # Get and verify the desired revoked status from the body json_data = request.get_json(silent=True) if not json_data: return jsonify({"msg": "Missing 'revoke' in body"}), 400 revoke = json_data.get('revoke', None) if revoke is None: return jsonify({"msg": "Missing 'revoke' in body"}), 400 if not isinstance(revoke, bool): return jsonify({"msg": "'revoke' must be a boolean"}), 400 # Revoke or unrevoke the token based on what was passed to this function user_identity = get_jwt_identity() try: if revoke: revoke_token(token_id, user_identity) return jsonify({'msg': 'Token revoked'}), 200 else: unrevoke_token(token_id, user_identity) return jsonify({'msg': 'Token unrevoked'}), 200 except TokenNotFound: return jsonify({'msg': 'The specified token was not found'}), 404 if __name__ == '__main__': app = create_app() app.run(debug=True) python-flask-jwt-extended-3.21.0/examples/database_blacklist/blacklist_helpers.py000066400000000000000000000060721352401742600303030ustar00rootroot00000000000000from datetime import datetime from sqlalchemy.orm.exc import NoResultFound from flask_jwt_extended import decode_token from exceptions import TokenNotFound from database import TokenBlacklist from extensions import db def _epoch_utc_to_datetime(epoch_utc): """ Helper function for converting epoch timestamps (as stored in JWTs) into python datetime objects (which are easier to use with sqlalchemy). """ return datetime.fromtimestamp(epoch_utc) def add_token_to_database(encoded_token, identity_claim): """ Adds a new token to the database. It is not revoked when it is added. :param identity_claim: """ decoded_token = decode_token(encoded_token) jti = decoded_token['jti'] token_type = decoded_token['type'] user_identity = decoded_token[identity_claim] expires = _epoch_utc_to_datetime(decoded_token['exp']) revoked = False db_token = TokenBlacklist( jti=jti, token_type=token_type, user_identity=user_identity, expires=expires, revoked=revoked, ) db.session.add(db_token) db.session.commit() def is_token_revoked(decoded_token): """ Checks if the given token is revoked or not. Because we are adding all the tokens that we create into this database, if the token is not present in the database we are going to consider it revoked, as we don't know where it was created. """ jti = decoded_token['jti'] try: token = TokenBlacklist.query.filter_by(jti=jti).one() return token.revoked except NoResultFound: return True def get_user_tokens(user_identity): """ Returns all of the tokens, revoked and unrevoked, that are stored for the given user """ return TokenBlacklist.query.filter_by(user_identity=user_identity).all() def revoke_token(token_id, user): """ Revokes the given token. Raises a TokenNotFound error if the token does not exist in the database """ try: token = TokenBlacklist.query.filter_by(id=token_id, user_identity=user).one() token.revoked = True db.session.commit() except NoResultFound: raise TokenNotFound("Could not find the token {}".format(token_id)) def unrevoke_token(token_id, user): """ Unrevokes the given token. Raises a TokenNotFound error if the token does not exist in the database """ try: token = TokenBlacklist.query.filter_by(id=token_id, user_identity=user).one() token.revoked = False db.session.commit() except NoResultFound: raise TokenNotFound("Could not find the token {}".format(token_id)) def prune_database(): """ Delete tokens that have expired from the database. How (and if) you call this is entirely up you. You could expose it to an endpoint that only administrators could call, you could run it as a cron, set it up with flask cli, etc. """ now = datetime.now() expired = TokenBlacklist.query.filter(TokenBlacklist.expires < now).all() for token in expired: db.session.delete(token) db.session.commit() python-flask-jwt-extended-3.21.0/examples/database_blacklist/database.py000066400000000000000000000012261352401742600263510ustar00rootroot00000000000000from extensions import db class TokenBlacklist(db.Model): id = db.Column(db.Integer, primary_key=True) jti = db.Column(db.String(36), nullable=False) token_type = db.Column(db.String(10), nullable=False) user_identity = db.Column(db.String(50), nullable=False) revoked = db.Column(db.Boolean, nullable=False) expires = db.Column(db.DateTime, nullable=False) def to_dict(self): return { 'token_id': self.id, 'jti': self.jti, 'token_type': self.token_type, 'user_identity': self.user_identity, 'revoked': self.revoked, 'expires': self.expires } python-flask-jwt-extended-3.21.0/examples/database_blacklist/exceptions.py000066400000000000000000000001711352401742600267640ustar00rootroot00000000000000 class TokenNotFound(Exception): """ Indicates that a token could not be found in the database """ pass python-flask-jwt-extended-3.21.0/examples/database_blacklist/extensions.py000066400000000000000000000001711352401742600270020ustar00rootroot00000000000000from flask_jwt_extended import JWTManager from flask_sqlalchemy import SQLAlchemy jwt = JWTManager() db = SQLAlchemy() python-flask-jwt-extended-3.21.0/examples/jwt_in_cookie.py000066400000000000000000000070201352401742600236320ustar00rootroot00000000000000from flask import Flask, jsonify, request from flask_jwt_extended import ( JWTManager, jwt_required, create_access_token, jwt_refresh_token_required, create_refresh_token, get_jwt_identity, set_access_cookies, set_refresh_cookies, unset_jwt_cookies ) # NOTE: This is just a basic example of how to enable cookies. This is # vulnerable to CSRF attacks, and should not be used as is. See # csrf_protection_with_cookies.py for a more complete example! app = Flask(__name__) # Configure application to store JWTs in cookies. Whenever you make # a request to a protected endpoint, you will need to send in the # access or refresh JWT via a cookie. app.config['JWT_TOKEN_LOCATION'] = ['cookies'] # Set the cookie paths, so that you are only sending your access token # cookie to the access endpoints, and only sending your refresh token # to the refresh endpoint. Technically this is optional, but it is in # your best interest to not send additional cookies in the request if # they aren't needed. app.config['JWT_ACCESS_COOKIE_PATH'] = '/api/' app.config['JWT_REFRESH_COOKIE_PATH'] = '/token/refresh' # Disable CSRF protection for this example. In almost every case, # this is a bad idea. See examples/csrf_protection_with_cookies.py # for how safely store JWTs in cookies app.config['JWT_COOKIE_CSRF_PROTECT'] = False # Set the secret key to sign the JWTs with app.config['JWT_SECRET_KEY'] = 'super-secret' # Change this! jwt = JWTManager(app) # Use the set_access_cookie() and set_refresh_cookie() on a response # object to set the JWTs in the response cookies. You can configure # the cookie names and other settings via various app.config options @app.route('/token/auth', methods=['POST']) def login(): username = request.json.get('username', None) password = request.json.get('password', None) if username != 'test' or password != 'test': return jsonify({'login': False}), 401 # Create the tokens we will be sending back to the user access_token = create_access_token(identity=username) refresh_token = create_refresh_token(identity=username) # Set the JWT cookies in the response resp = jsonify({'login': True}) set_access_cookies(resp, access_token) set_refresh_cookies(resp, refresh_token) return resp, 200 # Same thing as login here, except we are only setting a new cookie # for the access token. @app.route('/token/refresh', methods=['POST']) @jwt_refresh_token_required def refresh(): # Create the new access token current_user = get_jwt_identity() access_token = create_access_token(identity=current_user) # Set the JWT access cookie in the response resp = jsonify({'refresh': True}) set_access_cookies(resp, access_token) return resp, 200 # Because the JWTs are stored in an httponly cookie now, we cannot # log the user out by simply deleting the cookie in the frontend. # We need the backend to send us a response to delete the cookies # in order to logout. unset_jwt_cookies is a helper function to # do just that. @app.route('/token/remove', methods=['POST']) def logout(): resp = jsonify({'logout': True}) unset_jwt_cookies(resp) return resp, 200 # We do not need to make any changes to our protected endpoints. They # will all still function the exact same as they do when sending the # JWT in via a header instead of a cookie @app.route('/api/example', methods=['GET']) @jwt_required def protected(): username = get_jwt_identity() return jsonify({'hello': 'from {}'.format(username)}), 200 if __name__ == '__main__': app.run() python-flask-jwt-extended-3.21.0/examples/jwt_in_json.py000066400000000000000000000022541352401742600233360ustar00rootroot00000000000000from flask import Flask, jsonify, request from flask_jwt_extended import ( JWTManager, jwt_required, create_access_token, ) app = Flask(__name__) # IMPORTANT: Body is meaningless in GET requests, so using json # as the only lookup method means that the GET method will become # unauthorized in any protected route, as there's no body to look for. app.config['JWT_TOKEN_LOCATION'] = ['json'] app.config['JWT_SECRET_KEY'] = 'super-secret' # Change this! jwt = JWTManager(app) @app.route('/login', methods=['POST']) def login(): username = request.json.get('username', None) password = request.json.get('password', None) if username != 'test' or password != 'test': return jsonify({"msg": "Bad username or password"}), 401 access_token = create_access_token(identity=username) return jsonify(access_token=access_token) # The default attribute name where the JWT is looked for is `access_token`, # and can be changed with the JWT_JSON_KEY option. # Notice how the route is unreachable with GET requests. @app.route('/protected', methods=['GET', 'POST']) @jwt_required def protected(): return jsonify(foo='bar') if __name__ == '__main__': app.run() python-flask-jwt-extended-3.21.0/examples/jwt_in_query_string.py000066400000000000000000000024751352401742600251250ustar00rootroot00000000000000from flask import Flask, jsonify, request from flask_jwt_extended import ( JWTManager, jwt_required, create_access_token, ) # IMPORTANT NOTE: # In most cases this is not recommended! It can lead some some # security issues, such as: # - The browser saving GET request urls in it's history that # has a JWT in the query string # - The backend server logging JWTs that are in the url # # If possible, you should use headers instead! app = Flask(__name__) app.config['JWT_TOKEN_LOCATION'] = ['query_string'] app.config['JWT_SECRET_KEY'] = 'super-secret' # Change this! jwt = JWTManager(app) @app.route('/login', methods=['POST']) def login(): username = request.json.get('username', None) password = request.json.get('password', None) if username != 'test' or password != 'test': return jsonify({"msg": "Bad username or password"}), 401 access_token = create_access_token(identity=username) return jsonify(access_token=access_token) # The default query paramater where the JWT is looked for is `jwt`, # and can be changed with the JWT_QUERY_STRING_NAME option. Making # a request to this endpoint would look like: # /protected?jwt= @app.route('/protected', methods=['GET']) @jwt_required def protected(): return jsonify(foo='bar') if __name__ == '__main__': app.run() python-flask-jwt-extended-3.21.0/examples/loaders.py000066400000000000000000000022241352401742600224410ustar00rootroot00000000000000from flask import Flask, jsonify, request from flask_jwt_extended import ( JWTManager, jwt_required, create_access_token ) app = Flask(__name__) app.config['JWT_SECRET_KEY'] = 'super-secret' # Change this! jwt = JWTManager(app) # Using the expired_token_loader decorator, we will now call # this function whenever an expired but otherwise valid access # token attempts to access an endpoint @jwt.expired_token_loader def my_expired_token_callback(expired_token): token_type = expired_token['type'] return jsonify({ 'status': 401, 'sub_status': 42, 'msg': 'The {} token has expired'.format(token_type) }), 401 @app.route('/login', methods=['POST']) def login(): username = request.json.get('username', None) password = request.json.get('password', None) if username != 'test' or password != 'test': return jsonify({"msg": "Bad username or password"}), 401 ret = {'access_token': create_access_token(username)} return jsonify(ret), 200 @app.route('/protected', methods=['GET']) @jwt_required def protected(): return jsonify({'hello': 'world'}), 200 if __name__ == '__main__': app.run() python-flask-jwt-extended-3.21.0/examples/oidc.py000066400000000000000000000106601352401742600217310ustar00rootroot00000000000000from flask import Flask, jsonify from flask_restful import Api import requests import json from jwt.algorithms import RSAAlgorithm from functools import wraps from flask_jwt_extended import ( JWTManager, verify_jwt_in_request, get_raw_jwt, current_user ) import config # Setup Flask Server app = Flask(__name__) app.config.from_object(config.Config) api = Api(app) # ==== SETUP # Set OIDC entries for auto-discovery # Naming of 4 key input variables borrowed from Kubernetes (https://kubernetes.io/docs/reference/access-authn-authz/authentication/) # This was tested with Red Hat's https://www.keycloak.org/ but should work with any OIDC provider like Auth0, Octa, Google, Microsoft etc. # Issuer + Realm OIDC_ISSUER_URL = 'https://my-identity-server.example/auth/realms/master' # Client ID (Audience) OIDC_CLIENT_ID = 'example.my-identity-server' # Token variable holding unique username OIDC_USERNAME_CLAIM = 'email' # Token list variable holding groups that user belongs to (for role-based-access-control) # idea here is to have few but could be hundreds of groups, based on which groups user belongs to, grants them access to various endpoints # in identity server this is usually mapped directly to ldap, so ldap group membership defines which endpoints user can access # https://www.keycloak.org/docs/latest/server_admin/index.html#_ldap_mappers, but remember groups don't have to come from ldap # group mapper was setup for flat group structure not to include any prefixes so if you have to do that, please update code in group_required method OIDC_GROUPS_CLAIM = 'groups' # ==== END OF SETUP # Helper Methods def urljoin(*args): """ Joins given arguments into an url. Trailing but not leading slashes are stripped for each argument. """ return "/".join(map(lambda x: str(x).rstrip('/'), args)) def token_required(fn): @wraps(fn) def wrapper(*args, **kwargs): verify_jwt_in_request() return fn(*args, **kwargs) return wrapper def group_required(group=''): def decorator(fn): @wraps(fn) def wrapper(*args, **kwargs): # standard flask_jwt_extended token verifications verify_jwt_in_request() # custom group membership verification groups = get_raw_jwt()[OIDC_GROUPS_CLAIM] if group not in groups: return jsonify({'result': "user not in group required to access this endpoint"}), 401 return fn(*args, **kwargs) return wrapper return decorator # Setup Token Verification # force use of RS265 app.config['JWT_ALGORITHM'] = 'RS256' # retrieve master openid-configuration endpoint for issuer realm oidc_config = requests.get(urljoin(OIDC_ISSUER_URL, '.well-known/openid-configuration'), verify=False).json() # retrieve data from jwks_uri endpoint oidc_jwks_uri = requests.get(oidc_config['jwks_uri'], verify=False).json() # retrieve first jwk entry from jwks_uri endpoint and use it to construct the RSA public key app.config['JWT_PUBLIC_KEY'] = RSAAlgorithm.from_jwk(json.dumps(oidc_jwks_uri['keys'][0])) # audience is oidc client id (can be array starting https://github.com/vimalloc/flask-jwt-extended/issues/219) app.config['JWT_DECODE_AUDIENCE'] = OIDC_CLIENT_ID # name of token entry that will become distinct flask identity username app.config['JWT_IDENTITY_CLAIM'] = OIDC_USERNAME_CLAIM jwt = JWTManager(app) # TEST ENDPOINTS @app.route('/anonymous', methods=['GET']) def get_anonymous(): return jsonify({'result': "anonymous ok"}), 200 @app.route('/token-protected', methods=['GET']) @token_required def get_protected_by_token(): return jsonify({'result': "protected by token ok"}), 200 @app.route('/group-protected', methods=['GET']) @group_required('api-access') # currently one, could be one of or multiple required depending on your needs def get_protected_by_group(): return jsonify({'result': "protected by token AND group membership ok"}, {'user': current_user.username} ), 200 # Identity User Class class User: username = None def __init__(self): pass # User Class to get you started # Identity holds whatever variable in token you point at JWT_IDENTITY_CLAIM # good place to construct identity from token and other places, it is then available in method through current_user. @jwt.user_loader_callback_loader def user_loader_callback(identity): u = User() u.username = identity return u app.run(host='0.0.0.0') python-flask-jwt-extended-3.21.0/examples/optional_protected_endpoints.py000066400000000000000000000023551352401742600267760ustar00rootroot00000000000000from flask import Flask, jsonify, request from flask_jwt_extended import ( JWTManager, jwt_optional, create_access_token, get_jwt_identity ) app = Flask(__name__) # Setup the Flask-JWT-Extended extension app.config['JWT_SECRET_KEY'] = 'super-secret' # Change this! jwt = JWTManager(app) @app.route('/login', methods=['POST']) def login(): username = request.json.get('username', None) password = request.json.get('password', None) if not username: return jsonify({"msg": "Missing username parameter"}), 400 if not password: return jsonify({"msg": "Missing password parameter"}), 400 if username != 'test' or password != 'test': return jsonify({"msg": "Bad username or password"}), 401 access_token = create_access_token(identity=username) return jsonify(access_token=access_token), 200 @app.route('/partially-protected', methods=['GET']) @jwt_optional def partially_protected(): # If no JWT is sent in with the request, get_jwt_identity() # will return None current_user = get_jwt_identity() if current_user: return jsonify(logged_in_as=current_user), 200 else: return jsonify(logged_in_as='anonymous user'), 200 if __name__ == '__main__': app.run() python-flask-jwt-extended-3.21.0/examples/redis_blacklist.py000066400000000000000000000130321352401742600241450ustar00rootroot00000000000000# Redis is a very quick in memory store. The benefits of using redis is that # things will generally speedy, and it can be (mostly) persistent by dumping # the data to disk (see: https://redis.io/topics/persistence). The drawbacks # to using redis is you have a higher chance of encountering data loss (in # this case, 'forgetting' that a token was revoked), when events like # power outages occur. # # When does it make sense to use redis for a blacklist? If you are blacklisting # every token on logout, and not doing nothing besides that (such as keeping # track of what tokens are blacklisted, providing options to un-revoke # blacklisted tokens, or view tokens that are currently active for a user), # then redis is a great choice. In the worst case, a few tokens might slip # between the cracks in the case of a power outage or other such event, but # 99.99% of the time tokens will be properly blacklisted. # # Redis also has the benefit of supporting an expires time when storing data. # Utilizing this, you will not need to manually prune down the stored tokens # to keep it from blowing up over time. This code includes how to do this. # # If you intend to use some other features in your blacklist (tracking # what tokens are currently active, option to revoke or unrevoke specific # tokens, etc), data integrity is probably more important to you then # raw performance. In this case a database solution (such as postgres) is # probably a better fit for your blacklist. Check out the "database_blacklist" # example for how that might work. import redis from datetime import timedelta from flask import Flask, request, jsonify from flask_jwt_extended import ( JWTManager, create_access_token, create_refresh_token, get_jti, jwt_refresh_token_required, get_jwt_identity, jwt_required, get_raw_jwt ) app = Flask(__name__) app.secret_key = 'ChangeMe!' # Setup the flask-jwt-extended extension. See: ACCESS_EXPIRES = timedelta(minutes=15) REFRESH_EXPIRES = timedelta(days=30) app.config['JWT_ACCESS_TOKEN_EXPIRES'] = ACCESS_EXPIRES app.config['JWT_REFRESH_TOKEN_EXPIRES'] = REFRESH_EXPIRES app.config['JWT_BLACKLIST_ENABLED'] = True app.config['JWT_BLACKLIST_TOKEN_CHECKS'] = ['access', 'refresh'] jwt = JWTManager(app) # Setup our redis connection for storing the blacklisted tokens revoked_store = redis.StrictRedis(host='localhost', port=6379, db=0, decode_responses=True) # Create our function to check if a token has been blacklisted. In this simple # case, we will just store the tokens jti (unique identifier) in redis # whenever we create a new token (with the revoked status being 'false'). This # function will return the revoked status of a token. If a token doesn't # exist in this store, we don't know where it came from (as we are adding newly # created tokens to our store with a revoked status of 'false'). In this case # we will consider the token to be revoked, for safety purposes. @jwt.token_in_blacklist_loader def check_if_token_is_revoked(decrypted_token): jti = decrypted_token['jti'] entry = revoked_store.get(jti) if entry is None: return True return entry == 'true' @app.route('/auth/login', methods=['POST']) def login(): username = request.json.get('username', None) password = request.json.get('password', None) if username != 'test' or password != 'test': return jsonify({"msg": "Bad username or password"}), 401 # Create our JWTs access_token = create_access_token(identity=username) refresh_token = create_refresh_token(identity=username) # Store the tokens in redis with a status of not currently revoked. We # can use the `get_jti()` method to get the unique identifier string for # each token. We can also set an expires time on these tokens in redis, # so they will get automatically removed after they expire. We will set # everything to be automatically removed shortly after the token expires access_jti = get_jti(encoded_token=access_token) refresh_jti = get_jti(encoded_token=refresh_token) revoked_store.set(access_jti, 'false', ACCESS_EXPIRES * 1.2) revoked_store.set(refresh_jti, 'false', REFRESH_EXPIRES * 1.2) ret = { 'access_token': access_token, 'refresh_token': refresh_token } return jsonify(ret), 201 # A blacklisted refresh tokens will not be able to access this endpoint @app.route('/auth/refresh', methods=['POST']) @jwt_refresh_token_required def refresh(): # Do the same thing that we did in the login endpoint here current_user = get_jwt_identity() access_token = create_access_token(identity=current_user) access_jti = get_jti(encoded_token=access_token) revoked_store.set(access_jti, 'false', ACCESS_EXPIRES * 1.2) ret = {'access_token': access_token} return jsonify(ret), 201 # Endpoint for revoking the current users access token @app.route('/auth/access_revoke', methods=['DELETE']) @jwt_required def logout(): jti = get_raw_jwt()['jti'] revoked_store.set(jti, 'true', ACCESS_EXPIRES * 1.2) return jsonify({"msg": "Access token revoked"}), 200 # Endpoint for revoking the current users refresh token @app.route('/auth/refresh_revoke', methods=['DELETE']) @jwt_refresh_token_required def logout2(): jti = get_raw_jwt()['jti'] revoked_store.set(jti, 'true', REFRESH_EXPIRES * 1.2) return jsonify({"msg": "Refresh token revoked"}), 200 # A blacklisted access token will not be able to access this any more @app.route('/protected', methods=['GET']) @jwt_required def protected(): return jsonify({'hello': 'world'}) if __name__ == '__main__': app.run() python-flask-jwt-extended-3.21.0/examples/refresh_tokens.py000066400000000000000000000031171352401742600240330ustar00rootroot00000000000000from flask import Flask, jsonify, request from flask_jwt_extended import ( JWTManager, jwt_required, create_access_token, jwt_refresh_token_required, create_refresh_token, get_jwt_identity ) app = Flask(__name__) app.config['JWT_SECRET_KEY'] = 'super-secret' # Change this! jwt = JWTManager(app) @app.route('/login', methods=['POST']) def login(): username = request.json.get('username', None) password = request.json.get('password', None) if username != 'test' or password != 'test': return jsonify({"msg": "Bad username or password"}), 401 # Use create_access_token() and create_refresh_token() to create our # access and refresh tokens ret = { 'access_token': create_access_token(identity=username), 'refresh_token': create_refresh_token(identity=username) } return jsonify(ret), 200 # The jwt_refresh_token_required decorator insures a valid refresh # token is present in the request before calling this endpoint. We # can use the get_jwt_identity() function to get the identity of # the refresh token, and use the create_access_token() function again # to make a new access token for this identity. @app.route('/refresh', methods=['POST']) @jwt_refresh_token_required def refresh(): current_user = get_jwt_identity() ret = { 'access_token': create_access_token(identity=current_user) } return jsonify(ret), 200 @app.route('/protected', methods=['GET']) @jwt_required def protected(): username = get_jwt_identity() return jsonify(logged_in_as=username), 200 if __name__ == '__main__': app.run() python-flask-jwt-extended-3.21.0/examples/simple.py000066400000000000000000000030251352401742600223010ustar00rootroot00000000000000from flask import Flask, jsonify, request from flask_jwt_extended import ( JWTManager, jwt_required, create_access_token, get_jwt_identity ) app = Flask(__name__) # Setup the Flask-JWT-Extended extension app.config['JWT_SECRET_KEY'] = 'super-secret' # Change this! jwt = JWTManager(app) # Provide a method to create access tokens. The create_access_token() # function is used to actually generate the token, and you can return # it to the caller however you choose. @app.route('/login', methods=['POST']) def login(): if not request.is_json: return jsonify({"msg": "Missing JSON in request"}), 400 username = request.json.get('username', None) password = request.json.get('password', None) if not username: return jsonify({"msg": "Missing username parameter"}), 400 if not password: return jsonify({"msg": "Missing password parameter"}), 400 if username != 'test' or password != 'test': return jsonify({"msg": "Bad username or password"}), 401 # Identity can be any data that is json serializable access_token = create_access_token(identity=username) return jsonify(access_token=access_token), 200 # Protect a view with jwt_required, which requires a valid access token # in the request to access. @app.route('/protected', methods=['GET']) @jwt_required def protected(): # Access the identity of the current user with get_jwt_identity current_user = get_jwt_identity() return jsonify(logged_in_as=current_user), 200 if __name__ == '__main__': app.run() python-flask-jwt-extended-3.21.0/examples/token_freshness.py000066400000000000000000000053251352401742600242150ustar00rootroot00000000000000from flask import Flask, jsonify, request from flask_jwt_extended import ( JWTManager, jwt_required, create_access_token, jwt_refresh_token_required, create_refresh_token, get_jwt_identity, fresh_jwt_required ) app = Flask(__name__) app.config['JWT_SECRET_KEY'] = 'super-secret' # Change this! jwt = JWTManager(app) # Standard login endpoint. Will return a fresh access token and # a refresh token @app.route('/login', methods=['POST']) def login(): username = request.json.get('username', None) password = request.json.get('password', None) if username != 'test' or password != 'test': return jsonify({"msg": "Bad username or password"}), 401 # create_access_token supports an optional 'fresh' argument, # which marks the token as fresh or non-fresh accordingly. # As we just verified their username and password, we are # going to mark the token as fresh here. ret = { 'access_token': create_access_token(identity=username, fresh=True), 'refresh_token': create_refresh_token(identity=username) } return jsonify(ret), 200 # Refresh token endpoint. This will generate a new access token from # the refresh token, but will mark that access token as non-fresh, # as we do not actually verify a password in this endpoint. @app.route('/refresh', methods=['POST']) @jwt_refresh_token_required def refresh(): current_user = get_jwt_identity() new_token = create_access_token(identity=current_user, fresh=False) ret = {'access_token': new_token} return jsonify(ret), 200 # Fresh login endpoint. This is designed to be used if we need to # make a fresh token for a user (by verifying they have the # correct username and password). Unlike the standard login endpoint, # this will only return a new access token, so that we don't keep # generating new refresh tokens, which entirely defeats their point. @app.route('/fresh-login', methods=['POST']) def fresh_login(): username = request.json.get('username', None) password = request.json.get('password', None) if username != 'test' or password != 'test': return jsonify({"msg": "Bad username or password"}), 401 new_token = create_access_token(identity=username, fresh=True) ret = {'access_token': new_token} return jsonify(ret), 200 # Any valid JWT can access this endpoint @app.route('/protected', methods=['GET']) @jwt_required def protected(): username = get_jwt_identity() return jsonify(logged_in_as=username), 200 # Only fresh JWTs can access this endpoint @app.route('/protected-fresh', methods=['GET']) @fresh_jwt_required def protected_fresh(): username = get_jwt_identity() return jsonify(fresh_logged_in_as=username), 200 if __name__ == '__main__': app.run() python-flask-jwt-extended-3.21.0/examples/tokens_from_complex_objects.py000066400000000000000000000043101352401742600265740ustar00rootroot00000000000000from flask import Flask, jsonify, request from flask_jwt_extended import ( JWTManager, jwt_required, create_access_token, get_jwt_identity, get_jwt_claims ) app = Flask(__name__) app.config['JWT_SECRET_KEY'] = 'super-secret' # Change this! jwt = JWTManager(app) # This is an example of a complex object that we could build # a JWT from. In practice, this will likely be something # like a SQLAlchemy instance. class UserObject: def __init__(self, username, roles): self.username = username self.roles = roles # Create a function that will be called whenever create_access_token # is used. It will take whatever object is passed into the # create_access_token method, and lets us define what custom claims # should be added to the access token. @jwt.user_claims_loader def add_claims_to_access_token(user): return {'roles': user.roles} # Create a function that will be called whenever create_access_token # is used. It will take whatever object is passed into the # create_access_token method, and lets us define what the identity # of the access token should be. @jwt.user_identity_loader def user_identity_lookup(user): return user.username @app.route('/login', methods=['POST']) def login(): username = request.json.get('username', None) password = request.json.get('password', None) if username != 'test' or password != 'test': return jsonify({"msg": "Bad username or password"}), 401 # Create an example UserObject user = UserObject(username='test', roles=['foo', 'bar']) # We can now pass this complex object directly to the # create_access_token method. This will allow us to access # the properties of this object in the user_claims_loader # function, and get the identity of this object from the # user_identity_loader function. access_token = create_access_token(identity=user) ret = {'access_token': access_token} return jsonify(ret), 200 @app.route('/protected', methods=['GET']) @jwt_required def protected(): ret = { 'current_identity': get_jwt_identity(), # test 'current_roles': get_jwt_claims()['roles'] # ['foo', 'bar'] } return jsonify(ret), 200 if __name__ == '__main__': app.run() python-flask-jwt-extended-3.21.0/flask_jwt_extended/000077500000000000000000000000001352401742600224645ustar00rootroot00000000000000python-flask-jwt-extended-3.21.0/flask_jwt_extended/__init__.py000066400000000000000000000011351352401742600245750ustar00rootroot00000000000000from .jwt_manager import JWTManager from .view_decorators import ( fresh_jwt_required, jwt_optional, jwt_refresh_token_required, jwt_required, verify_fresh_jwt_in_request, verify_jwt_in_request, verify_jwt_in_request_optional, verify_jwt_refresh_token_in_request ) from .utils import ( create_access_token, create_refresh_token, current_user, decode_token, get_csrf_token, get_current_user, get_jti, get_jwt_claims, get_jwt_identity, get_raw_jwt, set_access_cookies, set_refresh_cookies, unset_access_cookies, unset_jwt_cookies, unset_refresh_cookies ) __version__ = '3.21.0' python-flask-jwt-extended-3.21.0/flask_jwt_extended/config.py000066400000000000000000000250421352401742600243060ustar00rootroot00000000000000import datetime from warnings import warn from six import raise_from # In Python 2.7 collections.abc is a part of the collections module. try: from collections.abc import Sequence, Set except ImportError: # pragma: no cover from collections import Sequence, Set from flask import current_app # Older versions of pyjwt do not have the requires_cryptography set. Also, # older versions will not be adding new algorithms to them, so I can hard code # the default version here and be safe. If there is a newer algorithm someone # wants to use, they will need newer versions of pyjwt and it will be included # in their requires_cryptography set, and if they attempt to use it in older # versions of pyjwt, it will kick it out as an unrecognized algorithm. try: from jwt.algorithms import requires_cryptography except ImportError: # pragma: no cover requires_cryptography = {'RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES521', 'ES512', 'PS256', 'PS384', 'PS512'} class _Config(object): """ Helper object for accessing and verifying options in this extension. This is meant for internal use of the application; modifying config options should be done with flasks ```app.config```. Default values for the configuration options are set in the jwt_manager object. All of these values are read only. This is simply a loose wrapper with some helper functionality for flasks `app.config`. """ @property def is_asymmetric(self): return self.algorithm in requires_cryptography @property def encode_key(self): return self._private_key if self.is_asymmetric else self._secret_key @property def decode_key(self): return self._public_key if self.is_asymmetric else self._secret_key @property def token_location(self): locations = current_app.config['JWT_TOKEN_LOCATION'] if isinstance(locations, str): locations = (locations,) elif not isinstance(locations, (Sequence, Set)): raise RuntimeError('JWT_TOKEN_LOCATION must be a sequence or a set') elif not locations: raise RuntimeError('JWT_TOKEN_LOCATION must contain at least one ' 'of "headers", "cookies", "query_string", or "json"') for location in locations: if location not in ('headers', 'cookies', 'query_string', 'json'): raise RuntimeError('JWT_TOKEN_LOCATION can only contain ' '"headers", "cookies", "query_string", or "json"') return locations @property def jwt_in_cookies(self): return 'cookies' in self.token_location @property def jwt_in_headers(self): return 'headers' in self.token_location @property def jwt_in_query_string(self): return 'query_string' in self.token_location @property def jwt_in_json(self): return 'json' in self.token_location @property def header_name(self): name = current_app.config['JWT_HEADER_NAME'] if not name: raise RuntimeError("JWT_ACCESS_HEADER_NAME cannot be empty") return name @property def header_type(self): return current_app.config['JWT_HEADER_TYPE'] @property def query_string_name(self): return current_app.config['JWT_QUERY_STRING_NAME'] @property def access_cookie_name(self): return current_app.config['JWT_ACCESS_COOKIE_NAME'] @property def refresh_cookie_name(self): return current_app.config['JWT_REFRESH_COOKIE_NAME'] @property def access_cookie_path(self): return current_app.config['JWT_ACCESS_COOKIE_PATH'] @property def refresh_cookie_path(self): return current_app.config['JWT_REFRESH_COOKIE_PATH'] @property def cookie_secure(self): return current_app.config['JWT_COOKIE_SECURE'] @property def cookie_domain(self): return current_app.config['JWT_COOKIE_DOMAIN'] @property def session_cookie(self): return current_app.config['JWT_SESSION_COOKIE'] @property def cookie_samesite(self): return current_app.config['JWT_COOKIE_SAMESITE'] @property def json_key(self): return current_app.config['JWT_JSON_KEY'] @property def refresh_json_key(self): return current_app.config['JWT_REFRESH_JSON_KEY'] @property def csrf_protect(self): return self.jwt_in_cookies and current_app.config['JWT_COOKIE_CSRF_PROTECT'] @property def csrf_request_methods(self): return current_app.config['JWT_CSRF_METHODS'] @property def csrf_in_cookies(self): return current_app.config['JWT_CSRF_IN_COOKIES'] @property def access_csrf_cookie_name(self): return current_app.config['JWT_ACCESS_CSRF_COOKIE_NAME'] @property def refresh_csrf_cookie_name(self): return current_app.config['JWT_REFRESH_CSRF_COOKIE_NAME'] @property def access_csrf_cookie_path(self): return current_app.config['JWT_ACCESS_CSRF_COOKIE_PATH'] @property def refresh_csrf_cookie_path(self): return current_app.config['JWT_REFRESH_CSRF_COOKIE_PATH'] @staticmethod def _get_depreciated_csrf_header_name(): # This used to be the same option for access and refresh header names. # This gives users a warning if they are still using the old behavior old_name = current_app.config.get('JWT_CSRF_HEADER_NAME', None) if old_name: msg = ( "JWT_CSRF_HEADER_NAME is depreciated. Use JWT_ACCESS_CSRF_HEADER_NAME " "or JWT_REFRESH_CSRF_HEADER_NAME instead" ) warn(msg, DeprecationWarning) return old_name @property def access_csrf_header_name(self): return self._get_depreciated_csrf_header_name() or \ current_app.config['JWT_ACCESS_CSRF_HEADER_NAME'] @property def refresh_csrf_header_name(self): return self._get_depreciated_csrf_header_name() or \ current_app.config['JWT_REFRESH_CSRF_HEADER_NAME'] @property def access_expires(self): delta = current_app.config['JWT_ACCESS_TOKEN_EXPIRES'] if type(delta) is int: delta = datetime.timedelta(seconds=delta) if delta is not False: try: delta + datetime.datetime.now() except TypeError as e: err = ( "must be able to add JWT_ACCESS_TOKEN_EXPIRES to datetime.datetime" ) raise_from(RuntimeError(err), e) return delta @property def refresh_expires(self): delta = current_app.config['JWT_REFRESH_TOKEN_EXPIRES'] if type(delta) is int: delta = datetime.timedelta(seconds=delta) if delta is not False: try: delta + datetime.datetime.now() except TypeError as e: err = ( "must be able to add JWT_REFRESH_TOKEN_EXPIRES to datetime.datetime" ) raise_from(RuntimeError(err), e) return delta @property def algorithm(self): return current_app.config['JWT_ALGORITHM'] @property def decode_algorithms(self): algorithms = current_app.config['JWT_DECODE_ALGORITHMS'] if not algorithms: return [self.algorithm] if self.algorithm not in algorithms: algorithms.append(self.algorithm) return algorithms @property def blacklist_enabled(self): return current_app.config['JWT_BLACKLIST_ENABLED'] @property def blacklist_checks(self): check_type = current_app.config['JWT_BLACKLIST_TOKEN_CHECKS'] if isinstance(check_type, str): check_type = (check_type,) elif not isinstance(check_type, (Sequence, Set)): raise RuntimeError('JWT_BLACKLIST_TOKEN_CHECKS must be a sequence or a set') for item in check_type: if item not in ('access', 'refresh'): err = 'JWT_BLACKLIST_TOKEN_CHECKS must be "access" or "refresh"' raise RuntimeError(err) return check_type @property def blacklist_access_tokens(self): return 'access' in self.blacklist_checks @property def blacklist_refresh_tokens(self): return 'refresh' in self.blacklist_checks @property def _secret_key(self): key = current_app.config['JWT_SECRET_KEY'] if not key: key = current_app.config.get('SECRET_KEY', None) if not key: raise RuntimeError('JWT_SECRET_KEY or flask SECRET_KEY ' 'must be set when using symmetric ' 'algorithm "{}"'.format(self.algorithm)) return key @property def _public_key(self): key = current_app.config['JWT_PUBLIC_KEY'] if not key: raise RuntimeError('JWT_PUBLIC_KEY must be set to use ' 'asymmetric cryptography algorithm ' '"{}"'.format(self.algorithm)) return key @property def _private_key(self): key = current_app.config['JWT_PRIVATE_KEY'] if not key: raise RuntimeError('JWT_PRIVATE_KEY must be set to use ' 'asymmetric cryptography algorithm ' '"{}"'.format(self.algorithm)) return key @property def cookie_max_age(self): # Returns the appropiate value for max_age for flask set_cookies. If # session cookie is true, return None, otherwise return a number of # seconds 1 year in the future return None if self.session_cookie else 31540000 # 1 year @property def identity_claim_key(self): return current_app.config['JWT_IDENTITY_CLAIM'] @property def user_claims_key(self): return current_app.config['JWT_USER_CLAIMS'] @property def user_claims_in_refresh_token(self): return current_app.config['JWT_CLAIMS_IN_REFRESH_TOKEN'] @property def exempt_methods(self): return {"OPTIONS"} @property def error_msg_key(self): return current_app.config['JWT_ERROR_MESSAGE_KEY'] @property def json_encoder(self): return current_app.json_encoder @property def audience(self): return current_app.config['JWT_DECODE_AUDIENCE'] @property def issuer(self): return current_app.config['JWT_DECODE_ISSUER'] @property def leeway(self): return current_app.config['JWT_DECODE_LEEWAY'] config = _Config() python-flask-jwt-extended-3.21.0/flask_jwt_extended/default_callbacks.py000066400000000000000000000075141352401742600264700ustar00rootroot00000000000000""" These are the default methods implementations that are used in this extension. All of these can be updated on an app by app basis using the JWTManager loader decorators. For further information, check out the following links: http://flask-jwt-extended.readthedocs.io/en/latest/changing_default_behavior.html http://flask-jwt-extended.readthedocs.io/en/latest/tokens_from_complex_object.html """ from flask import jsonify from flask_jwt_extended.config import config def default_user_claims_callback(userdata): """ By default, we add no additional claims to the access tokens. :param userdata: data passed in as the ```identity``` argument to the ```create_access_token``` and ```create_refresh_token``` functions """ return {} def default_user_identity_callback(userdata): """ By default, we use the passed in object directly as the jwt identity. See this for additional info: :param userdata: data passed in as the ```identity``` argument to the ```create_access_token``` and ```create_refresh_token``` functions """ return userdata def default_expired_token_callback(expired_token): """ By default, if an expired token attempts to access a protected endpoint, we return a generic error message with a 401 status """ return jsonify({config.error_msg_key: 'Token has expired'}), 401 def default_invalid_token_callback(error_string): """ By default, if an invalid token attempts to access a protected endpoint, we return the error string for why it is not valid with a 422 status code :param error_string: String indicating why the token is invalid """ return jsonify({config.error_msg_key: error_string}), 422 def default_unauthorized_callback(error_string): """ By default, if a protected endpoint is accessed without a JWT, we return the error string indicating why this is unauthorized, with a 401 status code :param error_string: String indicating why this request is unauthorized """ return jsonify({config.error_msg_key: error_string}), 401 def default_needs_fresh_token_callback(): """ By default, if a non-fresh jwt is used to access a ```fresh_jwt_required``` endpoint, we return a general error message with a 401 status code """ return jsonify({config.error_msg_key: 'Fresh token required'}), 401 def default_revoked_token_callback(): """ By default, if a revoked token is used to access a protected endpoint, we return a general error message with a 401 status code """ return jsonify({config.error_msg_key: 'Token has been revoked'}), 401 def default_user_loader_error_callback(identity): """ By default, if a user_loader callback is defined and the callback function returns None, we return a general error message with a 401 status code """ result = {config.error_msg_key: "Error loading the user {}".format(identity)} return jsonify(result), 401 def default_claims_verification_callback(user_claims): """ By default, we do not do any verification of the user claims. """ return True def default_verify_claims_failed_callback(): """ By default, if the user claims verification failed, we return a generic error message with a 400 status code """ return jsonify({config.error_msg_key: 'User claims verification failed'}), 400 def default_decode_key_callback(claims, headers): """ By default, the decode key specified via the JWT_SECRET_KEY or JWT_PUBLIC_KEY settings will be used to decode all tokens """ return config.decode_key def default_encode_key_callback(identity): """ By default, the encode key specified via the JWT_SECRET_KEY or JWT_PRIVATE_KEY settings will be used to encode all tokens """ return config.encode_key python-flask-jwt-extended-3.21.0/flask_jwt_extended/exceptions.py000066400000000000000000000030201352401742600252120ustar00rootroot00000000000000class JWTExtendedException(Exception): """ Base except which all flask_jwt_extended errors extend """ pass class JWTDecodeError(JWTExtendedException): """ An error decoding a JWT """ pass class InvalidHeaderError(JWTExtendedException): """ An error getting header information from a request """ pass class NoAuthorizationError(JWTExtendedException): """ An error raised when no authorization token was found in a protected endpoint """ pass class CSRFError(JWTExtendedException): """ An error with CSRF protection """ pass class WrongTokenError(JWTExtendedException): """ Error raised when attempting to use a refresh token to access an endpoint or vice versa """ pass class RevokedTokenError(JWTExtendedException): """ Error raised when a revoked token attempt to access a protected endpoint """ pass class FreshTokenRequired(JWTExtendedException): """ Error raised when a valid, non-fresh JWT attempt to access an endpoint protected by fresh_jwt_required """ pass class UserLoadError(JWTExtendedException): """ Error raised when a user_loader callback function returns None, indicating that it cannot or will not load a user for the given identity. """ pass class UserClaimsVerificationError(JWTExtendedException): """ Error raised when the claims_verification_callback function returns False, indicating that the expected user claims are invalid """ pass python-flask-jwt-extended-3.21.0/flask_jwt_extended/jwt_manager.py000066400000000000000000000517001352401742600253370ustar00rootroot00000000000000import datetime from warnings import warn from jwt import ( ExpiredSignatureError, InvalidTokenError, InvalidAudienceError, InvalidIssuerError, DecodeError ) try: from flask import _app_ctx_stack as ctx_stack except ImportError: # pragma: no cover from flask import _request_ctx_stack as ctx_stack from flask_jwt_extended.config import config from flask_jwt_extended.exceptions import ( JWTDecodeError, NoAuthorizationError, InvalidHeaderError, WrongTokenError, RevokedTokenError, FreshTokenRequired, CSRFError, UserLoadError, UserClaimsVerificationError ) from flask_jwt_extended.default_callbacks import ( default_expired_token_callback, default_user_claims_callback, default_user_identity_callback, default_invalid_token_callback, default_unauthorized_callback, default_needs_fresh_token_callback, default_revoked_token_callback, default_user_loader_error_callback, default_claims_verification_callback, default_verify_claims_failed_callback, default_decode_key_callback, default_encode_key_callback ) from flask_jwt_extended.tokens import ( encode_refresh_token, encode_access_token ) from flask_jwt_extended.utils import get_jwt_identity class JWTManager(object): """ An object used to hold JWT settings and callback functions for the Flask-JWT-Extended extension. Instances of :class:`JWTManager` are *not* bound to specific apps, so you can create one in the main body of your code and then bind it to your app in a factory function. """ def __init__(self, app=None): """ Create the JWTManager instance. You can either pass a flask application in directly here to register this extension with the flask app, or call init_app after creating this object (in a factory pattern). :param app: A flask application """ # Register the default error handler callback methods. These can be # overridden with the appropriate loader decorators self._user_claims_callback = default_user_claims_callback self._user_identity_callback = default_user_identity_callback self._expired_token_callback = default_expired_token_callback self._invalid_token_callback = default_invalid_token_callback self._unauthorized_callback = default_unauthorized_callback self._needs_fresh_token_callback = default_needs_fresh_token_callback self._revoked_token_callback = default_revoked_token_callback self._user_loader_callback = None self._user_loader_error_callback = default_user_loader_error_callback self._token_in_blacklist_callback = None self._claims_verification_callback = default_claims_verification_callback self._verify_claims_failed_callback = default_verify_claims_failed_callback self._decode_key_callback = default_decode_key_callback self._encode_key_callback = default_encode_key_callback # Register this extension with the flask app now (if it is provided) if app is not None: self.init_app(app) def init_app(self, app): """ Register this extension with the flask app. :param app: A flask application """ # Save this so we can use it later in the extension if not hasattr(app, 'extensions'): # pragma: no cover app.extensions = {} app.extensions['flask-jwt-extended'] = self # Set all the default configurations for this extension self._set_default_configuration_options(app) self._set_error_handler_callbacks(app) def _set_error_handler_callbacks(self, app): """ Sets the error handler callbacks used by this extension """ @app.errorhandler(NoAuthorizationError) def handle_auth_error(e): return self._unauthorized_callback(str(e)) @app.errorhandler(CSRFError) def handle_csrf_error(e): return self._unauthorized_callback(str(e)) @app.errorhandler(ExpiredSignatureError) def handle_expired_error(e): try: token = ctx_stack.top.expired_jwt return self._expired_token_callback(token) except TypeError: msg = ( "jwt.expired_token_loader callback now takes the expired token " "as an additional paramter. Example: expired_callback(token)" ) warn(msg, DeprecationWarning) return self._expired_token_callback() @app.errorhandler(InvalidHeaderError) def handle_invalid_header_error(e): return self._invalid_token_callback(str(e)) @app.errorhandler(DecodeError) def handle_invalid_header_error(e): return self._invalid_token_callback(str(e)) @app.errorhandler(InvalidTokenError) def handle_invalid_token_error(e): return self._invalid_token_callback(str(e)) @app.errorhandler(JWTDecodeError) def handle_jwt_decode_error(e): return self._invalid_token_callback(str(e)) @app.errorhandler(WrongTokenError) def handle_wrong_token_error(e): return self._invalid_token_callback(str(e)) @app.errorhandler(InvalidAudienceError) def handle_invalid_audience_error(e): return self._invalid_token_callback(str(e)) @app.errorhandler(InvalidIssuerError) def handle_invalid_issuer_error(e): return self._invalid_token_callback(str(e)) @app.errorhandler(RevokedTokenError) def handle_revoked_token_error(e): return self._revoked_token_callback() @app.errorhandler(FreshTokenRequired) def handle_fresh_token_required(e): return self._needs_fresh_token_callback() @app.errorhandler(UserLoadError) def handler_user_load_error(e): # The identity is already saved before this exception was raised, # otherwise a different exception would be raised, which is why we # can safely call get_jwt_identity() here identity = get_jwt_identity() return self._user_loader_error_callback(identity) @app.errorhandler(UserClaimsVerificationError) def handle_failed_user_claims_verification(e): return self._verify_claims_failed_callback() @staticmethod def _set_default_configuration_options(app): """ Sets the default configuration options used by this extension """ # Where to look for the JWT. Available options are cookies or headers app.config.setdefault('JWT_TOKEN_LOCATION', ('headers',)) # Options for JWTs when the TOKEN_LOCATION is headers app.config.setdefault('JWT_HEADER_NAME', 'Authorization') app.config.setdefault('JWT_HEADER_TYPE', 'Bearer') # Options for JWTs then the TOKEN_LOCATION is query_string app.config.setdefault('JWT_QUERY_STRING_NAME', 'jwt') # Option for JWTs when the TOKEN_LOCATION is cookies app.config.setdefault('JWT_ACCESS_COOKIE_NAME', 'access_token_cookie') app.config.setdefault('JWT_REFRESH_COOKIE_NAME', 'refresh_token_cookie') app.config.setdefault('JWT_ACCESS_COOKIE_PATH', '/') app.config.setdefault('JWT_REFRESH_COOKIE_PATH', '/') app.config.setdefault('JWT_COOKIE_SECURE', False) app.config.setdefault('JWT_COOKIE_DOMAIN', None) app.config.setdefault('JWT_SESSION_COOKIE', True) app.config.setdefault('JWT_COOKIE_SAMESITE', None) # Option for JWTs when the TOKEN_LOCATION is json app.config.setdefault('JWT_JSON_KEY', 'access_token') app.config.setdefault('JWT_REFRESH_JSON_KEY', 'refresh_token') # Options for using double submit csrf protection app.config.setdefault('JWT_COOKIE_CSRF_PROTECT', True) app.config.setdefault('JWT_CSRF_METHODS', ['POST', 'PUT', 'PATCH', 'DELETE']) app.config.setdefault('JWT_ACCESS_CSRF_HEADER_NAME', 'X-CSRF-TOKEN') app.config.setdefault('JWT_REFRESH_CSRF_HEADER_NAME', 'X-CSRF-TOKEN') app.config.setdefault('JWT_CSRF_IN_COOKIES', True) app.config.setdefault('JWT_ACCESS_CSRF_COOKIE_NAME', 'csrf_access_token') app.config.setdefault('JWT_REFRESH_CSRF_COOKIE_NAME', 'csrf_refresh_token') app.config.setdefault('JWT_ACCESS_CSRF_COOKIE_PATH', '/') app.config.setdefault('JWT_REFRESH_CSRF_COOKIE_PATH', '/') # How long an a token will live before they expire. app.config.setdefault('JWT_ACCESS_TOKEN_EXPIRES', datetime.timedelta(minutes=15)) app.config.setdefault('JWT_REFRESH_TOKEN_EXPIRES', datetime.timedelta(days=30)) # What algorithm to use to sign the token. See here for a list of options: # https://github.com/jpadilla/pyjwt/blob/master/jwt/api_jwt.py app.config.setdefault('JWT_ALGORITHM', 'HS256') # What algorithms are allowed to decode a token app.config.setdefault('JWT_DECODE_ALGORITHMS', None) # Secret key to sign JWTs with. Only used if a symmetric algorithm is # used (such as the HS* algorithms). We will use the app secret key # if this is not set. app.config.setdefault('JWT_SECRET_KEY', None) # Keys to sign JWTs with when use when using an asymmetric # (public/private key) algorithm, such as RS* or EC* app.config.setdefault('JWT_PRIVATE_KEY', None) app.config.setdefault('JWT_PUBLIC_KEY', None) # Options for blacklisting/revoking tokens app.config.setdefault('JWT_BLACKLIST_ENABLED', False) app.config.setdefault('JWT_BLACKLIST_TOKEN_CHECKS', ('access', 'refresh')) app.config.setdefault('JWT_IDENTITY_CLAIM', 'identity') app.config.setdefault('JWT_USER_CLAIMS', 'user_claims') app.config.setdefault('JWT_DECODE_AUDIENCE', None) app.config.setdefault('JWT_DECODE_ISSUER', None) app.config.setdefault('JWT_DECODE_LEEWAY', 0) app.config.setdefault('JWT_CLAIMS_IN_REFRESH_TOKEN', False) app.config.setdefault('JWT_ERROR_MESSAGE_KEY', 'msg') def user_claims_loader(self, callback): """ This decorator sets the callback function for adding custom claims to an access token when :func:`~flask_jwt_extended.create_access_token` is called. By default, no extra user claims will be added to the JWT. *HINT*: The callback function must be a function that takes only **one** argument, which is the object passed into :func:`~flask_jwt_extended.create_access_token`, and returns the custom claims you want included in the access tokens. This returned claims must be *JSON serializable*. """ self._user_claims_callback = callback return callback def user_identity_loader(self, callback): """ This decorator sets the callback function for getting the JSON serializable identity out of whatever object is passed into :func:`~flask_jwt_extended.create_access_token` and :func:`~flask_jwt_extended.create_refresh_token`. By default, this will return the unmodified object that is passed in as the `identity` kwarg to the above functions. *HINT*: The callback function must be a function that takes only **one** argument, which is the object passed into :func:`~flask_jwt_extended.create_access_token` or :func:`~flask_jwt_extended.create_refresh_token`, and returns the *JSON serializable* identity of this token. """ self._user_identity_callback = callback return callback def expired_token_loader(self, callback): """ This decorator sets the callback function that will be called if an expired JWT attempts to access a protected endpoint. The default implementation will return a 401 status code with the JSON: {"msg": "Token has expired"} *HINT*: The callback must be a function that takes **one** argument, which is a dictionary containing the data for the expired token, and and returns a *Flask response*. """ self._expired_token_callback = callback return callback def invalid_token_loader(self, callback): """ This decorator sets the callback function that will be called if an invalid JWT attempts to access a protected endpoint. The default implementation will return a 422 status code with the JSON: {"msg": ""} *HINT*: The callback must be a function that takes only **one** argument, which is a string which contains the reason why a token is invalid, and returns a *Flask response*. """ self._invalid_token_callback = callback return callback def unauthorized_loader(self, callback): """ This decorator sets the callback function that will be called if an no JWT can be found when attempting to access a protected endpoint. The default implementation will return a 401 status code with the JSON: {"msg": ""} *HINT*: The callback must be a function that takes only **one** argument, which is a string which contains the reason why a JWT could not be found, and returns a *Flask response*. """ self._unauthorized_callback = callback return callback def needs_fresh_token_loader(self, callback): """ This decorator sets the callback function that will be called if a valid and non-fresh token attempts to access an endpoint protected with the :func:`~flask_jwt_extended.fresh_jwt_required` decorator. The default implementation will return a 401 status code with the JSON: {"msg": "Fresh token required"} *HINT*: The callback must be a function that takes **no** arguments, and returns a *Flask response*. """ self._needs_fresh_token_callback = callback return callback def revoked_token_loader(self, callback): """ This decorator sets the callback function that will be called if a revoked token attempts to access a protected endpoint. The default implementation will return a 401 status code with the JSON: {"msg": "Token has been revoked"} *HINT*: The callback must be a function that takes **no** arguments, and returns a *Flask response*. """ self._revoked_token_callback = callback return callback def user_loader_callback_loader(self, callback): """ This decorator sets the callback function that will be called to automatically load an object when a protected endpoint is accessed. By default this is not used. *HINT*: The callback must take **one** argument which is the identity JWT accessing the protected endpoint, and it must return any object (which can then be accessed via the :attr:`~flask_jwt_extended.current_user` LocalProxy in the protected endpoint), or `None` in the case of a user not being able to be loaded for any reason. If this callback function returns `None`, the :meth:`~flask_jwt_extended.JWTManager.user_loader_error_loader` will be called. """ self._user_loader_callback = callback return callback def user_loader_error_loader(self, callback): """ This decorator sets the callback function that will be called if `None` is returned from the :meth:`~flask_jwt_extended.JWTManager.user_loader_callback_loader` callback function. The default implementation will return a 401 status code with the JSON: {"msg": "Error loading the user "} *HINT*: The callback must be a function that takes **one** argument, which is the identity of the user who failed to load, and must return a *Flask response*. """ self._user_loader_error_callback = callback return callback def token_in_blacklist_loader(self, callback): """ This decorator sets the callback function that will be called when a protected endpoint is accessed and will check if the JWT has been been revoked. By default, this callback is not used. *HINT*: The callback must be a function that takes **one** argument, which is the decoded JWT (python dictionary), and returns *`True`* if the token has been blacklisted (or is otherwise considered revoked), or *`False`* otherwise. """ self._token_in_blacklist_callback = callback return callback def claims_verification_loader(self, callback): """ This decorator sets the callback function that will be called when a protected endpoint is accessed, and will check if the custom claims in the JWT are valid. By default, this callback is not used. The error returned if the claims are invalid can be controlled via the :meth:`~flask_jwt_extended.JWTManager.claims_verification_failed_loader` decorator. *HINT*: This callback must be a function that takes **one** argument, which is the custom claims (python dict) present in the JWT, and returns *`True`* if the claims are valid, or *`False`* otherwise. """ self._claims_verification_callback = callback return callback def claims_verification_failed_loader(self, callback): """ This decorator sets the callback function that will be called if the :meth:`~flask_jwt_extended.JWTManager.claims_verification_loader` callback returns False, indicating that the user claims are not valid. The default implementation will return a 400 status code with the JSON: {"msg": "User claims verification failed"} *HINT*: This callback must be a function that takes **no** arguments, and returns a *Flask response*. """ self._verify_claims_failed_callback = callback return callback def decode_key_loader(self, callback): """ This decorator sets the callback function for getting the JWT decode key and can be used to dynamically choose the appropriate decode key based on token contents. The default implementation returns the decode key specified by `JWT_SECRET_KEY` or `JWT_PUBLIC_KEY`, depending on the signing algorithm. *HINT*: The callback function should be a function that takes **two** arguments, which are the unverified claims and headers of the jwt (dictionaries). The function must return a *string* which is the decode key in PEM format to verify the token. """ self._decode_key_callback = callback return callback def encode_key_loader(self, callback): """ This decorator sets the callback function for getting the JWT encode key and can be used to dynamically choose the appropriate encode key based on the token identity. The default implementation returns the encode key specified by `JWT_SECRET_KEY` or `JWT_PRIVATE_KEY`, depending on the signing algorithm. *HINT*: The callback function must be a function that takes only **one** argument, which is the identity as passed into the create_access_token or create_refresh_token functions, and must return a *string* which is the decode key to verify the token. """ self._encode_key_callback = callback return callback def _create_refresh_token(self, identity, expires_delta=None, user_claims=None): if expires_delta is None: expires_delta = config.refresh_expires if user_claims is None and config.user_claims_in_refresh_token: user_claims = self._user_claims_callback(identity) refresh_token = encode_refresh_token( identity=self._user_identity_callback(identity), secret=self._encode_key_callback(identity), algorithm=config.algorithm, expires_delta=expires_delta, user_claims=user_claims, csrf=config.csrf_protect, identity_claim_key=config.identity_claim_key, user_claims_key=config.user_claims_key, json_encoder=config.json_encoder ) return refresh_token def _create_access_token(self, identity, fresh=False, expires_delta=None, user_claims=None): if expires_delta is None: expires_delta = config.access_expires if user_claims is None: user_claims = self._user_claims_callback(identity) access_token = encode_access_token( identity=self._user_identity_callback(identity), secret=self._encode_key_callback(identity), algorithm=config.algorithm, expires_delta=expires_delta, fresh=fresh, user_claims=user_claims, csrf=config.csrf_protect, identity_claim_key=config.identity_claim_key, user_claims_key=config.user_claims_key, json_encoder=config.json_encoder ) return access_token python-flask-jwt-extended-3.21.0/flask_jwt_extended/tokens.py000066400000000000000000000141741352401742600243500ustar00rootroot00000000000000import datetime import uuid from calendar import timegm import jwt from werkzeug.security import safe_str_cmp from flask_jwt_extended.exceptions import JWTDecodeError, CSRFError def _create_csrf_token(): return str(uuid.uuid4()) def _encode_jwt(additional_token_data, expires_delta, secret, algorithm, json_encoder=None): uid = _create_csrf_token() now = datetime.datetime.utcnow() token_data = { 'iat': now, 'nbf': now, 'jti': uid, } # If expires_delta is False, the JWT should never expire # and the 'exp' claim is not set. if expires_delta: token_data['exp'] = now + expires_delta token_data.update(additional_token_data) encoded_token = jwt.encode(token_data, secret, algorithm, json_encoder=json_encoder).decode('utf-8') return encoded_token def encode_access_token(identity, secret, algorithm, expires_delta, fresh, user_claims, csrf, identity_claim_key, user_claims_key, json_encoder=None): """ Creates a new encoded (utf-8) access token. :param identity: Identifier for who this token is for (ex, username). This data must be json serializable :param secret: Secret key to encode the JWT with :param algorithm: Which algorithm to encode this JWT with :param expires_delta: How far in the future this token should expire (set to False to disable expiration) :type expires_delta: datetime.timedelta or False :param fresh: If this should be a 'fresh' token or not. If a datetime.timedelta is given this will indicate how long this token will remain fresh. :param user_claims: Custom claims to include in this token. This data must be json serializable :param csrf: Whether to include a csrf double submit claim in this token (boolean) :param identity_claim_key: Which key should be used to store the identity :param user_claims_key: Which key should be used to store the user claims :return: Encoded access token """ if isinstance(fresh, datetime.timedelta): now = datetime.datetime.utcnow() fresh = timegm((now + fresh).utctimetuple()) token_data = { identity_claim_key: identity, 'fresh': fresh, 'type': 'access', } # Don't add extra data to the token if user_claims is empty. if user_claims: token_data[user_claims_key] = user_claims if csrf: token_data['csrf'] = _create_csrf_token() return _encode_jwt(token_data, expires_delta, secret, algorithm, json_encoder=json_encoder) def encode_refresh_token(identity, secret, algorithm, expires_delta, user_claims, csrf, identity_claim_key, user_claims_key, json_encoder=None): """ Creates a new encoded (utf-8) refresh token. :param identity: Some identifier used to identify the owner of this token :param secret: Secret key to encode the JWT with :param algorithm: Which algorithm to use for the toek :param expires_delta: How far in the future this token should expire (set to False to disable expiration) :type expires_delta: datetime.timedelta or False :param user_claims: Custom claims to include in this token. This data must be json serializable :param csrf: Whether to include a csrf double submit claim in this token (boolean) :param identity_claim_key: Which key should be used to store the identity :param user_claims_key: Which key should be used to store the user claims :return: Encoded refresh token """ token_data = { identity_claim_key: identity, 'type': 'refresh', } # Don't add extra data to the token if user_claims is empty. if user_claims: token_data[user_claims_key] = user_claims if csrf: token_data['csrf'] = _create_csrf_token() return _encode_jwt(token_data, expires_delta, secret, algorithm, json_encoder=json_encoder) def decode_jwt(encoded_token, secret, algorithms, identity_claim_key, user_claims_key, csrf_value=None, audience=None, leeway=0, allow_expired=False, issuer=None): """ Decodes an encoded JWT :param encoded_token: The encoded JWT string to decode :param secret: Secret key used to encode the JWT :param algorithms: Algorithms allowed to decode the token :param identity_claim_key: expected key that contains the identity :param user_claims_key: expected key that contains the user claims :param csrf_value: Expected double submit csrf value :param audience: expected audience in the JWT :param issuer: expected issuer in the JWT :param leeway: optional leeway to add some margin around expiration times :param allow_expired: Options to ignore exp claim validation in token :return: Dictionary containing contents of the JWT """ options = {} if allow_expired: options['verify_exp'] = False # This call verifies the ext, iat, nbf, and aud claims data = jwt.decode(encoded_token, secret, algorithms=algorithms, audience=audience, leeway=leeway, options=options, issuer=issuer) # Make sure that any custom claims we expect in the token are present if 'jti' not in data: data['jti'] = None if identity_claim_key not in data: raise JWTDecodeError("Missing claim: {}".format(identity_claim_key)) if 'type' not in data: data['type'] = 'access' if data['type'] not in ('refresh', 'access'): raise JWTDecodeError("Missing or invalid claim: type") if data['type'] == 'access': if 'fresh' not in data: data['fresh'] = False if user_claims_key not in data: data[user_claims_key] = {} if csrf_value: if 'csrf' not in data: raise JWTDecodeError("Missing claim: csrf") if not safe_str_cmp(data['csrf'], csrf_value): raise CSRFError("CSRF double submit tokens do not match") return data python-flask-jwt-extended-3.21.0/flask_jwt_extended/utils.py000066400000000000000000000407731352401742600242110ustar00rootroot00000000000000from flask import current_app from werkzeug.local import LocalProxy from jwt import ExpiredSignatureError from warnings import warn try: from flask import _app_ctx_stack as ctx_stack except ImportError: # pragma: no cover from flask import _request_ctx_stack as ctx_stack from flask_jwt_extended.config import config from flask_jwt_extended.exceptions import ( RevokedTokenError, UserClaimsVerificationError, WrongTokenError ) from flask_jwt_extended.tokens import decode_jwt import jwt # Proxy to access the current user current_user = LocalProxy(lambda: get_current_user()) def get_raw_jwt(): """ In a protected endpoint, this will return the python dictionary which has all of the claims of the JWT that is accessing the endpoint. If no JWT is currently present, an empty dict is returned instead. """ return getattr(ctx_stack.top, 'jwt', {}) def get_jwt_identity(): """ In a protected endpoint, this will return the identity of the JWT that is accessing this endpoint. If no JWT is present,`None` is returned instead. """ return get_raw_jwt().get(config.identity_claim_key, None) def get_jwt_claims(): """ In a protected endpoint, this will return the dictionary of custom claims in the JWT that is accessing the endpoint. If no custom user claims are present, an empty dict is returned instead. """ return get_raw_jwt().get(config.user_claims_key, {}) def get_current_user(): """ In a protected endpoint, this will return the user object for the JWT that is accessing this endpoint. This is only present if the :meth:`~flask_jwt_extended.JWTManager.user_loader_callback_loader` is being used. If the user loader callback is not being used, this will return `None`. """ return getattr(ctx_stack.top, 'jwt_user', None) def get_jti(encoded_token): """ Returns the JTI (unique identifier) of an encoded JWT :param encoded_token: The encoded JWT to get the JTI from. """ return decode_token(encoded_token).get('jti') def decode_token(encoded_token, csrf_value=None, allow_expired=False): """ Returns the decoded token (python dict) from an encoded JWT. This does all the checks to insure that the decoded token is valid before returning it. :param encoded_token: The encoded JWT to decode into a python dict. :param csrf_value: Expected CSRF double submit value (optional) :param allow_expired: Options to ignore exp claim validation in token :return: Dictionary containing contents of the JWT """ jwt_manager = _get_jwt_manager() unverified_claims = jwt.decode( encoded_token, verify=False, algorithms=config.decode_algorithms ) unverified_headers = jwt.get_unverified_header(encoded_token) # Attempt to call callback with both claims and headers, but fallback to just claims # for backwards compatibility try: secret = jwt_manager._decode_key_callback(unverified_claims, unverified_headers) except TypeError: msg = ( "The single-argument (unverified_claims) form of decode_key_callback ", "is deprecated. Update your code to use the two-argument form ", "(unverified_claims, unverified_headers)." ) warn(msg, DeprecationWarning) secret = jwt_manager._decode_key_callback(unverified_claims) try: return decode_jwt( encoded_token=encoded_token, secret=secret, algorithms=config.decode_algorithms, identity_claim_key=config.identity_claim_key, user_claims_key=config.user_claims_key, csrf_value=csrf_value, audience=config.audience, issuer=config.issuer, leeway=config.leeway, allow_expired=allow_expired ) except ExpiredSignatureError: expired_token = decode_jwt( encoded_token=encoded_token, secret=secret, algorithms=config.decode_algorithms, identity_claim_key=config.identity_claim_key, user_claims_key=config.user_claims_key, csrf_value=csrf_value, audience=config.audience, issuer=config.issuer, leeway=config.leeway, allow_expired=True ) ctx_stack.top.expired_jwt = expired_token raise def _get_jwt_manager(): try: return current_app.extensions['flask-jwt-extended'] except KeyError: # pragma: no cover raise RuntimeError("You must initialize a JWTManager with this flask " "application before using this method") def create_access_token(identity, fresh=False, expires_delta=None, user_claims=None): """ Create a new access token. :param identity: The identity of this token, which can be any data that is json serializable. It can also be a python object, in which case you can use the :meth:`~flask_jwt_extended.JWTManager.user_identity_loader` to define a callback function that will be used to pull a json serializable identity out of the object. :param fresh: If this token should be marked as fresh, and can thus access :func:`~flask_jwt_extended.fresh_jwt_required` endpoints. Defaults to `False`. This value can also be a `datetime.timedelta` in which case it will indicate how long this token will be considered fresh. :param expires_delta: A `datetime.timedelta` for how long this token should last before it expires. Set to False to disable expiration. If this is None, it will use the 'JWT_ACCESS_TOKEN_EXPIRES` config value (see :ref:`Configuration Options`) :param user_claims: Optional JSON serializable to override user claims. :return: An encoded access token """ jwt_manager = _get_jwt_manager() return jwt_manager._create_access_token(identity, fresh, expires_delta, user_claims) def create_refresh_token(identity, expires_delta=None, user_claims=None): """ Creates a new refresh token. :param identity: The identity of this token, which can be any data that is json serializable. It can also be a python object, in which case you can use the :meth:`~flask_jwt_extended.JWTManager.user_identity_loader` to define a callback function that will be used to pull a json serializable identity out of the object. :param expires_delta: A `datetime.timedelta` for how long this token should last before it expires. Set to False to disable expiration. If this is None, it will use the 'JWT_REFRESH_TOKEN_EXPIRES` config value (see :ref:`Configuration Options`) :param user_claims: Optional JSON serializable to override user claims. :return: An encoded refresh token """ jwt_manager = _get_jwt_manager() return jwt_manager._create_refresh_token(identity, expires_delta, user_claims) def has_user_loader(): jwt_manager = _get_jwt_manager() return jwt_manager._user_loader_callback is not None def user_loader(*args, **kwargs): jwt_manager = _get_jwt_manager() return jwt_manager._user_loader_callback(*args, **kwargs) def has_token_in_blacklist_callback(): jwt_manager = _get_jwt_manager() return jwt_manager._token_in_blacklist_callback is not None def token_in_blacklist(*args, **kwargs): jwt_manager = _get_jwt_manager() return jwt_manager._token_in_blacklist_callback(*args, **kwargs) def verify_token_type(decoded_token, expected_type): if decoded_token['type'] != expected_type: raise WrongTokenError('Only {} tokens are allowed'.format(expected_type)) def verify_token_not_blacklisted(decoded_token, request_type): if not config.blacklist_enabled: return if not has_token_in_blacklist_callback(): raise RuntimeError("A token_in_blacklist_callback must be provided via " "the '@token_in_blacklist_loader' if " "JWT_BLACKLIST_ENABLED is True") if config.blacklist_access_tokens and request_type == 'access': if token_in_blacklist(decoded_token): raise RevokedTokenError('Token has been revoked') if config.blacklist_refresh_tokens and request_type == 'refresh': if token_in_blacklist(decoded_token): raise RevokedTokenError('Token has been revoked') def verify_token_claims(jwt_data): jwt_manager = _get_jwt_manager() user_claims = jwt_data[config.user_claims_key] if not jwt_manager._claims_verification_callback(user_claims): raise UserClaimsVerificationError('User claims verification failed') def get_csrf_token(encoded_token): """ Returns the CSRF double submit token from an encoded JWT. :param encoded_token: The encoded JWT :return: The CSRF double submit token """ token = decode_token(encoded_token) return token['csrf'] def set_access_cookies(response, encoded_access_token, max_age=None): """ Takes a flask response object, and an encoded access token, and configures the response to set in the access token in a cookie. If `JWT_CSRF_IN_COOKIES` is `True` (see :ref:`Configuration Options`), this will also set the CSRF double submit values in a separate cookie. :param response: The Flask response object to set the access cookies in. :param encoded_access_token: The encoded access token to set in the cookies. :param max_age: The max age of the cookie. If this is None, it will use the `JWT_SESSION_COOKIE` option (see :ref:`Configuration Options`). Otherwise, it will use this as the cookies `max-age` and the JWT_SESSION_COOKIE option will be ignored. Values should be the number of seconds (as an integer). """ if not config.jwt_in_cookies: raise RuntimeWarning("set_access_cookies() called without " "'JWT_TOKEN_LOCATION' configured to use cookies") # Set the access JWT in the cookie response.set_cookie(config.access_cookie_name, value=encoded_access_token, max_age=max_age or config.cookie_max_age, secure=config.cookie_secure, httponly=True, domain=config.cookie_domain, path=config.access_cookie_path, samesite=config.cookie_samesite) # If enabled, set the csrf double submit access cookie if config.csrf_protect and config.csrf_in_cookies: response.set_cookie(config.access_csrf_cookie_name, value=get_csrf_token(encoded_access_token), max_age=max_age or config.cookie_max_age, secure=config.cookie_secure, httponly=False, domain=config.cookie_domain, path=config.access_csrf_cookie_path, samesite=config.cookie_samesite) def set_refresh_cookies(response, encoded_refresh_token, max_age=None): """ Takes a flask response object, and an encoded refresh token, and configures the response to set in the refresh token in a cookie. If `JWT_CSRF_IN_COOKIES` is `True` (see :ref:`Configuration Options`), this will also set the CSRF double submit values in a separate cookie. :param response: The Flask response object to set the refresh cookies in. :param encoded_refresh_token: The encoded refresh token to set in the cookies. :param max_age: The max age of the cookie. If this is None, it will use the `JWT_SESSION_COOKIE` option (see :ref:`Configuration Options`). Otherwise, it will use this as the cookies `max-age` and the JWT_SESSION_COOKIE option will be ignored. Values should be the number of seconds (as an integer). """ if not config.jwt_in_cookies: raise RuntimeWarning("set_refresh_cookies() called without " "'JWT_TOKEN_LOCATION' configured to use cookies") # Set the refresh JWT in the cookie response.set_cookie(config.refresh_cookie_name, value=encoded_refresh_token, max_age=max_age or config.cookie_max_age, secure=config.cookie_secure, httponly=True, domain=config.cookie_domain, path=config.refresh_cookie_path, samesite=config.cookie_samesite) # If enabled, set the csrf double submit refresh cookie if config.csrf_protect and config.csrf_in_cookies: response.set_cookie(config.refresh_csrf_cookie_name, value=get_csrf_token(encoded_refresh_token), max_age=max_age or config.cookie_max_age, secure=config.cookie_secure, httponly=False, domain=config.cookie_domain, path=config.refresh_csrf_cookie_path, samesite=config.cookie_samesite) def unset_jwt_cookies(response): """ Takes a flask response object, and configures it to unset (delete) JWTs stored in cookies. :param response: The Flask response object to delete the JWT cookies in. """ unset_access_cookies(response) unset_refresh_cookies(response) def unset_access_cookies(response): """ takes a flask response object, and configures it to unset (delete) the access token from the response cookies. if `jwt_csrf_in_cookies` (see :ref:`configuration options`) is `true`, this will also remove the access csrf double submit value from the response cookies as well. :param response: the flask response object to delete the jwt cookies in. """ if not config.jwt_in_cookies: raise RuntimeWarning("unset_refresh_cookies() called without " "'JWT_TOKEN_LOCATION' configured to use cookies") response.set_cookie(config.access_cookie_name, value='', expires=0, secure=config.cookie_secure, httponly=True, domain=config.cookie_domain, path=config.access_cookie_path, samesite=config.cookie_samesite) if config.csrf_protect and config.csrf_in_cookies: response.set_cookie(config.access_csrf_cookie_name, value='', expires=0, secure=config.cookie_secure, httponly=False, domain=config.cookie_domain, path=config.access_csrf_cookie_path, samesite=config.cookie_samesite) def unset_refresh_cookies(response): """ takes a flask response object, and configures it to unset (delete) the refresh token from the response cookies. if `jwt_csrf_in_cookies` (see :ref:`configuration options`) is `true`, this will also remove the refresh csrf double submit value from the response cookies as well. :param response: the flask response object to delete the jwt cookies in. """ if not config.jwt_in_cookies: raise RuntimeWarning("unset_refresh_cookies() called without " "'JWT_TOKEN_LOCATION' configured to use cookies") response.set_cookie(config.refresh_cookie_name, value='', expires=0, secure=config.cookie_secure, httponly=True, domain=config.cookie_domain, path=config.refresh_cookie_path, samesite=config.cookie_samesite) if config.csrf_protect and config.csrf_in_cookies: response.set_cookie(config.refresh_csrf_cookie_name, value='', expires=0, secure=config.cookie_secure, httponly=False, domain=config.cookie_domain, path=config.refresh_csrf_cookie_path, samesite=config.cookie_samesite) python-flask-jwt-extended-3.21.0/flask_jwt_extended/view_decorators.py000066400000000000000000000242141352401742600262400ustar00rootroot00000000000000from functools import wraps from datetime import datetime from calendar import timegm from werkzeug.exceptions import BadRequest from flask import request try: from flask import _app_ctx_stack as ctx_stack except ImportError: # pragma: no cover from flask import _request_ctx_stack as ctx_stack from flask_jwt_extended.config import config from flask_jwt_extended.exceptions import ( CSRFError, FreshTokenRequired, InvalidHeaderError, NoAuthorizationError, UserLoadError ) from flask_jwt_extended.utils import ( decode_token, has_user_loader, user_loader, verify_token_claims, verify_token_not_blacklisted, verify_token_type ) def verify_jwt_in_request(): """ Ensure that the requester has a valid access token. This does not check the freshness of the access token. Raises an appropiate exception there is no token or if the token is invalid. """ if request.method not in config.exempt_methods: jwt_data = _decode_jwt_from_request(request_type='access') ctx_stack.top.jwt = jwt_data verify_token_claims(jwt_data) _load_user(jwt_data[config.identity_claim_key]) def verify_jwt_in_request_optional(): """ Optionally check if this request has a valid access token. If an access token in present in the request, :func:`~flask_jwt_extended.get_jwt_identity` will return the identity of the access token. If no access token is present in the request, this simply returns, and :func:`~flask_jwt_extended.get_jwt_identity` will return `None` instead. If there is an invalid access token in the request (expired, tampered with, etc), this will still raise the appropiate exception. """ try: if request.method not in config.exempt_methods: jwt_data = _decode_jwt_from_request(request_type='access') ctx_stack.top.jwt = jwt_data verify_token_claims(jwt_data) _load_user(jwt_data[config.identity_claim_key]) except (NoAuthorizationError, InvalidHeaderError): pass def verify_fresh_jwt_in_request(): """ Ensure that the requester has a valid and fresh access token. Raises an appropiate exception if there is no token, the token is invalid, or the token is not marked as fresh. """ if request.method not in config.exempt_methods: jwt_data = _decode_jwt_from_request(request_type='access') ctx_stack.top.jwt = jwt_data fresh = jwt_data['fresh'] if isinstance(fresh, bool): if not fresh: raise FreshTokenRequired('Fresh token required') else: now = timegm(datetime.utcnow().utctimetuple()) if fresh < now: raise FreshTokenRequired('Fresh token required') verify_token_claims(jwt_data) _load_user(jwt_data[config.identity_claim_key]) def verify_jwt_refresh_token_in_request(): """ Ensure that the requester has a valid refresh token. Raises an appropiate exception if there is no token or the token is invalid. """ if request.method not in config.exempt_methods: jwt_data = _decode_jwt_from_request(request_type='refresh') ctx_stack.top.jwt = jwt_data _load_user(jwt_data[config.identity_claim_key]) def jwt_required(fn): """ A decorator to protect a Flask endpoint. If you decorate an endpoint with this, it will ensure that the requester has a valid access token before allowing the endpoint to be called. This does not check the freshness of the access token. See also: :func:`~flask_jwt_extended.fresh_jwt_required` """ @wraps(fn) def wrapper(*args, **kwargs): verify_jwt_in_request() return fn(*args, **kwargs) return wrapper def jwt_optional(fn): """ A decorator to optionally protect a Flask endpoint If an access token in present in the request, this will call the endpoint with :func:`~flask_jwt_extended.get_jwt_identity` having the identity of the access token. If no access token is present in the request, this endpoint will still be called, but :func:`~flask_jwt_extended.get_jwt_identity` will return `None` instead. If there is an invalid access token in the request (expired, tampered with, etc), this will still call the appropriate error handler instead of allowing the endpoint to be called as if there is no access token in the request. """ @wraps(fn) def wrapper(*args, **kwargs): verify_jwt_in_request_optional() return fn(*args, **kwargs) return wrapper def fresh_jwt_required(fn): """ A decorator to protect a Flask endpoint. If you decorate an endpoint with this, it will ensure that the requester has a valid and fresh access token before allowing the endpoint to be called. See also: :func:`~flask_jwt_extended.jwt_required` """ @wraps(fn) def wrapper(*args, **kwargs): verify_fresh_jwt_in_request() return fn(*args, **kwargs) return wrapper def jwt_refresh_token_required(fn): """ A decorator to protect a Flask endpoint. If you decorate an endpoint with this, it will ensure that the requester has a valid refresh token before allowing the endpoint to be called. """ @wraps(fn) def wrapper(*args, **kwargs): verify_jwt_refresh_token_in_request() return fn(*args, **kwargs) return wrapper def _load_user(identity): if has_user_loader(): user = user_loader(identity) if user is None: raise UserLoadError("user_loader returned None for {}".format(identity)) else: ctx_stack.top.jwt_user = user def _decode_jwt_from_headers(): header_name = config.header_name header_type = config.header_type # Verify we have the auth header jwt_header = request.headers.get(header_name, None) if not jwt_header: raise NoAuthorizationError("Missing {} Header".format(header_name)) # Make sure the header is in a valid format that we are expecting, ie # : parts = jwt_header.split() if not header_type: if len(parts) != 1: msg = "Bad {} header. Expected value ''".format(header_name) raise InvalidHeaderError(msg) encoded_token = parts[0] else: if parts[0] != header_type or len(parts) != 2: msg = "Bad {} header. Expected value '{} '".format( header_name, header_type ) raise InvalidHeaderError(msg) encoded_token = parts[1] return encoded_token, None def _decode_jwt_from_cookies(request_type): if request_type == 'access': cookie_key = config.access_cookie_name csrf_header_key = config.access_csrf_header_name else: cookie_key = config.refresh_cookie_name csrf_header_key = config.refresh_csrf_header_name encoded_token = request.cookies.get(cookie_key) if not encoded_token: raise NoAuthorizationError('Missing cookie "{}"'.format(cookie_key)) if config.csrf_protect and request.method in config.csrf_request_methods: csrf_value = request.headers.get(csrf_header_key, None) if not csrf_value: raise CSRFError("Missing CSRF token in headers") else: csrf_value = None return encoded_token, csrf_value def _decode_jwt_from_query_string(): query_param = config.query_string_name encoded_token = request.args.get(query_param) if not encoded_token: raise NoAuthorizationError('Missing "{}" query paramater'.format(query_param)) return encoded_token, None def _decode_jwt_from_json(request_type): if request.content_type != 'application/json': raise NoAuthorizationError('Invalid content-type. Must be application/json.') if request_type == 'access': token_key = config.json_key else: token_key = config.refresh_json_key try: encoded_token = request.json.get(token_key, None) if not encoded_token: raise BadRequest() except BadRequest: raise NoAuthorizationError('Missing "{}" key in json data.'.format(token_key)) return encoded_token, None def _decode_jwt_from_request(request_type): # All the places we can get a JWT from in this request get_encoded_token_functions = [] locations = config.token_location # add the functions in the order specified in JWT_TOKEN_LOCATION for location in locations: if location == 'cookies': get_encoded_token_functions.append( lambda: _decode_jwt_from_cookies(request_type)) if location == 'query_string': get_encoded_token_functions.append(_decode_jwt_from_query_string) if location == 'headers': get_encoded_token_functions.append(_decode_jwt_from_headers) if location == 'json': get_encoded_token_functions.append( lambda: _decode_jwt_from_json(request_type)) # Try to find the token from one of these locations. It only needs to exist # in one place to be valid (not every location). errors = [] decoded_token = None for get_encoded_token_function in get_encoded_token_functions: try: encoded_token, csrf_token = get_encoded_token_function() decoded_token = decode_token(encoded_token, csrf_token) break except NoAuthorizationError as e: errors.append(str(e)) # Do some work to make a helpful and human readable error message if no # token was found in any of the expected locations. if not decoded_token: token_locations = config.token_location multiple_jwt_locations = len(token_locations) != 1 if multiple_jwt_locations: err_msg = "Missing JWT in {start_locs} or {end_locs} ({details})".format( start_locs=", ".join(token_locations[:-1]), end_locs=token_locations[-1], details="; ".join(errors) ) raise NoAuthorizationError(err_msg) else: raise NoAuthorizationError(errors[0]) verify_token_type(decoded_token, expected_type=request_type) verify_token_not_blacklisted(decoded_token, request_type) return decoded_token python-flask-jwt-extended-3.21.0/requirements.txt000066400000000000000000000014071352401742600221060ustar00rootroot00000000000000alabaster==0.7.12 asn1crypto==0.24.0 attrs==19.1.0 Babel==2.7.0 certifi==2019.6.16 cffi==1.12.3 chardet==3.0.4 Click==7.0 cryptography==2.7 docutils==0.15.2 filelock==3.0.12 Flask==1.1.1 idna==2.8 imagesize==1.1.0 importlib-metadata==0.19 itsdangerous==1.1.0 Jinja2==2.10.1 MarkupSafe==1.1.1 packaging==19.1 Pallets-Sphinx-Themes==1.2.1 pluggy==0.12.0 py==1.8.0 pycparser==2.19 Pygments==2.4.2 PyJWT==1.7.1 pyparsing==2.4.2 pytz==2019.2 requests==2.22.0 six==1.12.0 snowballstemmer==1.9.0 Sphinx==2.1.2 sphinxcontrib-applehelp==1.0.1 sphinxcontrib-devhelp==1.0.1 sphinxcontrib-htmlhelp==1.0.2 sphinxcontrib-jsmath==1.0.1 sphinxcontrib-qthelp==1.0.2 sphinxcontrib-serializinghtml==1.1.3 toml==0.10.0 tox==3.13.2 urllib3==1.25.3 virtualenv==16.7.2 Werkzeug==0.15.5 zipp==0.5.2 python-flask-jwt-extended-3.21.0/setup.cfg000066400000000000000000000001321352401742600204350ustar00rootroot00000000000000[bdist_wheel] universal=1 [metadata] description-file = README.md license_file = LICENSE python-flask-jwt-extended-3.21.0/setup.py000066400000000000000000000034151352401742600203350ustar00rootroot00000000000000""" Flask-JWT-Extended ------------------ Flask-Login provides jwt endpoint protection for Flask. """ import io import re from setuptools import setup with io.open('flask_jwt_extended/__init__.py', encoding='utf-8') as f: version = re.search(r"__version__ = '(.+)'", f.read()).group(1) with open("README.md", "r") as f: long_description = f.read() setup(name='Flask-JWT-Extended', version=version, url='https://github.com/vimalloc/flask-jwt-extended', license='MIT', author='Landon Gilbert-Bland', author_email='landogbland@gmail.com', description='Extended JWT integration with Flask', long_description=long_description, long_description_content_type="text/markdown", keywords=['flask', 'jwt', 'json web token'], packages=['flask_jwt_extended'], zip_safe=False, platforms='any', install_requires=[ 'Werkzeug>=0.14', # Needed for SameSite cookie functionality 'Flask>=1.0', 'PyJWT>=1.6.4', 'six', ], extras_require={ 'asymmetric_crypto': ["cryptography >= 2.3"] }, classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 'Topic :: Software Development :: Libraries :: Python Modules' ]) python-flask-jwt-extended-3.21.0/tests/000077500000000000000000000000001352401742600177625ustar00rootroot00000000000000python-flask-jwt-extended-3.21.0/tests/__init__.py000066400000000000000000000000001352401742600220610ustar00rootroot00000000000000python-flask-jwt-extended-3.21.0/tests/test_asymmetric_crypto.py000066400000000000000000000047471352401742600251640ustar00rootroot00000000000000import pytest from flask import Flask, jsonify from flask_jwt_extended import JWTManager, jwt_required, create_access_token RSA_PRIVATE = """ -----BEGIN RSA PRIVATE KEY----- MIICXgIBAAKBgQDN+p9a9oMyqRzkae8yLdJcEK0O0WesH6JiMz+KDrpUwAoAM/KP DnxFnROJDSBHyHEmPVn5x8GqV5lQ9+6l97jdEEcPo6wkshycM82fgcxOmvtAy4Uo xq/AeplYqplhcUTGVuo4ZldOLmN8ksGmzhWpsOdT0bkYipHCn5sWZxd21QIDAQAB AoGBAMJ0++KVXXEDZMpjFDWsOq898xNNMHG3/8ZzmWXN161RC1/7qt/RjhLuYtX9 NV9vZRrzyrDcHAKj5pMhLgUzpColKzvdG2vKCldUs2b0c8HEGmjsmpmgoI1Tdf9D G1QK+q9pKHlbj/MLr4vZPX6xEwAFeqRKlzL30JPD+O6mOXs1AkEA8UDzfadH1Y+H bcNN2COvCqzqJMwLNRMXHDmUsjHfR2gtzk6D5dDyEaL+O4FLiQCaNXGWWoDTy/HJ Clh1Z0+KYwJBANqRtJ+RvdgHMq0Yd45MMyy0ODGr1B3PoRbUK8EdXpyUNMi1g3iJ tXMbLywNkTfcEXZTlbbkVYwrEl6P2N1r42cCQQDb9UQLBEFSTRJE2RRYQ/CL4yt3 cTGmqkkfyr/v19ii2jEpMBzBo8eQnPL+fdvIhWwT3gQfb+WqxD9v10bzcmnRAkEA mzTgeHd7wg3KdJRtQYTmyhXn2Y3VAJ5SG+3qbCW466NqoCQVCeFwEh75rmSr/Giv lcDhDZCzFuf3EWNAcmuMfQJARsWfM6q7v2p6vkYLLJ7+VvIwookkr6wymF5Zgb9d E6oTM2EeUPSyyrj5IdsU2JCNBH1m3JnUflz8p8/NYCoOZg== -----END RSA PRIVATE KEY----- """ RSA_PUBLIC = """ -----BEGIN RSA PUBLIC KEY----- MIGJAoGBAM36n1r2gzKpHORp7zIt0lwQrQ7RZ6wfomIzP4oOulTACgAz8o8OfEWd E4kNIEfIcSY9WfnHwapXmVD37qX3uN0QRw+jrCSyHJwzzZ+BzE6a+0DLhSjGr8B6 mViqmWFxRMZW6jhmV04uY3ySwabOFamw51PRuRiKkcKfmxZnF3bVAgMBAAE= -----END RSA PUBLIC KEY----- """ @pytest.fixture(scope='function') def app(): app = Flask(__name__) app.config['JWT_SECRET_KEY'] = 'foobarbaz' app.config['JWT_PUBLIC_KEY'] = RSA_PUBLIC app.config['JWT_PRIVATE_KEY'] = RSA_PRIVATE JWTManager(app) @app.route('/protected', methods=['GET']) @jwt_required def protected(): return jsonify(foo='bar') return app def test_asymmetric_cropto(app): test_client = app.test_client() with app.test_request_context(): hs256_token = create_access_token('username') app.config['JWT_ALGORITHM'] = 'RS256' rs256_token = create_access_token('username') # Insure the symmetric token does not work now access_headers = {'Authorization': 'Bearer {}'.format(hs256_token)} response = test_client.get('/protected', headers=access_headers) assert response.status_code == 422 assert response.get_json() == {'msg': 'The specified alg value is not allowed'} # Insure the asymmetric token does work access_headers = {'Authorization': 'Bearer {}'.format(rs256_token)} response = test_client.get('/protected', headers=access_headers) assert response.status_code == 200 assert response.get_json() == {'foo': 'bar'} python-flask-jwt-extended-3.21.0/tests/test_blacklist.py000066400000000000000000000117321352401742600233470ustar00rootroot00000000000000import pytest from flask import Flask, jsonify from flask_jwt_extended import ( JWTManager, jwt_required, create_access_token, jwt_refresh_token_required, create_refresh_token ) from tests.utils import get_jwt_manager, make_headers @pytest.fixture(scope='function') def app(): app = Flask(__name__) app.config['JWT_SECRET_KEY'] = 'foobarbaz' app.config['JWT_BLACKLIST_ENABLED'] = True JWTManager(app) @app.route('/protected', methods=['GET']) @jwt_required def access_protected(): return jsonify(foo='bar') @app.route('/refresh_protected', methods=['GET']) @jwt_refresh_token_required def refresh_protected(): return jsonify(foo='bar') return app @pytest.mark.parametrize("blacklist_type", [['access'], ['refresh', 'access']]) def test_non_blacklisted_access_token(app, blacklist_type): jwt = get_jwt_manager(app) app.config['JWT_BLACKLIST_TOKEN_CHECKS'] = blacklist_type @jwt.token_in_blacklist_loader def check_blacklisted(decrypted_token): return False with app.test_request_context(): access_token = create_access_token('username') test_client = app.test_client() response = test_client.get('/protected', headers=make_headers(access_token)) assert response.get_json() == {'foo': 'bar'} assert response.status_code == 200 @pytest.mark.parametrize("blacklist_type", [['access'], ['refresh', 'access']]) def test_blacklisted_access_token(app, blacklist_type): jwt = get_jwt_manager(app) app.config['JWT_BLACKLIST_TOKEN_CHECKS'] = blacklist_type @jwt.token_in_blacklist_loader def check_blacklisted(decrypted_token): return True with app.test_request_context(): access_token = create_access_token('username') test_client = app.test_client() response = test_client.get('/protected', headers=make_headers(access_token)) assert response.get_json() == {'msg': 'Token has been revoked'} assert response.status_code == 401 @pytest.mark.parametrize("blacklist_type", [['refresh'], ['refresh', 'access']]) def test_non_blacklisted_refresh_token(app, blacklist_type): jwt = get_jwt_manager(app) app.config['JWT_BLACKLIST_TOKEN_CHECKS'] = blacklist_type @jwt.token_in_blacklist_loader def check_blacklisted(decrypted_token): return False with app.test_request_context(): refresh_token = create_refresh_token('username') test_client = app.test_client() response = test_client.get('/refresh_protected', headers=make_headers(refresh_token)) assert response.get_json() == {'foo': 'bar'} assert response.status_code == 200 @pytest.mark.parametrize("blacklist_type", [['refresh'], ['refresh', 'access']]) def test_blacklisted_refresh_token(app, blacklist_type): jwt = get_jwt_manager(app) app.config['JWT_BLACKLIST_TOKEN_CHECKS'] = blacklist_type @jwt.token_in_blacklist_loader def check_blacklisted(decrypted_token): return True with app.test_request_context(): refresh_token = create_refresh_token('username') test_client = app.test_client() response = test_client.get('/refresh_protected', headers=make_headers(refresh_token)) assert response.get_json() == {'msg': 'Token has been revoked'} assert response.status_code == 401 def test_no_blacklist_callback_method_provided(app): app.config['JWT_BLACKLIST_TOKEN_CHECKS'] = ['access'] with app.test_request_context(): access_token = create_access_token('username') test_client = app.test_client() response = test_client.get('/protected', headers=make_headers(access_token)) assert response.status_code == 500 def test_revoked_token_of_different_type(app): jwt = get_jwt_manager(app) test_client = app.test_client() @jwt.token_in_blacklist_loader def check_blacklisted(decrypted_token): return True with app.test_request_context(): access_token = create_access_token('username') refresh_token = create_refresh_token('username') app.config['JWT_BLACKLIST_TOKEN_CHECKS'] = ['access'] response = test_client.get('/refresh_protected', headers=make_headers(refresh_token)) assert response.get_json() == {'foo': 'bar'} assert response.status_code == 200 app.config['JWT_BLACKLIST_TOKEN_CHECKS'] = ['refresh'] response = test_client.get('/protected', headers=make_headers(access_token)) assert response.get_json() == {'foo': 'bar'} assert response.status_code == 200 def test_custom_blacklisted_message(app): jwt = get_jwt_manager(app) @jwt.token_in_blacklist_loader def check_blacklisted(decrypted_token): return True @jwt.revoked_token_loader def custom_error(): return jsonify(baz='foo'), 404 with app.test_request_context(): access_token = create_access_token('username') test_client = app.test_client() response = test_client.get('/protected', headers=make_headers(access_token)) assert response.get_json() == {'baz': 'foo'} assert response.status_code == 404 python-flask-jwt-extended-3.21.0/tests/test_claims_verification.py000066400000000000000000000067731352401742600254220ustar00rootroot00000000000000import pytest from flask import Flask, jsonify from flask_jwt_extended import ( JWTManager, jwt_required, create_access_token, get_jwt_identity, fresh_jwt_required, jwt_optional ) from tests.utils import get_jwt_manager, make_headers @pytest.fixture(scope='function') def app(): app = Flask(__name__) app.config['JWT_SECRET_KEY'] = 'foobarbaz' jwt = JWTManager(app) @jwt.user_claims_loader def add_user_claims(identity): return {'foo': 'bar'} @app.route('/protected1', methods=['GET']) @jwt_required def protected1(): return jsonify(foo='bar') @app.route('/protected2', methods=['GET']) @fresh_jwt_required def protected2(): return jsonify(foo='bar') @app.route('/protected3', methods=['GET']) @jwt_optional def protected3(): return jsonify(foo='bar') return app @pytest.mark.parametrize("url", ['/protected1', '/protected2', '/protected3']) def test_successful_claims_validation(app, url): jwt = get_jwt_manager(app) @jwt.claims_verification_loader def user_load_callback(user_claims): return user_claims == {'foo': 'bar'} test_client = app.test_client() with app.test_request_context(): access_token = create_access_token('username', fresh=True) response = test_client.get(url, headers=make_headers(access_token)) assert response.get_json() == {'foo': 'bar'} assert response.status_code == 200 @pytest.mark.parametrize("url", ['/protected1', '/protected2', '/protected3']) def test_unsuccessful_claims_validation(app, url): jwt = get_jwt_manager(app) @jwt.claims_verification_loader def user_load_callback(user_claims): return False test_client = app.test_client() with app.test_request_context(): access_token = create_access_token('username', fresh=True) response = test_client.get(url, headers=make_headers(access_token)) assert response.get_json() == {'msg': 'User claims verification failed'} assert response.status_code == 400 @pytest.mark.parametrize("url", ['/protected1', '/protected2', '/protected3']) def test_claims_validation_custom_error(app, url): jwt = get_jwt_manager(app) @jwt.claims_verification_loader def user_load_callback(user_claims): return False @jwt.claims_verification_failed_loader def custom_error(): # Make sure that we can get the jwt identity in here if we need it. user = get_jwt_identity() return jsonify(msg='claims failed for {}'.format(user)), 404 test_client = app.test_client() with app.test_request_context(): access_token = create_access_token('username', fresh=True) response = test_client.get(url, headers=make_headers(access_token)) assert response.get_json() == {'msg': 'claims failed for username'} assert response.status_code == 404 @pytest.mark.parametrize("url", ['/protected1', '/protected2', '/protected3']) def test_get_jwt_identity_in_verification_method(app, url): jwt = get_jwt_manager(app) @jwt.claims_verification_loader def user_load_callback(user_claims): # Make sure that we can get the jwt identity in here if we need it. user = get_jwt_identity() return user == 'username' test_client = app.test_client() with app.test_request_context(): access_token = create_access_token('username', fresh=True) response = test_client.get(url, headers=make_headers(access_token)) assert response.get_json() == {'foo': 'bar'} assert response.status_code == 200 python-flask-jwt-extended-3.21.0/tests/test_config.py000066400000000000000000000355371352401742600226550ustar00rootroot00000000000000import warnings import pytest from datetime import timedelta from dateutil.relativedelta import relativedelta from flask import Flask from flask.json import JSONEncoder from flask_jwt_extended import JWTManager from flask_jwt_extended.config import config @pytest.fixture(scope='function') def app(): app = Flask(__name__) JWTManager(app) return app def test_default_configs(app): with app.test_request_context(): assert config.token_location == ('headers',) assert config.jwt_in_query_string is False assert config.jwt_in_cookies is False assert config.jwt_in_json is False assert config.jwt_in_headers is True assert config.header_name == 'Authorization' assert config.header_type == 'Bearer' assert config.query_string_name == 'jwt' assert config.access_cookie_name == 'access_token_cookie' assert config.refresh_cookie_name == 'refresh_token_cookie' assert config.access_cookie_path == '/' assert config.refresh_cookie_path == '/' assert config.cookie_secure is False assert config.cookie_domain is None assert config.session_cookie is True assert config.cookie_samesite is None assert config.json_key == 'access_token' assert config.refresh_json_key == 'refresh_token' assert config.csrf_protect is False assert config.csrf_request_methods == ['POST', 'PUT', 'PATCH', 'DELETE'] assert config.csrf_in_cookies is True assert config.access_csrf_cookie_name == 'csrf_access_token' assert config.refresh_csrf_cookie_name == 'csrf_refresh_token' assert config.access_csrf_cookie_path == '/' assert config.refresh_csrf_cookie_path == '/' assert config.access_csrf_header_name == 'X-CSRF-TOKEN' assert config.refresh_csrf_header_name == 'X-CSRF-TOKEN' assert config.access_expires == timedelta(minutes=15) assert config.refresh_expires == timedelta(days=30) assert config.algorithm == 'HS256' assert config.decode_algorithms == ['HS256'] assert config.is_asymmetric is False assert config.blacklist_enabled is False assert config.blacklist_checks == ('access', 'refresh') assert config.blacklist_access_tokens is True assert config.blacklist_refresh_tokens is True assert config.cookie_max_age is None assert config.identity_claim_key == 'identity' assert config.user_claims_key == 'user_claims' assert config.user_claims_in_refresh_token is False assert config.json_encoder is app.json_encoder assert config.error_msg_key == 'msg' @pytest.mark.parametrize("delta_func", [timedelta, relativedelta]) def test_override_configs(app, delta_func): app.config['JWT_TOKEN_LOCATION'] = ['cookies', 'query_string', 'json'] app.config['JWT_HEADER_NAME'] = 'TestHeader' app.config['JWT_HEADER_TYPE'] = 'TestType' app.config['JWT_JSON_KEY'] = 'TestKey' app.config['JWT_REFRESH_JSON_KEY'] = 'TestRefreshKey' app.config['JWT_QUERY_STRING_NAME'] = 'banana' app.config['JWT_ACCESS_COOKIE_NAME'] = 'new_access_cookie' app.config['JWT_REFRESH_COOKIE_NAME'] = 'new_refresh_cookie' app.config['JWT_ACCESS_COOKIE_PATH'] = '/access/path' app.config['JWT_REFRESH_COOKIE_PATH'] = '/refresh/path' app.config['JWT_COOKIE_SECURE'] = True app.config['JWT_COOKIE_DOMAIN'] = ".example.com" app.config['JWT_SESSION_COOKIE'] = False app.config['JWT_COOKIE_SAMESITE'] = "Strict" app.config['JWT_COOKIE_CSRF_PROTECT'] = True app.config['JWT_CSRF_METHODS'] = ['GET'] app.config['JWT_CSRF_IN_COOKIES'] = False app.config['JWT_ACCESS_CSRF_COOKIE_NAME'] = 'access_csrf_cookie' app.config['JWT_REFRESH_CSRF_COOKIE_NAME'] = 'refresh_csrf_cookie' app.config['JWT_ACCESS_CSRF_COOKIE_PATH'] = '/csrf/access/path' app.config['JWT_REFRESH_CSRF_COOKIE_PATH'] = '/csrf/refresh/path' app.config['JWT_ACCESS_CSRF_HEADER_NAME'] = 'X-ACCESS-CSRF' app.config['JWT_REFRESH_CSRF_HEADER_NAME'] = 'X-REFRESH-CSRF' app.config['JWT_ACCESS_TOKEN_EXPIRES'] = delta_func(minutes=5) app.config['JWT_REFRESH_TOKEN_EXPIRES'] = delta_func(days=5) app.config['JWT_ALGORITHM'] = 'HS512' app.config['JWT_DECODE_ALGORITHMS'] = ['HS512', 'HS256'] app.config['JWT_BLACKLIST_ENABLED'] = True app.config['JWT_BLACKLIST_TOKEN_CHECKS'] = ('refresh',) app.config['JWT_IDENTITY_CLAIM'] = 'foo' app.config['JWT_USER_CLAIMS'] = 'bar' app.config['JWT_CLAIMS_IN_REFRESH_TOKEN'] = True app.config['JWT_ERROR_MESSAGE_KEY'] = 'message' class CustomJSONEncoder(JSONEncoder): pass app.json_encoder = CustomJSONEncoder with app.test_request_context(): assert config.token_location == ['cookies', 'query_string', 'json'] assert config.jwt_in_query_string is True assert config.jwt_in_cookies is True assert config.jwt_in_headers is False assert config.jwt_in_json is True assert config.header_name == 'TestHeader' assert config.header_type == 'TestType' assert config.json_key == 'TestKey' assert config.refresh_json_key == 'TestRefreshKey' assert config.query_string_name == 'banana' assert config.access_cookie_name == 'new_access_cookie' assert config.refresh_cookie_name == 'new_refresh_cookie' assert config.access_cookie_path == '/access/path' assert config.refresh_cookie_path == '/refresh/path' assert config.cookie_secure is True assert config.cookie_domain == ".example.com" assert config.session_cookie is False assert config.cookie_samesite == "Strict" assert config.csrf_protect is True assert config.csrf_request_methods == ['GET'] assert config.csrf_in_cookies is False assert config.access_csrf_cookie_name == 'access_csrf_cookie' assert config.refresh_csrf_cookie_name == 'refresh_csrf_cookie' assert config.access_csrf_cookie_path == '/csrf/access/path' assert config.refresh_csrf_cookie_path == '/csrf/refresh/path' assert config.access_csrf_header_name == 'X-ACCESS-CSRF' assert config.refresh_csrf_header_name == 'X-REFRESH-CSRF' assert config.access_expires == delta_func(minutes=5) assert config.refresh_expires == delta_func(days=5) assert config.algorithm == 'HS512' assert config.decode_algorithms == ['HS512', 'HS256'] assert config.blacklist_enabled is True assert config.blacklist_checks == ('refresh',) assert config.blacklist_access_tokens is False assert config.blacklist_refresh_tokens is True assert config.cookie_max_age == 31540000 assert config.identity_claim_key == 'foo' assert config.user_claims_key == 'bar' assert config.user_claims_in_refresh_token is True assert config.json_encoder is CustomJSONEncoder assert config.error_msg_key == 'message' def test_tokens_never_expire(app): app.config['JWT_ACCESS_TOKEN_EXPIRES'] = False app.config['JWT_REFRESH_TOKEN_EXPIRES'] = False with app.test_request_context(): assert config.access_expires is False assert config.refresh_expires is False def test_tokens_with_int_values(app): app.config['JWT_ACCESS_TOKEN_EXPIRES'] = 300 app.config['JWT_REFRESH_TOKEN_EXPIRES'] = 432000 with app.test_request_context(): assert config.access_expires == timedelta(minutes=5) assert config.refresh_expires == timedelta(days=5) # noinspection PyStatementEffect def test_symmetric_secret_key(app): with app.test_request_context(): assert config.is_asymmetric is False with pytest.raises(RuntimeError): config.encode_key with pytest.raises(RuntimeError): config.decode_key app.secret_key = 'foobar' with app.test_request_context(): assert config.encode_key == 'foobar' assert config.decode_key == 'foobar' app.config['JWT_SECRET_KEY'] = 'foobarbaz' with app.test_request_context(): assert config.encode_key == 'foobarbaz' assert config.decode_key == 'foobarbaz' # noinspection PyStatementEffect def test_default_with_asymmetric_secret_key(app): with app.test_request_context(): app.config['JWT_ALGORITHM'] = 'RS256' assert config.is_asymmetric is True # If no key is entered, should raise an error with pytest.raises(RuntimeError): config.encode_key with pytest.raises(RuntimeError): config.decode_key # Make sure the secret key isn't being used for asymmetric stuff app.secret_key = 'foobar' with pytest.raises(RuntimeError): config.encode_key with pytest.raises(RuntimeError): config.decode_key # Make sure the secret key isn't being used for asymmetric stuff app.config['JWT_SECRET_KEY'] = 'foobarbaz' with pytest.raises(RuntimeError): config.encode_key with pytest.raises(RuntimeError): config.decode_key app.config['JWT_PUBLIC_KEY'] = 'foo2' app.config['JWT_PRIVATE_KEY'] = 'bar2' app.config['JWT_ALGORITHM'] = 'RS256' with app.test_request_context(): assert config.decode_key == 'foo2' assert config.encode_key == 'bar2' # noinspection PyStatementEffect def test_invalid_config_options(app): with app.test_request_context(): app.config['JWT_TOKEN_LOCATION'] = [] with pytest.raises(RuntimeError): config.token_location app.config['JWT_TOKEN_LOCATION'] = 'banana' with pytest.raises(RuntimeError): config.token_location app.config['JWT_TOKEN_LOCATION'] = 1 with pytest.raises(RuntimeError): config.token_location app.config['JWT_TOKEN_LOCATION'] = {'location': 'headers'} with pytest.raises(RuntimeError): config.token_location app.config['JWT_TOKEN_LOCATION'] = range(99) with pytest.raises(RuntimeError): config.token_location app.config['JWT_TOKEN_LOCATION'] = ['headers', 'cookies', 'banana'] with pytest.raises(RuntimeError): config.token_location app.config['JWT_HEADER_NAME'] = '' with app.test_request_context(): with pytest.raises(RuntimeError): config.header_name app.config['JWT_ACCESS_TOKEN_EXPIRES'] = 'banana' with pytest.raises(RuntimeError): config.access_expires app.config['JWT_REFRESH_TOKEN_EXPIRES'] = 'banana' with pytest.raises(RuntimeError): config.refresh_expires app.config['JWT_ACCESS_TOKEN_EXPIRES'] = True with pytest.raises(RuntimeError): config.access_expires app.config['JWT_REFRESH_TOKEN_EXPIRES'] = True with pytest.raises(RuntimeError): config.refresh_expires app.config['JWT_BLACKLIST_TOKEN_CHECKS'] = 'banana' with pytest.raises(RuntimeError): config.blacklist_checks app.config['JWT_BLACKLIST_TOKEN_CHECKS'] = 1 with pytest.raises(RuntimeError): config.blacklist_checks app.config['JWT_BLACKLIST_TOKEN_CHECKS'] = {'token_type': 'access'} with pytest.raises(RuntimeError): config.blacklist_checks app.config['JWT_BLACKLIST_TOKEN_CHECKS'] = range(99) with pytest.raises(RuntimeError): config.blacklist_checks app.config['JWT_BLACKLIST_TOKEN_CHECKS'] = ['access', 'banana'] with pytest.raises(RuntimeError): config.blacklist_checks def test_jwt_token_locations_config(app): with app.test_request_context(): allowed_locations = ('headers', 'cookies', 'query_string', 'json') allowed_data_structures = (tuple, list, frozenset, set) for location in allowed_locations: app.config['JWT_TOKEN_LOCATION'] = location assert config.token_location == (location,) for locations in ( data_structure((location,)) for data_structure in allowed_data_structures for location in allowed_locations ): app.config['JWT_TOKEN_LOCATION'] = locations assert config.token_location == locations for locations in ( data_structure(allowed_locations[:i]) for data_structure in allowed_data_structures for i in range(1, len(allowed_locations)) ): app.config['JWT_TOKEN_LOCATION'] = locations assert config.token_location == locations def test_jwt_blacklist_token_checks_config(app): with app.test_request_context(): allowed_token_types = ('access', 'refresh') allowed_data_structures = (tuple, list, frozenset, set) for token_type in allowed_token_types: app.config['JWT_BLACKLIST_TOKEN_CHECKS'] = token_type assert config.blacklist_checks == (token_type,) for token_types in ( data_structure((token_type,)) for data_structure in allowed_data_structures for token_type in allowed_token_types ): app.config['JWT_BLACKLIST_TOKEN_CHECKS'] = token_types assert config.blacklist_checks == token_types for token_types in ( data_structure(allowed_token_types[:i]) for data_structure in allowed_data_structures for i in range(1, len(allowed_token_types)) ): app.config['JWT_BLACKLIST_TOKEN_CHECKS'] = token_types assert config.blacklist_checks == token_types def test_csrf_protect_config(app): with app.test_request_context(): app.config['JWT_TOKEN_LOCATION'] = ['headers'] app.config['JWT_COOKIE_CSRF_PROTECT'] = True assert config.csrf_protect is False app.config['JWT_TOKEN_LOCATION'] = ['cookies'] app.config['JWT_COOKIE_CSRF_PROTECT'] = True assert config.csrf_protect is True app.config['JWT_TOKEN_LOCATION'] = ['cookies'] app.config['JWT_COOKIE_CSRF_PROTECT'] = False assert config.csrf_protect is False def test_depreciated_options(app): app.config['JWT_CSRF_HEADER_NAME'] = 'Auth' # Cause all warnings to always be triggered. warnings.simplefilter("always") # Verify our warnings are thrown with app.test_request_context(): with warnings.catch_warnings(record=True) as w: assert config.access_csrf_header_name == 'Auth' assert config.refresh_csrf_header_name == 'Auth' assert len(w) == 2 assert w[0].category == DeprecationWarning assert w[1].category == DeprecationWarning def test_missing_algorithm_in_decode_algorithms(app): app.config['JWT_ALGORITHM'] = 'RS256' app.config['JWT_DECODE_ALGORITHMS'] = ['HS512'] with app.test_request_context(): assert config.decode_algorithms == ['HS512', 'RS256'] python-flask-jwt-extended-3.21.0/tests/test_cookies.py000066400000000000000000000400511352401742600230270ustar00rootroot00000000000000import pytest from flask import Flask, jsonify from flask_jwt_extended import ( jwt_required, JWTManager, jwt_refresh_token_required, create_access_token, create_refresh_token, set_access_cookies, set_refresh_cookies, unset_jwt_cookies, unset_access_cookies, unset_refresh_cookies, jwt_optional ) def _get_cookie_from_response(response, cookie_name): cookie_headers = response.headers.getlist('Set-Cookie') for header in cookie_headers: attributes = header.split(';') if cookie_name in attributes[0]: cookie = {} for attr in attributes: split = attr.split('=') cookie[split[0].strip().lower()] = split[1] if len(split) > 1 else True return cookie return None @pytest.fixture(scope='function') def app(): app = Flask(__name__) app.config['JWT_SECRET_KEY'] = 'foobarbaz' app.config['JWT_TOKEN_LOCATION'] = ['cookies'] JWTManager(app) @app.route('/access_token', methods=['GET']) def access_token(): resp = jsonify(login=True) access_token = create_access_token('username') set_access_cookies(resp, access_token) return resp @app.route('/refresh_token', methods=['GET']) def refresh_token(): resp = jsonify(login=True) refresh_token = create_refresh_token('username') set_refresh_cookies(resp, refresh_token) return resp @app.route('/delete_tokens', methods=['GET']) def delete_tokens(): resp = jsonify(logout=True) unset_jwt_cookies(resp) return resp @app.route('/delete_access_tokens', methods=['GET']) def delete_access_tokens(): resp = jsonify(access_revoked=True) unset_access_cookies(resp) return resp @app.route('/delete_refresh_tokens', methods=['GET']) def delete_refresh_tokens(): resp = jsonify(refresh_revoked=True) unset_refresh_cookies(resp) return resp @app.route('/protected', methods=['GET']) @jwt_required def protected(): return jsonify(foo='bar') @app.route('/post_protected', methods=['POST']) @jwt_required def post_protected(): return jsonify(foo='bar') @app.route('/refresh_protected', methods=['GET']) @jwt_refresh_token_required def refresh_protected(): return jsonify(foo='bar') @app.route('/post_refresh_protected', methods=['POST']) @jwt_refresh_token_required def post_refresh_protected(): return jsonify(foo='bar') @app.route('/optional_post_protected', methods=['POST']) @jwt_optional def optional_post_protected(): return jsonify(foo='bar') return app @pytest.mark.parametrize("options", [ ('/refresh_token', 'refresh_token_cookie', '/refresh_protected', '/delete_refresh_tokens'), # nopep8 ('/access_token', 'access_token_cookie', '/protected', '/delete_access_tokens') ]) def test_jwt_refresh_required_with_cookies(app, options): test_client = app.test_client() auth_url, cookie_name, protected_url, delete_url = options # Test without cookies response = test_client.get(protected_url) assert response.status_code == 401 assert response.get_json() == {'msg': 'Missing cookie "{}"'.format(cookie_name)} # Test after receiving cookies test_client.get(auth_url) response = test_client.get(protected_url) assert response.status_code == 200 assert response.get_json() == {'foo': 'bar'} # Test after issuing a 'logout' to delete the cookies test_client.get(delete_url) response = test_client.get(protected_url) assert response.status_code == 401 assert response.get_json() == {'msg': 'Missing cookie "{}"'.format(cookie_name)} # log back in once more to test that clearing all tokens works test_client.get(auth_url) response = test_client.get(protected_url) assert response.status_code == 200 test_client.get("/delete_tokens") response = test_client.get(protected_url) assert response.status_code == 401 assert response.get_json() == {'msg': 'Missing cookie "{}"'.format(cookie_name)} @pytest.mark.parametrize("options", [ ('/refresh_token', 'csrf_refresh_token', '/post_refresh_protected'), ('/access_token', 'csrf_access_token', '/post_protected') ]) def test_default_access_csrf_protection(app, options): test_client = app.test_client() auth_url, csrf_cookie_name, post_url = options # Get the jwt cookies and csrf double submit tokens response = test_client.get(auth_url) csrf_token = _get_cookie_from_response(response, csrf_cookie_name)[csrf_cookie_name] # Test you cannot post without the additional csrf protection response = test_client.post(post_url) assert response.status_code == 401 assert response.get_json() == {'msg': 'Missing CSRF token in headers'} # Test that you can post with the csrf double submit value csrf_headers = {'X-CSRF-TOKEN': csrf_token} response = test_client.post(post_url, headers=csrf_headers) assert response.status_code == 200 assert response.get_json() == {'foo': 'bar'} @pytest.mark.parametrize("options", [ ('/refresh_token', '/post_refresh_protected'), ('/access_token', '/post_protected') ]) def test_non_matching_csrf_token(app, options): test_client = app.test_client() auth_url, post_url = options # Get the jwt cookies and csrf double submit tokens test_client.get(auth_url) csrf_headers = {'X-CSRF-TOKEN': 'totally_wrong_token'} response = test_client.post(post_url, headers=csrf_headers) assert response.status_code == 401 assert response.get_json() == {'msg': 'CSRF double submit tokens do not match'} @pytest.mark.parametrize("options", [ ('/refresh_token', '/post_refresh_protected'), ('/access_token', '/post_protected') ]) def test_csrf_disabled(app, options): app.config['JWT_COOKIE_CSRF_PROTECT'] = False test_client = app.test_client() auth_url, post_url = options # Get the jwt cookies and csrf double submit tokens test_client.get(auth_url) response = test_client.post(post_url) assert response.status_code == 200 assert response.get_json() == {'foo': 'bar'} @pytest.mark.parametrize("options", [ ('/refresh_token', 'csrf_refresh_token', '/post_refresh_protected'), ('/access_token', 'csrf_access_token', '/post_protected') ]) def test_csrf_with_custom_header_names(app, options): app.config['JWT_ACCESS_CSRF_HEADER_NAME'] = 'FOO' app.config['JWT_REFRESH_CSRF_HEADER_NAME'] = 'FOO' test_client = app.test_client() auth_url, csrf_cookie_name, post_url = options # Get the jwt cookies and csrf double submit tokens response = test_client.get(auth_url) csrf_token = _get_cookie_from_response(response, csrf_cookie_name)[csrf_cookie_name] # Test that you can post with the csrf double submit value csrf_headers = {'FOO': csrf_token} response = test_client.post(post_url, headers=csrf_headers) assert response.status_code == 200 assert response.get_json() == {'foo': 'bar'} @pytest.mark.parametrize("options", [ ('/refresh_token', 'csrf_refresh_token', '/refresh_protected', '/post_refresh_protected'), # nopep8 ('/access_token', 'csrf_access_token', '/protected', '/post_protected') ]) def test_custom_csrf_methods(app, options): app.config['JWT_CSRF_METHODS'] = ['GET'] test_client = app.test_client() auth_url, csrf_cookie_name, get_url, post_url = options # Get the jwt cookies and csrf double submit tokens response = test_client.get(auth_url) csrf_token = _get_cookie_from_response(response, csrf_cookie_name)[csrf_cookie_name] # Insure we can now do posts without csrf response = test_client.post(post_url) assert response.status_code == 200 assert response.get_json() == {'foo': 'bar'} # Insure GET requests now fail without csrf response = test_client.get(get_url) assert response.status_code == 401 assert response.get_json() == {'msg': 'Missing CSRF token in headers'} # Insure GET requests now succeed with csrf csrf_headers = {'X-CSRF-TOKEN': csrf_token} response = test_client.get(get_url, headers=csrf_headers) assert response.status_code == 200 assert response.get_json() == {'foo': 'bar'} def test_setting_cookies_wihout_cookies_enabled(app): app.config['JWT_TOKEN_LOCATION'] = ['headers'] test_client = app.test_client() response = test_client.get('/access_token') assert response.status_code == 500 response = test_client.get('/refresh_token') assert response.status_code == 500 response = test_client.get('/delete_tokens') assert response.status_code == 500 response = test_client.get('/delete_access_tokens') assert response.status_code == 500 response = test_client.get('/delete_refresh_tokens') assert response.status_code == 500 def test_default_cookie_options(app): test_client = app.test_client() # Test the default access cookies response = test_client.get('/access_token') cookies = response.headers.getlist('Set-Cookie') assert len(cookies) == 2 # JWT and CSRF value access_cookie = _get_cookie_from_response(response, 'access_token_cookie') assert access_cookie is not None assert access_cookie['path'] == '/' assert access_cookie['httponly'] is True assert 'samesite' not in access_cookie access_csrf_cookie = _get_cookie_from_response(response, 'csrf_access_token') assert access_csrf_cookie is not None assert access_csrf_cookie['path'] == '/' assert 'httponly' not in access_csrf_cookie assert 'samesite' not in access_csrf_cookie # Test the default refresh cookies response = test_client.get('/refresh_token') cookies = response.headers.getlist('Set-Cookie') assert len(cookies) == 2 # JWT and CSRF value refresh_cookie = _get_cookie_from_response(response, 'refresh_token_cookie') assert refresh_cookie is not None assert refresh_cookie['path'] == '/' assert refresh_cookie['httponly'] is True assert 'samesite' not in refresh_cookie refresh_csrf_cookie = _get_cookie_from_response(response, 'csrf_refresh_token') assert refresh_csrf_cookie is not None assert refresh_csrf_cookie['path'] == '/' assert 'httponly' not in refresh_csrf_cookie assert 'samesite' not in refresh_csrf_cookie def test_custom_cookie_options(app): test_client = app.test_client() app.config['JWT_COOKIE_SECURE'] = True app.config['JWT_COOKIE_DOMAIN'] = 'test.com' app.config['JWT_SESSION_COOKIE'] = False app.config['JWT_COOKIE_SAMESITE'] = 'Strict' # Test access cookies with changed options response = test_client.get('/access_token') cookies = response.headers.getlist('Set-Cookie') assert len(cookies) == 2 # JWT and CSRF value access_cookie = _get_cookie_from_response(response, 'access_token_cookie') assert access_cookie is not None assert access_cookie['domain'] == 'test.com' assert access_cookie['path'] == '/' assert access_cookie['expires'] != '' assert access_cookie['httponly'] is True assert access_cookie['secure'] is True assert access_cookie['samesite'] == 'Strict' access_csrf_cookie = _get_cookie_from_response(response, 'csrf_access_token') assert access_csrf_cookie is not None assert access_csrf_cookie['path'] == '/' assert access_csrf_cookie['secure'] is True assert access_csrf_cookie['domain'] == 'test.com' assert access_csrf_cookie['expires'] != '' assert access_csrf_cookie['samesite'] == 'Strict' # Test refresh cookies with changed options response = test_client.get('/refresh_token') cookies = response.headers.getlist('Set-Cookie') assert len(cookies) == 2 # JWT and CSRF value refresh_cookie = _get_cookie_from_response(response, 'refresh_token_cookie') assert refresh_cookie is not None assert refresh_cookie['domain'] == 'test.com' assert refresh_cookie['path'] == '/' assert refresh_cookie['httponly'] is True assert refresh_cookie['secure'] is True assert refresh_cookie['expires'] != '' assert refresh_cookie['samesite'] == 'Strict' refresh_csrf_cookie = _get_cookie_from_response(response, 'csrf_refresh_token') assert refresh_csrf_cookie is not None assert refresh_csrf_cookie['path'] == '/' assert refresh_csrf_cookie['secure'] is True assert refresh_csrf_cookie['domain'] == 'test.com' assert refresh_csrf_cookie['expires'] != '' assert refresh_csrf_cookie['samesite'] == 'Strict' def test_custom_cookie_names_and_paths(app): test_client = app.test_client() app.config['JWT_ACCESS_CSRF_COOKIE_NAME'] = 'access_foo_csrf' app.config['JWT_REFRESH_CSRF_COOKIE_NAME'] = 'refresh_foo_csrf' app.config['JWT_ACCESS_CSRF_COOKIE_PATH'] = '/protected' app.config['JWT_REFRESH_CSRF_COOKIE_PATH'] = '/refresh_protected' app.config['JWT_ACCESS_COOKIE_NAME'] = 'access_foo' app.config['JWT_REFRESH_COOKIE_NAME'] = 'refresh_foo' app.config['JWT_ACCESS_COOKIE_PATH'] = '/protected' app.config['JWT_REFRESH_COOKIE_PATH'] = '/refresh_protected' # Test the default access cookies response = test_client.get('/access_token') cookies = response.headers.getlist('Set-Cookie') assert len(cookies) == 2 # JWT and CSRF value access_cookie = _get_cookie_from_response(response, 'access_foo') access_csrf_cookie = _get_cookie_from_response(response, 'access_foo_csrf') assert access_cookie is not None assert access_csrf_cookie is not None assert access_cookie['path'] == '/protected' assert access_csrf_cookie['path'] == '/protected' # Test the default refresh cookies response = test_client.get('/refresh_token') cookies = response.headers.getlist('Set-Cookie') assert len(cookies) == 2 # JWT and CSRF value refresh_cookie = _get_cookie_from_response(response, 'refresh_foo') refresh_csrf_cookie = _get_cookie_from_response(response, 'refresh_foo_csrf') assert refresh_cookie is not None assert refresh_csrf_cookie is not None assert refresh_cookie['path'] == '/refresh_protected' assert refresh_csrf_cookie['path'] == '/refresh_protected' def test_csrf_token_not_in_cookie(app): test_client = app.test_client() app.config['JWT_CSRF_IN_COOKIES'] = False # Test the default access cookies response = test_client.get('/access_token') cookies = response.headers.getlist('Set-Cookie') assert len(cookies) == 1 access_cookie = _get_cookie_from_response(response, 'access_token_cookie') assert access_cookie is not None # Test the default refresh cookies response = test_client.get('/refresh_token') cookies = response.headers.getlist('Set-Cookie') assert len(cookies) == 1 refresh_cookie = _get_cookie_from_response(response, 'refresh_token_cookie') assert refresh_cookie is not None def test_cookies_without_csrf(app): test_client = app.test_client() app.config['JWT_COOKIE_CSRF_PROTECT'] = False # Test the default access cookies response = test_client.get('/access_token') cookies = response.headers.getlist('Set-Cookie') assert len(cookies) == 1 access_cookie = _get_cookie_from_response(response, 'access_token_cookie') assert access_cookie is not None # Test the default refresh cookies response = test_client.get('/refresh_token') cookies = response.headers.getlist('Set-Cookie') assert len(cookies) == 1 refresh_cookie = _get_cookie_from_response(response, 'refresh_token_cookie') assert refresh_cookie is not None def test_jwt_optional_with_csrf_enabled(app): test_client = app.test_client() # User without a token should be able to reach the endpoint without # getting a CSRF error response = test_client.post('/optional_post_protected') assert response.status_code == 200 assert response.get_json() == {'foo': 'bar'} # User with a token should still get a CSRF error if csrf not present response = test_client.get('/access_token') csrf_cookie = _get_cookie_from_response(response, 'csrf_access_token') csrf_token = csrf_cookie['csrf_access_token'] response = test_client.post('/optional_post_protected') assert response.status_code == 401 assert response.get_json() == {'msg': 'Missing CSRF token in headers'} python-flask-jwt-extended-3.21.0/tests/test_decode_tokens.py000066400000000000000000000233541352401742600242100ustar00rootroot00000000000000import jwt import pytest from datetime import datetime, timedelta from dateutil.relativedelta import relativedelta import warnings from flask import Flask from jwt import ( ExpiredSignatureError, InvalidSignatureError, InvalidAudienceError, ImmatureSignatureError, InvalidIssuerError, DecodeError ) from flask_jwt_extended import ( JWTManager, create_access_token, decode_token, create_refresh_token, get_jti ) from flask_jwt_extended.config import config from flask_jwt_extended.exceptions import JWTDecodeError from tests.utils import get_jwt_manager, encode_token @pytest.fixture(scope='function') def app(): app = Flask(__name__) app.config['JWT_SECRET_KEY'] = 'change_me' app.config['JWT_TOKEN_LOCATION'] = ['cookies', 'headers'] app.config['JWT_COOKIE_CSRF_PROTECT'] = True JWTManager(app) return app @pytest.fixture(scope='function') def default_access_token(app): with app.test_request_context(): return { 'jti': '1234', config.identity_claim_key: 'username', 'type': 'access', 'fresh': True, 'csrf': 'abcd' } @pytest.fixture(scope='function') def patch_datetime_now(monkeypatch): date_in_future = datetime.utcnow() + timedelta(seconds=30) class mydatetime(datetime): @classmethod def utcnow(cls): return date_in_future monkeypatch.setattr(__name__ + ".datetime", mydatetime) monkeypatch.setattr("datetime.datetime", mydatetime) @pytest.mark.parametrize("user_loader_return", [{}, None]) def test_no_user_claims(app, user_loader_return): jwtM = get_jwt_manager(app) @jwtM.user_claims_loader def empty_user_loader_return(identity): return user_loader_return # Identity should not be in the actual token, but should be in the data # returned via the decode_token call with app.test_request_context(): token = create_access_token('username') pure_decoded = jwt.decode(token, config.decode_key, algorithms=[config.algorithm]) assert config.user_claims_key not in pure_decoded extension_decoded = decode_token(token) assert config.user_claims_key in extension_decoded @pytest.mark.parametrize("missing_claims", ['identity', 'csrf']) def test_missing_claims(app, default_access_token, missing_claims): del default_access_token[missing_claims] missing_jwt_token = encode_token(app, default_access_token) with pytest.raises(JWTDecodeError): with app.test_request_context(): decode_token(missing_jwt_token, csrf_value='abcd') def test_default_decode_token_values(app, default_access_token): del default_access_token['type'] del default_access_token['jti'] del default_access_token['fresh'] token = encode_token(app, default_access_token) with app.test_request_context(): decoded = decode_token(token) assert decoded['type'] == 'access' assert decoded['jti'] is None assert decoded['fresh'] is False def test_bad_token_type(app, default_access_token): default_access_token['type'] = 'banana' bad_type_token = encode_token(app, default_access_token) with pytest.raises(JWTDecodeError): with app.test_request_context(): decode_token(bad_type_token) @pytest.mark.parametrize("delta_func", [timedelta, relativedelta]) def test_expired_token(app, delta_func): with app.test_request_context(): delta = delta_func(minutes=-5) access_token = create_access_token('username', expires_delta=delta) refresh_token = create_refresh_token('username', expires_delta=delta) with pytest.raises(ExpiredSignatureError): decode_token(access_token) with pytest.raises(ExpiredSignatureError): decode_token(refresh_token) @pytest.mark.parametrize("delta_func", [timedelta, relativedelta]) def test_allow_expired_token(app, delta_func): with app.test_request_context(): delta = delta_func(minutes=-5) access_token = create_access_token('username', expires_delta=delta) refresh_token = create_refresh_token('username', expires_delta=delta) for token in (access_token, refresh_token): decoded = decode_token(token, allow_expired=True) assert decoded['identity'] == 'username' assert 'exp' in decoded def test_never_expire_token(app): with app.test_request_context(): access_token = create_access_token('username', expires_delta=False) refresh_token = create_refresh_token('username', expires_delta=False) for token in (access_token, refresh_token): decoded = decode_token(token) assert 'exp' not in decoded def test_nbf_token_in_future(app, patch_datetime_now): with pytest.raises(ImmatureSignatureError): with app.test_request_context(): access_token = create_access_token('username') decode_token(access_token) with app.test_request_context(): app.config['JWT_DECODE_LEEWAY'] = 30 access_token = create_access_token('username') decode_token(access_token) def test_alternate_identity_claim(app, default_access_token): app.config['JWT_IDENTITY_CLAIM'] = 'sub' # Insure decoding fails if the claim isn't there token = encode_token(app, default_access_token) with pytest.raises(JWTDecodeError): with app.test_request_context(): decode_token(token) # Insure the claim exists in the decoded jwt del default_access_token['identity'] default_access_token['sub'] = 'username' token = encode_token(app, default_access_token) with app.test_request_context(): decoded = decode_token(token) assert 'sub' in decoded assert 'identity' not in decoded def test_get_jti(app, default_access_token): token = encode_token(app, default_access_token) with app.test_request_context(): assert default_access_token['jti'] == get_jti(token) def test_encode_decode_callback_values(app, default_access_token): jwtM = get_jwt_manager(app) app.config['JWT_SECRET_KEY'] = 'foobarbaz' with app.test_request_context(): assert jwtM._decode_key_callback({}, {}) == 'foobarbaz' assert jwtM._encode_key_callback({}) == 'foobarbaz' @jwtM.encode_key_loader def get_encode_key_1(identity): return 'different secret' assert jwtM._encode_key_callback('') == 'different secret' @jwtM.decode_key_loader def get_decode_key_1(claims, headers): return 'different secret' assert jwtM._decode_key_callback({}, {}) == 'different secret' def test_legacy_decode_key_callback(app, default_access_token): jwtM = get_jwt_manager(app) app.config['JWT_SECRET_KEY'] = 'foobarbaz' # test decode key callback with one argument (backwards compatibility) with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") @jwtM.decode_key_loader def get_decode_key_legacy(claims): return 'foobarbaz' with app.test_request_context(): token = encode_token(app, default_access_token) decode_token(token) assert len(w) == 1 assert issubclass(w[-1].category, DeprecationWarning) def test_custom_encode_decode_key_callbacks(app, default_access_token): jwtM = get_jwt_manager(app) app.config['JWT_SECRET_KEY'] = 'foobarbaz' @jwtM.encode_key_loader def get_encode_key_1(identity): assert identity == 'username' return 'different secret' with pytest.raises(InvalidSignatureError): with app.test_request_context(): token = create_access_token('username') decode_token(token) with pytest.raises(InvalidSignatureError): with app.test_request_context(): token = create_refresh_token('username') decode_token(token) @jwtM.decode_key_loader def get_decode_key_1(claims, headers): assert claims['identity'] == 'username' return 'different secret' with app.test_request_context(): token = create_access_token('username') decode_token(token) token = create_refresh_token('username') decode_token(token) @pytest.mark.parametrize("token_aud", ['foo', ['bar'], ['foo', 'bar', 'baz']]) def test_valid_aud(app, default_access_token, token_aud): app.config['JWT_DECODE_AUDIENCE'] = ['foo', 'bar'] default_access_token['aud'] = token_aud valid_token = encode_token(app, default_access_token) with app.test_request_context(): decoded = decode_token(valid_token) assert decoded['aud'] == token_aud @pytest.mark.parametrize("token_aud", ['bar', ['bar'], ['bar', 'baz']]) def test_invalid_aud(app, default_access_token, token_aud): app.config['JWT_DECODE_AUDIENCE'] = 'foo' default_access_token['aud'] = token_aud invalid_token = encode_token(app, default_access_token) with pytest.raises(InvalidAudienceError): with app.test_request_context(): decode_token(invalid_token) def test_valid_iss(app, default_access_token): app.config['JWT_DECODE_ISSUER'] = 'foobar' default_access_token['iss'] = 'foobar' valid_token = encode_token(app, default_access_token) with app.test_request_context(): decoded = decode_token(valid_token) assert decoded['iss'] == 'foobar' def test_invalid_iss(app, default_access_token): app.config['JWT_DECODE_ISSUER'] = 'baz' default_access_token['iss'] = 'foobar' invalid_token = encode_token(app, default_access_token) with pytest.raises(InvalidIssuerError): with app.test_request_context(): decode_token(invalid_token) def test_malformed_token(app): invalid_token = 'foobarbaz' with pytest.raises(DecodeError): with app.test_request_context(): decode_token(invalid_token) python-flask-jwt-extended-3.21.0/tests/test_headers.py000066400000000000000000000067541352401742600230220ustar00rootroot00000000000000import pytest from flask import Flask, jsonify from flask_jwt_extended import JWTManager, jwt_required, create_access_token from tests.utils import get_jwt_manager @pytest.fixture(scope='function') def app(): app = Flask(__name__) app.config['JWT_SECRET_KEY'] = 'foobarbaz' JWTManager(app) @app.route('/protected', methods=['GET']) @jwt_required def access_protected(): return jsonify(foo='bar') return app def test_custom_header_name(app): app.config['JWT_HEADER_NAME'] = 'Foo' test_client = app.test_client() with app.test_request_context(): access_token = create_access_token('username') # Insure 'default' headers no longer work access_headers = {'Authorization': 'Bearer {}'.format(access_token)} response = test_client.get('/protected', headers=access_headers) assert response.status_code == 401 assert response.get_json() == {'msg': 'Missing Foo Header'} # Insure new headers do work access_headers = {'Foo': 'Bearer {}'.format(access_token)} response = test_client.get('/protected', headers=access_headers) assert response.status_code == 200 assert response.get_json() == {'foo': 'bar'} def test_custom_header_type(app): app.config['JWT_HEADER_TYPE'] = 'JWT' test_client = app.test_client() with app.test_request_context(): access_token = create_access_token('username') # Insure 'default' headers no longer work access_headers = {'Authorization': 'Bearer {}'.format(access_token)} response = test_client.get('/protected', headers=access_headers) expected_json = {'msg': "Bad Authorization header. Expected value 'JWT '"} assert response.status_code == 422 assert response.get_json() == expected_json # Insure new headers do work access_headers = {'Authorization': 'JWT {}'.format(access_token)} response = test_client.get('/protected', headers=access_headers) assert response.status_code == 200 assert response.get_json() == {'foo': 'bar'} # Insure new headers without a type also work app.config['JWT_HEADER_TYPE'] = '' access_headers = {'Authorization': access_token} response = test_client.get('/protected', headers=access_headers) assert response.status_code == 200 assert response.get_json() == {'foo': 'bar'} # Insure header with too many parts fails app.config['JWT_HEADER_TYPE'] = '' access_headers = {'Authorization': 'Bearer {}'.format(access_token)} response = test_client.get('/protected', headers=access_headers) expected_json = {'msg': "Bad Authorization header. Expected value ''"} assert response.get_json() == expected_json assert response.status_code == 422 def test_missing_headers(app): test_client = app.test_client() jwtM = get_jwt_manager(app) # Insure 'default' no headers response response = test_client.get('/protected', headers=None) assert response.status_code == 401 assert response.get_json() == {'msg': "Missing Authorization Header"} # Test custom no headers response @jwtM.unauthorized_loader def custom_response(err_str): return jsonify(foo='bar'), 201 response = test_client.get('/protected', headers=None) assert response.status_code == 201 assert response.get_json() == {'foo': "bar"} def test_custom_error_msg_key(app): app.config['JWT_ERROR_MESSAGE_KEY'] = 'message' response = app.test_client().get('/protected', headers=None) assert response.get_json() == {'message': 'Missing Authorization Header'} python-flask-jwt-extended-3.21.0/tests/test_json.py000066400000000000000000000074361352401742600223560ustar00rootroot00000000000000import pytest from flask import Flask, jsonify from flask_jwt_extended import ( JWTManager, jwt_required, jwt_refresh_token_required, create_access_token, create_refresh_token ) from tests.utils import get_jwt_manager @pytest.fixture(scope='function') def app(): app = Flask(__name__) app.config['JWT_SECRET_KEY'] = 'foobarbaz' app.config['JWT_TOKEN_LOCATION'] = 'json' JWTManager(app) @app.route('/protected', methods=['POST']) @jwt_required def access_protected(): return jsonify(foo='bar') @app.route('/refresh', methods=['POST']) @jwt_refresh_token_required def refresh_protected(): return jsonify(foo='bar') return app def test_content_type(app): test_client = app.test_client() with app.test_request_context(): access_token = create_access_token('username') refresh_token = create_refresh_token('username') data = {'access_token': access_token} response = test_client.post('/protected', data=data) expected_json = {'msg': 'Invalid content-type. Must be application/json.'} assert response.status_code == 401 assert response.get_json() == expected_json data = {'refresh_token': refresh_token} response = test_client.post('/refresh', data=data) expected_json = {'msg': 'Invalid content-type. Must be application/json.'} assert response.status_code == 401 assert response.get_json() == expected_json def test_custom_body_key(app): app.config['JWT_JSON_KEY'] = 'Foo' app.config['JWT_REFRESH_JSON_KEY'] = 'Bar' test_client = app.test_client() with app.test_request_context(): access_token = create_access_token('username') refresh_token = create_refresh_token('username') # Ensure 'default' keys no longer work data = {'access_token': access_token} response = test_client.post('/protected', json=data) assert response.status_code == 401 assert response.get_json() == {'msg': 'Missing "Foo" key in json data.'} data = {'refresh_token': refresh_token} response = test_client.post('/refresh', json=data) assert response.status_code == 401 assert response.get_json() == {'msg': 'Missing "Bar" key in json data.'} # Ensure new keys do work data = {'Foo': access_token} response = test_client.post('/protected', json=data) assert response.status_code == 200 assert response.get_json() == {'foo': 'bar'} data = {'Bar': refresh_token} response = test_client.post('/refresh', json=data) assert response.status_code == 200 assert response.get_json() == {'foo': 'bar'} def test_missing_keys(app): test_client = app.test_client() jwtM = get_jwt_manager(app) headers = {'content-type': 'application/json'} # Ensure 'default' no json response response = test_client.post('/protected', headers=headers) assert response.status_code == 401 assert response.get_json() == {'msg': 'Missing "access_token" key in json data.'} # Test custom no json response @jwtM.unauthorized_loader def custom_response(err_str): return jsonify(foo='bar'), 201 response = test_client.post('/protected', headers=headers) assert response.status_code == 201 assert response.get_json() == {'foo': "bar"} def test_defaults(app): test_client = app.test_client() with app.test_request_context(): access_token = create_access_token('username') refresh_token = create_refresh_token('username') data = {'access_token': access_token} response = test_client.post('/protected', json=data) assert response.status_code == 200 assert response.get_json() == {'foo': 'bar'} data = {'refresh_token': refresh_token} response = test_client.post('/refresh', json=data) assert response.status_code == 200 assert response.get_json() == {'foo': 'bar'} python-flask-jwt-extended-3.21.0/tests/test_multiple_token_locations.py000066400000000000000000000110451352401742600265020ustar00rootroot00000000000000import pytest from flask import Flask, jsonify from flask_jwt_extended import ( JWTManager, jwt_required, create_access_token, set_access_cookies ) @pytest.fixture(scope='function') def app(): app = Flask(__name__) app.config['JWT_SECRET_KEY'] = 'foobarbaz' app.config['JWT_TOKEN_LOCATION'] = ['headers', 'cookies', 'query_string', 'json'] JWTManager(app) @app.route('/cookie_login', methods=['GET']) def cookie_login(): resp = jsonify(login=True) access_token = create_access_token('username') set_access_cookies(resp, access_token) return resp @app.route('/protected', methods=['GET', 'POST']) @jwt_required def access_protected(): return jsonify(foo='bar') return app def test_header_access(app): test_client = app.test_client() with app.test_request_context(): access_token = create_access_token('username') access_headers = {'Authorization': 'Bearer {}'.format(access_token)} response = test_client.get('/protected', headers=access_headers) assert response.status_code == 200 assert response.get_json() == {'foo': 'bar'} def test_cookie_access(app): test_client = app.test_client() test_client.get('/cookie_login') response = test_client.get('/protected') assert response.status_code == 200 assert response.get_json() == {'foo': 'bar'} def test_query_string_access(app): test_client = app.test_client() with app.test_request_context(): access_token = create_access_token('username') url = '/protected?jwt={}'.format(access_token) response = test_client.get(url) assert response.status_code == 200 assert response.get_json() == {'foo': 'bar'} def test_json_access(app): test_client = app.test_client() with app.test_request_context(): access_token = create_access_token('username') data = {'access_token': access_token} response = test_client.post('/protected', json=data) assert response.status_code == 200 assert response.get_json() == {'foo': 'bar'} @pytest.mark.parametrize("options", [ (['cookies', 'headers'], ('Missing JWT in cookies or headers (Missing cookie ' '"access_token_cookie"; Missing Authorization Header)')), (['json', 'query_string'], ('Missing JWT in json or query_string (Invalid ' 'content-type. Must be application/json.; ' 'Missing "jwt" query paramater)')), ]) def test_no_jwt_in_request(app, options): token_locations, expected_err = options app.config['JWT_TOKEN_LOCATION'] = token_locations test_client = app.test_client() response = test_client.get('/protected') assert response.status_code == 401 assert response.get_json() == {'msg': expected_err} @pytest.mark.parametrize("options", [ (['cookies', 'headers'], 200, None, {'foo': 'bar'}), (['headers', 'cookies'], 200, None, {'foo': 'bar'}), ]) def test_order_of_jwt_locations_in_request(app, options): """ test order doesn't matter if at least one valid token is set""" token_locations, status_code, expected_err, expected_dict = options app.config['JWT_TOKEN_LOCATION'] = token_locations test_client = app.test_client() test_client.get('/cookie_login') response = test_client.get('/protected') assert response.status_code == status_code if expected_dict: assert response.get_json() == expected_dict else: assert response.get_json() == {'msg': expected_err} @pytest.mark.parametrize("options", [ (['cookies', 'headers'], 200, None, {'foo': 'bar'}), (['headers', 'cookies'], 422, ('Invalid header padding'), None), ]) def test_order_of_jwt_locations_with_one_invalid_token_in_request(app, options): """ test order doesn't matter if at least one valid token is set""" token_locations, status_code, expected_err, expected_dict = options app.config['JWT_TOKEN_LOCATION'] = token_locations test_client = app.test_client() with app.test_request_context(): access_token = create_access_token('username') # invalidate the token, to check token location precedence access_token = "000000{}".format(access_token[5:]) access_headers = {'Authorization': 'Bearer {}'.format(access_token)} # set valid cookies test_client.get('/cookie_login') response = test_client.get('/protected', headers=access_headers) assert response.status_code == status_code if expected_dict: assert response.get_json() == expected_dict else: assert response.get_json() == {'msg': expected_err} python-flask-jwt-extended-3.21.0/tests/test_options_method.py000066400000000000000000000024051352401742600244270ustar00rootroot00000000000000from flask import Flask from flask_jwt_extended import ( JWTManager, jwt_required, fresh_jwt_required, jwt_refresh_token_required ) import pytest @pytest.fixture(scope='function') def app(): app = Flask(__name__) app.config['JWT_SECRET_KEY'] = 'secret' JWTManager(app) @app.route('/jwt_required', methods=["GET", "OPTIONS"]) @jwt_required def jwt_required_endpoint(): return b'ok' @app.route('/fresh_jwt_required', methods=["GET", "OPTIONS"]) @fresh_jwt_required def fresh_jwt_required_endpoint(): return b'ok' @app.route('/jwt_refresh_token_required', methods=["GET", "OPTIONS"]) @jwt_refresh_token_required def jwt_refresh_token_required_endpoint(): return b'ok' return app def test_access_jwt_required_enpoint(app): res = app.test_client().options('/jwt_required') assert res.status_code == 200 assert res.data == b'ok' def test_access_jwt_refresh_token_required_enpoint(app): res = app.test_client().options('/jwt_refresh_token_required') assert res.status_code == 200 assert res.data == b'ok' def test_access_fresh_jwt_required_enpoint(app): res = app.test_client().options('/fresh_jwt_required') assert res.status_code == 200 assert res.data == b'ok' python-flask-jwt-extended-3.21.0/tests/test_query_string.py000066400000000000000000000047271352401742600241400ustar00rootroot00000000000000import pytest from flask import Flask, jsonify from flask_jwt_extended import JWTManager, jwt_required, create_access_token from tests.utils import get_jwt_manager @pytest.fixture(scope='function') def app(): app = Flask(__name__) app.config['JWT_SECRET_KEY'] = 'foobarbaz' app.config['JWT_TOKEN_LOCATION'] = ['query_string'] JWTManager(app) @app.route('/protected', methods=['GET']) @jwt_required def access_protected(): return jsonify(foo='bar') return app def test_default_query_paramater(app): test_client = app.test_client() with app.test_request_context(): access_token = create_access_token('username') url = '/protected?jwt={}'.format(access_token) response = test_client.get(url) assert response.status_code == 200 assert response.get_json() == {'foo': 'bar'} def test_custom_query_paramater(app): app.config['JWT_QUERY_STRING_NAME'] = 'foo' test_client = app.test_client() with app.test_request_context(): access_token = create_access_token('username') # Insure 'default' query paramaters no longer work url = '/protected?jwt={}'.format(access_token) response = test_client.get(url) assert response.status_code == 401 assert response.get_json() == {'msg': 'Missing "foo" query paramater'} # Insure new query_string does work url = '/protected?foo={}'.format(access_token) response = test_client.get(url) assert response.status_code == 200 assert response.get_json() == {'foo': 'bar'} def test_missing_query_paramater(app): test_client = app.test_client() jwtM = get_jwt_manager(app) with app.test_request_context(): access_token = create_access_token('username') # Insure no query paramaters doesn't give a response response = test_client.get('/protected') assert response.status_code == 401 assert response.get_json() == {'msg': 'Missing "jwt" query paramater'} # Insure headers don't work access_headers = {'Authorization': 'Bearer {}'.format(access_token)} response = test_client.get('/protected', headers=access_headers) assert response.status_code == 401 assert response.get_json() == {'msg': 'Missing "jwt" query paramater'} # Test custom response works @jwtM.unauthorized_loader def custom_response(err_str): return jsonify(foo='bar'), 201 response = test_client.get('/protected') assert response.status_code == 201 assert response.get_json() == {'foo': "bar"} python-flask-jwt-extended-3.21.0/tests/test_user_claims_loader.py000066400000000000000000000125471352401742600252400ustar00rootroot00000000000000import pytest from flask import Flask, jsonify from flask_jwt_extended import ( JWTManager, create_access_token, jwt_required, get_jwt_claims, decode_token, jwt_refresh_token_required, create_refresh_token ) from tests.utils import get_jwt_manager, make_headers @pytest.fixture(scope='function') def app(): app = Flask(__name__) app.config['JWT_SECRET_KEY'] = 'foobarbaz' JWTManager(app) @app.route('/protected', methods=['GET']) @jwt_required def get_claims(): return jsonify(get_jwt_claims()) @app.route('/protected2', methods=['GET']) @jwt_refresh_token_required def get_refresh_claims(): return jsonify(get_jwt_claims()) return app def test_user_claim_in_access_token(app): jwt = get_jwt_manager(app) @jwt.user_claims_loader def add_claims(identity): return {'foo': 'bar'} with app.test_request_context(): access_token = create_access_token('username') test_client = app.test_client() response = test_client.get('/protected', headers=make_headers(access_token)) assert response.get_json() == {'foo': 'bar'} assert response.status_code == 200 def test_non_serializable_user_claims(app): jwt = get_jwt_manager(app) @jwt.user_claims_loader def add_claims(identity): return app with pytest.raises(TypeError): with app.test_request_context(): create_access_token('username') def test_token_from_complex_object(app): class TestObject: def __init__(self, username): self.username = username jwt = get_jwt_manager(app) @jwt.user_claims_loader def add_claims(test_obj): return {'username': test_obj.username} @jwt.user_identity_loader def add_claims(test_obj): return test_obj.username with app.test_request_context(): access_token = create_access_token(TestObject('username')) # Make sure the changes appear in the token decoded_token = decode_token(access_token) assert decoded_token['identity'] == 'username' assert decoded_token['user_claims'] == {'username': 'username'} test_client = app.test_client() response = test_client.get('/protected', headers=make_headers(access_token)) assert response.get_json() == {'username': 'username'} assert response.status_code == 200 def test_user_claims_with_different_name(app): jwt = get_jwt_manager(app) app.config['JWT_USER_CLAIMS'] = 'banana' @jwt.user_claims_loader def add_claims(identity): return {'foo': 'bar'} with app.test_request_context(): access_token = create_access_token('username') # Make sure the name is actually different in the token decoded_token = decode_token(access_token) assert decoded_token['banana'] == {'foo': 'bar'} # Make sure the correct data is returned to us from the full call test_client = app.test_client() response = test_client.get('/protected', headers=make_headers(access_token)) assert response.get_json() == {'foo': 'bar'} assert response.status_code == 200 def test_user_claim_not_in_refresh_token(app): jwt = get_jwt_manager(app) @jwt.user_claims_loader def add_claims(identity): return {'foo': 'bar'} with app.test_request_context(): refresh_token = create_refresh_token('username') test_client = app.test_client() response = test_client.get('/protected2', headers=make_headers(refresh_token)) assert response.get_json() == {} assert response.status_code == 200 def test_user_claim_in_refresh_token(app): app.config['JWT_CLAIMS_IN_REFRESH_TOKEN'] = True jwt = get_jwt_manager(app) @jwt.user_claims_loader def add_claims(identity): return {'foo': 'bar'} with app.test_request_context(): refresh_token = create_refresh_token('username') test_client = app.test_client() response = test_client.get('/protected2', headers=make_headers(refresh_token)) assert response.get_json() == {'foo': 'bar'} assert response.status_code == 200 def test_user_claim_in_refresh_token_specified_at_creation(app): app.config['JWT_CLAIMS_IN_REFRESH_TOKEN'] = True with app.test_request_context(): refresh_token = create_refresh_token('username', user_claims={'foo': 'bar'}) test_client = app.test_client() response = test_client.get('/protected2', headers=make_headers(refresh_token)) assert response.get_json() == {'foo': 'bar'} assert response.status_code == 200 def test_user_claims_in_access_token_specified_at_creation(app): with app.test_request_context(): access_token = create_access_token('username', user_claims={'foo': 'bar'}) test_client = app.test_client() response = test_client.get('/protected', headers=make_headers(access_token)) assert response.get_json() == {'foo': 'bar'} assert response.status_code == 200 def test_user_claims_in_access_token_specified_at_creation_override(app): jwt = get_jwt_manager(app) @jwt.user_claims_loader def add_claims(identity): return {'default': 'value'} with app.test_request_context(): access_token = create_access_token('username', user_claims={'foo': 'bar'}) test_client = app.test_client() response = test_client.get('/protected', headers=make_headers(access_token)) assert response.get_json() == {'foo': 'bar'} assert response.status_code == 200 python-flask-jwt-extended-3.21.0/tests/test_user_loader.py000066400000000000000000000045241352401742600237040ustar00rootroot00000000000000import pytest from flask import Flask, jsonify from flask_jwt_extended import ( JWTManager, jwt_required, current_user, get_current_user, create_access_token ) from tests.utils import get_jwt_manager, make_headers @pytest.fixture(scope='function') def app(): app = Flask(__name__) app.config['JWT_SECRET_KEY'] = 'foobarbaz' JWTManager(app) @app.route('/get_user1', methods=['GET']) @jwt_required def get_user1(): return jsonify(foo=get_current_user()['username']) @app.route('/get_user2', methods=['GET']) @jwt_required def get_user2(): return jsonify(foo=current_user['username']) return app @pytest.mark.parametrize("url", ['/get_user1', '/get_user2']) def test_load_valid_user(app, url): jwt = get_jwt_manager(app) @jwt.user_loader_callback_loader def user_load_callback(identity): return {'username': identity} test_client = app.test_client() with app.test_request_context(): access_token = create_access_token('username') response = test_client.get(url, headers=make_headers(access_token)) assert response.status_code == 200 assert response.get_json() == {'foo': 'username'} @pytest.mark.parametrize("url", ['/get_user1', '/get_user2']) def test_load_invalid_user(app, url): jwt = get_jwt_manager(app) @jwt.user_loader_callback_loader def user_load_callback(identity): return None test_client = app.test_client() with app.test_request_context(): access_token = create_access_token('username') response = test_client.get(url, headers=make_headers(access_token)) assert response.status_code == 401 assert response.get_json() == {'msg': "Error loading the user username"} @pytest.mark.parametrize("url", ['/get_user1', '/get_user2']) def test_custom_user_loader_errors(app, url): jwt = get_jwt_manager(app) @jwt.user_loader_callback_loader def user_load_callback(identity): return None @jwt.user_loader_error_loader def user_loader_error(identity): return jsonify(foo='bar'), 201 test_client = app.test_client() with app.test_request_context(): access_token = create_access_token('username') response = test_client.get(url, headers=make_headers(access_token)) assert response.status_code == 201 assert response.get_json() == {'foo': "bar"} python-flask-jwt-extended-3.21.0/tests/test_view_decorators.py000066400000000000000000000314011352401742600245710ustar00rootroot00000000000000import pytest import warnings from datetime import timedelta from dateutil.relativedelta import relativedelta from flask import Flask, jsonify from flask_jwt_extended import ( jwt_required, fresh_jwt_required, JWTManager, jwt_refresh_token_required, jwt_optional, create_access_token, create_refresh_token, get_jwt_identity, decode_token ) from tests.utils import make_headers, encode_token, get_jwt_manager @pytest.fixture(scope='function') def app(): app = Flask(__name__) app.config['JWT_SECRET_KEY'] = 'foobarbaz' JWTManager(app) @app.route('/protected', methods=['GET']) @jwt_required def protected(): return jsonify(foo='bar') @app.route('/fresh_protected', methods=['GET']) @fresh_jwt_required def fresh_protected(): return jsonify(foo='bar') @app.route('/refresh_protected', methods=['GET']) @jwt_refresh_token_required def refresh_protected(): return jsonify(foo='bar') @app.route('/optional_protected', methods=['GET']) @jwt_optional def optional_protected(): if get_jwt_identity(): return jsonify(foo='baz') else: return jsonify(foo='bar') return app def test_jwt_required(app): url = '/protected' test_client = app.test_client() with app.test_request_context(): access_token = create_access_token('username') fresh_access_token = create_access_token('username', fresh=True) refresh_token = create_refresh_token('username') # Access and fresh access should be able to access this for token in (access_token, fresh_access_token): response = test_client.get(url, headers=make_headers(token)) assert response.status_code == 200 assert response.get_json() == {'foo': 'bar'} # Test accessing jwt_required with no jwt in the request response = test_client.get(url, headers=None) assert response.status_code == 401 assert response.get_json() == {'msg': 'Missing Authorization Header'} # Test refresh token access to jwt_required response = test_client.get(url, headers=make_headers(refresh_token)) assert response.status_code == 422 assert response.get_json() == {'msg': 'Only access tokens are allowed'} def test_fresh_jwt_required(app): jwtM = get_jwt_manager(app) url = '/fresh_protected' test_client = app.test_client() with app.test_request_context(): access_token = create_access_token('username') fresh_access_token = create_access_token('username', fresh=True) refresh_token = create_refresh_token('username') fresh_timed_access_token = create_access_token( identity='username', fresh=timedelta(minutes=5) ) stale_timed_access_token = create_access_token( identity='username', fresh=timedelta(minutes=-1) ) response = test_client.get(url, headers=make_headers(fresh_access_token)) assert response.status_code == 200 assert response.get_json() == {'foo': 'bar'} response = test_client.get(url, headers=make_headers(access_token)) assert response.status_code == 401 assert response.get_json() == {'msg': 'Fresh token required'} response = test_client.get(url, headers=make_headers(fresh_timed_access_token)) assert response.status_code == 200 assert response.get_json() == {'foo': 'bar'} response = test_client.get(url, headers=make_headers(stale_timed_access_token)) assert response.status_code == 401 assert response.get_json() == {'msg': 'Fresh token required'} response = test_client.get(url, headers=None) assert response.status_code == 401 assert response.get_json() == {'msg': 'Missing Authorization Header'} response = test_client.get(url, headers=make_headers(refresh_token)) assert response.status_code == 422 assert response.get_json() == {'msg': 'Only access tokens are allowed'} # Test with custom response @jwtM.needs_fresh_token_loader def custom_response(): return jsonify(msg='foobar'), 201 response = test_client.get(url, headers=make_headers(access_token)) assert response.status_code == 201 assert response.get_json() == {'msg': 'foobar'} def test_refresh_jwt_required(app): url = '/refresh_protected' test_client = app.test_client() with app.test_request_context(): access_token = create_access_token('username') fresh_access_token = create_access_token('username', fresh=True) refresh_token = create_refresh_token('username') response = test_client.get(url, headers=make_headers(fresh_access_token)) assert response.status_code == 422 assert response.get_json() == {'msg': 'Only refresh tokens are allowed'} response = test_client.get(url, headers=make_headers(access_token)) assert response.status_code == 422 assert response.get_json() == {'msg': 'Only refresh tokens are allowed'} response = test_client.get(url, headers=None) assert response.status_code == 401 assert response.get_json() == {'msg': 'Missing Authorization Header'} response = test_client.get(url, headers=make_headers(refresh_token)) assert response.status_code == 200 assert response.get_json() == {'foo': 'bar'} @pytest.mark.parametrize("delta_func", [timedelta, relativedelta]) def test_jwt_optional(app, delta_func): url = '/optional_protected' test_client = app.test_client() with app.test_request_context(): access_token = create_access_token('username') fresh_access_token = create_access_token('username', fresh=True) refresh_token = create_refresh_token('username') expired_token = create_access_token( identity='username', expires_delta=delta_func(minutes=-1) ) response = test_client.get(url, headers=make_headers(fresh_access_token)) assert response.status_code == 200 assert response.get_json() == {'foo': 'baz'} response = test_client.get(url, headers=make_headers(access_token)) assert response.status_code == 200 assert response.get_json() == {'foo': 'baz'} response = test_client.get(url, headers=make_headers(refresh_token)) assert response.status_code == 422 assert response.get_json() == {'msg': 'Only access tokens are allowed'} response = test_client.get(url, headers=None) assert response.status_code == 200 assert response.get_json() == {'foo': 'bar'} response = test_client.get(url, headers=make_headers(expired_token)) assert response.status_code == 401 assert response.get_json() == {'msg': 'Token has expired'} def test_invalid_jwt(app): url = '/protected' jwtM = get_jwt_manager(app) test_client = app.test_client() invalid_token = "aaaaa.bbbbb.ccccc" # Test default response response = test_client.get(url, headers=make_headers(invalid_token)) assert response.status_code == 422 assert response.get_json() == {'msg': 'Invalid header padding'} # Test custom response @jwtM.invalid_token_loader def custom_response(err_str): return jsonify(msg='foobar'), 201 response = test_client.get(url, headers=make_headers(invalid_token)) assert response.status_code == 201 assert response.get_json() == {'msg': 'foobar'} def test_jwt_missing_claims(app): url = '/protected' test_client = app.test_client() token = encode_token(app, {'foo': 'bar'}) response = test_client.get(url, headers=make_headers(token)) assert response.status_code == 422 assert response.get_json() == {'msg': 'Missing claim: identity'} def test_jwt_invalid_audience(app): url = '/protected' test_client = app.test_client() # No audience claim expected or provided - OK access_token = encode_token(app, {'identity': 'me'}) response = test_client.get(url, headers=make_headers(access_token)) assert response.status_code == 200 # Audience claim expected and not provided - not OK app.config['JWT_DECODE_AUDIENCE'] = 'my_audience' access_token = encode_token(app, {'identity': 'me'}) response = test_client.get(url, headers=make_headers(access_token)) assert response.status_code == 422 assert response.get_json() == {'msg': 'Token is missing the "aud" claim'} # Audience claim still expected and wrong one provided - not OK access_token = encode_token(app, {'aud': 'different_audience', 'identity': 'me'}) response = test_client.get(url, headers=make_headers(access_token)) assert response.status_code == 422 assert response.get_json() == {'msg': 'Invalid audience'} def test_jwt_invalid_issuer(app): url = '/protected' test_client = app.test_client() # No issuer claim expected or provided - OK access_token = encode_token(app, {'identity': 'me'}) response = test_client.get(url, headers=make_headers(access_token)) assert response.status_code == 200 # Issuer claim expected and not provided - not OK app.config['JWT_DECODE_ISSUER'] = 'my_issuer' access_token = encode_token(app, {'identity': 'me'}) response = test_client.get(url, headers=make_headers(access_token)) assert response.status_code == 422 assert response.get_json() == {'msg': 'Token is missing the "iss" claim'} # Issuer claim still expected and wrong one provided - not OK access_token = encode_token(app, {'iss': 'different_issuer', 'identity': 'me'}) response = test_client.get(url, headers=make_headers(access_token)) assert response.status_code == 422 assert response.get_json() == {'msg': 'Invalid issuer'} def test_malformed_token(app): url = '/protected' test_client = app.test_client() access_token = 'foobarbaz' response = test_client.get(url, headers=make_headers(access_token)) assert response.status_code == 422 assert response.get_json() == {'msg': 'Not enough segments'} @pytest.mark.parametrize("delta_func", [timedelta, relativedelta]) def test_expired_token(app, delta_func): url = '/protected' jwtM = get_jwt_manager(app) test_client = app.test_client() with app.test_request_context(): token = create_access_token('username', expires_delta=delta_func(minutes=-1)) # Test default response response = test_client.get(url, headers=make_headers(token)) assert response.status_code == 401 assert response.get_json() == {'msg': 'Token has expired'} # Test depreciated custom response @jwtM.expired_token_loader def depreciated_custom_response(): return jsonify(msg='foobar'), 201 warnings.simplefilter("always") with warnings.catch_warnings(record=True) as w: response = test_client.get(url, headers=make_headers(token)) assert response.status_code == 201 assert response.get_json() == {'msg': 'foobar'} assert w[0].category == DeprecationWarning # Test new custom response @jwtM.expired_token_loader def custom_response(token): assert token['identity'] == 'username' assert token['type'] == 'access' return jsonify(msg='foobar'), 201 warnings.simplefilter("always") with warnings.catch_warnings(record=True) as w: response = test_client.get(url, headers=make_headers(token)) assert response.status_code == 201 assert response.get_json() == {'msg': 'foobar'} assert len(w) == 0 def test_expired_token_via_decode_token(app): jwtM = get_jwt_manager(app) @jwtM.expired_token_loader def depreciated_custom_response(expired_token): assert expired_token['identity'] == 'username' return jsonify(msg='foobar'), 401 @app.route('/test') def test_route(): token = create_access_token('username', expires_delta=timedelta(minutes=-1)) decode_token(token) return jsonify(msg='baz'), 200 test_client = app.test_client() response = test_client.get('/test') assert response.get_json() == {'msg': 'foobar'} assert response.status_code == 401 def test_no_token(app): url = '/protected' jwtM = get_jwt_manager(app) test_client = app.test_client() # Test default response response = test_client.get(url, headers=None) assert response.status_code == 401 assert response.get_json() == {'msg': 'Missing Authorization Header'} # Test custom response @jwtM.unauthorized_loader def custom_response(err_str): return jsonify(msg='foobar'), 201 response = test_client.get(url, headers=None) assert response.status_code == 201 assert response.get_json() == {'msg': 'foobar'} def test_different_token_algorightm(app): url = '/protected' test_client = app.test_client() with app.test_request_context(): token = create_access_token('username') app.config['JWT_ALGORITHM'] = 'HS512' response = test_client.get(url, headers=make_headers(token)) assert response.status_code == 422 assert response.get_json() == {'msg': 'The specified alg value is not allowed'} python-flask-jwt-extended-3.21.0/tests/utils.py000066400000000000000000000007651352401742600215040ustar00rootroot00000000000000import jwt from flask_jwt_extended.config import config def encode_token(app, token_data): with app.test_request_context(): token = jwt.encode( token_data, config.decode_key, algorithm=config.algorithm, json_encoder=config.json_encoder ) return token.decode('utf-8') def get_jwt_manager(app): return app.extensions['flask-jwt-extended'] def make_headers(jwt): return {'Authorization': 'Bearer {}'.format(jwt)} python-flask-jwt-extended-3.21.0/tox.ini000066400000000000000000000010401352401742600201260ustar00rootroot00000000000000# Tox (http://tox.testrun.org/) is a tool for running tests # in multiple virtualenvs. This configuration file will run the # test suite on all supported python versions. To use it, "pip install tox" # and then run "tox" from this directory. [tox] envlist = py27, py34, py35, py36 [testenv] commands = coverage run --source flask_jwt_extended -m pytest tests/ coverage report -m deps = pytest coverage cryptography python-dateutil # TODO why does this not work? # extras = # asymmetric_crypto [pycodestyle] max-line-length = 90