pax_global_header00006660000000000000000000000064141300572520014511gustar00rootroot0000000000000052 comment=dcffd2f143d5be89e40d1cdcb4d191c5a3b8343b flask-jwt-extended-4.3.1/000077500000000000000000000000001413005725200152165ustar00rootroot00000000000000flask-jwt-extended-4.3.1/.github/000077500000000000000000000000001413005725200165565ustar00rootroot00000000000000flask-jwt-extended-4.3.1/.github/FUNDING.yml000066400000000000000000000001021413005725200203640ustar00rootroot00000000000000# These are supported funding model platforms github: [vimalloc] flask-jwt-extended-4.3.1/.github/workflows/000077500000000000000000000000001413005725200206135ustar00rootroot00000000000000flask-jwt-extended-4.3.1/.github/workflows/coverage.yml000066400000000000000000000005601413005725200231320ustar00rootroot00000000000000name: Coverage on: - push - pull_request jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python 3.x uses: actions/setup-python@v2 with: python-version: '3.9' - name: Install Dependencies run: pip install tox - name: Coverage run: tox -e coverage flask-jwt-extended-4.3.1/.github/workflows/documentation.yml000066400000000000000000000005741413005725200242150ustar00rootroot00000000000000name: Documentation on: - push - pull_request jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python 3.x uses: actions/setup-python@v2 with: python-version: '3.9' - name: Install Dependencies run: pip install tox - name: Check Documentation run: tox -e docs flask-jwt-extended-4.3.1/.github/workflows/lint.yml000066400000000000000000000005451413005725200223100ustar00rootroot00000000000000name: Lint on: - push - pull_request jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python 3.x uses: actions/setup-python@v2 with: python-version: '3.9' - name: Install Dependencies run: pip install tox - name: Lint run: tox -e style flask-jwt-extended-4.3.1/.github/workflows/publish_flask_jwt_extended.yml000066400000000000000000000015451413005725200267350ustar00rootroot00000000000000# This workflow will upload a Python Package using Twine when a release is created # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries name: Publish Flask-JWT-Extended on: release: types: [created] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install setuptools wheel twine - name: Build and publish env: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | python setup.py sdist bdist_wheel twine upload dist/* flask-jwt-extended-4.3.1/.github/workflows/unit_tests.yml000066400000000000000000000007661413005725200235500ustar00rootroot00000000000000name: Unit Tests on: - push - pull_request jobs: build: runs-on: ubuntu-latest strategy: matrix: python: [3.6, 3.7, 3.8, 3.9, pypy3] steps: - uses: actions/checkout@v2 - name: Setup Python uses: actions/setup-python@v2 with: python-version: ${{ matrix.python }} - name: Install Dependencies run: pip install tox - name: Run Tox run: tox -e py - name: Run Tox Flask 1 run: tox -e flask1 flask-jwt-extended-4.3.1/.gitignore000066400000000000000000000021261413005725200172070ustar00rootroot00000000000000# 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 flask-jwt-extended-4.3.1/.pre-commit-config.yaml000066400000000000000000000012111413005725200214720ustar00rootroot00000000000000repos: - repo: https://github.com/asottile/reorder_python_imports rev: v1.5.0 hooks: - id: reorder-python-imports name: Reorder Python imports (src, tests, examples) args: ["--application-directories", ".:src"] - repo: https://github.com/python/black rev: 19.3b0 hooks: - id: black - repo: https://gitlab.com/pycqa/flake8 rev: 3.7.7 hooks: - id: flake8 additional_dependencies: [flake8-bugbear] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.2.3 hooks: - id: check-byte-order-marker - id: trailing-whitespace - id: end-of-file-fixer flask-jwt-extended-4.3.1/.readthedocs.yml000066400000000000000000000002151413005725200203020ustar00rootroot00000000000000version: 2 sphinx: builder: htmldir configuration: docs/conf.py python: version: 3.7 install: - requirements: requirements.txt flask-jwt-extended-4.3.1/LICENSE000066400000000000000000000020471413005725200162260ustar00rootroot00000000000000MIT 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. flask-jwt-extended-4.3.1/MANIFEST.in000066400000000000000000000001201413005725200167450ustar00rootroot00000000000000include LICENSE README.md requirements.txt tox.ini recursive-include tests *.py flask-jwt-extended-4.3.1/README.md000066400000000000000000000041721413005725200165010ustar00rootroot00000000000000# Flask-JWT-Extended ### Features Flask-JWT-Extended not only adds support for using JSON Web Tokens (JWT) to Flask for protecting routes, but also many helpful (and **optional**) features built in to make working with JSON Web Tokens easier. These include: * Adding custom claims to JSON Web Tokens * Automatic user loading (`current_user`). * Custom claims validation on received tokens * [Refresh tokens](https://auth0.com/blog/refresh-tokens-what-are-they-and-when-to-use-them/) * First class support for fresh tokens for making sensitive changes. * Token revoking/blocklisting * Storing tokens in cookies and CSRF protection ### Usage [View the documentation online](https://flask-jwt-extended.readthedocs.io/en/stable/) ### Upgrading from 3.x.x to 4.0.0 [View the changes](https://flask-jwt-extended.readthedocs.io/en/stable/v4_upgrade_guide/) ### 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://discord.gg/EJBsbFd ### Contributing Before making any changes, make sure to install the development requirements and setup the git hooks which will automatically lint and format your changes. ```bash pip install -r requirements.txt pre-commit install ``` We require 100% code coverage in our unit tests. You can run the tests locally with `tox` which insures that all tests pass, tests provide complete code coverage, documentation builds, and style guide are adhered to ```bash tox ``` A subset of checks can also be ran by adding an argument to tox. The available arguments are: * py36, py37, py38, py39, pypy3 * Run unit tests on the given python version * coverage * Run a code coverage check * docs * Insure documentation builds and there are no broken links * style * Insure style guide is adhered to ```bash tox -e py38 ``` We also require features to be well documented. You can generate a local copy of the documentation by going to the `docs` directory and running: ```bash make clean && make html && open _build/html/index.html ``` flask-jwt-extended-4.3.1/docs/000077500000000000000000000000001413005725200161465ustar00rootroot00000000000000flask-jwt-extended-4.3.1/docs/Makefile000066400000000000000000000167711413005725200176220ustar00rootroot00000000000000# 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 -n -W --keep-going @echo @echo "Build finished. Dummy builder generates no files." flask-jwt-extended-4.3.1/docs/__init__.py000066400000000000000000000000001413005725200202450ustar00rootroot00000000000000flask-jwt-extended-4.3.1/docs/add_custom_data_claims.rst000066400000000000000000000032541413005725200233470ustar00rootroot00000000000000Storing Additional Data in JWTs =============================== You may want to store additional information in the access token which you could later access in the protected views. This can be done using the ``additional_claims`` argument with the :func:`~flask_jwt_extended.create_access_token` or :func:`~flask_jwt_extended.create_refresh_token` functions. The claims can be accessed in a protected route via the :func:`~flask_jwt_extended.get_jwt` function. It is important to remember that JWTs are not encrypted and the contents of a JWT can be trivially decoded by anyone who has access to it. As such, you should never put any sensitive information in a JWT. .. literalinclude:: ../examples/additional_data_in_access_token.py Alternately you can use the :meth:`~flask_jwt_extended.JWTManager.additional_claims_loader` decorator to register a callback function that will be called whenever a new JWT is created, and return a dictionary of claims to add to that token. In the case that both :meth:`~flask_jwt_extended.JWTManager.additional_claims_loader` and the ``additional_claims`` argument are used, both results are merged together, with ties going to the data supplied by the ``additional_claims`` argument. .. code-block:: python # Using the additional_claims_loader, we can specify a method that will be # called when creating JWTs. The decorated method must take the identity # we are creating a token for and return a dictionary of additional # claims to add to the JWT. @jwt.additional_claims_loader def add_claims_to_access_token(identity): return = { "aud": "some_audience", "foo": "bar", "upcase_name": identity.upper(), } flask-jwt-extended-4.3.1/docs/api.rst000066400000000000000000000021351413005725200174520ustar00rootroot00000000000000API Documentation ================= This is the documentation for all of the API that is exported in this extension. Configuring Flask-JWT-Extended ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. currentmodule:: flask_jwt_extended .. module:: flask_jwt_extended .. autoclass:: JWTManager :members: Verify Tokens in Request ~~~~~~~~~~~~~~~~~~~~~~~~ .. autofunction:: jwt_required .. autofunction:: verify_jwt_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_csrf_token .. autofunction:: get_current_user .. autofunction:: get_jti .. autofunction:: get_jwt .. autofunction:: get_jwt_header .. autofunction:: get_jwt_identity .. autofunction:: get_unverified_jwt_headers .. autofunction:: set_access_cookies .. autofunction:: set_refresh_cookies .. autofunction:: unset_access_cookies .. autofunction:: unset_jwt_cookies .. autofunction:: unset_refresh_cookies flask-jwt-extended-4.3.1/docs/automatic_user_loading.rst000066400000000000000000000036761413005725200234350ustar00rootroot00000000000000Automatic User Loading ====================== In most web applications it is important to have access to the user who is accessing a protected route. We provide a couple callback functions that make this seamless while working with JWTs. The first is :meth:`~flask_jwt_extended.JWTManager.user_identity_loader`, which will convert any ``User`` object used to create a JWT into a JSON serializable format. On the flip side, you can use :meth:`~flask_jwt_extended.JWTManager.user_lookup_loader` to automatically load your ``User`` object when a JWT is present in the request. The loaded user is available in your protected routes via :attr:`~flask_jwt_extended.current_user`. Lets see an example of this while utilizing SQLAlchemy to store our users: .. literalinclude:: ../examples/automatic_user_loading.py We can see this in action using `HTTPie `_. .. code-block :: bash $ http POST :5000/login username=panther password=password HTTP/1.0 200 OK Content-Length: 281 Content-Type: application/json Date: Sun, 24 Jan 2021 17:23:31 GMT Server: Werkzeug/1.0.1 Python/3.8.6 { "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTYxMTUwOTAxMSwianRpIjoiNGFmN2ViNTAtMjk3Yy00ZmY4LWJmOTYtMTZlMDE5MWEzYzMwIiwibmJmIjoxNjExNTA5MDExLCJ0eXBlIjoiYWNjZXNzIiwic3ViIjoyLCJleHAiOjE2MTQxMDEwMTF9.2UhZo-xo19NXaqKLwcMz0NBLAcxxEUeK4Ziqk1T_9h0" } $ export JWT="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTYxMTUwOTAxMSwianRpIjoiNGFmN2ViNTAtMjk3Yy00ZmY4LWJmOTYtMTZlMDE5MWEzYzMwIiwibmJmIjoxNjExNTA5MDExLCJ0eXBlIjoiYWNjZXNzIiwic3ViIjoyLCJleHAiOjE2MTQxMDEwMTF9.2UhZo-xo19NXaqKLwcMz0NBLAcxxEUeK4Ziqk1T_9h0" $ http GET :5000/who_am_i Authorization:"Bearer $JWT" HTTP/1.0 200 OK Content-Length: 57 Content-Type: application/json Date: Sun, 24 Jan 2021 17:31:34 GMT Server: Werkzeug/1.0.1 Python/3.8.6 { "id": 2, "full_name": "Ann Takamaki", "username": "panther" } flask-jwt-extended-4.3.1/docs/basic_usage.rst000066400000000000000000000043011413005725200211430ustar00rootroot00000000000000Basic Usage =========== In its simplest form, there is not much to using this extension. You use :func:`~flask_jwt_extended.create_access_token` to make JSON Web Tokens, :func:`~flask_jwt_extended.jwt_required` to protect routes, and :func:`~flask_jwt_extended.get_jwt_identity` to get the identity of a JWT in a protected route. .. literalinclude:: ../examples/simple.py To access a jwt_required protected view you need to send in the JWT with each 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 `HTTPie `_. .. code-block :: bash $ http GET :5000/protected HTTP/1.0 401 UNAUTHORIZED Content-Length: 39 Content-Type: application/json Date: Sun, 24 Jan 2021 18:09:17 GMT Server: Werkzeug/1.0.1 Python/3.8.6 { "msg": "Missing Authorization Header" } $ http POST :5000/login username=test password=test HTTP/1.0 200 OK Content-Length: 288 Content-Type: application/json Date: Sun, 24 Jan 2021 18:10:39 GMT Server: Werkzeug/1.0.1 Python/3.8.6 { "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTYxMTUxMTgzOSwianRpIjoiMmI0NzliNTQtYTI0OS00ZDNjLWE4NjItZGVkZGIzODljNmVlIiwibmJmIjoxNjExNTExODM5LCJ0eXBlIjoiYWNjZXNzIiwic3ViIjoidGVzdCIsImV4cCI6MTYxNDEwMzgzOX0.UpTueBRwNLK8e-06-oo5Y_9eWbaN5T3IHwKsy6Jauaw" } $ export JWT="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTYxMTUxMTgzOSwianRpIjoiMmI0NzliNTQtYTI0OS00ZDNjLWE4NjItZGVkZGIzODljNmVlIiwibmJmIjoxNjExNTExODM5LCJ0eXBlIjoiYWNjZXNzIiwic3ViIjoidGVzdCIsImV4cCI6MTYxNDEwMzgzOX0.UpTueBRwNLK8e-06-oo5Y_9eWbaN5T3IHwKsy6Jauaw" $ http GET :5000/protected Authorization:"Bearer $JWT" HTTP/1.0 200 OK Content-Length: 24 Content-Type: application/json Date: Sun, 24 Jan 2021 18:12:02 GMT Server: Werkzeug/1.0.1 Python/3.8.6 { "logged_in_as": "test" } **Important** Remember to change the JWT secret key in your application, and ensure that it is secure. The JWTs are signed with this key, and if someone gets their hands on it they will be able to create arbitrary tokens that are accepted by your web flask application. flask-jwt-extended-4.3.1/docs/blocklist_and_token_revoking.rst000066400000000000000000000032501413005725200246140ustar00rootroot00000000000000.. _Blocklist and Token Revoking: JWT Revoking / Blocklist ======================== JWT revoking is a mechanism for preventing an otherwise valid JWT from accessing your routes while still letting other valid JWTs in. To utilize JWT revoking in this extension, you must defining a callback function via the :meth:`~flask_jwt_extended.JWTManager.token_in_blocklist_loader` decorator. This function is called whenever a valid JWT is used to access a protected route. The callback will receive the JWT header and JWT payload as arguments, and must return ``True`` if the JWT has been revoked. In production, you will want to use some form of persistent storage (database, redis, etc) to store your JWTs. It would be bad if your application forgot that a JWT was revoked if it was restarted. We can provide some general recommendations on what type of storage engine to use, but ultimately the choice will depend on your specific application and tech stack. Redis ~~~~~ If your only requirements are to check if a JWT has been revoked, our recommendation is to use redis. It is blazing fast, can be configured to persist data to disc, and can automatically clear out JWTs after they expire by utilizing the Time To Live (TTL) functionality when storing a JWT. Here is an example using redis: .. literalinclude:: ../examples/blocklist_redis.py Database ~~~~~~~~ If you need to keep track of information about revoked JWTs our recommendation is to utilize a database. This allows you to easily store and utilize metadata for revoked tokens, such as when it was revoked, who revoked it, can it be un-revoked, etc. Here is an example using SQLAlchemy: .. literalinclude:: ../examples/blocklist_database.py flask-jwt-extended-4.3.1/docs/changing_default_behavior.rst000066400000000000000000000013711413005725200240430ustar00rootroot00000000000000Changing Default Behaviors ========================== This extension provides sensible default behaviors. For example, if an expired token attempts to access a protected endpoint, you will get a JSON response back like ``{"msg": "Token has expired"}`` and a 401 status code. However there may be various behaviors of this extension that you want to customize to your application's needs. We can do that with the various loader functions. Here is an example of how to do that. .. literalinclude:: ../examples/loaders.py There are all sorts of callbacks that can be defined to customize the behaviors of this extension. See the :ref:`Configuring Flask-JWT-Extended` API Documentation for a full list of callback functions that are available in this extension. flask-jwt-extended-4.3.1/docs/conf.py000066400000000000000000000243101413005725200174450ustar00rootroot00000000000000# -*- coding: utf-8 -*- # import io import os import re import sys # 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", "sphinx.ext.autosectionlabel", ] # 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 flask-jwt-extended-4.3.1/docs/custom_decorators.rst000066400000000000000000000010631413005725200224370ustar00rootroot00000000000000Custom 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 the current user is an administrator. :func:`flask_jwt_extended.verify_jwt_in_request` can be used to build your own decorators. This is the same function used by :func:`flask_jwt_extended.jwt_required`. Here is an example of how this might look. .. literalinclude:: ../examples/custom_decorators.py flask-jwt-extended-4.3.1/docs/index.rst000066400000000000000000000011231413005725200200040ustar00rootroot00000000000000.. 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 automatic_user_loading add_custom_data_claims optional_endpoints token_locations refreshing_tokens blocklist_and_token_revoking options changing_default_behavior custom_decorators api v4_upgrade_guide flask-jwt-extended-4.3.1/docs/installation.rst000066400000000000000000000012231413005725200213770ustar00rootroot00000000000000Installation ============== 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 flask-jwt-extended-4.3.1/docs/optional_endpoints.rst000066400000000000000000000015161413005725200226130ustar00rootroot00000000000000Partially protecting routes =========================== There may be cases where you want to use the same route regardless of if a JWT is present in the request or not. In these situations, you can use :func:`~flask_jwt_extended.jwt_required` with the ``optional=True`` argument. This will allow the endpoint to be accessed regardless of if a JWT is sent in with the request. If no JWT is present, :func:`~flask_jwt_extended.get_jwt` and :func:`~flask_jwt_extended.get_jwt_header`, will return an empty dictionary. :func:`~flask_jwt_extended.get_jwt_identity`, :attr:`~flask_jwt_extended.current_user`, and :func:`~flask_jwt_extended.get_current_user` will return None. If a JWT that is expired or not verifiable is in the request, an error will be still returned like normal. .. literalinclude:: ../examples/optional_protected_endpoints.py flask-jwt-extended-4.3.1/docs/options.rst000066400000000000000000000264021413005725200203770ustar00rootroot00000000000000Configuration Options ===================== You can change many options for this extension works via `Flask's Configuration Handling `_. For example: .. code-block:: python app.config["OPTION_NAME"] = option_value General Options: ~~~~~~~~~~~~~~~~ .. py:data:: JWT_TOKEN_LOCATION Where to look for a JWT when processing a request. The available options are ``"headers"``, ``"cookies"``, ``"query_string"``, and ``"json"``. You can pass in a list to check more then one location, for example ``["headers", "cookies"]``. The order of the list sets the precedence of where JWTs will be looked for. This can be overridden on a per-route basis by using the ``locations`` argument in :func:`flask_jwt_extended.jwt_required`. Default: ``"headers"`` .. py:data:: JWT_ACCESS_TOKEN_EXPIRES How long an access token should be valid before it expires. This can be a `datetime.timedelta `_, `dateutil.relativedelta `_, or a number of seconds (``Integer``). If set to ``False`` tokens will never expire. **This is dangerous and should be avoided in most case** This can be overridden on a per token basis by passing the ``expires_delta`` argument to :func:`flask_jwt_extended.create_access_token` Default: ``datetime.timedelta(minutes=15)`` .. py:data:: JWT_REFRESH_TOKEN_EXPIRES How long an access token should be valid before it expires. This can be a `datetime.timedelta `_, `dateutil.relativedelta `_, or a number of seconds (``Integer``). If set to ``False`` tokens will never expire. **This is dangerous and should be avoided in most case** This can be overridden on a per token basis by passing the ``expires_delta`` argument to :func:`flask_jwt_extended.create_refresh_token` Default: ``datetime.timedelta(days=30)`` .. py:data:: JWT_ALGORITHM Which algorithm to sign the JWT with. See `PyJWT `_ for the available algorithms. Default: ``"HS256"`` .. py:data:: JWT_DECODE_ALGORITHMS Which algorithms to use when decoding a JWT. See `PyJWT `_ for the available algorithms. By default this will always be the same algorithm that is defined in ``JWT_ALGORITHM``. Default: ``["HS256"]`` .. py:data:: JWT_SECRET_KEY The secret key used to encode and decode JWTs when using a symmetric signing algorithm (such as ``HS*``). It should be a long random string of bytes, although unicode is accepted too. For example, copy the output of this to your config. .. code-block :: $ python -c 'import os; print(os.urandom(16))' b'_5#y2L"F4Q8z\n\xec]/' If this value is not set, Flask's `SECRET_KEY `_ is used instead. **Do not reveal the secret key when posting questions or committing code.** Default: ``None`` .. py:data:: JWT_PRIVATE_KEY The secret key used to encode JWTs when using an asymmetric signing algorithm (such as ``RS*`` or ``ES*``). The key must be in PEM format. **Do not reveal the secret key when posting questions or committing code.** Default: ``None`` .. py:data:: JWT_PUBLIC_KEY The secret key used to decode JWTs when using an asymmetric signing algorithm (such as ``RS*`` or ``ES*``). The key must be in PEM format. Default: ``None`` .. py:data:: JWT_DECODE_AUDIENCE The string or list of audiences (``aud``) expected in a JWT when decoding it. Default: ``None`` .. py:data:: JWT_ENCODE_AUDIENCE The string or list of audiences (``aud``) for created JWTs. Default: ``None`` .. py:data:: JWT_DECODE_ISSUER The issuer (``iss``) you expect in a JWT when decoding it. Default: ``None`` .. py:data:: JWT_ENCODE_ISSUER The issuer (``iss``) for created JWTs. Default: ``None`` .. py:data:: JWT_ENCODE_NBF The not before (``nbf``) claim which defines that a JWT MUST NOT be accepted for processing during decode. Default: ``True`` .. py:data:: JWT_DECODE_LEEWAY The number of seconds a token will be considered valid before the Not Before Time (`nbf) and after the Expires Time (`exp`). This can be useful when dealing with clock drift between clients. Default: ``0`` .. py:data:: JWT_IDENTITY_CLAIM The claim in a JWT that is used as the source of identity. Default: ``"sub"`` .. py:data:: JWT_ERROR_MESSAGE_KEY The key for error messages in a JSON response returned by this extension. Default: ``"msg"`` Header Options: ~~~~~~~~~~~~~~~ These are only applicable if a route is configured to accept JWTs via headers. .. py:data:: JWT_HEADER_NAME What header should contain the JWT in a request Default: ``"Authorization"`` .. py:data:: JWT_HEADER_TYPE What type of header the JWT is in. If this is an empty string, the header should contain nothing besides the JWT. Default: ``"Bearer"`` Cookie Options: ~~~~~~~~~~~~~~~ These are only applicable if a route is configured to accept JWTs via cookies. .. py:data:: JWT_COOKIE_SECURE Controls if the ``secure`` flag should be placed on cookies created by this extension. If a cookie is marked as ``secure`` it will only be sent by the web browser over an HTTPS connection. **This should always be True in production.** Default: ``False`` .. py:data:: JWT_COOKIE_SAMESITE Controls how the cookies should be sent in a cross-site browsing context. Available options are ``"None"``, ``"Lax"``, or ``"Strict"``. To use ``SameSite=None``, you must set this option to the string ``"None"`` as well as setting ``JWT_COOKIE_SECURE`` to ``True``. See the `MDN docs `_ for more information. Default: ``None``, which is treated as ``"Lax"`` by browsers. .. py:data:: JWT_ACCESS_COOKIE_NAME The name of the cookie that will hold the access token. Default: ``"access_token_cookie"`` .. py:data:: JWT_REFRESH_COOKIE_NAME The name of the cookie that will hold the refresh token. Note: We generally do not recommend using refresh tokens with cookies. See :ref:`Implicit Refreshing With Cookies`. Default: ``"refresh_token_cookie"`` .. py:data:: JWT_ACCESS_COOKIE_PATH The path for the access cookies Default: ``"/"`` .. py:data:: JWT_REFRESH_COOKIE_PATH The path for the refresh cookies Note: We generally do not recommend using refresh tokens with cookies. See :ref:`Implicit Refreshing With Cookies`. Default: ``"/"`` .. py:data:: JWT_COOKIE_DOMAIN Value to use for cross domain cookies. For example, if ``JWT_COOKIE_DOMAIN`` is ``".example.com"``, the cookies will be set so they are readable by the domains www.example.com, foo.example.com etc. Otherwise, a cookie will only be readable by the domain that set it. Default: ``None`` .. py:data:: JWT_SESSION_COOKIE Controls if the cookies will be set as session cookies, which are deleted when the browser is closed. Default: ``True`` .. py:data:: JWT_COOKIE_CSRF_PROTECT Controls if Cross Site Request Forgery (CSRF) protection is enabled when using cookies. **This should always be True in production** Default: ``True`` Cross Site Request Forgery Options ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ These are only applicable if a route is configured to accept JWTs via cookies and ``JWT_COOKIE_CSRF_PROTECT`` is ``True``. .. py:data:: JWT_CSRF_METHODS A list of HTTP methods that we should do CSRF checks on. Default: ``["POST", "PUT", "PATCH", "DELETE"]`` .. py:data:: JWT_ACCESS_CSRF_HEADER_NAME The name of the header on an incoming request that should contain the CSRF double submit token. Default: ``"X-CSRF-TOKEN"`` .. py:data:: JWT_REFRESH_CSRF_HEADER_NAME The name of the header on an incoming request that should contain the CSRF double submit token. Note: We generally do not recommend using refresh tokens with cookies. See :ref:`Implicit Refreshing With Cookies`. Default: ``"X-CSRF-TOKEN"`` .. py:data:: JWT_CSRF_IN_COOKIES Controls if the CSRF double submit token will be stored in additional cookies. If setting this to ``False``, you can use :func:`flask_jwt_extended.get_csrf_token` to get the csrf token from an encoded JWT, and return it to your frontend in whatever way suites your application. Default: ``True`` .. py:data:: JWT_ACCESS_CSRF_COOKIE_NAME The name of the cookie that contains the CSRF double submit token. Only applicable if ``JWT_CSRF_IN_COOKIES`` is ``True`` Default: ``csrf_access_token`` .. py:data:: JWT_REFRESH_CSRF_COOKIE_NAME The name of the cookie that contains the CSRF double submit token. Only applicable if ``JWT_CSRF_IN_COOKIES`` is ``True`` Note: We generally do not recommend using refresh tokens with cookies. See :ref:`Implicit Refreshing With Cookies`. Default: ``csrf_refresh_token`` .. py:data:: JWT_ACCESS_CSRF_COOKIE_PATH The path of the access CSRF double submit cookie. Default: ``"/"`` .. py:data:: JWT_REFRESH_CSRF_COOKIE_PATH The path of the refresh CSRF double submit cookie. Note: We generally do not recommend using refresh tokens with cookies. See :ref:`Implicit Refreshing With Cookies`. Default: ``"/"`` .. py:data:: JWT_CSRF_CHECK_FORM Controls if form data should also be check for the CSRF double submit token. Default: ``False`` .. py:data:: JWT_ACCESS_CSRF_FIELD_NAME Name of the form field that should contain the CSRF double submit token for an access token. Only applicable if ``JWT_CSRF_CHECK_FORM`` is ``True`` Default: ``"csrf_token"`` .. py:data:: JWT_REFRESH_CSRF_FIELD_NAME Name of the form field that should contain the CSRF double submit token for a refresh token. Only applicable if ``JWT_CSRF_CHECK_FORM`` is ``True`` Note: We generally do not recommend using refresh tokens with cookies. See :ref:`Implicit Refreshing With Cookies`. Default: ``"csrf_token"`` Query String Options: ~~~~~~~~~~~~~~~~~~~~~ These are only applicable if a route is configured to accept JWTs via query string. .. py:data:: JWT_QUERY_STRING_NAME What query string parameter should contain the JWT. Default: ``"jwt"`` .. py:data:: JWT_QUERY_STRING_VALUE_PREFIX An optional prefix string that should show up before the JWT in a query string parameter. For example, if this was ``"Bearer "``, the query string should look like ``"/endpoint?jwt=Bearer "`` Default: ``""`` JSON Body Options: ~~~~~~~~~~~~~~~~~~ These are only applicable if a route is configured to accept JWTs via the JSON body. .. py:data:: JWT_JSON_KEY What key should contain the access token in the JSON body of a request. Default: ``"access_token"`` .. py:data:: JWT_REFRESH_JSON_KEY What key should contain the refresh token in the JSON body of a request. Default: ``"access_token"`` flask-jwt-extended-4.3.1/docs/refreshing_tokens.rst000066400000000000000000000074601413005725200224260ustar00rootroot00000000000000Refreshing Tokens ================= In most web applications, it would not be ideal if a user was logged out in the middle of doing something because their JWT expired. Unfortunately we can't just change the expires time on a JWT on each request, as once a JWT is created it cannot be modified. Lets take a look at some options for solving this problem by refreshing JWTs. Implicit Refreshing With Cookies ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ One huge benefit to storing your JWTs in cookies (when your frontend is a website) is that the frontend does not have to handle any logic when it comes to refreshing a token. It can all happen implicitly with the cookies your Flask application sets. The basic idea here is that at the end of every request, we will check if there is a JWT that is close to expiring. If we find a JWT that is nearly expired, we will replace the current cookie containing the JWT with a new JWT that has a longer time until it expires. This is our recommended approach when your frontend is a website. .. literalinclude:: ../examples/implicit_refresh.py Explicit Refreshing With Refresh Tokens ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Alternatively, this extension comes out of the box with refresh token support. A refresh token is a long lived JWT that can only be used to creating new access tokens. You have a couple choices about how to utilize a refresh token. You could store the expires time of your access token on your frontend, and each time you make an API request first check if the current access token is near or already expired, and refresh it as needed. This approach is pretty simple and will work fine in most cases, but do be aware that if your frontend has a clock that is significantly off, you might run into issues. An alternative approach involves making an API request with your access token and then checking the result to see if it worked. If the result of the request is an error message saying that your token is expired, use the refresh token to generate a new access token and redo the request with the new token. This approach will work regardless of the clock on your frontend, but it does require having some potentially more complicated logic. Using refresh tokens is our recommended approach when your frontend is not a website (mobile, api only, etc). .. literalinclude:: ../examples/refresh_tokens.py Making a request with a refresh token looks just like making a request with an access token. Here is an example using `HTTPie `_. .. code-block :: bash $ http POST :5000/refresh Authorization:"Bearer $REFRESH_TOKEN" Token Freshness Pattern ~~~~~~~~~~~~~~~~~~~~~~~ The token freshness pattern is a very simple idea. Every time a user authenticates by providing a username and password, they receive a ``fresh`` access token that can access any route. But after some time, that token should no longer be considered ``fresh``, and some critical or dangerous routes will be blocked until the user verifies their password again. All other routes will still work normally for the user even though their token is no longer ``fresh``. As an example, we might not allow users to change their email address unless they have a ``fresh`` token, but we do allow them use the rest of our Flask application normally. The token freshness pattern is built into this extension, and works seamlessly with both token refreshing strategies discussed above. Lets take a look at this with the explicit refresh example (it will look basically same in the implicit refresh example). .. literalinclude:: ../examples/token_freshness.py We also support marking a token as fresh for a given amount of time after it is created. You can do this by passing a ``datetime.timedelta`` to the ``fresh`` option when creating JWTs: .. code-block :: python create_access_token(identity, fresh=datetime.timedelta(minutes=15)) flask-jwt-extended-4.3.1/docs/token_locations.rst000066400000000000000000000154551413005725200221050ustar00rootroot00000000000000JWT Locations ============= JWTs can be sent in with a request in many different ways. You can control which ways you want to accept JWTs in your Flask application via the ``JWT_TOKEN_LOCATION`` :ref:`configuration option`. You can also override that global configuration on a per route basis via the ``locations`` argument in :func:`~flask_jwt_extended.jwt_required`. .. literalinclude:: ../examples/jwt_locations.py Lets take a look at how you could utilize all of these locations using some javascript in a web browser. Headers ~~~~~~~ Working JWTs via headers is a pretty simple process. All you need to do is store the token when you login, and add the token as a header each time you make a request to a protected route. Logging out is as simple as deleting the token. .. code-block :: javascript async function login() { const response = await fetch('/login_without_cookies', {method: 'post'}); const result = await response.json(); localStorage.setItem('jwt', result.access_token); } function logout() { localStorage.removeItem('jwt'); } async function makeRequestWithJWT() { const options = { method: 'post', headers: { Authorization: `Bearer ${localStorage.getItem('jwt')}`, } }; const response = await fetch('/protected', options); const result = await response.json(); return result; } Cookies ~~~~~~~ Cookies are a fantastic way of handling JWTs if you are using a web browser. They offer some nice benefits compared to the headers approach: * They can be configured to send only over HTTPS. This prevents a JWT from accidentally being sent, and possibly compromised, over an unsecure connection. * They are stored in an http-only cookie, which prevents XSS attacks from being able to steal the underlying JWT. * You Flask application can implicitly refresh JWTs that are close to expiring, which simplifies the logic of keeping active users logged in. More on this in the next section! Of course, when using cookies you also need to do some additional work to prevent Cross Site Request Forgery (CSRF) attacks. In this extension we handle this via something called double submit verification. The basic idea behind double submit verification is that a JWT coming from a cookie will only be considered valid if a special double submit token is also present in the request, and that double submit token must not be something that is automatically sent by a web browser (ie it cannot be another cookie). By default, we accomplish this by setting two cookies when someone logging in. The first cookie contains the JWT, and encoded in that JWT is the double submit token. This cookie is set as http-only, so that it cannot be access via javascript (this is what prevents XSS attacks from being able to steal the JWT). The second cookie we set contains only the same double submit token, but this time in a cookie that is readable by javascript. Whenever a request is made, it needs to include an ``X-CSRF-TOKEN`` header, with the value of the double submit token. If the value in this header does not match the value stored in the JWT, the request is kicked out as invalid. Because the double submit token needs to be present as a header (which wont be automatically sent on a request), and some malicious javascript running on a different domain will not be able to read the cookie containing the double submit token on your website, we have successfully thwarted any CSRF attacks. This does mean that whenever you are making a request, you need to manually include the ``X-CSRF-TOKEN`` header, otherwise your requests will be kicked out as invalid too. Lets look at how to do that: .. code-block :: javascript async function login() { await fetch('/login_with_cookies', {method: 'post'}); } async function logout() { await fetch('/logout_with_cookies', {method: 'post'}); } function getCookie(name) { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) return parts.pop().split(';').shift(); } async function makeRequestWithJWT() { const options = { method: 'post', credentials: 'same-origin', headers: { 'X-CSRF-TOKEN': getCookie('csrf_access_token'), }, }; const response = await fetch('/protected', options); const result = await response.json(); return result; } Note that there are additional CSRF options, such as looking for the double submit token in a form, changing cookie paths, etc, that can be used to tailor things to the needs of your application. See :ref:`Cross Site Request Forgery Options` for details. Query String ~~~~~~~~~~~~~ You can also send in the JWT as part of the query string. However, It is very important to note that in most cases we recommend *NOT* doing this. It can lead to some non-obvious security issues, such as saving the JWT in a browsers history or the JWT being logged in your backend server, which could both potentially lead to a compromised token. However, this feature might provide some limited usefulness, such as sending password reset links, and therefore we support it in this extension. .. code-block :: javascript async function login() { const response = await fetch('/login_without_cookies', {method: 'post'}); const result = await response.json(); localStorage.setItem('jwt', result.access_token); } function logout() { localStorage.removeItem('jwt'); } async function makeRequestWithJWT() { const jwt = localStorage.getItem('jwt') const response = await fetch(`/protected?jwt=${jwt}`, {method: 'post'}); const result = await response.json(); return result; } JSON Body ~~~~~~~~~ This looks very similar to the Headers approach, except that we send the JWT in as part of the JSON Body instead of a header. Be aware that HEAD or GET requests cannot have a JSON body as part of the request, so this only works for actions like POST/PUT/PATCH/DELETE/etc. Sending JWTs in a JSON body is probably not very useful most of the time, but we include the option for it regardless. .. code-block :: javascript async function login() { const response = await fetch('/login_without_cookies', {method: 'post'}); const result = await response.json(); localStorage.setItem('jwt', result.access_token); } function logout() { localStorage.removeItem('jwt'); } // Note that if we change the method to `get` this will blow up with a // "TypeError: Window.fetch: HEAD or GET Request cannot have a body" async function makeRequestWithJWT() { const options = { method: 'post', body: JSON.stringify({access_token: localStorage.getItem('jwt')}), headers: { 'Content-Type': 'application/json', }, }; const response = await fetch('/protected', options); const result = await response.json(); return result; } flask-jwt-extended-4.3.1/docs/v4_upgrade_guide.rst000066400000000000000000000162001413005725200221140ustar00rootroot000000000000004.0.0 Breaking Changes and Upgrade Guide ======================================== This release includes a lot of breaking changes that have been a long time coming, and will require some manual intervention to upgrade your application. Breaking changes are never fun, but I really believe they are for the best. As a result of all these changes, this extension should be simpler to use, provide more flexibility, and allow for easier additions to the API without introducing further breaking changes. Here is everything you will need to be aware of when upgrading to 4.0.0. Encoded JWT Changes (IMPORTANT) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - The ``JWT_USER_CLAIMS`` configuration option has been removed. Now when creating JWTs with additional claims, those claims are put on the top level of the token, insetad of inside the the nested ``user_claims`` dictionary. This has the very important benefit of allowing you to override reserved claims (such as ``nbf``) which was not previously possible in this extension. **IMPORTANT NOTE**: This has the unfortunate side effect that any existing JWTs your application is using will not work correctly if they utilize additional claims. We **strongly** suggest changing your secret key to force all users to get the new format of JWTs. If that is not feasible for your appilication you could build a shim to handle both the old JWTs which store additional claims in the ``user_claims`` key, and the new format where additional claims are now stored at the top level, until all the JWTs have had a chance to cycle to the new format. - The default ``JWT_IDENTITY_CLAIM`` option is now ``sub`` instead of ``identity``. General Changes ~~~~~~~~~~~~~~~ - Dropped support for everything before Python 3.6 (including Python 2). - Requires PyJWT >= 2.0.0. - Depreciation warnings in ``3.25.2`` have been removed and are now errors: - The ``JWT_CSRF_HEADER_NAME`` option has removed. - The ``jwt.expired_token_loader`` will error if the callback does not take an argument for the expired token header and expired token payload. - The ``jwt.decode_key_loader`` will error if the callback does not take an argument for the unverified_headers and the unverified_payload. - Calling ``get_jwt()``, ``get_jwt_header()``, or ``get_jwt_identity()`` will raise a ``RuntimeError`` when called outside of a protected context (ie if you forgot ``@jwt_required()`` or ``verify_jwt_in_request()``). Previously these calls would return ``None``. - Calling ``get_jwt()`` or ``get_jwt_header()`` will return an empty dictionary if called from an optionally protected endpoint. Previously this would return ``None``. - Calling ``get_current_user()`` or ``current_user`` will raise a ``RuntimeError`` if no ``@jwt.user_lookup_loader`` callback is defined. Blacklist Changes ~~~~~~~~~~~~~~~~~ - All occurrences of ``blacklist`` have been renamed to ``blocklist`` - The ``JWT_BLACKLIST_ENABLED`` option has been removed. If you do not want to check a JWT against your blocklist, do not register a callback function with ``@jwt.token_in_blocklist_loader``. - The ``JWT_BLACKLIST_TOKEN_CHECKS`` option has been removed. If you don't want to check a given token type against the blocklist, specifically ignore it in your callback function by checking the ``jwt_payload["type"]`` and short circuiting accordingly. ``jwt_payload["type"]`` will be either ``"access"`` or ``"refresh"``. Callback Function Changes ~~~~~~~~~~~~~~~~~~~~~~~~~ - Renamed ``@jwt.claims_verification_loader`` to ``@jwt.token_verification_loader`` - Renamed ``@jwt.claims_verification_failed_loader`` to ``@jwt.token_verification_failed_loader`` - Renamed ``@jwt.user_claims_loader`` to ``@jwt.additional_claims_loader`` - Renamed ``@jwt.user_in_blacklist_loader`` to ``@jwt.user_in_blocklist_loader`` - Renamed ``@jwt.user_loader_callback_loader`` to ``@jwt.user_lookup_loader`` - Renamed ``@jwt.user_loader_error_loader`` to ``@jwt.user_lookup_error_loader`` - The following callback functions have all been changed to take two arguments. Those arguments are the ``jwt_headers`` and ``jwt_payload``. - ``@jwt.needs_fresh_token_loader`` - ``@jwt.revoked_token_loader`` - ``@jwt.user_lookup_loader`` - ``@jwt.user_lookup_error_loader`` - ``@jwt.expired_token_loader`` - ``@jwt.token_in_blocklist_loader`` - ``@jwt.token_verification_loader`` - ``@jwt.token_verification_failed_loader`` .. code-block :: python @jwt.revoked_token_loader def revoked_token_response(jwt_header, jwt_payload): return jsonify(msg=f"I'm sorry {jwt_payload['sub']} I can't let you do that") - The arguments for ``@jwt.decode_key_loader`` have been reversed to be consistent with the rest of the application. Previously the arguments were ``(jwt_payload, jwt_headers)``. Now they are ``(jwt_headers, jwt_payload)``. API Changes ~~~~~~~~~~~ - All view decorators have been moved to a single decorator: - ``@jwt_required`` is now ``@jwt_required()`` - ``@jwt_optional`` is now ``@jwt_required(optional=True)`` - ``@fresh_jwt_required`` is now ``@jwt_required(fresh=True)`` - ``@jwt_refresh_token_required`` is now ``@jwt_required(refresh=True)`` - All additional ``verify_jwt_in_request`` functions have been moved to a single method: - ``verify_jwt_in_request_optional()`` is now ``verify_jwt_in_request(optional=True)`` - ``verify_jwt_refresh_token_in_request()`` is now ``verify_jwt_in_request(refresh=True)`` - ``verify_fresh_jwt_in_request()`` is now ``verify_jwt_in_request(fresh=True)`` - Renamed ``get_raw_jwt()`` to ``get_jwt()`` - Renamed ``get_raw_jwt_headers()`` to ``get_jwt_headers()`` - Removed ``get_jwt_claims()``. Use ``get_jwt()`` instead. - The ``headers`` argument in ``create_access_token()`` and ``create_refresh_token()`` has been renamed to ``additional_headers``. - If you pass in the ``additional_headers``, it will now be merged with the headers returned by the ``@jwt.additional_headers_loader`` callback, with ties going to the ``additional_headers`` argument. - The ``user_claims`` argument in ``create_access_token()`` and ``create_refresh_token()`` has been renamed to ``additional_claims``. - If you pass in the ``additional_claims`` option, it will now be merged with the claims returned by the ``@jwt.additional_claims_loader`` callback, with ties going to the ``additional_claims`` argument. - The ``JWT_VERIFY_AUDIENCE`` option has been removed. If you do not want to verify the JWT audience (``aud``) claim, simply do not set the ``JWT_DECODE_AUDIENCE`` option. - The ``JWT_CLAIMS_IN_REFRESH_TOKEN`` option has been removed. Additional claims will now always be put in the JWT regardless of if it is an access or refresh tokens. If you don't want additional claims in your refresh tokens, do not include any additional claims when creating the refresh token. New Stuff ~~~~~~~~~ - Add ``locations`` argument to ``@jwt_required()`` and ``verify_jwt_in_request``. This will allow you to override the ``JWT_LOCATIONS`` option on a per route basis. - Revamped and cleaned up documentation. It should be clearer how to work with this extension both on the backend and frontend now. - Lots of code cleanup behind the scenes. flask-jwt-extended-4.3.1/examples/000077500000000000000000000000001413005725200170345ustar00rootroot00000000000000flask-jwt-extended-4.3.1/examples/additional_data_in_access_token.py000066400000000000000000000022771413005725200257260ustar00rootroot00000000000000from flask import Flask from flask import jsonify from flask import request from flask_jwt_extended import create_access_token from flask_jwt_extended import get_jwt from flask_jwt_extended import jwt_required from flask_jwt_extended import JWTManager 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 # You can use the additional_claims argument to either add # custom claims or override default claims in the JWT. additional_claims = {"aud": "some_audience", "foo": "bar"} access_token = create_access_token(username, additional_claims=additional_claims) return jsonify(access_token=access_token) # In a protected view, get the claims you added to the jwt with the # get_jwt() method @app.route("/protected", methods=["GET"]) @jwt_required() def protected(): claims = get_jwt() return jsonify(foo=claims["foo"]) if __name__ == "__main__": app.run() flask-jwt-extended-4.3.1/examples/automatic_user_loading.py000066400000000000000000000052241413005725200241320ustar00rootroot00000000000000from hmac import compare_digest from flask import Flask from flask import jsonify from flask import request from flask_sqlalchemy import SQLAlchemy from flask_jwt_extended import create_access_token from flask_jwt_extended import current_user from flask_jwt_extended import jwt_required from flask_jwt_extended import JWTManager app = Flask(__name__) app.config["JWT_SECRET_KEY"] = "super-secret" # Change this! app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite://" app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False jwt = JWTManager(app) db = SQLAlchemy(app) class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.Text, nullable=False, unique=True) full_name = db.Column(db.Text, nullable=False) # NOTE: In a real application make sure to properly hash and salt passwords def check_password(self, password): return compare_digest(password, "password") # Register a callback function that takes whatever object is passed in as the # identity when creating JWTs and converts it to a JSON serializable format. @jwt.user_identity_loader def user_identity_lookup(user): return user.id # Register a callback function that loads a user from your database whenever # a protected route is accessed. This should return any python object on a # successful lookup, or None if the lookup failed for any reason (for example # if the user has been deleted from the database). @jwt.user_lookup_loader def user_lookup_callback(_jwt_header, jwt_data): identity = jwt_data["sub"] return User.query.filter_by(id=identity).one_or_none() @app.route("/login", methods=["POST"]) def login(): username = request.json.get("username", None) password = request.json.get("password", None) user = User.query.filter_by(username=username).one_or_none() if not user or not user.check_password(password): return jsonify("Wrong username or password"), 401 # Notice that we are passing in the actual sqlalchemy user object here access_token = create_access_token(identity=user) return jsonify(access_token=access_token) @app.route("/who_am_i", methods=["GET"]) @jwt_required() def protected(): # We can now access our sqlalchemy User object via `current_user`. return jsonify( id=current_user.id, full_name=current_user.full_name, username=current_user.username, ) if __name__ == "__main__": db.create_all() db.session.add(User(full_name="Bruce Wayne", username="batman")) db.session.add(User(full_name="Ann Takamaki", username="panther")) db.session.add(User(full_name="Jester Lavore", username="little_sapphire")) db.session.commit() app.run() flask-jwt-extended-4.3.1/examples/blocklist_database.py000066400000000000000000000043711413005725200232250ustar00rootroot00000000000000from datetime import datetime from datetime import timedelta from datetime import timezone from flask import Flask from flask import jsonify from flask_sqlalchemy import SQLAlchemy from flask_jwt_extended import create_access_token from flask_jwt_extended import get_jwt from flask_jwt_extended import jwt_required from flask_jwt_extended import JWTManager app = Flask(__name__) ACCESS_EXPIRES = timedelta(hours=1) app.config["JWT_SECRET_KEY"] = "super-secret" # Change this! app.config["JWT_ACCESS_TOKEN_EXPIRES"] = ACCESS_EXPIRES jwt = JWTManager(app) # We are using an in memory database here as an example. Make sure to use a # database with persistent storage in production! app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite://" app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False db = SQLAlchemy(app) # This could be expanded to fit the needs of your application. For example, # it could track who revoked a JWT, when a token expires, notes for why a # JWT was revoked, an endpoint to un-revoked a JWT, etc. class TokenBlocklist(db.Model): id = db.Column(db.Integer, primary_key=True) jti = db.Column(db.String(36), nullable=False) created_at = db.Column(db.DateTime, nullable=False) # Callback function to check if a JWT exists in the database blocklist @jwt.token_in_blocklist_loader def check_if_token_revoked(jwt_header, jwt_payload): jti = jwt_payload["jti"] token = db.session.query(TokenBlocklist.id).filter_by(jti=jti).scalar() return token is not None @app.route("/login", methods=["POST"]) def login(): access_token = create_access_token(identity="example_user") return jsonify(access_token=access_token) # Endpoint for revoking the current users access token. Saved the unique # identifier (jti) for the JWT into our database. @app.route("/logout", methods=["DELETE"]) @jwt_required() def modify_token(): jti = get_jwt()["jti"] now = datetime.now(timezone.utc) db.session.add(TokenBlocklist(jti=jti, created_at=now)) db.session.commit() return jsonify(msg="JWT revoked") # A blocklisted 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__": db.create_all() app.run() flask-jwt-extended-4.3.1/examples/blocklist_redis.py000066400000000000000000000035721413005725200225710ustar00rootroot00000000000000from datetime import timedelta import redis from flask import Flask from flask import jsonify from flask_jwt_extended import create_access_token from flask_jwt_extended import get_jwt from flask_jwt_extended import jwt_required from flask_jwt_extended import JWTManager ACCESS_EXPIRES = timedelta(hours=1) app = Flask(__name__) app.config["JWT_SECRET_KEY"] = "super-secret" # Change this! app.config["JWT_ACCESS_TOKEN_EXPIRES"] = ACCESS_EXPIRES jwt = JWTManager(app) # Setup our redis connection for storing the blocklisted tokens. You will probably # want your redis instance configured to persist data to disk, so that a restart # does not cause your application to forget that a JWT was revoked. jwt_redis_blocklist = redis.StrictRedis( host="localhost", port=6379, db=0, decode_responses=True ) # Callback function to check if a JWT exists in the redis blocklist @jwt.token_in_blocklist_loader def check_if_token_is_revoked(jwt_header, jwt_payload): jti = jwt_payload["jti"] token_in_redis = jwt_redis_blocklist.get(jti) return token_in_redis is not None @app.route("/login", methods=["POST"]) def login(): access_token = create_access_token(identity="example_user") return jsonify(access_token=access_token) # Endpoint for revoking the current users access token. Save the JWTs unique # identifier (jti) in redis. Also set a Time to Live (TTL) when storing the JWT # so that it will automatically be cleared out of redis after the token expires. @app.route("/logout", methods=["DELETE"]) @jwt_required() def logout(): jti = get_jwt()["jti"] jwt_redis_blocklist.set(jti, "", ex=ACCESS_EXPIRES) return jsonify(msg="Access token revoked") # A blocklisted 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() flask-jwt-extended-4.3.1/examples/custom_decorators.py000066400000000000000000000024101413005725200231420ustar00rootroot00000000000000from functools import wraps from flask import Flask from flask import jsonify from flask_jwt_extended import create_access_token from flask_jwt_extended import get_jwt from flask_jwt_extended import JWTManager from flask_jwt_extended import verify_jwt_in_request 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 the JWT has a claim indicating that this user is # an administrator def admin_required(): def wrapper(fn): @wraps(fn) def decorator(*args, **kwargs): verify_jwt_in_request() claims = get_jwt() if claims["is_administrator"]: return fn(*args, **kwargs) else: return jsonify(msg="Admins only!"), 403 return decorator return wrapper @app.route("/login", methods=["POST"]) def login(): access_token = create_access_token( "admin_user", additional_claims={"is_administrator": True} ) return jsonify(access_token=access_token) @app.route("/protected", methods=["GET"]) @admin_required() def protected(): return jsonify(foo="bar") if __name__ == "__main__": app.run() flask-jwt-extended-4.3.1/examples/implicit_refresh.py000066400000000000000000000041241413005725200227370ustar00rootroot00000000000000from datetime import datetime from datetime import timedelta from datetime import timezone from flask import Flask from flask import jsonify from flask_jwt_extended import create_access_token from flask_jwt_extended import get_jwt from flask_jwt_extended import get_jwt_identity from flask_jwt_extended import jwt_required from flask_jwt_extended import JWTManager from flask_jwt_extended import set_access_cookies from flask_jwt_extended import unset_jwt_cookies app = Flask(__name__) # If true this will only allow the cookies that contain your JWTs to be sent # over https. In production, this should always be set to True app.config["JWT_COOKIE_SECURE"] = False app.config["JWT_TOKEN_LOCATION"] = ["cookies"] app.config["JWT_SECRET_KEY"] = "super-secret" # Change this in your code! app.config["JWT_ACCESS_TOKEN_EXPIRES"] = timedelta(hours=1) jwt = JWTManager(app) # Using an `after_request` callback, we refresh any token that is within 30 # minutes of expiring. Change the timedeltas to match the needs of your application. @app.after_request def refresh_expiring_jwts(response): try: exp_timestamp = get_jwt()["exp"] now = datetime.now(timezone.utc) target_timestamp = datetime.timestamp(now + timedelta(minutes=30)) if target_timestamp > exp_timestamp: access_token = create_access_token(identity=get_jwt_identity()) set_access_cookies(response, access_token) return response except (RuntimeError, KeyError): # Case where there is not a valid JWT. Just return the original respone return response @app.route("/login", methods=["POST"]) def login(): response = jsonify({"msg": "login successful"}) access_token = create_access_token(identity="example_user") set_access_cookies(response, access_token) return response @app.route("/logout", methods=["POST"]) def logout(): response = jsonify({"msg": "logout successful"}) unset_jwt_cookies(response) return response @app.route("/protected") @jwt_required() def protected(): return jsonify(foo="bar") if __name__ == "__main__": app.run() flask-jwt-extended-4.3.1/examples/jwt_locations.py000066400000000000000000000033141413005725200222660ustar00rootroot00000000000000from flask import Flask from flask import jsonify from flask_jwt_extended import create_access_token from flask_jwt_extended import jwt_required from flask_jwt_extended import JWTManager from flask_jwt_extended import set_access_cookies from flask_jwt_extended import unset_jwt_cookies app = Flask(__name__) # Here you can globally configure all the ways you want to allow JWTs to # be sent to your web application. By default, this will be only headers. app.config["JWT_TOKEN_LOCATION"] = ["headers", "cookies", "json", "query_string"] # If true this will only allow the cookies that contain your JWTs to be sent # over https. In production, this should always be set to True app.config["JWT_COOKIE_SECURE"] = False # Change this in your code! app.config["JWT_SECRET_KEY"] = "super-secret" jwt = JWTManager(app) @app.route("/login_without_cookies", methods=["POST"]) def login_without_cookies(): access_token = create_access_token(identity="example_user") return jsonify(access_token=access_token) @app.route("/login_with_cookies", methods=["POST"]) def login_with_cookies(): response = jsonify({"msg": "login successful"}) access_token = create_access_token(identity="example_user") set_access_cookies(response, access_token) return response @app.route("/logout_with_cookies", methods=["POST"]) def logout_with_cookies(): response = jsonify({"msg": "logout successful"}) unset_jwt_cookies(response) return response @app.route("/protected", methods=["GET", "POST"]) @jwt_required() def protected(): return jsonify(foo="bar") @app.route("/only_headers") @jwt_required(locations=["headers"]) def only_headers(): return jsonify(foo="baz") if __name__ == "__main__": app.run() flask-jwt-extended-4.3.1/examples/loaders.py000066400000000000000000000021231413005725200210350ustar00rootroot00000000000000from flask import Flask from flask import jsonify from flask_jwt_extended import create_access_token from flask_jwt_extended import jwt_required from flask_jwt_extended import JWTManager app = Flask(__name__) app.config["JWT_SECRET_KEY"] = "super-secret" # Change this! jwt = JWTManager(app) # Set a callback function to return a custom response whenever an expired # token attempts to access a protected route. This particular callback function # takes the jwt_header and jwt_payload as arguments, and must return a Flask # response. Check the API documentation to see the required argument and return # values for other callback functions. @jwt.expired_token_loader def my_expired_token_callback(jwt_header, jwt_payload): return jsonify(code="dave", err="I can't let you do that"), 401 @app.route("/login", methods=["POST"]) def login(): access_token = create_access_token("example_user") return jsonify(access_token=access_token) @app.route("/protected", methods=["GET"]) @jwt_required() def protected(): return jsonify(hello="world") if __name__ == "__main__": app.run() flask-jwt-extended-4.3.1/examples/oidc.py000066400000000000000000000112461413005725200203300ustar00rootroot00000000000000import json from functools import wraps import config import requests from flask import Flask from flask import jsonify from flask_restful import Api from jwt.algorithms import RSAAlgorithm from flask_jwt_extended import current_user from flask_jwt_extended import get_jwt from flask_jwt_extended import JWTManager from flask_jwt_extended import verify_jwt_in_request # 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_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_lookup_loader def user_lookup_callback(identity): u = User() u.username = identity return u app.run(host="0.0.0.0") flask-jwt-extended-4.3.1/examples/optional_protected_endpoints.py000066400000000000000000000021441413005725200253700ustar00rootroot00000000000000from flask import Flask from flask import jsonify from flask import request from flask_jwt_extended import create_access_token from flask_jwt_extended import get_jwt_identity from flask_jwt_extended import jwt_required from flask_jwt_extended import JWTManager 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 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) @app.route("/optionally_protected", methods=["GET"]) @jwt_required(optional=True) def optionally_protected(): current_identity = get_jwt_identity() if current_identity: return jsonify(logged_in_as=current_identity) else: return jsonify(logged_in_as="anonymous user") if __name__ == "__main__": app.run() flask-jwt-extended-4.3.1/examples/refresh_tokens.py000066400000000000000000000024151413005725200224310ustar00rootroot00000000000000from datetime import timedelta from flask import Flask from flask import jsonify from flask_jwt_extended import create_access_token from flask_jwt_extended import create_refresh_token from flask_jwt_extended import get_jwt_identity from flask_jwt_extended import jwt_required from flask_jwt_extended import JWTManager app = Flask(__name__) app.config["JWT_SECRET_KEY"] = "super-secret" # Change this! app.config["JWT_ACCESS_TOKEN_EXPIRES"] = timedelta(hours=1) app.config["JWT_REFRESH_TOKEN_EXPIRES"] = timedelta(days=30) jwt = JWTManager(app) @app.route("/login", methods=["POST"]) def login(): access_token = create_access_token(identity="example_user") refresh_token = create_refresh_token(identity="example_user") return jsonify(access_token=access_token, refresh_token=refresh_token) # We are using the `refresh=True` options in jwt_required to only allow # refresh tokens to access this route. @app.route("/refresh", methods=["POST"]) @jwt_required(refresh=True) def refresh(): identity = get_jwt_identity() access_token = create_access_token(identity=identity) return jsonify(access_token=access_token) @app.route("/protected", methods=["GET"]) @jwt_required() def protected(): return jsonify(foo="bar") if __name__ == "__main__": app.run() flask-jwt-extended-4.3.1/examples/simple.py000066400000000000000000000024161413005725200207020ustar00rootroot00000000000000from flask import Flask from flask import jsonify from flask import request from flask_jwt_extended import create_access_token from flask_jwt_extended import get_jwt_identity from flask_jwt_extended import jwt_required from flask_jwt_extended import JWTManager app = Flask(__name__) # Setup the Flask-JWT-Extended extension app.config["JWT_SECRET_KEY"] = "super-secret" # Change this! jwt = JWTManager(app) # Create a route to authenticate your users and return JWTs. The # create_access_token() function is used to actually generate the JWT. @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) # Protect a route with jwt_required, which will kick out requests # without a valid JWT present. @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() flask-jwt-extended-4.3.1/examples/token_freshness.py000066400000000000000000000033341413005725200226110ustar00rootroot00000000000000from datetime import timedelta from flask import Flask from flask import jsonify from flask import request from flask_jwt_extended import create_access_token from flask_jwt_extended import create_refresh_token from flask_jwt_extended import get_jwt_identity from flask_jwt_extended import jwt_required from flask_jwt_extended import JWTManager app = Flask(__name__) app.config["JWT_SECRET_KEY"] = "super-secret" # Change this! app.config["JWT_ACCESS_TOKEN_EXPIRES"] = timedelta(hours=1) app.config["JWT_REFRESH_TOKEN_EXPIRES"] = timedelta(days=30) jwt = JWTManager(app) # We verify the users password here, so we are returning a fresh access 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 access_token = create_access_token(identity="example_user", fresh=True) refresh_token = create_refresh_token(identity="example_user") return jsonify(access_token=access_token, refresh_token=refresh_token) # If we are refreshing a token here we have not verified the users password in # a while, so mark the newly created access token as not fresh @app.route("/refresh", methods=["POST"]) @jwt_required(refresh=True) def refresh(): identity = get_jwt_identity() access_token = create_access_token(identity=identity, fresh=False) return jsonify(access_token=access_token) # Only allow fresh JWTs to access this route with the `fresh=True` arguement. @app.route("/protected", methods=["GET"]) @jwt_required(fresh=True) def protected(): return jsonify(foo="bar") if __name__ == "__main__": app.run() flask-jwt-extended-4.3.1/flask_jwt_extended/000077500000000000000000000000001413005725200210625ustar00rootroot00000000000000flask-jwt-extended-4.3.1/flask_jwt_extended/__init__.py000066400000000000000000000014061413005725200231740ustar00rootroot00000000000000from .jwt_manager import JWTManager from .utils import create_access_token from .utils import create_refresh_token from .utils import current_user from .utils import decode_token from .utils import get_csrf_token from .utils import get_current_user from .utils import get_jti from .utils import get_jwt from .utils import get_jwt_header from .utils import get_jwt_identity from .utils import get_jwt_request_location from .utils import get_unverified_jwt_headers from .utils import set_access_cookies from .utils import set_refresh_cookies from .utils import unset_access_cookies from .utils import unset_jwt_cookies from .utils import unset_refresh_cookies from .view_decorators import jwt_required from .view_decorators import verify_jwt_in_request __version__ = "4.3.1" flask-jwt-extended-4.3.1/flask_jwt_extended/config.py000066400000000000000000000215021413005725200227010ustar00rootroot00000000000000from collections.abc import Sequence from collections.abc import Set from datetime import datetime from datetime import timedelta from datetime import timezone from flask import current_app from jwt.algorithms import requires_cryptography 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 query_string_value_prefix(self): return current_app.config["JWT_QUERY_STRING_VALUE_PREFIX"] @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"] @property def access_csrf_header_name(self): return current_app.config["JWT_ACCESS_CSRF_HEADER_NAME"] @property def refresh_csrf_header_name(self): return current_app.config["JWT_REFRESH_CSRF_HEADER_NAME"] @property def csrf_check_form(self): return current_app.config["JWT_CSRF_CHECK_FORM"] @property def access_csrf_field_name(self): return current_app.config["JWT_ACCESS_CSRF_FIELD_NAME"] @property def refresh_csrf_field_name(self): return current_app.config["JWT_REFRESH_CSRF_FIELD_NAME"] @property def access_expires(self): delta = current_app.config["JWT_ACCESS_TOKEN_EXPIRES"] if type(delta) is int: delta = timedelta(seconds=delta) if delta is not False: try: delta + datetime.now(timezone.utc) except TypeError as e: err = ( "must be able to add JWT_ACCESS_TOKEN_EXPIRES to datetime.datetime" ) raise RuntimeError(err) from e return delta @property def refresh_expires(self): delta = current_app.config["JWT_REFRESH_TOKEN_EXPIRES"] if type(delta) is int: delta = timedelta(seconds=delta) if delta is not False: try: delta + datetime.now(timezone.utc) except TypeError as e: err = ( "must be able to add JWT_REFRESH_TOKEN_EXPIRES to datetime.datetime" ) raise RuntimeError(err) from 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 _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 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 decode_audience(self): return current_app.config["JWT_DECODE_AUDIENCE"] @property def encode_audience(self): return current_app.config["JWT_ENCODE_AUDIENCE"] @property def encode_issuer(self): return current_app.config["JWT_ENCODE_ISSUER"] @property def decode_issuer(self): return current_app.config["JWT_DECODE_ISSUER"] @property def leeway(self): return current_app.config["JWT_DECODE_LEEWAY"] @property def encode_nbf(self): return current_app.config["JWT_ENCODE_NBF"] config = _Config() flask-jwt-extended-4.3.1/flask_jwt_extended/default_callbacks.py000066400000000000000000000107341413005725200250640ustar00rootroot00000000000000""" 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_additional_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_blocklist_callback(jwt_headers, jwt_data): return False def default_jwt_headers_callback(default_headers): """ By default header typically consists of two parts: the type of the token, which is JWT, and the signing algorithm being used, such as HMAC SHA256 or RSA. But we don't set the default header here we set it as empty which further by default set while encoding the token :return: default we set None here """ 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_jwt_header, _expired_jwt_data): """ 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(jwt_header, jwt_data): """ 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(jwt_header, jwt_data): """ 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_lookup_error_callback(_jwt_header, jwt_data): """ By default, if a user_lookup callback is defined and the callback function returns None, we return a general error message with a 401 status code """ identity = jwt_data[config.identity_claim_key] result = {config.error_msg_key: "Error loading the user {}".format(identity)} return jsonify(result), 401 def default_token_verification_callback(_jwt_header, _jwt_data): """ By default, we do not do any verification of the user claims. """ return True def default_token_verification_failed_callback(_jwt_header, _jwt_data): """ 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(jwt_header, jwt_data): """ 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 flask-jwt-extended-4.3.1/flask_jwt_extended/exceptions.py000066400000000000000000000044201413005725200236150ustar00rootroot00000000000000class 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 InvalidQueryParamError(JWTExtendedException): """ An error when a query string param is not in the correct format """ 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 """ def __init__(self, jwt_header, jwt_data): super().__init__("Token has been revoked") self.jwt_header = jwt_header self.jwt_data = jwt_data class FreshTokenRequired(JWTExtendedException): """ Error raised when a valid, non-fresh JWT attempt to access an endpoint protected by fresh_jwt_required """ def __init__(self, message, jwt_header, jwt_data): super().__init__(message) self.jwt_header = jwt_header self.jwt_data = jwt_data class UserLookupError(JWTExtendedException): """ Error raised when a user_lookup callback function returns None, indicating that it cannot or will not load a user for the given identity. """ def __init__(self, message, jwt_header, jwt_data): super().__init__(message) self.jwt_header = jwt_header self.jwt_data = jwt_data class UserClaimsVerificationError(JWTExtendedException): """ Error raised when the claims_verification_callback function returns False, indicating that the expected user claims are invalid """ def __init__(self, message, jwt_header, jwt_data): super().__init__(message) self.jwt_header = jwt_header self.jwt_data = jwt_data flask-jwt-extended-4.3.1/flask_jwt_extended/internal_utils.py000066400000000000000000000030621413005725200244710ustar00rootroot00000000000000from flask import current_app from flask_jwt_extended.exceptions import RevokedTokenError from flask_jwt_extended.exceptions import UserClaimsVerificationError from flask_jwt_extended.exceptions import WrongTokenError 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" ) from None def has_user_lookup(): jwt_manager = get_jwt_manager() return jwt_manager._user_lookup_callback is not None def user_lookup(*args, **kwargs): jwt_manager = get_jwt_manager() return jwt_manager._user_lookup_callback(*args, **kwargs) def verify_token_type(decoded_token, refresh): if not refresh and decoded_token["type"] == "refresh": raise WrongTokenError("Only non-refresh tokens are allowed") elif refresh and decoded_token["type"] != "refresh": raise WrongTokenError("Only refresh tokens are allowed") def verify_token_not_blocklisted(jwt_header, jwt_data): jwt_manager = get_jwt_manager() if jwt_manager._token_in_blocklist_callback(jwt_header, jwt_data): raise RevokedTokenError(jwt_header, jwt_data) def custom_verification_for_token(jwt_header, jwt_data): jwt_manager = get_jwt_manager() if not jwt_manager._token_verification_callback(jwt_header, jwt_data): error_msg = "User claims verification failed" raise UserClaimsVerificationError(error_msg, jwt_header, jwt_data) flask-jwt-extended-4.3.1/flask_jwt_extended/jwt_manager.py000066400000000000000000000534121413005725200237370ustar00rootroot00000000000000import datetime import jwt from jwt import DecodeError from jwt import ExpiredSignatureError from jwt import InvalidAudienceError from jwt import InvalidIssuerError from jwt import InvalidTokenError from jwt import MissingRequiredClaimError from flask_jwt_extended.config import config from flask_jwt_extended.default_callbacks import default_additional_claims_callback from flask_jwt_extended.default_callbacks import default_blocklist_callback from flask_jwt_extended.default_callbacks import default_decode_key_callback from flask_jwt_extended.default_callbacks import default_encode_key_callback from flask_jwt_extended.default_callbacks import default_expired_token_callback from flask_jwt_extended.default_callbacks import default_invalid_token_callback from flask_jwt_extended.default_callbacks import default_jwt_headers_callback from flask_jwt_extended.default_callbacks import default_needs_fresh_token_callback from flask_jwt_extended.default_callbacks import default_revoked_token_callback from flask_jwt_extended.default_callbacks import default_token_verification_callback from flask_jwt_extended.default_callbacks import ( default_token_verification_failed_callback, ) from flask_jwt_extended.default_callbacks import default_unauthorized_callback from flask_jwt_extended.default_callbacks import default_user_identity_callback from flask_jwt_extended.default_callbacks import default_user_lookup_error_callback from flask_jwt_extended.exceptions import CSRFError from flask_jwt_extended.exceptions import FreshTokenRequired from flask_jwt_extended.exceptions import InvalidHeaderError from flask_jwt_extended.exceptions import InvalidQueryParamError from flask_jwt_extended.exceptions import JWTDecodeError from flask_jwt_extended.exceptions import NoAuthorizationError from flask_jwt_extended.exceptions import RevokedTokenError from flask_jwt_extended.exceptions import UserClaimsVerificationError from flask_jwt_extended.exceptions import UserLookupError from flask_jwt_extended.exceptions import WrongTokenError from flask_jwt_extended.tokens import _decode_jwt from flask_jwt_extended.tokens import _encode_jwt 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: The Flask Application object """ # Register the default error handler callback methods. These can be # overridden with the appropriate loader decorators self._decode_key_callback = default_decode_key_callback self._encode_key_callback = default_encode_key_callback self._expired_token_callback = default_expired_token_callback self._invalid_token_callback = default_invalid_token_callback self._jwt_additional_header_callback = default_jwt_headers_callback self._needs_fresh_token_callback = default_needs_fresh_token_callback self._revoked_token_callback = default_revoked_token_callback self._token_in_blocklist_callback = default_blocklist_callback self._token_verification_callback = default_token_verification_callback self._unauthorized_callback = default_unauthorized_callback self._user_claims_callback = default_additional_claims_callback self._user_identity_callback = default_user_identity_callback self._user_lookup_callback = None self._user_lookup_error_callback = default_user_lookup_error_callback self._token_verification_failed_callback = ( default_token_verification_failed_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: The Flask Application object """ # 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): @app.errorhandler(CSRFError) def handle_csrf_error(e): return self._unauthorized_callback(str(e)) @app.errorhandler(DecodeError) def handle_decode_error(e): return self._invalid_token_callback(str(e)) @app.errorhandler(ExpiredSignatureError) def handle_expired_error(e): return self._expired_token_callback(e.jwt_header, e.jwt_data) @app.errorhandler(FreshTokenRequired) def handle_fresh_token_required(e): return self._needs_fresh_token_callback(e.jwt_header, e.jwt_data) @app.errorhandler(MissingRequiredClaimError) def handle_missing_required_claim_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(InvalidHeaderError) 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(NoAuthorizationError) def handle_auth_error(e): return self._unauthorized_callback(str(e)) @app.errorhandler(InvalidQueryParamError) def handle_invalid_query_param_error(e): return self._invalid_token_callback(str(e)) @app.errorhandler(RevokedTokenError) def handle_revoked_token_error(e): return self._revoked_token_callback(e.jwt_header, e.jwt_data) @app.errorhandler(UserClaimsVerificationError) def handle_failed_token_verification(e): return self._token_verification_failed_callback(e.jwt_header, e.jwt_data) @app.errorhandler(UserLookupError) def handler_user_lookup_error(e): return self._user_lookup_error_callback(e.jwt_header, e.jwt_data) @app.errorhandler(WrongTokenError) def handle_wrong_token_error(e): return self._invalid_token_callback(str(e)) @staticmethod def _set_default_configuration_options(app): app.config.setdefault( "JWT_ACCESS_TOKEN_EXPIRES", datetime.timedelta(minutes=15) ) app.config.setdefault("JWT_ACCESS_COOKIE_NAME", "access_token_cookie") app.config.setdefault("JWT_ACCESS_COOKIE_PATH", "/") app.config.setdefault("JWT_ACCESS_CSRF_COOKIE_NAME", "csrf_access_token") app.config.setdefault("JWT_ACCESS_CSRF_COOKIE_PATH", "/") app.config.setdefault("JWT_ACCESS_CSRF_FIELD_NAME", "csrf_token") app.config.setdefault("JWT_ACCESS_CSRF_HEADER_NAME", "X-CSRF-TOKEN") app.config.setdefault("JWT_ALGORITHM", "HS256") app.config.setdefault("JWT_COOKIE_CSRF_PROTECT", True) app.config.setdefault("JWT_COOKIE_DOMAIN", None) app.config.setdefault("JWT_COOKIE_SAMESITE", None) app.config.setdefault("JWT_COOKIE_SECURE", False) app.config.setdefault("JWT_CSRF_CHECK_FORM", False) app.config.setdefault("JWT_CSRF_IN_COOKIES", True) app.config.setdefault("JWT_CSRF_METHODS", ["POST", "PUT", "PATCH", "DELETE"]) app.config.setdefault("JWT_DECODE_ALGORITHMS", None) 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_ENCODE_AUDIENCE", None) app.config.setdefault("JWT_ENCODE_ISSUER", None) app.config.setdefault("JWT_ERROR_MESSAGE_KEY", "msg") app.config.setdefault("JWT_HEADER_NAME", "Authorization") app.config.setdefault("JWT_HEADER_TYPE", "Bearer") app.config.setdefault("JWT_IDENTITY_CLAIM", "sub") app.config.setdefault("JWT_JSON_KEY", "access_token") app.config.setdefault("JWT_PRIVATE_KEY", None) app.config.setdefault("JWT_PUBLIC_KEY", None) app.config.setdefault("JWT_QUERY_STRING_NAME", "jwt") app.config.setdefault("JWT_QUERY_STRING_VALUE_PREFIX", "") app.config.setdefault("JWT_REFRESH_COOKIE_NAME", "refresh_token_cookie") app.config.setdefault("JWT_REFRESH_COOKIE_PATH", "/") app.config.setdefault("JWT_REFRESH_CSRF_COOKIE_NAME", "csrf_refresh_token") app.config.setdefault("JWT_REFRESH_CSRF_COOKIE_PATH", "/") app.config.setdefault("JWT_REFRESH_CSRF_FIELD_NAME", "csrf_token") app.config.setdefault("JWT_REFRESH_CSRF_HEADER_NAME", "X-CSRF-TOKEN") app.config.setdefault("JWT_REFRESH_JSON_KEY", "refresh_token") app.config.setdefault("JWT_REFRESH_TOKEN_EXPIRES", datetime.timedelta(days=30)) app.config.setdefault("JWT_SECRET_KEY", None) app.config.setdefault("JWT_SESSION_COOKIE", True) app.config.setdefault("JWT_TOKEN_LOCATION", ("headers",)) app.config.setdefault("JWT_ENCODE_NBF", True) def additional_claims_loader(self, callback): """ This decorator sets the callback function used to add additional claims when creating a JWT. The claims returned by this function will be merged with any claims passed in via the ``additional_claims`` argument to :func:`~flask_jwt_extended.create_access_token` or :func:`~flask_jwt_extended.create_refresh_token`. The decorated function must take **one** argument. The argument is the identity that was used when creating a JWT. The decorated function must return a dictionary of claims to add to the JWT. """ self._user_claims_callback = callback return callback def additional_headers_loader(self, callback): """ This decorator sets the callback function used to add additional headers when creating a JWT. The headers returned by this function will be merged with any headers passed in via the ``additional_headers`` argument to :func:`~flask_jwt_extended.create_access_token` or :func:`~flask_jwt_extended.create_refresh_token`. The decorated function must take **one** argument. The argument is the identity that was used when creating a JWT. The decorated function must return a dictionary of headers to add to the JWT. """ self._jwt_additional_header_callback = callback return callback def decode_key_loader(self, callback): """ This decorator sets the callback function for dynamically setting the JWT decode key based on the **UNVERIFIED** contents of the token. Think carefully before using this functionality, in most cases you probably don't need it. The decorated function must take **two** arguments. The first argument is a dictionary containing the header data of the unverified JWT. The second argument is a dictionary containing the payload data of the unverified JWT. The decorated function must return a *string* that is used to decode and verify the token. """ self._decode_key_callback = callback return callback def encode_key_loader(self, callback): """ This decorator sets the callback function for dynamically setting the JWT encode key based on the tokens identity. Think carefully before using this functionality, in most cases you probably don't need it. The decorated function must take **one** argument. The argument is the identity used to create this JWT. The decorated function must return a *string* which is the secrete key used to encode the JWT. """ self._encode_key_callback = callback return callback def expired_token_loader(self, callback): """ This decorator sets the callback function for returning a custom response when an expired JWT is encountered. The decorated function must take **two** arguments. The first argument is a dictionary containing the header data of the JWT. The second argument is a dictionary containing the payload data of the JWT. The decorated function must return a Flask Response. """ self._expired_token_callback = callback return callback def invalid_token_loader(self, callback): """ This decorator sets the callback function for returning a custom response when an invalid JWT is encountered. This decorator sets the callback function that will be used if an invalid JWT attempts to access a protected endpoint. The decorated function must take **one** argument. The argument is a string which contains the reason why a token is invalid. The decorated function must return a Flask Response. """ self._invalid_token_callback = callback return callback def needs_fresh_token_loader(self, callback): """ This decorator sets the callback function for returning a custom response when a valid and non-fresh token is used on an endpoint that is marked as ``fresh=True``. The decorated function must take **two** arguments. The first argument is a dictionary containing the header data of the JWT. The second argument is a dictionary containing the payload data of the JWT. The decorated function must return a Flask Response. """ self._needs_fresh_token_callback = callback return callback def revoked_token_loader(self, callback): """ This decorator sets the callback function for returning a custom response when a revoked token is encountered. The decorated function must take **two** arguments. The first argument is a dictionary containing the header data of the JWT. The second argument is a dictionary containing the payload data of the JWT. The decorated function must return a Flask Response. """ self._revoked_token_callback = callback return callback def token_in_blocklist_loader(self, callback): """ This decorator sets the callback function used to check if a JWT has been revoked. The decorated function must take **two** arguments. The first argument is a dictionary containing the header data of the JWT. The second argument is a dictionary containing the payload data of the JWT. The decorated function must be return ``True`` if the token has been revoked, ``False`` otherwise. """ self._token_in_blocklist_callback = callback return callback def token_verification_failed_loader(self, callback): """ This decorator sets the callback function used to return a custom response when the claims verification check fails. The decorated function must take **two** arguments. The first argument is a dictionary containing the header data of the JWT. The second argument is a dictionary containing the payload data of the JWT. The decorated function must return a Flask Response. """ self._token_verification_failed_callback = callback return callback def token_verification_loader(self, callback): """ This decorator sets the callback function used for custom verification of a valid JWT. The decorated function must take **two** arguments. The first argument is a dictionary containing the header data of the JWT. The second argument is a dictionary containing the payload data of the JWT. The decorated function must return ``True`` if the token is valid, or ``False`` otherwise. """ self._token_verification_callback = callback return callback def unauthorized_loader(self, callback): """ This decorator sets the callback function used to return a custom response when no JWT is present. The decorated function must take **one** argument. The argument is a string that explains why the JWT could not be found. The decorated function must return a Flask Response. """ self._unauthorized_callback = callback return callback def user_identity_loader(self, callback): """ This decorator sets the callback function used to convert an identity to a JSON serializable format when creating JWTs. This is useful for using objects (such as SQLAlchemy instances) as the identity when creating your tokens. The decorated function must take **one** argument. The argument is the identity that was used when creating a JWT. The decorated function must return JSON serializable data. """ self._user_identity_callback = callback return callback def user_lookup_loader(self, callback): """ This decorator sets the callback function used to convert a JWT into a python object that can be used in a protected endpoint. This is useful for automatically loading a SQLAlchemy instance based on the contents of the JWT. The object returned from this function can be accessed via :attr:`~flask_jwt_extended.current_user` or :meth:`~flask_jwt_extended.get_current_user` The decorated function must take **two** arguments. The first argument is a dictionary containing the header data of the JWT. The second argument is a dictionary containing the payload data of the JWT. The decorated function can return any python object, which can then be accessed in a protected endpoint. If an object cannot be loaded, for example if a user has been deleted from your database, ``None`` must be returned to indicate that an error occurred loading the user. """ self._user_lookup_callback = callback return callback def user_lookup_error_loader(self, callback): """ This decorator sets the callback function used to return a custom response when loading a user via :meth:`~flask_jwt_extended.JWTManager.user_lookup_loader` fails. The decorated function must take **two** arguments. The first argument is a dictionary containing the header data of the JWT. The second argument is a dictionary containing the payload data of the JWT. The decorated function must return a Flask Response. """ self._user_lookup_error_callback = callback return callback def _encode_jwt_from_config( self, identity, token_type, claims=None, fresh=False, expires_delta=None, headers=None, ): header_overrides = self._jwt_additional_header_callback(identity) if headers is not None: header_overrides.update(headers) claim_overrides = self._user_claims_callback(identity) if claims is not None: claim_overrides.update(claims) if expires_delta is None: if token_type == "access": expires_delta = config.access_expires else: expires_delta = config.refresh_expires return _encode_jwt( algorithm=config.algorithm, audience=config.encode_audience, claim_overrides=claim_overrides, csrf=config.csrf_protect, expires_delta=expires_delta, fresh=fresh, header_overrides=header_overrides, identity=self._user_identity_callback(identity), identity_claim_key=config.identity_claim_key, issuer=config.encode_issuer, json_encoder=config.json_encoder, secret=self._encode_key_callback(identity), token_type=token_type, nbf=config.encode_nbf, ) def _decode_jwt_from_config( self, encoded_token, csrf_value=None, allow_expired=False ): unverified_claims = jwt.decode( encoded_token, algorithms=config.decode_algorithms, options={"verify_signature": False}, ) unverified_headers = jwt.get_unverified_header(encoded_token) secret = self._decode_key_callback(unverified_headers, unverified_claims) kwargs = { "algorithms": config.decode_algorithms, "audience": config.decode_audience, "csrf_value": csrf_value, "encoded_token": encoded_token, "identity_claim_key": config.identity_claim_key, "issuer": config.decode_issuer, "leeway": config.leeway, "secret": secret, "verify_aud": config.decode_audience is not None, } try: return _decode_jwt(**kwargs, allow_expired=allow_expired) except ExpiredSignatureError as e: e.jwt_header = unverified_headers e.jwt_data = _decode_jwt(**kwargs, allow_expired=True) raise flask-jwt-extended-4.3.1/flask_jwt_extended/tokens.py000066400000000000000000000050601413005725200227400ustar00rootroot00000000000000import uuid from datetime import datetime from datetime import timedelta from datetime import timezone from hmac import compare_digest import jwt from flask_jwt_extended.exceptions import CSRFError from flask_jwt_extended.exceptions import JWTDecodeError def _encode_jwt( algorithm, audience, claim_overrides, csrf, expires_delta, fresh, header_overrides, identity, identity_claim_key, issuer, json_encoder, secret, token_type, nbf, ): now = datetime.now(timezone.utc) if isinstance(fresh, timedelta): fresh = datetime.timestamp(now + fresh) token_data = { "fresh": fresh, "iat": now, "jti": str(uuid.uuid4()), "type": token_type, identity_claim_key: identity, } if nbf: token_data["nbf"] = now if csrf: token_data["csrf"] = str(uuid.uuid4()) if audience: token_data["aud"] = audience if issuer: token_data["iss"] = issuer if expires_delta: token_data["exp"] = now + expires_delta if claim_overrides: token_data.update(claim_overrides) return jwt.encode( token_data, secret, algorithm, json_encoder=json_encoder, headers=header_overrides, ) def _decode_jwt( algorithms, allow_expired, audience, csrf_value, encoded_token, identity_claim_key, issuer, leeway, secret, verify_aud, ): options = {"verify_aud": verify_aud} if allow_expired: options["verify_exp"] = False # This call verifies the ext, iat, and nbf claims # This optionally verifies the exp and aud claims if enabled decoded_token = jwt.decode( encoded_token, secret, algorithms=algorithms, audience=audience, issuer=issuer, leeway=leeway, options=options, ) # Make sure that any custom claims we expect in the token are present if identity_claim_key not in decoded_token: raise JWTDecodeError("Missing claim: {}".format(identity_claim_key)) if "type" not in decoded_token: decoded_token["type"] = "access" if "fresh" not in decoded_token: decoded_token["fresh"] = False if "jti" not in decoded_token: decoded_token["jti"] = None if csrf_value: if "csrf" not in decoded_token: raise JWTDecodeError("Missing claim: csrf") if not compare_digest(decoded_token["csrf"], csrf_value): raise CSRFError("CSRF double submit tokens do not match") return decoded_token flask-jwt-extended-4.3.1/flask_jwt_extended/utils.py000066400000000000000000000360411413005725200226000ustar00rootroot00000000000000import jwt from flask import _request_ctx_stack from werkzeug.local import LocalProxy from flask_jwt_extended.config import config from flask_jwt_extended.internal_utils import get_jwt_manager # Proxy to access the current user current_user = LocalProxy(lambda: get_current_user()) def get_jwt(): """ In a protected endpoint, this will return the python dictionary which has the payload of the JWT that is accessing the endpoint. If no JWT is present due to ``jwt_required(optional=True)``, an empty dictionary is returned. :return: The payload (claims) of the JWT in the current request """ decoded_jwt = getattr(_request_ctx_stack.top, "jwt", None) if decoded_jwt is None: raise RuntimeError( "You must call `@jwt_required()` or `verify_jwt_in_request()` " "before using this method" ) return decoded_jwt def get_jwt_header(): """ In a protected endpoint, this will return the python dictionary which has the header of the JWT that is accessing the endpoint. If no JWT is present due to ``jwt_required(optional=True)``, an empty dictionary is returned. :return: The headers of the JWT in the current request """ decoded_header = getattr(_request_ctx_stack.top, "jwt_header", None) if decoded_header is None: raise RuntimeError( "You must call `@jwt_required()` or `verify_jwt_in_request()` " "before using this method" ) return decoded_header def get_jwt_identity(): """ In a protected endpoint, this will return the identity of the JWT that is accessing the endpoint. If no JWT is present due to ``jwt_required(optional=True)``, ``None`` is returned. :return: The identity of the JWT in the current request """ return get_jwt().get(config.identity_claim_key, None) def get_jwt_request_location(): """ In a protected endpoint, this will return the "location" at which the JWT that is accessing the endpoint was found--e.g., "cookies", "query-string", "headers", or "json". If no JWT is present due to ``jwt_required(optional=True)``, None is returned. :return: The location of the JWT in the current request; e.g., cookies", "query-string", "headers", or "json" """ location = getattr(_request_ctx_stack.top, "jwt_location", None) return location def get_current_user(): """ In a protected endpoint, this will return the user object for the JWT that is accessing the endpoint. This is only usable if :meth:`~flask_jwt_extended.JWTManager.user_lookup_loader` is configured. If the user loader callback is not being used, this will raise an error. If no JWT is present due to ``jwt_required(optional=True)``, ``None`` is returned. :return: The current user object for the JWT in the current request """ get_jwt() # Raise an error if not in a decorated context jwt_user_dict = getattr(_request_ctx_stack.top, "jwt_user", None) if jwt_user_dict is None: raise RuntimeError( "You must provide a `@jwt.user_lookup_loader` callback to use " "this method" ) return jwt_user_dict["loaded_user"] 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. This will not fire the user loader callbacks, save the token for access in protected endpoints, checked if a token is revoked, etc. This is puerly used to insure that a JWT is valid. :param encoded_token: The encoded JWT to decode. :param csrf_value: Expected CSRF double submit value (optional). :param allow_expired: If ``True``, do not raise an error if the JWT is expired. Defaults to ``False`` :return: Dictionary containing the payload of the JWT decoded JWT. """ jwt_manager = get_jwt_manager() return jwt_manager._decode_jwt_from_config(encoded_token, csrf_value, allow_expired) def create_access_token( identity, fresh=False, expires_delta=None, additional_claims=None, additional_headers=None, ): """ Create a new access token. :param identity: The identity of this token. It can be any data that is json serializable. You can use :meth:`~flask_jwt_extended.JWTManager.user_identity_loader` to define a callback function to convert any object passed in into a json serializable format. :param fresh: If this token should be marked as fresh, and can thus access endpoints protected with ``@jwt_required(fresh=True)``. Defaults to ``False``. This value can also be a ``datetime.timedelta``, which 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 additional_claims: Optional. A hash of claims to include in the access token. These claims are merged into the default claims (exp, iat, etc) and claims returned from the :meth:`~flask_jwt_extended.JWTManager.additional_claims_loader` callback. On conflict, these claims take presidence. :param headers: Optional. A hash of headers to include in the access token. These headers are merged into the default headers (alg, typ) and headers returned from the :meth:`~flask_jwt_extended.JWTManager.additional_headers_loader` callback. On conflict, these headers take presidence. :return: An encoded access token """ jwt_manager = get_jwt_manager() return jwt_manager._encode_jwt_from_config( claims=additional_claims, expires_delta=expires_delta, fresh=fresh, headers=additional_headers, identity=identity, token_type="access", ) def create_refresh_token( identity, expires_delta=None, additional_claims=None, additional_headers=None ): """ Create a new refresh token. :param identity: The identity of this token. It can be any data that is json serializable. You can use :meth:`~flask_jwt_extended.JWTManager.user_identity_loader` to define a callback function to convert any object passed in into a json serializable format. :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 additional_claims: Optional. A hash of claims to include in the refresh token. These claims are merged into the default claims (exp, iat, etc) and claims returned from the :meth:`~flask_jwt_extended.JWTManager.additional_claims_loader` callback. On conflict, these claims take presidence. :param headers: Optional. A hash of headers to include in the refresh token. These headers are merged into the default headers (alg, typ) and headers returned from the :meth:`~flask_jwt_extended.JWTManager.additional_headers_loader` callback. On conflict, these headers take presidence. :return: An encoded refresh token """ jwt_manager = get_jwt_manager() return jwt_manager._encode_jwt_from_config( claims=additional_claims, expires_delta=expires_delta, fresh=False, headers=additional_headers, identity=identity, token_type="refresh", ) def get_unverified_jwt_headers(encoded_token): """ Returns the Headers of an encoded JWT without verifying the signature of the JWT. :param encoded_token: The encoded JWT to get the Header from. :return: JWT header parameters as python dict() """ return jwt.get_unverified_header(encoded_token) 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: The JTI (unique identifier) of a JWT. """ return decode_token(encoded_token).get("jti") 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 (string) """ token = decode_token(encoded_token) return token["csrf"] def set_access_cookies(response, encoded_access_token, max_age=None, domain=None): """ Modifiy a Flask Response to set a cookie containing the access JWT. Also sets the corresponding CSRF cookies if ``JWT_CSRF_IN_COOKIES`` is ``True`` (see :ref:`Configuration Options`) :param response: A Flask Response object. :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). :param domain: The domain of the cookie. If this is None, it will use the ``JWT_COOKIE_DOMAIN`` option (see :ref:`Configuration Options`). Otherwise, it will use this as the cookies ``domain`` and the JWT_COOKIE_DOMAIN option will be ignored. """ 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=domain or 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=get_csrf_token(encoded_access_token), max_age=max_age or config.cookie_max_age, secure=config.cookie_secure, httponly=False, domain=domain or config.cookie_domain, path=config.access_csrf_cookie_path, samesite=config.cookie_samesite, ) def set_refresh_cookies(response, encoded_refresh_token, max_age=None, domain=None): """ Modifiy a Flask Response to set a cookie containing the refresh JWT. Also sets the corresponding CSRF cookies if ``JWT_CSRF_IN_COOKIES`` is ``True`` (see :ref:`Configuration Options`) :param response: A Flask Response object. :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). :param domain: The domain of the cookie. If this is None, it will use the ``JWT_COOKIE_DOMAIN`` option (see :ref:`Configuration Options`). Otherwise, it will use this as the cookies ``domain`` and the JWT_COOKIE_DOMAIN option will be ignored. """ 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=domain or 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=get_csrf_token(encoded_refresh_token), max_age=max_age or config.cookie_max_age, secure=config.cookie_secure, httponly=False, domain=domain or config.cookie_domain, path=config.refresh_csrf_cookie_path, samesite=config.cookie_samesite, ) def unset_jwt_cookies(response, domain=None): """ Modifiy a Flask Response to delete the cookies containing access or refresh JWTs. Also deletes the corresponding CSRF cookies if applicable. :param response: A Flask Response object """ unset_access_cookies(response, domain) unset_refresh_cookies(response, domain) def unset_access_cookies(response, domain=None): """ Modifiy a Flask Response to delete the cookie containing an access JWT. Also deletes the corresponding CSRF cookie if applicable. :param response: A Flask Response object :param domain: The domain of the cookie. If this is None, it will use the ``JWT_COOKIE_DOMAIN`` option (see :ref:`Configuration Options`). Otherwise, it will use this as the cookies ``domain`` and the JWT_COOKIE_DOMAIN option will be ignored. """ response.set_cookie( config.access_cookie_name, value="", expires=0, secure=config.cookie_secure, httponly=True, domain=domain or 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=domain or config.cookie_domain, path=config.access_csrf_cookie_path, samesite=config.cookie_samesite, ) def unset_refresh_cookies(response, domain=None): """ Modifiy a Flask Response to delete the cookie containing a refresh JWT. Also deletes the corresponding CSRF cookie if applicable. :param response: A Flask Response object :param domain: The domain of the cookie. If this is None, it will use the ``JWT_COOKIE_DOMAIN`` option (see :ref:`Configuration Options`). Otherwise, it will use this as the cookies ``domain`` and the JWT_COOKIE_DOMAIN option will be ignored. """ response.set_cookie( config.refresh_cookie_name, value="", expires=0, secure=config.cookie_secure, httponly=True, domain=domain or 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=domain or config.cookie_domain, path=config.refresh_csrf_cookie_path, samesite=config.cookie_samesite, ) flask-jwt-extended-4.3.1/flask_jwt_extended/view_decorators.py000066400000000000000000000271641413005725200246450ustar00rootroot00000000000000from datetime import datetime from datetime import timezone from functools import wraps from re import split from flask import _request_ctx_stack from flask import current_app from flask import request from werkzeug.exceptions import BadRequest from flask_jwt_extended.config import config from flask_jwt_extended.exceptions import CSRFError from flask_jwt_extended.exceptions import FreshTokenRequired from flask_jwt_extended.exceptions import InvalidHeaderError from flask_jwt_extended.exceptions import InvalidQueryParamError from flask_jwt_extended.exceptions import NoAuthorizationError from flask_jwt_extended.exceptions import UserLookupError from flask_jwt_extended.internal_utils import custom_verification_for_token from flask_jwt_extended.internal_utils import has_user_lookup from flask_jwt_extended.internal_utils import user_lookup from flask_jwt_extended.internal_utils import verify_token_not_blocklisted from flask_jwt_extended.internal_utils import verify_token_type from flask_jwt_extended.utils import decode_token from flask_jwt_extended.utils import get_unverified_jwt_headers def _verify_token_is_fresh(jwt_header, jwt_data): fresh = jwt_data["fresh"] if isinstance(fresh, bool): if not fresh: raise FreshTokenRequired("Fresh token required", jwt_header, jwt_data) else: now = datetime.timestamp(datetime.now(timezone.utc)) if fresh < now: raise FreshTokenRequired("Fresh token required", jwt_header, jwt_data) def verify_jwt_in_request(optional=False, fresh=False, refresh=False, locations=None): """ Verify that a valid JWT is present in the request, unless ``optional=True`` in which case no JWT is also considered valid. :param optional: If ``True``, do not raise an error if no JWT is present in the request. Defaults to ``False``. :param fresh: If ``True``, require a JWT marked as ``fresh`` in order to be verified. Defaults to ``False``. :param refresh: If ``True``, require a refresh JWT to be verified. :param locations: A location or list of locations to look for the JWT in this request, for example ``'headers'`` or ``['headers', 'cookies']``. Defaults to ``None`` which indicates that JWTs will be looked for in the locations defined by the ``JWT_TOKEN_LOCATION`` configuration option. """ if request.method in config.exempt_methods: return try: if refresh: jwt_data, jwt_header, jwt_location = _decode_jwt_from_request( locations, fresh, refresh=True ) else: jwt_data, jwt_header, jwt_location = _decode_jwt_from_request( locations, fresh ) except NoAuthorizationError: if not optional: raise _request_ctx_stack.top.jwt = {} _request_ctx_stack.top.jwt_header = {} _request_ctx_stack.top.jwt_user = {"loaded_user": None} _request_ctx_stack.top.jwt_location = None return # Save these at the very end so that they are only saved in the requet # context if the token is valid and all callbacks succeed _request_ctx_stack.top.jwt_user = _load_user(jwt_header, jwt_data) _request_ctx_stack.top.jwt_header = jwt_header _request_ctx_stack.top.jwt = jwt_data _request_ctx_stack.top.jwt_location = jwt_location return jwt_header, jwt_data def jwt_required(optional=False, fresh=False, refresh=False, locations=None): """ A decorator to protect a Flask endpoint with JSON Web Tokens. Any route decorated with this will require a valid JWT to be present in the request (unless optional=True, in which case no JWT is also valid) before the endpoint can be called. :param optional: If ``True``, allow the decorated endpoint to be accessed if no JWT is present in the request. Defaults to ``False``. :param fresh: If ``True``, require a JWT marked with ``fresh`` to be able to access this endpoint. Defaults to ``False``. :param refresh: If ``True``, requires a refresh JWT to access this endpoint. If ``False``, requires an access JWT to access this endpoint. Defaults to ``False``. :param locations: A location or list of locations to look for the JWT in this request, for example ``'headers'`` or ``['headers', 'cookies']``. Defaults to ``None`` which indicates that JWTs will be looked for in the locations defined by the ``JWT_TOKEN_LOCATION`` configuration option. """ def wrapper(fn): @wraps(fn) def decorator(*args, **kwargs): verify_jwt_in_request(optional, fresh, refresh, locations) # Compatibility with flask < 2.0 if hasattr(current_app, "ensure_sync") and callable( getattr(current_app, "ensure_sync", None) ): return current_app.ensure_sync(fn)(*args, **kwargs) return fn(*args, **kwargs) # pragma: no cover return decorator return wrapper def _load_user(jwt_header, jwt_data): if not has_user_lookup(): return None identity = jwt_data[config.identity_claim_key] user = user_lookup(jwt_header, jwt_data) if user is None: error_msg = "user_lookup returned None for {}".format(identity) raise UserLookupError(error_msg, jwt_header, jwt_data) return {"loaded_user": user} def _decode_jwt_from_headers(): header_name = config.header_name header_type = config.header_type # Verify we have the auth header auth_header = request.headers.get(header_name, "").strip().strip(",") if not auth_header: raise NoAuthorizationError(f"Missing {header_name} Header") # Make sure the header is in a valid format that we are expecting, ie # : . # # Also handle the fact that the header that can be comma delimited, ie # : , , etc... if header_type: field_values = split(r",\s*", auth_header) jwt_headers = [s for s in field_values if s.split()[0] == header_type] if len(jwt_headers) != 1: msg = ( f"Missing '{header_type}' type in '{header_name}' header. " f"Expected '{header_name}: {header_type} '" ) raise NoAuthorizationError(msg) parts = jwt_headers[0].split() if len(parts) != 2: msg = ( f"Bad {header_name} header. " f"Expected '{header_name}: {header_type} '" ) raise InvalidHeaderError(msg) encoded_token = parts[1] else: parts = auth_header.split() if len(parts) != 1: msg = f"Bad {header_name} header. Expected '{header_name}: '" raise InvalidHeaderError(msg) encoded_token = parts[0] return encoded_token, None def _decode_jwt_from_cookies(refresh): if refresh: cookie_key = config.refresh_cookie_name csrf_header_key = config.refresh_csrf_header_name csrf_field_key = config.refresh_csrf_field_name else: cookie_key = config.access_cookie_name csrf_header_key = config.access_csrf_header_name csrf_field_key = config.access_csrf_field_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 and config.csrf_check_form: csrf_value = request.form.get(csrf_field_key, None) if not csrf_value: raise CSRFError("Missing CSRF token") else: csrf_value = None return encoded_token, csrf_value def _decode_jwt_from_query_string(): param_name = config.query_string_name prefix = config.query_string_value_prefix value = request.args.get(param_name) if not value: raise NoAuthorizationError(f"Missing '{param_name}' query paramater") if not value.startswith(prefix): raise InvalidQueryParamError( f"Invalid value for query parameter '{param_name}'. " f"Expected the value to start with '{prefix}'" ) encoded_token = value[len(prefix) :] # noqa: E203 return encoded_token, None def _decode_jwt_from_json(refresh): content_type = request.content_type or "" if not content_type.startswith("application/json"): raise NoAuthorizationError("Invalid content-type. Must be application/json.") if refresh: token_key = config.refresh_json_key else: token_key = config.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) ) from None return encoded_token, None def _decode_jwt_from_request(locations, fresh, refresh=False): # Figure out what locations to look for the JWT in this request if isinstance(locations, str): locations = [locations] if not locations: locations = config.token_location # Get the decode functions in the order specified by locations. # Each entry in this list is a tuple (, ) get_encoded_token_functions = [] for location in locations: if location == "cookies": get_encoded_token_functions.append( (location, lambda: _decode_jwt_from_cookies(refresh)) ) elif location == "query_string": get_encoded_token_functions.append( (location, _decode_jwt_from_query_string) ) elif location == "headers": get_encoded_token_functions.append((location, _decode_jwt_from_headers)) elif location == "json": get_encoded_token_functions.append( (location, lambda: _decode_jwt_from_json(refresh)) ) else: raise RuntimeError(f"'{location}' is not a valid location") # 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 jwt_header = None jwt_location = None for location, 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) jwt_location = location jwt_header = get_unverified_jwt_headers(encoded_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: if len(locations) > 1: err_msg = "Missing JWT in {start_locs} or {end_locs} ({details})".format( start_locs=", ".join(locations[:-1]), end_locs=locations[-1], details="; ".join(errors), ) raise NoAuthorizationError(err_msg) else: raise NoAuthorizationError(errors[0]) # Additional verifications provided by this extension verify_token_type(decoded_token, refresh) if fresh: _verify_token_is_fresh(jwt_header, decoded_token) verify_token_not_blocklisted(jwt_header, decoded_token) custom_verification_for_token(jwt_header, decoded_token) return decoded_token, jwt_header, jwt_location flask-jwt-extended-4.3.1/requirements.txt000066400000000000000000000002071413005725200205010ustar00rootroot00000000000000black==21.6b0 cryptography==35.0.0 Flask==2.0.1 Pallets-Sphinx-Themes==2.0.1 pre-commit==2.13.0 PyJWT==2.1.0 Sphinx==4.0.2 tox==3.23.1 flask-jwt-extended-4.3.1/setup.cfg000066400000000000000000000007651413005725200170470ustar00rootroot00000000000000[bdist_wheel] universal=1 [metadata] description-file = README.md license_file = LICENSE [flake8] # B = bugbear # E = pycodestyle errors # F = flake8 pyflakes # W = pycodestyle warnings # B9 = bugbear opinions select = B, E, F, W, B9 ignore = # Line too long. Handled by E501 and Black B950 max-line-length = 90 per-file-ignores = # __init__ modules export names **/__init__.py: F401 # Don't care about using data classes examples. For demo purposes only examples/*.py: B903 flask-jwt-extended-4.3.1/setup.py000066400000000000000000000034601413005725200167330ustar00rootroot00000000000000import 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="landongb@hey.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,<3.0", "PyJWT>=2.0,<3.0", ], extras_require={"asymmetric_crypto": ["cryptography>=35.0.0"]}, python_requires=">=3.6,<4", classifiers=[ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Flask", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries :: Python Modules", ], ) flask-jwt-extended-4.3.1/tests/000077500000000000000000000000001413005725200163605ustar00rootroot00000000000000flask-jwt-extended-4.3.1/tests/__init__.py000066400000000000000000000000001413005725200204570ustar00rootroot00000000000000flask-jwt-extended-4.3.1/tests/test_additional_claims_loader.py000066400000000000000000000115151413005725200247620ustar00rootroot00000000000000import pytest from flask import Flask from flask import jsonify from flask_jwt_extended import create_access_token from flask_jwt_extended import create_refresh_token from flask_jwt_extended import decode_token from flask_jwt_extended import get_jwt from flask_jwt_extended import jwt_required from flask_jwt_extended import JWTManager from tests.utils import get_jwt_manager from tests.utils import 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()) @app.route("/protected2", methods=["GET"]) @jwt_required(refresh=True) def get_refresh_claims(): return jsonify(get_jwt()) return app def test_additional_claims_in_access_token(app): jwt = get_jwt_manager(app) @jwt.additional_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_claims(app): jwt = get_jwt_manager(app) @jwt.additional_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: # noqa: B903 def __init__(self, username): self.username = username jwt = get_jwt_manager(app) @jwt.additional_claims_loader def add_claims(test_obj): return {"username": test_obj.username} @jwt.user_identity_loader def add_identity(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["sub"] == "username" assert decoded_token["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_additional_claims_in_refresh_token(app): jwt = get_jwt_manager(app) @jwt.additional_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_additional_claims_in_refresh_token_specified_at_creation(app): with app.test_request_context(): refresh_token = create_refresh_token( "username", additional_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_additional_claims_in_access_token_specified_at_creation(app): with app.test_request_context(): access_token = create_access_token("username", additional_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_addition_claims_merge(app): jwt = get_jwt_manager(app) @jwt.additional_claims_loader def add_claims(identity): return {"default": "value"} with app.test_request_context(): access_token = create_access_token("username", additional_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.get_json()["default"] == "value" assert response.status_code == 200 def test_addition_claims_merge_tie_goes_to_create_access_token(app): jwt = get_jwt_manager(app) @jwt.additional_claims_loader def add_claims(identity): return {"default": "value"} with app.test_request_context(): access_token = create_access_token( "username", additional_claims={"default": "foo"} ) test_client = app.test_client() response = test_client.get("/protected", headers=make_headers(access_token)) assert response.get_json()["default"] == "foo" assert response.status_code == 200 flask-jwt-extended-4.3.1/tests/test_asymmetric_crypto.py000066400000000000000000000050661413005725200235550ustar00rootroot00000000000000import pytest from flask import Flask from flask import jsonify from flask_jwt_extended import create_access_token from flask_jwt_extended import jwt_required from flask_jwt_extended import JWTManager 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"} flask-jwt-extended-4.3.1/tests/test_blocklist.py000066400000000000000000000075331413005725200217670ustar00rootroot00000000000000import pytest from flask import Flask from flask import jsonify from flask_jwt_extended import create_access_token from flask_jwt_extended import create_refresh_token from flask_jwt_extended import jwt_required from flask_jwt_extended import JWTManager from tests.utils import get_jwt_manager from tests.utils import 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 access_protected(): return jsonify(foo="bar") @app.route("/refresh_protected", methods=["GET"]) @jwt_required(refresh=True) def refresh_protected(): return jsonify(foo="bar") return app @pytest.mark.parametrize("blocklist_type", [["access"], ["refresh", "access"]]) def test_non_blocklisted_access_token(app, blocklist_type): jwt = get_jwt_manager(app) @jwt.token_in_blocklist_loader def check_blocklisted(jwt_header, jwt_data): assert jwt_header["alg"] == "HS256" assert jwt_data["sub"] == "username" 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("blocklist_type", [["access"], ["refresh", "access"]]) def test_blocklisted_access_token(app, blocklist_type): jwt = get_jwt_manager(app) @jwt.token_in_blocklist_loader def check_blocklisted(jwt_header, jwt_data): 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("blocklist_type", [["refresh"], ["refresh", "access"]]) def test_non_blocklisted_refresh_token(app, blocklist_type): jwt = get_jwt_manager(app) @jwt.token_in_blocklist_loader def check_blocklisted(jwt_header, jwt_data): 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("blocklist_type", [["refresh"], ["refresh", "access"]]) def test_blocklisted_refresh_token(app, blocklist_type): jwt = get_jwt_manager(app) @jwt.token_in_blocklist_loader def check_blocklisted(jwt_header, jwt_data): 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_custom_blocklisted_message(app): jwt = get_jwt_manager(app) @jwt.token_in_blocklist_loader def check_blocklisted(jwt_header, jwt_data): return True @jwt.revoked_token_loader def custom_error(jwt_header, jwt_data): assert jwt_header["alg"] == "HS256" assert jwt_data["sub"] == "username" 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 flask-jwt-extended-4.3.1/tests/test_claims_verification.py000066400000000000000000000061251413005725200240070ustar00rootroot00000000000000import pytest from flask import Flask from flask import jsonify from flask_jwt_extended import create_access_token from flask_jwt_extended import jwt_required from flask_jwt_extended import JWTManager from tests.utils import get_jwt_manager from tests.utils import make_headers @pytest.fixture(scope="function") def app(): app = Flask(__name__) app.config["JWT_SECRET_KEY"] = "foobarbaz" jwt = JWTManager(app) @jwt.additional_claims_loader def add_claims(identity): return {"foo": "bar"} @app.route("/protected1", methods=["GET"]) @jwt_required() def protected1(): return jsonify(foo="bar") @app.route("/protected2", methods=["GET"]) @jwt_required(fresh=True) def protected2(): return jsonify(foo="bar") @app.route("/protected3", methods=["GET"]) @jwt_required(optional=True) 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.token_verification_loader def claims_verification_callback(jwt_header, jwt_data): assert jwt_header["alg"] == "HS256" assert jwt_data["sub"] == "username" return True 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.token_verification_loader def claims_verification_callback(jwt_header, jwt_data): assert jwt_header["alg"] == "HS256" assert jwt_data["sub"] == "username" 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.token_verification_loader def claims_verification_callback(jwt_header, jwt_data): return False @jwt.token_verification_failed_loader def custom_error(jwt_header, jwt_data): assert jwt_header["alg"] == "HS256" assert jwt_data["sub"] == "username" return jsonify(msg="claims failed for {}".format(jwt_data["sub"])), 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 flask-jwt-extended-4.3.1/tests/test_config.py000066400000000000000000000303221413005725200212360ustar00rootroot00000000000000from datetime import timedelta import pytest 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.encode_issuer is None assert config.decode_issuer is None assert config.header_name == "Authorization" assert config.header_type == "Bearer" assert config.query_string_name == "jwt" assert config.query_string_value_prefix == "" 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.cookie_max_age is None assert config.identity_claim_key == "sub" 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_DECODE_ISSUER"] = "TestDecodeIssuer" app.config["JWT_ENCODE_ISSUER"] = "TestEncodeIssuer" app.config["JWT_QUERY_STRING_NAME"] = "banana" app.config["JWT_QUERY_STRING_VALUE_PREFIX"] = "kiwi" 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_IDENTITY_CLAIM"] = "foo" 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.decode_issuer == "TestDecodeIssuer" assert config.encode_issuer == "TestEncodeIssuer" assert config.query_string_name == "banana" assert config.query_string_value_prefix == "kiwi" 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.cookie_max_age == 31540000 assert config.identity_claim_key == "foo" 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 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_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_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"] flask-jwt-extended-4.3.1/tests/test_cookies.py000066400000000000000000000467151413005725200214420ustar00rootroot00000000000000import pytest from flask import Flask from flask import jsonify from flask import request from flask_jwt_extended import create_access_token from flask_jwt_extended import create_refresh_token from flask_jwt_extended import jwt_required from flask_jwt_extended import JWTManager from flask_jwt_extended import set_access_cookies from flask_jwt_extended import set_refresh_cookies from flask_jwt_extended import unset_access_cookies from flask_jwt_extended import unset_jwt_cookies from flask_jwt_extended import unset_refresh_cookies 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(): domain = request.args.get("domain") resp = jsonify(login=True) access_token = create_access_token("username") set_access_cookies(resp, access_token, domain=domain) return resp @app.route("/refresh_token", methods=["GET"]) def refresh_token(): domain = request.args.get("domain") resp = jsonify(login=True) refresh_token = create_refresh_token("username") set_refresh_cookies(resp, refresh_token, domain=domain) return resp @app.route("/delete_tokens", methods=["GET"]) def delete_tokens(): domain = request.args.get("domain") resp = jsonify(logout=True) unset_jwt_cookies(resp, domain=domain) return resp @app.route("/delete_access_tokens", methods=["GET"]) def delete_access_tokens(): domain = request.args.get("domain") resp = jsonify(access_revoked=True) unset_access_cookies(resp, domain=domain) return resp @app.route("/delete_refresh_tokens", methods=["GET"]) def delete_refresh_tokens(): domain = request.args.get("domain") resp = jsonify(refresh_revoked=True) unset_refresh_cookies(resp, domain=domain) 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_required(refresh=True) def refresh_protected(): return jsonify(foo="bar") @app.route("/post_refresh_protected", methods=["POST"]) @jwt_required(refresh=True) def post_refresh_protected(): return jsonify(foo="bar") @app.route("/optional_post_protected", methods=["POST"]) @jwt_required(optional=True) 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"} # 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", "/post_refresh_protected"), ("/access_token", "csrf_access_token", "/post_protected"), ], ) def test_csrf_with_default_form_field(app, options): app.config["JWT_CSRF_CHECK_FORM"] = True 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_data = {"csrf_token": csrf_token} response = test_client.post(post_url, data=csrf_data) 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_form_field(app, options): app.config["JWT_CSRF_CHECK_FORM"] = True app.config["JWT_ACCESS_CSRF_FIELD_NAME"] = "FOO" app.config["JWT_REFRESH_CSRF_FIELD_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_data = {"FOO": csrf_token} response = test_client.post(post_url, data=csrf_data) 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"} # 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_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") response = test_client.post("/optional_post_protected") assert response.status_code == 401 assert response.get_json() == {"msg": "Missing CSRF token"} @pytest.mark.parametrize( "options", [ ( "/access_token", "/delete_access_tokens", "access_token_cookie", "csrf_access_token", ), ( "/refresh_token", "/delete_refresh_tokens", "refresh_token_cookie", "csrf_refresh_token", ), ], ) def test_override_domain_option(app, options): auth_url, delete_url, auth_cookie_name, csrf_cookie_name = options domain = "yolo.com" test_client = app.test_client() app.config["JWT_COOKIE_DOMAIN"] = "test.com" # Test set access cookies with custom domain response = test_client.get(f"{auth_url}?domain={domain}") cookies = response.headers.getlist("Set-Cookie") assert len(cookies) == 2 # JWT and CSRF value access_cookie = _get_cookie_from_response(response, auth_cookie_name) assert access_cookie is not None assert access_cookie["domain"] == domain access_csrf_cookie = _get_cookie_from_response(response, csrf_cookie_name) assert access_csrf_cookie is not None assert access_csrf_cookie["domain"] == domain # Test unset access cookies with custom domain response = test_client.get(f"{delete_url}?domain={domain}") cookies = response.headers.getlist("Set-Cookie") assert len(cookies) == 2 # JWT and CSRF value access_cookie = _get_cookie_from_response(response, auth_cookie_name) assert access_cookie is not None assert access_cookie["domain"] == domain access_csrf_cookie = _get_cookie_from_response(response, csrf_cookie_name) assert access_csrf_cookie is not None assert access_csrf_cookie["domain"] == domain flask-jwt-extended-4.3.1/tests/test_decode_tokens.py000066400000000000000000000312121413005725200225760ustar00rootroot00000000000000from datetime import datetime from datetime import timedelta from datetime import timezone import pytest from dateutil.relativedelta import relativedelta from flask import Flask from jwt import DecodeError from jwt import ExpiredSignatureError from jwt import ImmatureSignatureError from jwt import InvalidAudienceError from jwt import InvalidIssuerError from jwt import InvalidSignatureError from jwt import MissingRequiredClaimError from flask_jwt_extended import create_access_token from flask_jwt_extended import create_refresh_token from flask_jwt_extended import decode_token from flask_jwt_extended import get_jti from flask_jwt_extended import get_unverified_jwt_headers from flask_jwt_extended import JWTManager from flask_jwt_extended.config import config from flask_jwt_extended.exceptions import JWTDecodeError from tests.utils import encode_token from tests.utils import get_jwt_manager @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.mark.parametrize("missing_claims", ["sub", "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_supports_decoding_other_token_types(app, default_access_token): default_access_token["type"] = "app" other_token = encode_token(app, default_access_token) with app.test_request_context(): decoded = decode_token(other_token) assert decoded["type"] == "app" def test_encode_decode_audience(app): # Default, no audience with app.test_request_context(): encoded_token = create_access_token("username") decoded_token = decode_token(encoded_token) with pytest.raises(KeyError): decoded_token["aud"] # Encode and decode audience configured app.config["JWT_ENCODE_AUDIENCE"] = "foo" app.config["JWT_DECODE_AUDIENCE"] = "foo" with app.test_request_context(): encoded_token = create_access_token("username") decoded_token = decode_token(encoded_token) assert decoded_token["aud"] == "foo" # Encode and decode mismatch app.config["JWT_ENCODE_AUDIENCE"] = "foo" app.config["JWT_DECODE_AUDIENCE"] = "bar" with app.test_request_context(): encoded_token = create_access_token("username") with pytest.raises(InvalidAudienceError): decode_token(encoded_token) # No encode defined app.config["JWT_ENCODE_AUDIENCE"] = None app.config["JWT_DECODE_AUDIENCE"] = "foo" with app.test_request_context(): encoded_token = create_access_token("username") with pytest.raises(MissingRequiredClaimError): decode_token(encoded_token) # No decode defined app.config["JWT_ENCODE_AUDIENCE"] = "foo" app.config["JWT_DECODE_AUDIENCE"] = None with app.test_request_context(): encoded_token = create_access_token("username") decoded_token = decode_token(encoded_token) assert decoded_token["aud"] == "foo" @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["sub"] == "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): date_in_future = datetime.utcnow() + timedelta(seconds=30) with pytest.raises(ImmatureSignatureError): with app.test_request_context(): access_token = create_access_token( "username", additional_claims={"nbf": date_in_future} ) 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"] = "banana" # 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["sub"] default_access_token["banana"] = "username" token = encode_token(app, default_access_token) with app.test_request_context(): decoded = decode_token(token) assert "banana" in decoded assert "sub" 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(jwt_header, jwt_data): return "different secret" assert jwtM._decode_key_callback({}, {}) == "different secret" 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(jwt_header, jwt_data): assert jwt_header["alg"] == "HS256" assert jwt_data["sub"] == "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) @pytest.mark.parametrize("token_aud", ["bar", ["bar"], ["bar", "baz"]]) def test_verify_no_aud(app, default_access_token, token_aud): app.config["JWT_DECODE_AUDIENCE"] = None 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 def test_encode_iss(app, default_access_token): app.config["JWT_ENCODE_ISSUER"] = "foobar" with app.test_request_context(): access_token = create_access_token("username") decoded = decode_token(access_token) assert decoded["iss"] == "foobar" def test_mismatch_iss(app, default_access_token): app.config["JWT_ENCODE_ISSUER"] = "foobar" app.config["JWT_DECODE_ISSUER"] = "baz" with pytest.raises(InvalidIssuerError): with app.test_request_context(): invalid_token = create_access_token("username") decode_token(invalid_token) def test_valid_decode_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_decode_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) def test_jwt_headers(app): jwt_header = {"foo": "bar"} with app.test_request_context(): access_token = create_access_token("username", additional_headers=jwt_header) refresh_token = create_refresh_token("username", additional_headers=jwt_header) assert get_unverified_jwt_headers(access_token)["foo"] == "bar" assert get_unverified_jwt_headers(refresh_token)["foo"] == "bar" def test_token_expires_time(app): app.config["JWT_ACCESS_TOKEN_EXPIRES"] = timedelta(hours=1) app.config["JWT_REFRESH_TOKEN_EXPIRES"] = timedelta(hours=2) now_timestamp = datetime.timestamp(datetime.now(timezone.utc)) with app.test_request_context(): access_token = create_access_token("username") refresh_token = create_refresh_token("username") access_timestamp = decode_token(access_token)["exp"] refresh_timestamp = decode_token(refresh_token)["exp"] # < 2 for a little bit of leeway from when we calculated now vs when # the tokens are created assert (access_timestamp - (now_timestamp + 3600)) < 2 assert (refresh_timestamp - (now_timestamp + 7200)) < 2 def test_nbf_is_present_by_default(app): with app.test_request_context(): access_token = create_access_token("username", fresh=True) decoded = decode_token(access_token) assert "nbf" in decoded def test_disable_nbf_encoding(app): app.config["JWT_ENCODE_NBF"] = False with app.test_request_context(): access_token = create_access_token("username", fresh=True) decoded = decode_token(access_token) assert "nbf" not in decoded flask-jwt-extended-4.3.1/tests/test_get_jwt_in_non_protected_contexts.py000066400000000000000000000023051413005725200267740ustar00rootroot00000000000000import pytest from flask import Flask from flask_jwt_extended import current_user from flask_jwt_extended import get_current_user from flask_jwt_extended import get_jwt from flask_jwt_extended import get_jwt_header from flask_jwt_extended import get_jwt_identity from flask_jwt_extended import JWTManager @pytest.fixture(scope="function") def app(): app = Flask(__name__) JWTManager(app) return app def test_get_jwt_in_non_protected_route(app): with app.test_request_context(): with pytest.raises(RuntimeError): get_jwt() def test_get_jwt_header_in_non_protected_route(app): with app.test_request_context(): with pytest.raises(RuntimeError): get_jwt_header() def test_get_jwt_identity_in_non_protected_route(app): with app.test_request_context(): with pytest.raises(RuntimeError): get_jwt_identity() def test_current_user_in_non_protected_route(app): with app.test_request_context(): with pytest.raises(RuntimeError): current_user.foo def test_get_current_user_in_non_protected_route(app): with app.test_request_context(): with pytest.raises(RuntimeError): get_current_user() flask-jwt-extended-4.3.1/tests/test_get_methods_in_optional_endpoint.py000066400000000000000000000023371413005725200265730ustar00rootroot00000000000000import pytest from flask import Flask from flask import jsonify from flask_jwt_extended import current_user from flask_jwt_extended import get_current_user from flask_jwt_extended import get_jwt from flask_jwt_extended import get_jwt_header from flask_jwt_extended import get_jwt_identity from flask_jwt_extended import jwt_required from flask_jwt_extended import JWTManager @pytest.fixture(scope="function") def app(): app = Flask(__name__) app.config["JWT_SECRET_KEY"] = "foobarbaz" jwt = JWTManager(app) @app.route("/optional", methods=["GET"]) @jwt_required(optional=True) def access_protected(): assert get_jwt() == {} assert get_jwt_header() == {} assert get_jwt_identity() == None # noqa: E711 assert get_current_user() == None # noqa: E711 assert current_user == None # noqa: E711 return jsonify(foo="bar") @jwt.user_lookup_loader def user_lookup_callback(_jwt_header, _jwt_data): assert True == False # noqa: E712 return app def test_get_jwt_in_optional_route(app): test_client = app.test_client() response = test_client.get("/optional") assert response.status_code == 200 assert response.get_json() == {"foo": "bar"} flask-jwt-extended-4.3.1/tests/test_headers.py000066400000000000000000000157721413005725200214200ustar00rootroot00000000000000import pytest from flask import Flask from flask import jsonify from flask_jwt_extended import create_access_token from flask_jwt_extended import jwt_required from flask_jwt_extended import JWTManager 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_default_headers(app): test_client = app.test_client() with app.test_request_context(): access_token = create_access_token("username") # Ensure other authorization types don't work access_headers = {"Authorization": "Basic basiccreds"} response = test_client.get("/protected", headers=access_headers) error_msg = ( "Missing 'Bearer' type in 'Authorization' header. " "Expected 'Authorization: Bearer '" ) assert response.status_code == 401 assert response.get_json() == {"msg": error_msg} # Ensure default headers work 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"} # Ensure default headers work with multiple field values access_headers = {"Authorization": "Bearer {}, Basic creds".format(access_token)} response = test_client.get("/protected", headers=access_headers) assert response.status_code == 200 assert response.get_json() == {"foo": "bar"} # Ensure default headers work with multiple field values in any position access_headers = {"Authorization": "Basic creds, 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_header_with_trailing_spaces_and_commas(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_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"} # Ensure new headers work with multiple field values access_headers = {"Foo": "Bearer {}, Basic randomcredshere".format(access_token)} response = test_client.get("/protected", headers=access_headers) assert response.status_code == 200 assert response.get_json() == {"foo": "bar"} # Ensure new headers work with multiple field values in any position access_headers = {"Foo": "Basic randomcredshere, 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) error_msg = ( "Missing 'JWT' type in 'Authorization' header. " "Expected 'Authorization: JWT '" ) assert response.status_code == 401 assert response.get_json() == {"msg": error_msg} # 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"} # Ensure new headers work with multiple field values access_headers = {"Authorization": "JWT {}, Basic creds".format(access_token)} response = test_client.get("/protected", headers=access_headers) assert response.status_code == 200 assert response.get_json() == {"foo": "bar"} # Ensure new headers work with multiple field values in any position access_headers = {"Authorization": "Basic creds, 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 'Authorization: '"} 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_header_without_jwt(app): test_client = app.test_client() access_headers = {"Authorization": "Bearer "} response = test_client.get("/protected", headers=access_headers) assert response.status_code == 422 assert response.get_json() == { "msg": "Bad Authorization header. Expected 'Authorization: Bearer '" } 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"} flask-jwt-extended-4.3.1/tests/test_json.py000066400000000000000000000110441413005725200207420ustar00rootroot00000000000000import pytest from flask import Flask from flask import jsonify from flask_jwt_extended import create_access_token from flask_jwt_extended import create_refresh_token from flask_jwt_extended import jwt_required from flask_jwt_extended import JWTManager 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_required(refresh=True) 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"} def test_custom_content_type(app): test_client = app.test_client() content_type = "application/json;charset=UTF-8" 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, content_type=content_type) assert response.status_code == 200 assert response.get_json() == {"foo": "bar"} data = {"refresh_token": refresh_token} response = test_client.post("/refresh", json=data, content_type=content_type) assert response.status_code == 200 assert response.get_json() == {"foo": "bar"} flask-jwt-extended-4.3.1/tests/test_jwt_header_loader.py000066400000000000000000000077171413005725200234470ustar00rootroot00000000000000import pytest from flask import Flask from flask import jsonify from flask_jwt_extended import create_access_token from flask_jwt_extended import create_refresh_token from flask_jwt_extended import get_jwt_header from flask_jwt_extended import jwt_required from flask_jwt_extended import JWTManager from tests.utils import get_jwt_manager from tests.utils import 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_header()) @app.route("/protected2", methods=["GET"]) @jwt_required(refresh=True) def get_refresh_claims(): return jsonify(get_jwt_header()) return app def test_jwt_headers_in_access_token(app): jwt = get_jwt_manager(app) @jwt.additional_headers_loader def add_jwt_headers(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().get("foo") == "bar" assert response.status_code == 200 def test_non_serializable_headers(app): jwt = get_jwt_manager(app) @jwt.additional_headers_loader def add_jwt_headers(identity): return app with pytest.raises(TypeError): with app.test_request_context(): create_access_token("username") def test_jwt_headers_in_refresh_token(app): jwt = get_jwt_manager(app) @jwt.additional_headers_loader def add_jwt_headers(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().get("foo") == "bar" assert response.status_code == 200 def test_jwt_header_in_refresh_token_specified_at_creation(app): with app.test_request_context(): refresh_token = create_refresh_token( "username", additional_headers={"foo": "bar"} ) test_client = app.test_client() response = test_client.get("/protected2", headers=make_headers(refresh_token)) assert response.get_json().get("foo") == "bar" assert response.status_code == 200 def test_jwt_header_in_access_token_specified_at_creation(app): with app.test_request_context(): access_token = create_access_token( "username", additional_headers={"foo": "bar"} ) test_client = app.test_client() response = test_client.get("/protected", headers=make_headers(access_token)) assert response.get_json().get("foo") == "bar" assert response.status_code == 200 def test_jwt_header_in_access_token_specified_at_creation_override(app): jwt = get_jwt_manager(app) @jwt.additional_headers_loader def add_jwt_headers(identity): return {"ping": "pong"} with app.test_request_context(): access_token = create_access_token( "username", additional_headers={"foo": "bar"} ) test_client = app.test_client() response = test_client.get("/protected", headers=make_headers(access_token)) assert response.get_json().get("foo") == "bar" assert response.status_code == 200 def test_jwt_header_in_refresh_token_specified_at_creation_override(app): jwt = get_jwt_manager(app) @jwt.additional_headers_loader def add_jwt_headers(identity): return {"ping": "pong"} with app.test_request_context(): access_token = create_refresh_token( "username", additional_headers={"foo": "bar"} ) test_client = app.test_client() response = test_client.get("/protected2", headers=make_headers(access_token)) assert response.get_json().get("foo") == "bar" assert response.status_code == 200 flask-jwt-extended-4.3.1/tests/test_multiple_token_locations.py000066400000000000000000000141061413005725200251010ustar00rootroot00000000000000import pytest from flask import Flask from flask import jsonify from flask_jwt_extended import create_access_token from flask_jwt_extended import get_jwt_request_location from flask_jwt_extended import jwt_required from flask_jwt_extended import JWTManager from flask_jwt_extended import 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", location=get_jwt_request_location()) return app @pytest.fixture(scope="function") def app_with_locations(): app = Flask(__name__) app.config["JWT_SECRET_KEY"] = "foobarbaz" app.config["JWT_TOKEN_LOCATION"] = ["headers"] locations = ["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(locations=locations) def access_protected(): return jsonify(foo="bar", location=get_jwt_request_location()) return app def test_header_access(app, app_with_locations): for app in (app, app_with_locations): 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", "location": "headers"} def test_cookie_access(app, app_with_locations): for app in (app, app_with_locations): 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", "location": "cookies"} def test_query_string_access(app, app_with_locations): for app in (app, app_with_locations): 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", "location": "query_string"} def test_json_access(app, app_with_locations): for app in (app, app_with_locations): 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", "location": "json"} @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", "location": "cookies"}), (["headers", "cookies"], 200, None, {"foo": "bar", "location": "cookies"}), ], ) 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", "location": "cookies"}), (["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} flask-jwt-extended-4.3.1/tests/test_options_method.py000066400000000000000000000023611413005725200230260ustar00rootroot00000000000000import pytest from flask import Flask from flask_jwt_extended import jwt_required from flask_jwt_extended import JWTManager @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 "ok" @app.route("/fresh_jwt_required", methods=["GET", "OPTIONS"]) @jwt_required(fresh=True) def fresh_jwt_required_endpoint(): return "ok" @app.route("/jwt_refresh_token_required", methods=["GET", "OPTIONS"]) @jwt_required(refresh=True) def jwt_refresh_token_required_endpoint(): return "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" flask-jwt-extended-4.3.1/tests/test_query_string.py000066400000000000000000000064401413005725200225300ustar00rootroot00000000000000import pytest from flask import Flask from flask import jsonify from flask_jwt_extended import create_access_token from flask_jwt_extended import jwt_required from flask_jwt_extended import JWTManager 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_query_string_value_prefix(app): app.config["JWT_QUERY_STRING_VALUE_PREFIX"] = "bearer " test_client = app.test_client() with app.test_request_context(): access_token = create_access_token("username") # Valid string prefix url = f"/protected?jwt=bearer {access_token}" response = test_client.get(url) assert response.status_code == 200 assert response.get_json() == {"foo": "bar"} # Invalid string prefix url = f"/protected?jwt={access_token}" response = test_client.get(url) error_msg = ( "Invalid value for query parameter 'jwt'. " "Expected the value to start with 'bearer '" ) assert response.status_code == 422 assert response.get_json() == {"msg": error_msg} 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"} flask-jwt-extended-4.3.1/tests/test_user_lookup.py000066400000000000000000000062441413005725200223460ustar00rootroot00000000000000import pytest from flask import Flask from flask import jsonify from flask_jwt_extended import create_access_token from flask_jwt_extended import current_user from flask_jwt_extended import get_current_user from flask_jwt_extended import jwt_required from flask_jwt_extended import JWTManager from tests.utils import get_jwt_manager from tests.utils import 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(): try: return jsonify(foo=get_current_user()["username"]) except RuntimeError as e: return jsonify(error=str(e)) @app.route("/get_user2", methods=["GET"]) @jwt_required() def get_user2(): try: return jsonify(foo=current_user["username"]) except RuntimeError as e: return jsonify(error=str(e)) return app @pytest.mark.parametrize("url", ["/get_user1", "/get_user2"]) def test_no_user_lookup_loader_specified(app, url): 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 "@jwt.user_lookup_loader" in response.get_json()["error"] @pytest.mark.parametrize("url", ["/get_user1", "/get_user2"]) def test_load_valid_user(app, url): jwt = get_jwt_manager(app) @jwt.user_lookup_loader def user_lookup_callback(_jwt_header, jwt_data): return {"username": jwt_data["sub"]} 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_lookup_loader def user_lookup_callback(_jwt_header, jwt_data): 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_lookup_errors(app, url): jwt = get_jwt_manager(app) @jwt.user_lookup_loader def user_lookup_callback(_jwt_header, jwt_data): return None @jwt.user_lookup_error_loader def user_lookup_error(jwt_header, jwt_data): assert jwt_header["alg"] == "HS256" assert jwt_data["sub"] == "username" 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"} flask-jwt-extended-4.3.1/tests/test_view_decorators.py000066400000000000000000000375351413005725200232050ustar00rootroot00000000000000from datetime import timedelta import pytest from dateutil.relativedelta import relativedelta from flask import Flask from flask import jsonify from flask_jwt_extended import create_access_token from flask_jwt_extended import create_refresh_token from flask_jwt_extended import decode_token from flask_jwt_extended import get_jwt_identity from flask_jwt_extended import jwt_required from flask_jwt_extended import JWTManager from flask_jwt_extended import verify_jwt_in_request from tests.utils import encode_token from tests.utils import get_jwt_manager from tests.utils import 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 protected(): return jsonify(foo="bar") @app.route("/fresh_protected", methods=["GET"]) @jwt_required(fresh=True) def fresh_protected(): return jsonify(foo="bar") @app.route("/refresh_protected", methods=["GET"]) @jwt_required(refresh=True) def refresh_protected(): return jsonify(foo="bar") @app.route("/optional_protected", methods=["GET"]) @jwt_required(optional=True) 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 non-refresh 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 non-refresh tokens are allowed"} # Test with custom response @jwtM.needs_fresh_token_loader def custom_response(jwt_header, jwt_data): assert jwt_header["alg"] == "HS256" assert jwt_data["sub"] == "username" 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 non-refresh tokens are allowed"} 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_jwt_optional_with_no_valid_jwt(app): url = "/optional_protected" test_client = app.test_client() # No auth headers response = test_client.get(url, headers=None) assert response.status_code == 200 assert response.get_json() == {"foo": "bar"} # auth header with type that isn't configured to be checked response = test_client.get(url, headers={"Authorization": "basic creds"}) assert response.status_code == 200 assert response.get_json() == {"foo": "bar"} # auth header with Bearer type but no JWT response = test_client.get(url, headers={"Authorization": "Bearer "}) assert response.status_code == 422 assert response.get_json() == { "msg": "Bad Authorization header. Expected 'Authorization: Bearer '" } # Bearer token malformed response = test_client.get(url, headers={"Authorization": "Bearer xxx"}) assert response.status_code == 422 assert response.get_json() == {"msg": "Not enough segments"} # auth header comma seperated with no bearer token response = test_client.get(url, headers={"Authorization": "Foo 1, Bar 2, Baz, 3"}) assert response.status_code == 200 assert response.get_json() == {"foo": "bar"} # auth header comma seperated with missing bearer token response = test_client.get(url, headers={"Authorization": "Foo 1, Bearer, Baz, 3"}) assert response.status_code == 422 assert response.get_json() == { "msg": "Bad Authorization header. Expected 'Authorization: Bearer '" } # Bearer token comma seperated with malformed bearer token response = test_client.get( url, headers={"Authorization": "Foo 1, Bearer 2, Baz, 3"} ) assert response.status_code == 422 assert response.get_json() == {"msg": "Not enough segments"} def test_override_jwt_location(app): app.config["JWT_TOKEN_LOCATION"] = ["cookies"] @app.route("/protected_other") @jwt_required(locations="headers") def protected_other(): return jsonify(foo="bar") @app.route("/protected_invalid") @jwt_required(locations="INVALID_LOCATION") def protected_invalid(): return jsonify(foo="bar") test_client = app.test_client() with app.test_request_context(): access_token = create_access_token("username") url = "/protected_other" response = test_client.get(url, headers=make_headers(access_token)) assert response.get_json() == {"foo": "bar"} assert response.status_code == 200 url = "/protected" response = test_client.get(url, headers=make_headers(access_token)) assert response.status_code == 401 assert response.get_json() == {"msg": 'Missing cookie "access_token_cookie"'} url = "/protected_invalid" response = test_client.get(url, headers=make_headers(access_token)) assert response.status_code == 500 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: sub"} 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, {"sub": "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, {"sub": "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", "sub": "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, {"sub": "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, {"sub": "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", "sub": "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 new custom response @jwtM.expired_token_loader def custom_response(expired_jwt_header, expired_jwt_data): assert expired_jwt_header["alg"] == "HS256" assert expired_jwt_data["sub"] == "username" assert expired_jwt_data["type"] == "access" return jsonify(msg="foobar"), 201 response = test_client.get(url, headers=make_headers(token)) assert response.status_code == 201 assert response.get_json() == {"msg": "foobar"} def test_expired_token_via_decode_token(app): jwtM = get_jwt_manager(app) @jwtM.expired_token_loader def depreciated_custom_response(expired_jwt_header, expired_jwt_data): assert expired_jwt_header["alg"] == "HS256" assert expired_jwt_data["sub"] == "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"} def test_verify_jwt_in_request_returns_decoded_token(app): @app.route("/custom", methods=["GET"]) def custom(): jwt_header, jwt_data = verify_jwt_in_request() assert jwt_header["alg"] == "HS256" assert jwt_data["sub"] == "username" return jsonify(foo="bar") url = "/custom" test_client = app.test_client() with app.test_request_context(): token = create_access_token("username") response = test_client.get(url, headers=make_headers(token)) assert response.status_code == 200 assert response.get_json() == {"foo": "bar"} flask-jwt-extended-4.3.1/tests/utils.py000066400000000000000000000007731413005725200201010ustar00rootroot00000000000000import jwt from flask_jwt_extended.config import config def encode_token(app, token_data, headers=None): with app.test_request_context(): return jwt.encode( token_data, config.decode_key, algorithm=config.algorithm, json_encoder=config.json_encoder, headers=headers, ) def get_jwt_manager(app): return app.extensions["flask-jwt-extended"] def make_headers(jwt): return {"Authorization": "Bearer {}".format(jwt)} flask-jwt-extended-4.3.1/tox.ini000066400000000000000000000016711413005725200165360ustar00rootroot00000000000000# 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 = py36,py37,py38,py39,pypy3,flask1,coverage,style,docs [testenv] commands = pytest tests/ deps = pytest cryptography python-dateutil flask1: flask == 1.1.4 [testenv:coverage] commands = coverage run --source flask_jwt_extended -m pytest -qq tests/ coverage report --fail-under=100 --show-missing deps = coverage pytest cryptography python-dateutil [testenv:style] deps = pre-commit skip_install = true commands = pre-commit run --all-files --show-diff-on-failure [testenv:docs] # This gets color output working when run through tox passenv=TERM deps = Sphinx Pallets-Sphinx-Themes changedir = docs allowlist_externals = make commands = make dummy make linkcheck