├── .circleci └── config.yml ├── .github └── dependabot.yml ├── .gitignore ├── .mypy.ini ├── .travis.yml ├── LICENSE ├── README.rst ├── jwt ├── __init__.py ├── exceptions.py ├── jwa.py ├── jwk.py ├── jwkset.py ├── jws.py ├── jwt.py ├── py.typed ├── tests │ ├── __init__.py │ ├── helper.py │ ├── test_jwa.py │ ├── test_jwk.py │ ├── test_jwkset.py │ ├── test_jws.py │ ├── test_jwt.py │ ├── test_utils.py │ └── testdata │ │ ├── dsa_privkey.pem │ │ ├── oct.json │ │ ├── rsa_privkey.der │ │ ├── rsa_privkey.json │ │ ├── rsa_privkey.pem │ │ ├── rsa_privkey_full.json │ │ ├── rsa_pubkey.json │ │ └── rsa_pubkey.pem └── utils.py ├── requirements-dev.txt ├── setup.cfg ├── setup.py └── tox.ini /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2.1 3 | orbs: 4 | python: circleci/python@1.4.0 5 | jobs: 6 | tests: 7 | parameters: 8 | pythonversion: 9 | type: string 10 | executor: 11 | name: python/default 12 | tag: << parameters.pythonversion >> 13 | environment: 14 | ARTIFACTS: /tmp/python-jwt 15 | steps: 16 | - checkout: {} 17 | - python/install-packages: 18 | pkg-manager: pip-dist 19 | pip-dependency-file: setup.py 20 | args: -r requirements-dev.txt 21 | pre-install-steps: 22 | - run: python3 -m pip install --upgrade pip 23 | - run: 24 | name: Run tests 25 | command: | 26 | mypy 27 | py.test \ 28 | --verbose --capture=tee-sys \ 29 | --junit-xml="${ARTIFACTS}/junit/jwt.xml" \ 30 | --cov-report="html:${ARTIFACTS}/coverage" \ 31 | jwt 32 | - store_artifacts: 33 | path: "/tmp/python-jwt" 34 | - store_test_results: 35 | path: "/tmp/python-jwt" 36 | workflows: 37 | version: 2 38 | tests: 39 | jobs: 40 | - tests: 41 | matrix: 42 | parameters: 43 | pythonversion: 44 | - '3.6' 45 | - '3.7' 46 | - '3.8' 47 | - '3.9' 48 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | --- 5 | version: 2 6 | updates: 7 | - package-ecosystem: "pip" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | commit-message: 12 | prefix: dependabot 13 | include: scope 14 | labels: 15 | - dependencies 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/python 2 | 3 | ### Python ### 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | .pytest_cache/ 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # Jupyter Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule.* 77 | 78 | # SageMath parsed files 79 | *.sage.py 80 | 81 | # Environments 82 | .env 83 | .venv 84 | env/ 85 | venv/ 86 | ENV/ 87 | env.bak/ 88 | venv.bak/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | 104 | # End of https://www.gitignore.io/api/python 105 | -------------------------------------------------------------------------------- /.mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | files = jwt 3 | check_untyped_defs = True 4 | follow_imports_for_stubs = True 5 | #disallow_any_decorated = True 6 | disallow_any_generics = True 7 | #disallow_incomplete_defs = True 8 | disallow_subclassing_any = True 9 | disallow_untyped_calls = True 10 | disallow_untyped_decorators = True 11 | #disallow_untyped_defs = True 12 | implicit_reexport = False 13 | no_implicit_optional = True 14 | show_error_codes = True 15 | strict_equality = True 16 | warn_incomplete_stub = True 17 | warn_redundant_casts = True 18 | warn_unreachable = True 19 | warn_unused_ignores = True 20 | disallow_any_unimported = True 21 | #warn_return_any = True 22 | 23 | [mypy-jwt.tests.*] 24 | disallow_untyped_calls = False 25 | disallow_untyped_defs = False 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dist: bionic 3 | language: python 4 | python: 5 | - "3.6" 6 | - "3.7" 7 | - "3.8" 8 | - "3.9" 9 | install: 10 | - pip install tox-travis coveralls 11 | - pip install -r requirements-dev.txt 12 | script: 13 | - mypy 14 | - tox 15 | after_success: 16 | - coveralls 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2017 Gehirn Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://travis-ci.org/GehirnInc/python-jwt.svg?branch=master 2 | :target: https://travis-ci.org/GehirnInc/python-jwt 3 | .. image:: https://coveralls.io/repos/GehirnInc/python-jwt/badge.png?branch=master 4 | :target: https://coveralls.io/r/GehirnInc/python-jwt?branch=master 5 | .. image:: https://badge.fury.io/py/jwt.svg?dummy 6 | :target: http://badge.fury.io/py/jwt 7 | 8 | python-jwt 9 | ========== 10 | 11 | *python-jwt* is a JSON Web Token (JWT) implementation in Python developed by `Gehirn Inc`_. 12 | 13 | 14 | Examples 15 | -------- 16 | 17 | .. code-block:: python 18 | 19 | import json 20 | from datetime import datetime, timedelta, timezone 21 | 22 | from jwt import ( 23 | JWT, 24 | jwk_from_dict, 25 | jwk_from_pem, 26 | ) 27 | from jwt.utils import get_int_from_datetime 28 | 29 | 30 | instance = JWT() 31 | 32 | message = { 33 | 'iss': 'https://example.com/', 34 | 'sub': 'yosida95', 35 | 'iat': get_int_from_datetime(datetime.now(timezone.utc)), 36 | 'exp': get_int_from_datetime( 37 | datetime.now(timezone.utc) + timedelta(hours=1)), 38 | } 39 | 40 | """ 41 | Encode the message to JWT(JWS). 42 | """ 43 | 44 | # Load a RSA key from a JWK dict. 45 | signing_key = jwk_from_dict({ 46 | 'kty': 'RSA', 47 | 'e': 'AQAB', 48 | 'n': '...', 49 | 'd': '...'}) 50 | # Or load a RSA key from a PEM file. 51 | with open('rsa_private_key.pem', 'rb') as fh: 52 | signing_key = jwk_from_pem(fh.read()) 53 | # You can also load an octet key in the same manner as the RSA. 54 | # signing_key = jwk_from_dict({'kty': 'oct', 'k': '...'}) 55 | 56 | compact_jws = instance.encode(message, signing_key, alg='RS256') 57 | 58 | """ 59 | Decode the JWT with verifying the signature. 60 | """ 61 | 62 | # Load a public key from PEM file corresponding to the signing private key. 63 | with open('rsa_public_key.json', 'r') as fh: 64 | verifying_key = jwk_from_dict(json.load(fh)) 65 | 66 | message_received = instance.decode( 67 | compact_jws, verifying_key, do_time_check=True) 68 | 69 | """ 70 | Successfuly retrieved the `message` from the `compact_jws` 71 | """ 72 | assert message == message_received 73 | 74 | 75 | Installation 76 | ------------ 77 | 78 | You can install python-jwt with pip. 79 | 80 | .. code-block:: shell 81 | 82 | $ pip install jwt 83 | 84 | 85 | Implementation Details 86 | ------------------------- 87 | 88 | Supported Algorithms 89 | ~~~~~~~~~~~~~~~~~~~~ 90 | 91 | - Unsecured 92 | 93 | - none (disabled by default for security) 94 | 95 | - Symmetric 96 | 97 | - HS256 98 | - HS384 99 | - HS512 100 | 101 | - Asymmetric 102 | 103 | - PS256 104 | - PS384 105 | - PS512 106 | - RS256 107 | - RS384 108 | - RS512 109 | 110 | Supported Python Versions 111 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 112 | 113 | - Python 3.6+ 114 | 115 | 116 | License 117 | ------- 118 | python-jwt is licensed under the Apache License version 2. See ./LICENSE.rst. 119 | 120 | 121 | .. _Gehirn Inc: http://www.gehirn.co.jp/ 122 | -------------------------------------------------------------------------------- /jwt/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2017 Gehirn Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from .jwa import std_hash_by_alg 18 | from .jwk import ( 19 | AbstractJWKBase, 20 | jwk_from_dict, 21 | jwk_from_bytes, 22 | jwk_from_pem, 23 | jwk_from_der, 24 | supported_key_types, 25 | ) 26 | from .jwkset import JWKSet 27 | from .jwa import ( 28 | AbstractSigningAlgorithm, 29 | supported_signing_algorithms, 30 | ) 31 | from .jwt import JWT 32 | 33 | 34 | __all__ = [ 35 | # .jwa 36 | 'std_hash_by_alg', 37 | # .jwk 38 | 'AbstractJWKBase', 39 | 'jwk_from_bytes', 40 | 'jwk_from_dict', 41 | 'jwk_from_pem', 42 | 'jwk_from_der', 43 | 'supported_key_types', 44 | # .jwkset 45 | 'JWKSet', 46 | # .jws 47 | 'AbstractSigningAlgorithm', 48 | 'supported_signing_algorithms', 49 | # .jwt 50 | 'JWT', 51 | ] 52 | -------------------------------------------------------------------------------- /jwt/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2017 Gehirn Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | class JWTException(Exception): 19 | """ 20 | common base class for all exceptions used in python-jwt 21 | """ 22 | 23 | 24 | class MalformedJWKError(JWTException): 25 | pass 26 | 27 | 28 | class UnsupportedKeyTypeError(JWTException): 29 | pass 30 | 31 | 32 | class InvalidKeyTypeError(JWTException): 33 | pass 34 | 35 | 36 | class JWSEncodeError(JWTException): 37 | pass 38 | 39 | 40 | class JWSDecodeError(JWTException): 41 | pass 42 | 43 | 44 | class JWTEncodeError(JWTException): 45 | pass 46 | 47 | 48 | class JWTDecodeError(JWTException): 49 | pass 50 | -------------------------------------------------------------------------------- /jwt/jwa.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2017 Gehirn Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import hashlib 18 | import hmac 19 | from typing import ( 20 | Any, 21 | Dict, 22 | Callable, 23 | Optional, 24 | ) 25 | 26 | from cryptography.hazmat.primitives.asymmetric import padding 27 | from cryptography.hazmat.primitives.hashes import ( 28 | SHA256, 29 | SHA384, 30 | SHA512, 31 | ) 32 | 33 | from .exceptions import InvalidKeyTypeError 34 | from .jwk import AbstractJWKBase 35 | 36 | 37 | def std_hash_by_alg(alg: str) -> Callable[[bytes], object]: 38 | if alg.endswith('S256'): 39 | return hashlib.sha256 40 | if alg.endswith('S384'): 41 | return hashlib.sha384 42 | if alg.endswith('S512'): 43 | return hashlib.sha512 44 | raise ValueError('{} is not supported'.format(alg)) 45 | 46 | 47 | class AbstractSigningAlgorithm: 48 | 49 | def sign(self, message: bytes, key: Optional[AbstractJWKBase]) -> bytes: 50 | raise NotImplementedError() # pragma: no cover 51 | 52 | def verify(self, message: bytes, key: Optional[AbstractJWKBase], 53 | signature: bytes) -> bool: 54 | raise NotImplementedError() # pragma: no cover 55 | 56 | 57 | class NoneAlgorithm(AbstractSigningAlgorithm): 58 | 59 | def sign(self, message: bytes, key: Optional[AbstractJWKBase]) -> bytes: 60 | return b'' 61 | 62 | def verify(self, message: bytes, key: Optional[AbstractJWKBase], 63 | signature: bytes) -> bool: 64 | return hmac.compare_digest(signature, b'') 65 | 66 | 67 | none = NoneAlgorithm() 68 | 69 | 70 | class HMACAlgorithm(AbstractSigningAlgorithm): 71 | 72 | def __init__(self, hash_fun: Callable[[], object]) -> None: 73 | self.hash_fun = hash_fun 74 | 75 | def _check_key(self, key: Optional[AbstractJWKBase]) -> AbstractJWKBase: 76 | if not key or key.get_kty() != 'oct': 77 | raise InvalidKeyTypeError('Octet key is required') 78 | return key 79 | 80 | def _sign(self, message: bytes, key: bytes) -> bytes: 81 | return hmac.new(key, message, self.hash_fun).digest() 82 | 83 | def sign(self, message: bytes, key: Optional[AbstractJWKBase]) -> bytes: 84 | key = self._check_key(key) 85 | return key.sign(message, signer=self._sign) 86 | 87 | def verify(self, message: bytes, key: Optional[AbstractJWKBase], 88 | signature: bytes) -> bool: 89 | key = self._check_key(key) 90 | return key.verify(message, signature, signer=self._sign) 91 | 92 | 93 | HS256 = HMACAlgorithm(hashlib.sha256) 94 | HS384 = HMACAlgorithm(hashlib.sha384) 95 | HS512 = HMACAlgorithm(hashlib.sha512) 96 | 97 | 98 | class RSAAlgorithm(AbstractSigningAlgorithm): 99 | 100 | def __init__(self, hash_fun: object) -> None: 101 | self.hash_fun = hash_fun 102 | 103 | def _check_key( 104 | self, 105 | key: Optional[AbstractJWKBase], 106 | must_sign_key: bool = False, 107 | ) -> AbstractJWKBase: 108 | if not key or key.get_kty() != 'RSA': 109 | raise InvalidKeyTypeError('RSA key is required') 110 | if must_sign_key and not key.is_sign_key(): 111 | raise InvalidKeyTypeError( 112 | 'a RSA private key is required, but passed is RSA public key') 113 | return key 114 | 115 | def sign(self, message: bytes, key: Optional[AbstractJWKBase]) -> bytes: 116 | key = self._check_key(key, must_sign_key=True) 117 | return key.sign(message, hash_fun=self.hash_fun, 118 | padding=padding.PKCS1v15()) 119 | 120 | def verify( 121 | self, 122 | message: bytes, 123 | key: Optional[AbstractJWKBase], 124 | signature: bytes, 125 | ) -> bool: 126 | key = self._check_key(key) 127 | return key.verify(message, signature, hash_fun=self.hash_fun, 128 | padding=padding.PKCS1v15()) 129 | 130 | 131 | RS256 = RSAAlgorithm(SHA256) 132 | RS384 = RSAAlgorithm(SHA384) 133 | RS512 = RSAAlgorithm(SHA512) 134 | 135 | 136 | class PSSRSAAlgorithm(AbstractSigningAlgorithm): 137 | def __init__(self, hash_fun: Callable[[], Any]) -> None: 138 | self.hash_fun = hash_fun 139 | 140 | def _check_key( 141 | self, 142 | key: Optional[AbstractJWKBase], 143 | must_sign_key: bool = False, 144 | ) -> AbstractJWKBase: 145 | if not key or key.get_kty() != 'RSA': 146 | raise InvalidKeyTypeError('RSA key is required') 147 | if must_sign_key and not key.is_sign_key(): 148 | raise InvalidKeyTypeError( 149 | 'a RSA private key is required, but passed is RSA public key') 150 | return key 151 | 152 | def sign(self, message: bytes, key: Optional[AbstractJWKBase]) -> bytes: 153 | key = self._check_key(key, must_sign_key=True) 154 | return key.sign( 155 | message, 156 | hash_fun=self.hash_fun, 157 | padding=padding.PSS( # type: ignore[no-untyped-call] 158 | mgf=padding.MGF1(self.hash_fun()), 159 | salt_length=self.hash_fun().digest_size, 160 | ), 161 | ) 162 | 163 | def verify( 164 | self, 165 | message: bytes, 166 | key: Optional[AbstractJWKBase], 167 | signature: bytes 168 | ) -> bool: 169 | key = self._check_key(key) 170 | return key.verify( 171 | message, 172 | signature, 173 | hash_fun=self.hash_fun, 174 | padding=padding.PSS( # type: ignore[no-untyped-call] 175 | mgf=padding.MGF1(self.hash_fun()), 176 | salt_length=self.hash_fun().digest_size, 177 | ), 178 | ) 179 | 180 | 181 | PS256 = PSSRSAAlgorithm(SHA256) 182 | PS384 = PSSRSAAlgorithm(SHA384) 183 | PS512 = PSSRSAAlgorithm(SHA512) 184 | 185 | 186 | def supported_signing_algorithms() -> Dict[str, AbstractSigningAlgorithm]: 187 | # NOTE(yosida95): exclude vulnerable 'none' algorithm by default. 188 | return { 189 | 'HS256': HS256, 190 | 'HS384': HS384, 191 | 'HS512': HS512, 192 | 'RS256': RS256, 193 | 'RS384': RS384, 194 | 'RS512': RS512, 195 | 'PS256': PS256, 196 | 'PS384': PS384, 197 | 'PS512': PS512, 198 | } 199 | -------------------------------------------------------------------------------- /jwt/jwk.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2017 Gehirn Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import hmac 18 | from warnings import warn 19 | from abc import ( 20 | ABC, 21 | abstractmethod, 22 | ) 23 | from typing import ( 24 | Any, 25 | Callable, 26 | Dict, 27 | Mapping, 28 | Type, 29 | TypeVar, 30 | Union, 31 | Optional 32 | ) 33 | from functools import wraps 34 | 35 | import cryptography.hazmat.primitives.serialization as serialization_module 36 | from cryptography.exceptions import InvalidSignature 37 | from cryptography.hazmat.backends import default_backend 38 | from cryptography.hazmat.primitives.asymmetric import padding 39 | from cryptography.hazmat.primitives.asymmetric.rsa import ( 40 | rsa_crt_dmp1, 41 | rsa_crt_dmq1, 42 | rsa_crt_iqmp, 43 | rsa_recover_prime_factors, 44 | RSAPrivateKey, 45 | RSAPrivateNumbers, 46 | RSAPublicKey, 47 | RSAPublicNumbers, 48 | ) 49 | from cryptography.hazmat.primitives.hashes import HashAlgorithm 50 | 51 | from .exceptions import ( 52 | MalformedJWKError, 53 | UnsupportedKeyTypeError, 54 | ) 55 | from .utils import ( 56 | b64encode, 57 | b64decode, 58 | uint_b64encode, 59 | uint_b64decode, 60 | ) 61 | 62 | _AJWK = TypeVar("_AJWK", bound="AbstractJWKBase") 63 | _T = TypeVar("_T") 64 | 65 | 66 | class AbstractJWKBase(ABC): 67 | 68 | @abstractmethod 69 | def get_kty(self) -> str: 70 | pass # pragma: no cover 71 | 72 | @abstractmethod 73 | def get_kid(self) -> str: 74 | pass # pragma: no cover 75 | 76 | @abstractmethod 77 | def is_sign_key(self) -> bool: 78 | pass # pragma: no cover 79 | 80 | @abstractmethod 81 | def sign(self, message: bytes, **options) -> bytes: 82 | pass # pragma: no cover 83 | 84 | @abstractmethod 85 | def verify(self, message: bytes, signature: bytes, **options) -> bool: 86 | pass # pragma: no cover 87 | 88 | @abstractmethod 89 | def to_dict(self, public_only: bool = True) -> Dict[str, str]: 90 | pass # pragma: no cover 91 | 92 | @classmethod 93 | @abstractmethod 94 | def from_dict(cls: Type[_AJWK], dct: Dict[str, object]) -> _AJWK: 95 | pass # pragma: no cover 96 | 97 | 98 | class OctetJWK(AbstractJWKBase): 99 | 100 | def __init__(self, key: bytes, kid=None, **options) -> None: 101 | super(AbstractJWKBase, self).__init__() 102 | self.key = key 103 | self.kid = kid 104 | 105 | optnames = {'use', 'key_ops', 'alg', 'x5u', 'x5c', 'x5t', 'x5t#s256'} 106 | self.options = {k: v for k, v in options.items() if k in optnames} 107 | 108 | def get_kty(self): 109 | return 'oct' 110 | 111 | def get_kid(self): 112 | return self.kid 113 | 114 | def is_sign_key(self) -> bool: 115 | return True 116 | 117 | def _get_signer(self, options) -> Callable[[bytes, bytes], bytes]: 118 | return options['signer'] 119 | 120 | def sign(self, message: bytes, **options) -> bytes: 121 | signer = self._get_signer(options) 122 | return signer(message, self.key) 123 | 124 | def verify(self, message: bytes, signature: bytes, **options) -> bool: 125 | signer = self._get_signer(options) 126 | return hmac.compare_digest(signature, signer(message, self.key)) 127 | 128 | def to_dict(self, public_only=True): 129 | dct = { 130 | 'kty': 'oct', 131 | 'k': b64encode(self.key), 132 | } 133 | dct.update(self.options) 134 | if self.kid: 135 | dct['kid'] = self.kid 136 | return dct 137 | 138 | @classmethod 139 | def from_dict(cls, dct): 140 | try: 141 | return cls(b64decode(dct['k']), **dct) 142 | except KeyError as why: 143 | raise MalformedJWKError('k is required') from why 144 | 145 | 146 | class RSAJWK(AbstractJWKBase): 147 | """ 148 | https://tools.ietf.org/html/rfc7518.html#section-6.3.1 149 | """ 150 | 151 | def __init__(self, keyobj: Union[RSAPrivateKey, RSAPublicKey], 152 | **options) -> None: 153 | super(AbstractJWKBase, self).__init__() 154 | self.keyobj = keyobj 155 | 156 | optnames = {'use', 'key_ops', 'alg', 'kid', 157 | 'x5u', 'x5c', 'x5t', 'x5t#s256', } 158 | self.options = {k: v for k, v in options.items() if k in optnames} 159 | 160 | def is_sign_key(self) -> bool: 161 | return isinstance(self.keyobj, RSAPrivateKey) 162 | 163 | def _get_hash_fun(self, options) -> Callable[[], HashAlgorithm]: 164 | return options['hash_fun'] 165 | 166 | def _get_padding(self, options) -> padding.AsymmetricPadding: 167 | try: 168 | return options['padding'] 169 | except KeyError: 170 | warn('you should not use RSAJWK.verify/sign without jwa ' 171 | 'intermiediary, used legacy padding') 172 | return padding.PKCS1v15() 173 | 174 | def sign(self, message: bytes, **options) -> bytes: 175 | if isinstance(self.keyobj, RSAPublicKey): 176 | raise ValueError("Requires a private key.") 177 | hash_fun = self._get_hash_fun(options) 178 | _padding = self._get_padding(options) 179 | return self.keyobj.sign(message, _padding, hash_fun()) 180 | 181 | def verify(self, message: bytes, signature: bytes, **options) -> bool: 182 | hash_fun = self._get_hash_fun(options) 183 | _padding = self._get_padding(options) 184 | if isinstance(self.keyobj, RSAPrivateKey): 185 | pubkey = self.keyobj.public_key() 186 | else: 187 | pubkey = self.keyobj 188 | try: 189 | pubkey.verify(signature, message, _padding, hash_fun()) 190 | return True 191 | except InvalidSignature: 192 | return False 193 | 194 | def get_kty(self): 195 | return 'RSA' 196 | 197 | def get_kid(self): 198 | return self.options.get('kid') 199 | 200 | def to_dict(self, public_only=True): 201 | dct = { 202 | 'kty': 'RSA', 203 | } 204 | dct.update(self.options) 205 | 206 | if isinstance(self.keyobj, RSAPrivateKey): 207 | priv_numbers = self.keyobj.private_numbers() 208 | pub_numbers = priv_numbers.public_numbers 209 | dct.update({ 210 | 'e': uint_b64encode(pub_numbers.e), 211 | 'n': uint_b64encode(pub_numbers.n), 212 | }) 213 | if not public_only: 214 | dct.update({ 215 | 'e': uint_b64encode(pub_numbers.e), 216 | 'n': uint_b64encode(pub_numbers.n), 217 | 'd': uint_b64encode(priv_numbers.d), 218 | 'p': uint_b64encode(priv_numbers.p), 219 | 'q': uint_b64encode(priv_numbers.q), 220 | 'dp': uint_b64encode(priv_numbers.dmp1), 221 | 'dq': uint_b64encode(priv_numbers.dmq1), 222 | 'qi': uint_b64encode(priv_numbers.iqmp), 223 | }) 224 | return dct 225 | pub_numbers = self.keyobj.public_numbers() 226 | dct.update({ 227 | 'e': uint_b64encode(pub_numbers.e), 228 | 'n': uint_b64encode(pub_numbers.n), 229 | }) 230 | return dct 231 | 232 | @classmethod 233 | def from_dict(cls, dct): 234 | if 'oth' in dct: 235 | raise UnsupportedKeyTypeError( 236 | 'RSA keys with multiples primes are not supported') 237 | 238 | try: 239 | e = uint_b64decode(dct['e']) 240 | n = uint_b64decode(dct['n']) 241 | except KeyError as why: 242 | raise MalformedJWKError('e and n are required') from why 243 | pub_numbers = RSAPublicNumbers(e, n) 244 | if 'd' not in dct: 245 | return cls( 246 | pub_numbers.public_key(backend=default_backend()), **dct) 247 | d = uint_b64decode(dct['d']) 248 | 249 | privparams = {'p', 'q', 'dp', 'dq', 'qi'} 250 | product = set(dct.keys()) & privparams 251 | if len(product) == 0: 252 | p, q = rsa_recover_prime_factors(n, e, d) 253 | priv_numbers = RSAPrivateNumbers( 254 | d=d, 255 | p=p, 256 | q=q, 257 | dmp1=rsa_crt_dmp1(d, p), 258 | dmq1=rsa_crt_dmq1(d, q), 259 | iqmp=rsa_crt_iqmp(p, q), 260 | public_numbers=pub_numbers) 261 | elif product == privparams: 262 | priv_numbers = RSAPrivateNumbers( 263 | d=d, 264 | p=uint_b64decode(dct['p']), 265 | q=uint_b64decode(dct['q']), 266 | dmp1=uint_b64decode(dct['dp']), 267 | dmq1=uint_b64decode(dct['dq']), 268 | iqmp=uint_b64decode(dct['qi']), 269 | public_numbers=pub_numbers) 270 | else: 271 | # If the producer includes any of the other private key parameters, 272 | # then all of the others MUST be present, with the exception of 273 | # "oth", which MUST only be present when more than two prime 274 | # factors were used. 275 | raise MalformedJWKError( 276 | 'p, q, dp, dq, qi MUST be present or' 277 | 'all of them MUST be absent') 278 | return cls(priv_numbers.private_key(backend=default_backend()), **dct) 279 | 280 | 281 | def supported_key_types() -> Dict[str, Type[AbstractJWKBase]]: 282 | return { 283 | 'oct': OctetJWK, 284 | 'RSA': RSAJWK, 285 | } 286 | 287 | 288 | def jwk_from_dict(dct: Mapping[str, Any]) -> AbstractJWKBase: 289 | if not isinstance(dct, dict): # pragma: no cover 290 | raise TypeError('dct must be a dict') 291 | if 'kty' not in dct: 292 | raise MalformedJWKError('kty MUST be present') 293 | 294 | supported = supported_key_types() 295 | kty = dct['kty'] 296 | if kty not in supported: 297 | raise UnsupportedKeyTypeError('unsupported key type: {}'.format(kty)) 298 | return supported[kty].from_dict(dct) 299 | 300 | 301 | PublicKeyLoaderT = Union[str, Callable[[bytes, object], object]] 302 | PrivateKeyLoaderT = Union[ 303 | str, 304 | Callable[[bytes, Optional[str], object], object]] 305 | _Loader = TypeVar("_Loader", PublicKeyLoaderT, PrivateKeyLoaderT) 306 | _C = TypeVar("_C", bound=Callable[..., Any]) 307 | 308 | 309 | # The above LoaderTs should actually not be Union, and this function should be 310 | # typed something like this. But, this will lose any kwargs from the typing 311 | # information. Probably needs: https://github.com/python/mypy/issues/3157 312 | # (func: Callable[[bytes, _Loader], _T]) 313 | # -> Callable[[bytes, Union[str, _Loader]], _T] 314 | def jwk_from_bytes_argument_conversion(func: _C) -> _C: 315 | if not ('private' in func.__name__ or 'public' in func.__name__): 316 | raise Exception("the wrapped function must have either public" 317 | " or private in it's name") 318 | 319 | @wraps(func) 320 | def wrapper(content, loader, **kwargs): 321 | # now convert it to a Callable if it's a string 322 | if isinstance(loader, str): 323 | loader = getattr(serialization_module, loader) 324 | 325 | if kwargs.get('options') is None: 326 | kwargs['options'] = {} 327 | 328 | return func(content, loader, **kwargs) 329 | return wrapper # type: ignore[return-value] 330 | 331 | 332 | @jwk_from_bytes_argument_conversion 333 | def jwk_from_private_bytes( 334 | content: bytes, 335 | private_loader: PrivateKeyLoaderT, 336 | *, 337 | password: Optional[str] = None, 338 | backend: Optional[object] = None, 339 | options: Optional[Mapping[str, object]] = None, 340 | ) -> AbstractJWKBase: 341 | """This function is meant to be called from jwk_from_bytes""" 342 | if options is None: 343 | options = {} 344 | try: 345 | privkey = private_loader(content, password, backend) # type: ignore[operator] # noqa: E501 346 | if isinstance(privkey, RSAPrivateKey): 347 | return RSAJWK(privkey, **options) 348 | raise UnsupportedKeyTypeError('unsupported key type') 349 | except ValueError as ex: 350 | raise UnsupportedKeyTypeError('this is probably a public key') from ex 351 | 352 | 353 | @jwk_from_bytes_argument_conversion 354 | def jwk_from_public_bytes( 355 | content: bytes, 356 | public_loader: PublicKeyLoaderT, 357 | *, 358 | backend: Optional[object] = None, 359 | options: Optional[Mapping[str, object]] = None 360 | ) -> AbstractJWKBase: 361 | """This function is meant to be called from jwk_from_bytes""" 362 | if options is None: 363 | options = {} 364 | try: 365 | pubkey = public_loader(content, backend) # type: ignore[operator] 366 | if isinstance(pubkey, RSAPublicKey): 367 | return RSAJWK(pubkey, **options) 368 | raise UnsupportedKeyTypeError( 369 | 'unsupported key type') # pragma: no cover 370 | except ValueError as why: 371 | raise UnsupportedKeyTypeError('could not deserialize') from why 372 | 373 | 374 | def jwk_from_bytes( 375 | content: bytes, 376 | private_loader: PrivateKeyLoaderT, 377 | public_loader: PublicKeyLoaderT, 378 | *, 379 | private_password: Optional[str] = None, 380 | backend: Optional[object] = None, 381 | options: Optional[Mapping[str, object]] = None, 382 | ) -> AbstractJWKBase: 383 | try: 384 | return jwk_from_private_bytes( 385 | content, 386 | private_loader, 387 | password=private_password, 388 | backend=backend, 389 | options=options, 390 | ) 391 | except UnsupportedKeyTypeError: 392 | return jwk_from_public_bytes( 393 | content, 394 | public_loader, 395 | backend=backend, 396 | options=options, 397 | ) 398 | 399 | 400 | def jwk_from_pem( 401 | pem_content: bytes, 402 | private_password: Optional[str] = None, 403 | options: Optional[Mapping[str, object]] = None, 404 | ) -> AbstractJWKBase: 405 | return jwk_from_bytes( 406 | pem_content, 407 | private_loader='load_pem_private_key', 408 | public_loader='load_pem_public_key', 409 | private_password=private_password, 410 | backend=None, 411 | options=options, 412 | ) 413 | 414 | 415 | def jwk_from_der( 416 | der_content: bytes, 417 | private_password: Optional[str] = None, 418 | options: Optional[Mapping[str, object]] = None, 419 | ) -> AbstractJWKBase: 420 | return jwk_from_bytes( 421 | der_content, 422 | private_loader='load_der_private_key', 423 | public_loader='load_der_public_key', 424 | private_password=private_password, 425 | backend=None, 426 | options=options, 427 | ) 428 | -------------------------------------------------------------------------------- /jwt/jwkset.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2017 Gehirn Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from collections import UserList 18 | from typing import TYPE_CHECKING 19 | 20 | from .jwk import AbstractJWKBase, jwk_from_dict 21 | 22 | if TYPE_CHECKING: 23 | UserListBase = UserList[AbstractJWKBase] 24 | else: 25 | UserListBase = UserList 26 | 27 | 28 | class JWKSet(UserListBase): 29 | 30 | def filter_keys(self, kid=None, kty=None): 31 | # When "kid" values are used within a JWK Set, different 32 | # keys within the JWK Set SHOULD use distinct "kid" values. (One 33 | # example in which different keys might use the same "kid" value is if 34 | # they have different "kty" (key type) values but are considered to be 35 | # equivalent alternatives by the application using them.) 36 | 37 | if kid and kty: 38 | return [key for key in self.data 39 | if key.get_kty() == kty and key.get_kid() == kid] 40 | if kid: 41 | return [key for key in self.data if key.get_kid() == kid] 42 | if kty: 43 | return [key for key in self.data if key.get_kty() == kty] 44 | 45 | return self.data.copy() 46 | 47 | def to_dict(self, public_only=True): 48 | keys = [key.to_dict(public_only=public_only) for key in self.data] 49 | return {'keys': keys} 50 | 51 | @classmethod 52 | def from_dict(cls, dct): 53 | keys = [jwk_from_dict(key_dct) for key_dct in dct.get('keys', [])] 54 | return cls(keys) 55 | -------------------------------------------------------------------------------- /jwt/jws.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2017 Gehirn Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import json 18 | from typing import ( 19 | AbstractSet, 20 | Dict, 21 | Optional, 22 | Tuple, 23 | ) 24 | 25 | from .exceptions import ( 26 | JWSEncodeError, 27 | JWSDecodeError, 28 | ) 29 | from .jwa import ( 30 | supported_signing_algorithms, 31 | AbstractSigningAlgorithm, 32 | ) 33 | from .jwk import AbstractJWKBase 34 | from .utils import ( 35 | b64encode, 36 | b64decode, 37 | ) 38 | 39 | __all__ = ['JWS'] 40 | 41 | 42 | class JWS: 43 | 44 | def __init__(self) -> None: 45 | self._supported_algs = supported_signing_algorithms() 46 | 47 | def _retrieve_alg(self, alg: str) -> AbstractSigningAlgorithm: 48 | try: 49 | return self._supported_algs[alg] 50 | except KeyError: 51 | raise JWSDecodeError('Unsupported signing algorithm.') 52 | 53 | def encode(self, message: bytes, key: Optional[AbstractJWKBase] = None, 54 | alg='HS256', 55 | optional_headers: Optional[Dict[str, str]] = None) -> str: 56 | if alg not in self._supported_algs: # pragma: no cover 57 | raise JWSEncodeError('unsupported algorithm: {}'.format(alg)) 58 | alg_impl = self._retrieve_alg(alg) 59 | 60 | header = optional_headers.copy() if optional_headers else {} 61 | header['alg'] = alg 62 | 63 | header_b64 = b64encode( 64 | json.dumps(header, separators=(',', ':')).encode('ascii')) 65 | message_b64 = b64encode(message) 66 | signing_message = header_b64 + '.' + message_b64 67 | 68 | signature = alg_impl.sign(signing_message.encode('ascii'), key) 69 | signature_b64 = b64encode(signature) 70 | 71 | return signing_message + '.' + signature_b64 72 | 73 | def _decode_segments( 74 | self, message: str) -> Tuple[Dict[str, str], bytes, bytes, str]: 75 | try: 76 | signing_message, signature_b64 = message.rsplit('.', 1) 77 | header_b64, message_b64 = signing_message.split('.') 78 | except ValueError: 79 | raise JWSDecodeError('malformed JWS payload') 80 | 81 | header = json.loads(b64decode(header_b64).decode('ascii')) 82 | message_bin = b64decode(message_b64) 83 | signature = b64decode(signature_b64) 84 | return header, message_bin, signature, signing_message 85 | 86 | def decode(self, message: str, key: Optional[AbstractJWKBase] = None, 87 | do_verify=True, 88 | algorithms: Optional[AbstractSet[str]] = None) -> bytes: 89 | if algorithms is None: 90 | algorithms = set(supported_signing_algorithms().keys()) 91 | 92 | header, message_bin, signature, signing_message = \ 93 | self._decode_segments(message) 94 | 95 | alg_value = header['alg'] 96 | if alg_value not in algorithms: 97 | raise JWSDecodeError('Unsupported signing algorithm.') 98 | 99 | alg_impl = self._retrieve_alg(alg_value) 100 | if do_verify and not alg_impl.verify( 101 | signing_message.encode('ascii'), key, signature): 102 | raise JWSDecodeError('JWS passed could not be validated') 103 | 104 | return message_bin 105 | -------------------------------------------------------------------------------- /jwt/jwt.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2017 Gehirn Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import json 18 | from datetime import datetime, timezone 19 | from typing import AbstractSet, Any, Dict, Optional 20 | 21 | from jwt.utils import ( 22 | get_time_from_int, 23 | ) 24 | from .exceptions import ( 25 | JWSEncodeError, 26 | JWSDecodeError, 27 | JWTEncodeError, 28 | JWTDecodeError, 29 | ) 30 | from .jwk import AbstractJWKBase 31 | from .jws import JWS 32 | 33 | 34 | class JWT: 35 | 36 | def __init__(self): 37 | self._jws = JWS() 38 | 39 | def encode(self, payload: Dict[str, Any], 40 | key: Optional[AbstractJWKBase] = None, alg='HS256', 41 | optional_headers: Optional[Dict[str, str]] = None) -> str: 42 | if not isinstance(self, JWT): # pragma: no cover 43 | # https://github.com/GehirnInc/python-jwt/issues/15 44 | raise RuntimeError( 45 | 'encode must be called on a jwt.JWT() instance. ' 46 | 'Do jwt.JWT().encode(...)') 47 | if not isinstance(payload, dict): # pragma: no cover 48 | raise TypeError('payload must be a dict') 49 | if not (key is None 50 | or isinstance(key, AbstractJWKBase)): # pragma: no cover 51 | raise TypeError( 52 | 'key must be an instance of a class implements ' 53 | 'jwt.AbstractJWKBase') 54 | if not (optional_headers is None 55 | or isinstance(optional_headers, dict)): # pragma: no cover 56 | raise TypeError('optional_headers must be a dict') 57 | 58 | try: 59 | message = json.dumps(payload).encode('utf-8') 60 | except ValueError as why: 61 | raise JWTEncodeError( 62 | 'payload must be able to be encoded to JSON') from why 63 | 64 | optional_headers = optional_headers and optional_headers.copy() or {} 65 | optional_headers['typ'] = 'JWT' 66 | try: 67 | return self._jws.encode(message, key, alg, optional_headers) 68 | except JWSEncodeError as why: 69 | raise JWTEncodeError('failed to encode to JWT') from why 70 | 71 | def decode(self, message: str, key: Optional[AbstractJWKBase] = None, 72 | do_verify=True, algorithms: Optional[AbstractSet[str]] = None, 73 | do_time_check: bool = True) -> Dict[str, Any]: 74 | if not isinstance(self, JWT): # pragma: no cover 75 | # https://github.com/GehirnInc/python-jwt/issues/15 76 | raise RuntimeError( 77 | 'decode must be called on a jwt.JWT() instance. ' 78 | 'Do jwt.JWT().decode(...)') 79 | if not isinstance(message, str): # pragma: no cover 80 | raise TypeError('message must be a str') 81 | if not (key is None 82 | or isinstance(key, AbstractJWKBase)): # pragma: no cover 83 | raise TypeError( 84 | 'key must be an instance of a class implements ' 85 | 'jwt.AbstractJWKBase') 86 | 87 | # utc now with timezone 88 | now = datetime.now(timezone.utc) 89 | try: 90 | message_bin = self._jws.decode(message, key, do_verify, algorithms) 91 | except JWSDecodeError as why: 92 | raise JWTDecodeError('failed to decode JWT') from why 93 | try: 94 | payload = json.loads(message_bin.decode('utf-8')) 95 | except ValueError as why: 96 | raise JWTDecodeError( 97 | 'a payload of the JWT is not valid JSON') from why 98 | 99 | # The "exp" (expiration time) claim identifies the expiration time on 100 | # or after which the JWT MUST NOT be accepted for processing. 101 | if 'exp' in payload and do_time_check: 102 | try: 103 | exp = get_time_from_int(payload['exp']) 104 | except TypeError: 105 | raise JWTDecodeError("Invalid Expired value") 106 | if now >= exp: 107 | raise JWTDecodeError("JWT Expired") 108 | 109 | # The "nbf" (not before) claim identifies the time before which the JWT 110 | # MUST NOT be accepted for processing. 111 | if 'nbf' in payload and do_time_check: 112 | try: 113 | nbf = get_time_from_int(payload['nbf']) 114 | except TypeError: 115 | raise JWTDecodeError('Invalid "Not valid yet" value') 116 | if now < nbf: 117 | raise JWTDecodeError("JWT Not valid yet") 118 | 119 | return payload 120 | -------------------------------------------------------------------------------- /jwt/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GehirnInc/python-jwt/068db420c9ae957925daf0f5a2baa9319ac20c82/jwt/py.typed -------------------------------------------------------------------------------- /jwt/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2017 Gehirn Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | -------------------------------------------------------------------------------- /jwt/tests/helper.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2017 Gehirn Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import os 18 | 19 | 20 | here = os.path.dirname(os.path.abspath(__file__)) 21 | 22 | 23 | def load_testdata(name, mode='rb'): 24 | abspath = os.path.normpath(os.path.join(here, 'testdata', name)) 25 | with open(abspath, mode=mode) as fh: 26 | return fh.read() 27 | -------------------------------------------------------------------------------- /jwt/tests/test_jwa.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2017 Gehirn Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import json 18 | from unittest import TestCase 19 | 20 | from jwt.jwa import ( 21 | HS256, 22 | none, 23 | ) 24 | from jwt.jwk import jwk_from_dict 25 | from jwt.utils import b64decode 26 | 27 | from .helper import load_testdata 28 | 29 | 30 | class NoneTest(TestCase): 31 | 32 | def setUp(self): 33 | self.message = ( 34 | b'eyJhbGciOiJub25lIn0' 35 | b'.' 36 | b'eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFt' 37 | b'cGxlLmNvbS9pc19yb290Ijp0cnVlfQ' 38 | ) 39 | 40 | def test_sign(self): 41 | signature = none.sign(self.message, None) 42 | self.assertEqual(signature, b'') 43 | 44 | def test_verify(self): 45 | self.assertTrue(none.verify(self.message, None, b'')) 46 | 47 | 48 | class HS256Test(TestCase): 49 | 50 | def setUp(self): 51 | self.key = jwk_from_dict(json.loads(load_testdata('oct.json', 'r'))) 52 | self.signature = b64decode( 53 | 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk' 54 | ) 55 | 56 | self.message = ( 57 | b'eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9' 58 | b'.' 59 | b'eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFt' 60 | b'cGxlLmNvbS9pc19yb290Ijp0cnVlfQ' 61 | ) 62 | 63 | def test_sign(self): 64 | signature = HS256.sign(self.message, self.key) 65 | self.assertEqual(signature, self.signature) 66 | 67 | def test_verify(self): 68 | assert HS256.verify(self.message, self.key, self.signature) 69 | assert not HS256.verify( 70 | self.message + b'asd', self.key, self.signature) 71 | -------------------------------------------------------------------------------- /jwt/tests/test_jwk.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2017 Gehirn Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import json 18 | import unittest 19 | 20 | from cryptography.hazmat.primitives.asymmetric.rsa import ( 21 | RSAPrivateKey, 22 | RSAPublicKey, 23 | ) 24 | 25 | from jwt.exceptions import MalformedJWKError, UnsupportedKeyTypeError 26 | from jwt.jwk import ( 27 | OctetJWK, 28 | RSAJWK, 29 | 30 | jwk_from_dict, 31 | jwk_from_pem, 32 | jwk_from_der, 33 | jwk_from_bytes_argument_conversion, 34 | ) 35 | 36 | from pytest import raises, warns 37 | 38 | from .helper import load_testdata 39 | 40 | 41 | def test_jwk_from_pem(): 42 | jwk_priv = jwk_from_pem(load_testdata('rsa_privkey.pem')) 43 | 44 | assert isinstance(jwk_priv, RSAJWK) 45 | assert isinstance(jwk_priv.keyobj, RSAPrivateKey) 46 | 47 | 48 | def test_jwk_from_dict(): 49 | jwk_priv = jwk_from_dict( 50 | json.loads(load_testdata('rsa_privkey.json', 'r'))) 51 | 52 | assert isinstance(jwk_priv, RSAJWK) 53 | assert isinstance(jwk_priv.keyobj, RSAPrivateKey) 54 | 55 | 56 | def test_jwk_from_dict_malformed_kty(): 57 | json_priv = json.loads(load_testdata('rsa_privkey.json', 'r')) 58 | del json_priv['kty'] 59 | with raises(MalformedJWKError): 60 | jwk_from_dict(json_priv) 61 | 62 | 63 | def test_jwk_from_dict_unsupported_kty(): 64 | json_priv = json.loads(load_testdata('rsa_privkey.json', 'r')) 65 | json_priv['kty'] = 'unknown' 66 | with raises(UnsupportedKeyTypeError): 67 | jwk_from_dict(json_priv) 68 | 69 | 70 | def test_jwk_from_bytes_argument_conversion_confusing_name(): 71 | with raises(Exception) as ex: 72 | @jwk_from_bytes_argument_conversion 73 | def confusing(): # pylint: disable=unused-variable # pragma: no cover 74 | pass 75 | assert ("the wrapped function must have either public" 76 | " or private in it's name" in str(ex)) 77 | 78 | 79 | def test_jwk_from_unsupported_pem(): 80 | with raises(UnsupportedKeyTypeError): 81 | jwk_from_pem(load_testdata('dsa_privkey.pem')) 82 | 83 | 84 | def test_jwk_from_pem_not_deserializable(): 85 | with raises(UnsupportedKeyTypeError): 86 | jwk_from_pem(b'') 87 | 88 | 89 | def test_jwk_from_der(): 90 | jwk_priv = jwk_from_der(load_testdata('rsa_privkey.der')) 91 | 92 | assert isinstance(jwk_priv, RSAJWK) 93 | assert isinstance(jwk_priv.keyobj, RSAPrivateKey) 94 | 95 | 96 | class OctetJWKTest(unittest.TestCase): 97 | 98 | def setUp(self): 99 | self.key_json = json.loads(load_testdata('oct.json', 'r')) 100 | self.inst = OctetJWK.from_dict(self.key_json) 101 | 102 | def test_get_kty(self): 103 | self.assertEqual(self.inst.get_kty(), 'oct') 104 | 105 | def test_get_kid(self): 106 | self.assertEqual( 107 | self.inst.get_kid(), 'HMAC key used in JWS A.1 example') 108 | 109 | def test_is_sign_key(self): 110 | self.assertTrue(self.inst.is_sign_key()) 111 | 112 | def test_to_dict(self): 113 | self.assertEqual(self.inst.to_dict(public_only=False), self.key_json) 114 | 115 | def test_from_dict_missing_k(self): 116 | key_json = self.key_json.copy() 117 | del key_json['k'] 118 | with raises(MalformedJWKError): 119 | OctetJWK.from_dict(key_json) 120 | 121 | 122 | class RSAJWKTest(unittest.TestCase): 123 | 124 | def setUp(self): 125 | self.privkey_pem = load_testdata('rsa_privkey.pem') 126 | self.inst_priv = jwk_from_pem(self.privkey_pem) 127 | 128 | self.pubkey_pem = load_testdata('rsa_pubkey.pem') 129 | self.inst_pub = jwk_from_pem(self.pubkey_pem) 130 | 131 | self.privkey_json = json.loads( 132 | load_testdata('rsa_privkey.json', 'r')) 133 | self.privkey_full_json = json.loads( 134 | load_testdata('rsa_privkey_full.json', 'r')) 135 | self.pubkey_json = json.loads( 136 | load_testdata('rsa_pubkey.json', 'r')) 137 | 138 | def test_is_sign_key(self): 139 | self.assertTrue(self.inst_priv.is_sign_key()) 140 | self.assertFalse(self.inst_pub.is_sign_key()) 141 | 142 | def test_get_kty(self): 143 | self.assertEqual(self.inst_priv.get_kty(), 'RSA') 144 | self.assertEqual(self.inst_pub.get_kty(), 'RSA') 145 | 146 | def test_to_dict_pub(self): 147 | self.assertEqual( 148 | self.inst_pub.to_dict(public_only=False), 149 | self.pubkey_json) 150 | 151 | def test_to_dict_priv(self): 152 | self.assertEqual( 153 | self.inst_priv.to_dict(public_only=False), 154 | self.privkey_full_json) 155 | 156 | def test_to_dict_pubonly(self): 157 | self.assertEqual( 158 | self.inst_priv.to_dict(public_only=True), 159 | self.inst_pub.to_dict()) 160 | 161 | def test_from_dict_pub(self): 162 | inst = RSAJWK.from_dict(self.pubkey_json) 163 | self.assertIsInstance(inst, RSAJWK) 164 | self.assertIsInstance(inst.keyobj, RSAPublicKey) 165 | 166 | self.assertEqual(inst.to_dict(public_only=False), self.pubkey_json) 167 | 168 | def test_from_dict_priv_full(self): 169 | inst = RSAJWK.from_dict(self.privkey_full_json) 170 | self.assertIsInstance(inst, RSAJWK) 171 | self.assertIsInstance(inst.keyobj, RSAPrivateKey) 172 | 173 | self.assertEqual( 174 | inst.to_dict(public_only=False), self.privkey_full_json) 175 | 176 | def test_from_dict_priv_oth_unsupported(self): 177 | with raises(UnsupportedKeyTypeError): 178 | _json = self.privkey_full_json.copy() 179 | _json['oth'] = 'unsupported' 180 | RSAJWK.from_dict(_json) 181 | 182 | def test_from_dict_priv_malformed_e(self): 183 | with raises(MalformedJWKError): 184 | _json = self.privkey_full_json.copy() 185 | del _json['e'] 186 | RSAJWK.from_dict(_json) 187 | 188 | def test_from_dict_priv_malformed_q(self): 189 | with raises(MalformedJWKError): 190 | _json = self.privkey_full_json.copy() 191 | del _json['q'] 192 | RSAJWK.from_dict(_json) 193 | 194 | def test_verify_invalid(self): 195 | from cryptography.hazmat.primitives.hashes import SHA256 196 | inst = RSAJWK.from_dict(self.privkey_full_json) 197 | with warns(UserWarning): 198 | assert not inst.verify(b'hello everyone', b'', hash_fun=SHA256) 199 | -------------------------------------------------------------------------------- /jwt/tests/test_jwkset.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2017 Gehirn Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import json 18 | from unittest import TestCase 19 | 20 | from jwt.jwk import jwk_from_dict 21 | from jwt.jwkset import JWKSet 22 | 23 | from .helper import load_testdata 24 | 25 | 26 | class JWKSetTest(TestCase): 27 | 28 | def setUp(self): 29 | self.inst = JWKSet() 30 | 31 | self.oct_json = json.loads(load_testdata('oct.json', 'r')) 32 | self.inst.append(jwk_from_dict(self.oct_json)) 33 | 34 | self.rsa_json = json.loads(load_testdata('rsa_privkey_full.json', 'r')) 35 | self.rsa_pub_json = json.loads(load_testdata('rsa_pubkey.json', 'r')) 36 | self.inst.append(jwk_from_dict(self.rsa_json)) 37 | 38 | def test_filter_keys(self): 39 | self.assertEqual( 40 | [key.to_dict(public_only=True) for key in self.inst.filter_keys()], 41 | [self.oct_json, self.rsa_pub_json]) 42 | 43 | self.assertEqual( 44 | [key.to_dict(public_only=True) 45 | for key in self.inst.filter_keys( 46 | kid='HMAC key used in JWS A.1 example')], 47 | [self.oct_json]) 48 | 49 | self.assertEqual( 50 | [key.to_dict(public_only=True) 51 | for key in self.inst.filter_keys(kty='RSA')], 52 | [self.rsa_pub_json]) 53 | 54 | self.assertEqual( 55 | [key.to_dict(public_only=True) 56 | for key in self.inst.filter_keys( 57 | kid='HMAC key used in JWS A.1 example', kty='oct')], 58 | [self.oct_json]) 59 | 60 | def test_to_dict(self): 61 | self.maxDiff = None 62 | self.assertEqual( 63 | self.inst.to_dict(public_only=True), 64 | {'keys': [self.oct_json, self.rsa_pub_json]}) 65 | 66 | def test_from_dict(self): 67 | inst = JWKSet.from_dict({'keys': [self.oct_json, self.rsa_pub_json]}) 68 | self.assertEqual(inst[0].to_dict(public_only=True), self.oct_json) 69 | self.assertEqual(inst[1].to_dict(public_only=True), self.rsa_pub_json) 70 | -------------------------------------------------------------------------------- /jwt/tests/test_jws.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2017 Gehirn Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import json 18 | from unittest import TestCase 19 | 20 | from freezegun import freeze_time 21 | 22 | from jwt.jws import JWS 23 | from jwt.jwk import jwk_from_dict 24 | 25 | from .helper import load_testdata 26 | 27 | 28 | class JWSTest(TestCase): 29 | 30 | def setUp(self): 31 | self.inst = JWS() 32 | self.key = jwk_from_dict( 33 | json.loads(load_testdata('rsa_privkey.json', 'r'))) 34 | self.pubkey = jwk_from_dict( 35 | json.loads(load_testdata('rsa_pubkey.json', 'r'))) 36 | 37 | self.message = ( 38 | b'{"iss":"joe",\r\n' 39 | b' "exp":1300819380,\r\n' 40 | b' "http://example.com/is_root":true}' 41 | ) 42 | self.compact_jws = ( 43 | 'eyJhbGciOiJSUzI1NiJ9' 44 | '.' 45 | 'eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFt' 46 | 'cGxlLmNvbS9pc19yb290Ijp0cnVlfQ' 47 | '.' 48 | 'cC4hiUPoj9Eetdgtv3hF80EGrhuB__dzERat0XF9g2VtQgr9PJbu3XOiZj5RZmh7' 49 | 'AAuHIm4Bh-0Qc_lF5YKt_O8W2Fp5jujGbds9uJdbF9CUAr7t1dnZcAcQjbKBYNX4' 50 | 'BAynRFdiuB--f_nZLgrnbyTyWzO75vRK5h6xBArLIARNPvkSjtQBMHlb1L07Qe7K' 51 | '0GarZRmB_eSN9383LcOLn6_dO--xi12jzDwusC-eOkHWEsqtFZESc6BfI7noOPqv' 52 | 'hJ1phCnvWh6IeYI2w9QOYEUipUTI8np6LbgGY9Fs98rqVt5AXLIhWkWywlVmtVrB' 53 | 'p0igcN_IoypGlUPQGe77Rw' 54 | ) 55 | 56 | def test_encode(self): 57 | compact_jws = self.inst.encode(self.message, self.key, alg='RS256') 58 | self.assertEqual(compact_jws, self.compact_jws) 59 | 60 | def test_decode(self): 61 | message = self.inst.decode(self.compact_jws, self.key) 62 | self.assertEqual(message, self.message) 63 | 64 | @freeze_time("2011-03-22 18:00:00", tz_offset=0) 65 | def test_decode_pubkey(self): 66 | message = self.inst.decode(self.compact_jws, self.pubkey) 67 | self.assertEqual(message, self.message) 68 | -------------------------------------------------------------------------------- /jwt/tests/test_jwt.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2017 Gehirn Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import json 18 | from datetime import timedelta, datetime, timezone 19 | from unittest import TestCase 20 | 21 | from freezegun import freeze_time 22 | 23 | from jwt.exceptions import JWTDecodeError 24 | from jwt.jwk import jwk_from_dict 25 | from jwt.jwt import JWT 26 | from jwt.utils import get_int_from_datetime 27 | 28 | from .helper import load_testdata 29 | 30 | 31 | class JWTTest(TestCase): 32 | 33 | def setUp(self): 34 | self.inst = JWT() 35 | self.key = jwk_from_dict( 36 | json.loads(load_testdata('oct.json', 'r'))) 37 | 38 | self.message = { 39 | 'iss': 'joe', 40 | 'exp': 1300819380, 41 | 'http://example.com/is_root': True, 42 | } 43 | 44 | self.compact_jws = ( 45 | 'eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9' 46 | '.' 47 | 'eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFt' 48 | 'cGxlLmNvbS9pc19yb290Ijp0cnVlfQ' 49 | '.' 50 | 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk' 51 | ) 52 | 53 | @freeze_time("2011-03-22 18:00:00", tz_offset=0) 54 | def test_decode(self): 55 | message = self.inst.decode(self.compact_jws, self.key) 56 | self.assertEqual(message, self.message) 57 | 58 | def test_decode_with_do_time_check_disabled(self): 59 | message = self.inst.decode( 60 | self.compact_jws, self.key, do_time_check=False) 61 | self.assertEqual(message, self.message) 62 | 63 | def test_expiration(self): 64 | self.assertRaisesRegex( 65 | JWTDecodeError, 'JWT Expired', 66 | self.inst.decode, self.compact_jws, self.key 67 | ) 68 | 69 | def test_no_before_used_before(self): 70 | compact_jws = self.inst.encode({ 71 | 'nbf': get_int_from_datetime( 72 | datetime.now(timezone.utc) + timedelta(hours=1)) 73 | }, self.key) 74 | self.assertRaisesRegex( 75 | JWTDecodeError, 'JWT Not valid yet', 76 | self.inst.decode, compact_jws, self.key 77 | ) 78 | 79 | def test_no_before_used_after(self): 80 | message = { 81 | 'nbf': get_int_from_datetime( 82 | datetime.now(timezone.utc) - timedelta(hours=1)) 83 | } 84 | compact_jws = self.inst.encode(message, self.key) 85 | self.assertEqual(self.inst.decode(compact_jws, self.key), message) 86 | 87 | def test_encoded_with_rs(self): 88 | message = {'hello': 'there'} 89 | key = jwk_from_dict( 90 | json.loads(load_testdata('rsa_privkey.json', 'r'))) 91 | comp = self.inst.encode(message, key, alg='RS256') 92 | assert self.inst.decode(comp, key) == message 93 | 94 | def test_encoded_with_pss(self): 95 | message = {'hello': 'there'} 96 | key = jwk_from_dict( 97 | json.loads(load_testdata('rsa_privkey.json', 'r'))) 98 | comp = self.inst.encode(message, key, alg='PS256') 99 | assert self.inst.decode(comp, key) == message 100 | -------------------------------------------------------------------------------- /jwt/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2017 Gehirn Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from datetime import ( 18 | datetime, 19 | timedelta, 20 | timezone, 21 | ) 22 | 23 | from jwt.utils import ( 24 | b64encode, 25 | b64decode, 26 | get_time_from_int, 27 | get_int_from_datetime, 28 | uint_b64encode, 29 | uint_b64decode, 30 | ) 31 | 32 | 33 | def test_b64encode(): 34 | ret = (b'{"iss":"joe",\r\n "exp":1300819380,\r\n ' 35 | b'"http://example.com/is_root":true}') 36 | expected = ('eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQog' 37 | 'Imh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ') 38 | assert b64encode(ret) == expected 39 | 40 | 41 | def test_b64decode(): 42 | ret = ('eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQog' 43 | 'Imh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ') 44 | expected = (b'{"iss":"joe",\r\n "exp":1300819380,\r\n ' 45 | b'"http://example.com/is_root":true}') 46 | assert b64decode(ret) == expected 47 | 48 | 49 | def test_uint_b64encode(): 50 | assert uint_b64encode(65537) == 'AQAB' 51 | 52 | 53 | def test_uint_b64decode(): 54 | assert uint_b64decode('AQAB') == 65537 55 | 56 | 57 | def test_get_time_from_int(): 58 | expected = datetime(2011, 3, 22, 18, 43, tzinfo=timezone.utc) 59 | assert get_time_from_int(1300819380) == expected 60 | 61 | 62 | def test_get_int_from_datetime_with_utc_timezone(): 63 | param = datetime(2011, 3, 22, 18, 43, tzinfo=timezone.utc) 64 | assert get_int_from_datetime(param) == 1300819380 65 | 66 | 67 | def test_get_int_from_datetime_with_timezone(): 68 | param = datetime(2011, 3, 22, 19, 43, tzinfo=timezone(timedelta(hours=1))) 69 | assert get_int_from_datetime(param) == 1300819380 70 | -------------------------------------------------------------------------------- /jwt/tests/testdata/dsa_privkey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN DSA PRIVATE KEY----- 2 | MIIDVgIBAAKCAQEA5dnCTyloWER4Lz7Si1fKbFd9zZLBj9MAJ3sRC52oM7Y5uApU 3 | yBM72VXOoJtbruxuyMUGBEHeYJwJ18hBBGGCy3c2TAYyJ23QJa4MwKEN07W26gpw 4 | rodCMVRODSWVfSIqKOh6sEUOPqDko7kTOiToO8tGaEWqu+WEGo5Io26Lu6/vBbQT 5 | /CLdXNK3Jki9A2ZRpgsmCcm0f877y7/PBx7t0EDFvgcLrgr/Cied6ZlgA1WqcyDl 6 | Y3PG7ov8FiwQbLzmI4iPtlDUmHlEDja5WnGL+Ze8mMNzhWKR29ysIm/q2TvUE5aI 7 | Zx+dH8ql9YuqggGsTiapy8aS4SUVD6FFy/j5DQIhAMpVj3f1loMLpHNmNxsqiqPw 8 | sob12MqK33cR6TOIpjsxAoIBAGVWdxB0jkscWpN0hCG7WELKcrIG6UFBVGstYP+c 9 | hRE8v3eiysEuqH8J4F+D2j82yf+2NprG/uDfbFm5nX8VAYIkcj5QliP4v41J/dwU 10 | 0wDUwIk2U+s3tzDsfvY9iaaVYF351ByiqzT25rE8kZaZ1/sFjkWap+9eI5ZwOTQT 11 | tnI+NuqcOtcdFsPoRT3V+0VjBublgqGyqKNLxNOW6mEnsLy+OC7KhTFTjjRAoO0Y 12 | 1UNccUTDMmklUjwjroSUkexLPkdOWWbROg3aR1XQ89+hC2NXcqsAzXTFDNKggRQ6 13 | WIki0eKmZcCPVuQI5VaD8jPTF22vjuckZ2iu64pOZYkb3MkCggEBALGnGus/+MxW 14 | SJxiU1uC6otpeN2V8WIxASBQE85ygEJGU2v1yO0JWYl0zthCc6/Nc9GCXvzN0Y+j 15 | tNezYFBIxjaRrDP4KI/LyBjGc32C/gCDRjMytEu4hhOhL6Ja+D+RLvFyyUorHxn7 16 | 7L1jI3CLb3QbI9TUktmAxtd/IbfIZudioYrsXu8ZEsIxW2rZ1pU2OME+JaFbv84+ 17 | yoMloviK5LUA3Ycv0rLpUogNNd2KCTixGWu8L0Q/N4uG1Y1uYZRUy4OcOsEvZeT7 18 | p9a0us4wVPXVBm3RRMgbu7knCrIER5d69dwdZP/krbpEVGY5HcIzG4c/sNfrjbsU 19 | mEEwfAOxPY8CICP0GbrUokc/ymAD+mlc3BDtmXu+mjYyUlLnwe5ui1CO 20 | -----END DSA PRIVATE KEY----- 21 | -------------------------------------------------------------------------------- /jwt/tests/testdata/oct.json: -------------------------------------------------------------------------------- 1 | { 2 | "kty": "oct", 3 | "kid": "HMAC key used in JWS A.1 example", 4 | "k": "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow" 5 | } 6 | -------------------------------------------------------------------------------- /jwt/tests/testdata/rsa_privkey.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GehirnInc/python-jwt/068db420c9ae957925daf0f5a2baa9319ac20c82/jwt/tests/testdata/rsa_privkey.der -------------------------------------------------------------------------------- /jwt/tests/testdata/rsa_privkey.json: -------------------------------------------------------------------------------- 1 | {"kty":"RSA", 2 | "e":"AQAB", 3 | "n":"ofgWCuLjybRlzo0tZWJjNiuSfb4p4fAkd_wWJcyQoTbji9k0l8W26mPddxHmfHQp-Vaw-4qPCJrcS2mJPMEzP1Pt0Bm4d4QlL-yRT-SFd2lZS-pCgNMsD1W_YpRPEwOWvG6b32690r2jZ47soMZo9wGzjb_7OMg0LOL-bSf63kpaSHSXndS5z5rexMdbBYUsLA9e-KXBdQOS-UTo7WTBEMa2R2CapHg665xsmtdVMTBQY4uDZlxvb3qCo5ZwKh9kG4LT6_I5IhlJH7aGhyxXFvUK-DWNmoudF8NAco9_h9iaGNj8q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXpoQ", 4 | "d":"Eq5xpGnNCivDflJsRQBXHx1hdR1k6Ulwe2JZD50LpXyWPEAeP88vLNO97IjlA7_GQ5sLKMgvfTeXZx9SE-7YwVol2NXOoAJe46sui395IW_GO-pWJ1O0BkTGoVEn2bKVRUCgu-GjBVaYLU6f3l9kJfFNS3E0QbVdxzubSu3Mkqzjkn439X0M_V51gfpRLI9JYanrC4D4qAdGcopV_0ZHHzQlBjudU2QvXt4ehNYTCBr6XCLQUShb1juUO1ZdiYoFaFQT5Tw8bGUl_x_jTj3ccPDVZFD9pIuhLhBOneufuBiB4cS98l2SR_RQyGWSeWjnczT0QU91p1DhOVRuOopznQ" 5 | } 6 | -------------------------------------------------------------------------------- /jwt/tests/testdata/rsa_privkey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCh+BYK4uPJtGXO 3 | jS1lYmM2K5J9vinh8CR3/BYlzJChNuOL2TSXxbbqY913EeZ8dCn5VrD7io8ImtxL 4 | aYk8wTM/U+3QGbh3hCUv7JFP5IV3aVlL6kKA0ywPVb9ilE8TA5a8bpvfbr3SvaNn 5 | juygxmj3AbONv/s4yDQs4v5tJ/reSlpIdJed1LnPmt7Ex1sFhSwsD174pcF1A5L5 6 | ROjtZMEQxrZHYJqkeDrrnGya11UxMFBji4NmXG9veoKjlnAqH2QbgtPr8jkiGUkf 7 | toaHLFcW9Qr4NY2ai50Xw0Byj3+H2JoY2PyrZ62EWQwuz3WTOTY8BwNNb2BvniHg 8 | VFbK5emhAgMBAAECggEAEq5xpGnNCivDflJsRQBXHx1hdR1k6Ulwe2JZD50LpXyW 9 | PEAeP88vLNO97IjlA7/GQ5sLKMgvfTeXZx9SE+7YwVol2NXOoAJe46sui395IW/G 10 | O+pWJ1O0BkTGoVEn2bKVRUCgu+GjBVaYLU6f3l9kJfFNS3E0QbVdxzubSu3Mkqzj 11 | kn439X0M/V51gfpRLI9JYanrC4D4qAdGcopV/0ZHHzQlBjudU2QvXt4ehNYTCBr6 12 | XCLQUShb1juUO1ZdiYoFaFQT5Tw8bGUl/x/jTj3ccPDVZFD9pIuhLhBOneufuBiB 13 | 4cS98l2SR/RQyGWSeWjnczT0QU91p1DhOVRuOopznQKBgQDgHMQQ60imZV1URk0K 14 | pLttoLhyt3SmqdEf/kgHePDdtzEafpAu+cS191R2JiuoF2yzWXnwFDcqGigp5BNr 15 | 0AHQfD8/Lk8l1Mk09jxxCfui5nYooNyac8YFjm3vItzVCVDnEd3BbVWG8qf6deqE 16 | lMGAg+C2V0L4oNXnP7LZcPAZRwKBgQC5A8R+CZW2MvRTLLHzwZkUXV866cff7tx6 17 | kqa0finGG0KgeqlaZPxgCZnFp7AeAcCMtiynVlJ7vMVgePsLq6XtON4tB5kP9jAP 18 | rq3rbAOal78eUH5OcED6eNuCV8ixEu1eWcPNCS/l1OW1EnXUEoEHKl54Xrrz2uNw 19 | kgMTP1FZ1wKBgAcCn1dwJKufzBWQxWQp1vsM5fggqPN1qGb5y0MAk3g7/Ls5bkUp 20 | 5u9SN0Ai3Ya6hNnvWJMb7sXQX6U/zyO2M/hTip7tUeh7CXgwo59dkpN75gJLVds2 21 | 9+DAncu3KXU4f2Fa+7bLNrur53k8KwPOq2bbuTG69QtV7Jr5MR0AHWKNAoGBAIf/ 22 | evpitUf+4JYbLpvNXWcY052Mpz22aR84mY3nh3F2LF2mjMJDpTg7FmuyPcVw6EcG 23 | yoAe9fa65iNqCq+jdw6PVNGo2hxfjSiZ8IIzHdsPXI89//pMjZcQK9r+CCoRjaZj 24 | OYiIDktVWZzmevJuv6WywUqd57LE3Zar3dLSIkx1AoGAIYd7DHOhrWvxkwPQsRM2 25 | tOgrjbcrfvtQJipd+DlcxyVuuM9sQLdgjVk2oy26F0EmpScGLq2MowX7fhd/QJQ3 26 | ydy5cY7YIBi87w93IKLEdfnbJtoOPLUW0ITrJReOgo1cq9SbsxYawBgfp/gh6A56 27 | 03k2+ZQwVK0JKSHuLFkuQ3U= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /jwt/tests/testdata/rsa_privkey_full.json: -------------------------------------------------------------------------------- 1 | {"kty":"RSA", 2 | "e":"AQAB", 3 | "n":"ofgWCuLjybRlzo0tZWJjNiuSfb4p4fAkd_wWJcyQoTbji9k0l8W26mPddxHmfHQp-Vaw-4qPCJrcS2mJPMEzP1Pt0Bm4d4QlL-yRT-SFd2lZS-pCgNMsD1W_YpRPEwOWvG6b32690r2jZ47soMZo9wGzjb_7OMg0LOL-bSf63kpaSHSXndS5z5rexMdbBYUsLA9e-KXBdQOS-UTo7WTBEMa2R2CapHg665xsmtdVMTBQY4uDZlxvb3qCo5ZwKh9kG4LT6_I5IhlJH7aGhyxXFvUK-DWNmoudF8NAco9_h9iaGNj8q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXpoQ", 4 | "d":"Eq5xpGnNCivDflJsRQBXHx1hdR1k6Ulwe2JZD50LpXyWPEAeP88vLNO97IjlA7_GQ5sLKMgvfTeXZx9SE-7YwVol2NXOoAJe46sui395IW_GO-pWJ1O0BkTGoVEn2bKVRUCgu-GjBVaYLU6f3l9kJfFNS3E0QbVdxzubSu3Mkqzjkn439X0M_V51gfpRLI9JYanrC4D4qAdGcopV_0ZHHzQlBjudU2QvXt4ehNYTCBr6XCLQUShb1juUO1ZdiYoFaFQT5Tw8bGUl_x_jTj3ccPDVZFD9pIuhLhBOneufuBiB4cS98l2SR_RQyGWSeWjnczT0QU91p1DhOVRuOopznQ", 5 | "p":"4BzEEOtIpmVdVEZNCqS7baC4crd0pqnRH_5IB3jw3bcxGn6QLvnEtfdUdiYrqBdss1l58BQ3KhooKeQTa9AB0Hw_Py5PJdTJNPY8cQn7ouZ2KKDcmnPGBY5t7yLc1QlQ5xHdwW1VhvKn-nXqhJTBgIPgtldC-KDV5z-y2XDwGUc", 6 | "q":"uQPEfgmVtjL0Uyyx88GZFF1fOunH3-7cepKmtH4pxhtCoHqpWmT8YAmZxaewHgHAjLYsp1ZSe7zFYHj7C6ul7TjeLQeZD_YwD66t62wDmpe_HlB-TnBA-njbglfIsRLtXlnDzQkv5dTltRJ11BKBBypeeF6689rjcJIDEz9RWdc", 7 | "dp":"BwKfV3Akq5_MFZDFZCnW-wzl-CCo83WoZvnLQwCTeDv8uzluRSnm71I3QCLdhrqE2e9YkxvuxdBfpT_PI7Yz-FOKnu1R6HsJeDCjn12Sk3vmAktV2zb34MCdy7cpdTh_YVr7tss2u6vneTwrA86rZtu5Mbr1C1XsmvkxHQAdYo0", 8 | "dq":"h_96-mK1R_7glhsum81dZxjTnYynPbZpHziZjeeHcXYsXaaMwkOlODsWa7I9xXDoRwbKgB719rrmI2oKr6N3Do9U0ajaHF-NKJnwgjMd2w9cjz3_-kyNlxAr2v4IKhGNpmM5iIgOS1VZnOZ68m6_pbLBSp3nssTdlqvd0tIiTHU", 9 | "qi":"IYd7DHOhrWvxkwPQsRM2tOgrjbcrfvtQJipd-DlcxyVuuM9sQLdgjVk2oy26F0EmpScGLq2MowX7fhd_QJQ3ydy5cY7YIBi87w93IKLEdfnbJtoOPLUW0ITrJReOgo1cq9SbsxYawBgfp_gh6A5603k2-ZQwVK0JKSHuLFkuQ3U" 10 | } 11 | -------------------------------------------------------------------------------- /jwt/tests/testdata/rsa_pubkey.json: -------------------------------------------------------------------------------- 1 | {"kty":"RSA", 2 | "e":"AQAB", 3 | "n":"ofgWCuLjybRlzo0tZWJjNiuSfb4p4fAkd_wWJcyQoTbji9k0l8W26mPddxHmfHQp-Vaw-4qPCJrcS2mJPMEzP1Pt0Bm4d4QlL-yRT-SFd2lZS-pCgNMsD1W_YpRPEwOWvG6b32690r2jZ47soMZo9wGzjb_7OMg0LOL-bSf63kpaSHSXndS5z5rexMdbBYUsLA9e-KXBdQOS-UTo7WTBEMa2R2CapHg665xsmtdVMTBQY4uDZlxvb3qCo5ZwKh9kG4LT6_I5IhlJH7aGhyxXFvUK-DWNmoudF8NAco9_h9iaGNj8q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXpoQ" 4 | } 5 | -------------------------------------------------------------------------------- /jwt/tests/testdata/rsa_pubkey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAofgWCuLjybRlzo0tZWJj 3 | NiuSfb4p4fAkd/wWJcyQoTbji9k0l8W26mPddxHmfHQp+Vaw+4qPCJrcS2mJPMEz 4 | P1Pt0Bm4d4QlL+yRT+SFd2lZS+pCgNMsD1W/YpRPEwOWvG6b32690r2jZ47soMZo 5 | 9wGzjb/7OMg0LOL+bSf63kpaSHSXndS5z5rexMdbBYUsLA9e+KXBdQOS+UTo7WTB 6 | EMa2R2CapHg665xsmtdVMTBQY4uDZlxvb3qCo5ZwKh9kG4LT6/I5IhlJH7aGhyxX 7 | FvUK+DWNmoudF8NAco9/h9iaGNj8q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXp 8 | oQIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /jwt/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2017 Gehirn Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from base64 import ( 18 | urlsafe_b64encode, 19 | urlsafe_b64decode, 20 | ) 21 | from datetime import datetime, timezone 22 | 23 | 24 | def b64encode(s: bytes) -> str: 25 | s_bin = urlsafe_b64encode(s) 26 | s_bin = s_bin.replace(b'=', b'') 27 | return s_bin.decode('ascii') 28 | 29 | 30 | def b64decode(s: str) -> bytes: 31 | s_bin = s.encode('ascii') 32 | s_bin += b'=' * (4 - len(s_bin) % 4) 33 | return urlsafe_b64decode(s_bin) 34 | 35 | 36 | def uint_b64encode(value: int) -> str: 37 | length = 1 38 | rem = value >> 8 39 | while rem: 40 | length += 1 41 | rem >>= 8 42 | 43 | uint_bin = value.to_bytes(length, 'big', signed=False) 44 | return b64encode(uint_bin) 45 | 46 | 47 | def uint_b64decode(uint_b64: str) -> int: 48 | uint_bin = b64decode(uint_b64) 49 | 50 | value = 0 51 | for b in uint_bin: 52 | value <<= 8 53 | value += int(b) 54 | return value 55 | 56 | 57 | def get_time_from_int(value: int) -> datetime: 58 | """ 59 | :param value: seconds since the Epoch 60 | :return: datetime 61 | """ 62 | if not isinstance(value, int): # pragma: no cover 63 | raise TypeError('an int is required') 64 | return datetime.fromtimestamp(value, timezone.utc) 65 | 66 | 67 | def get_int_from_datetime(value: datetime) -> int: 68 | """ 69 | :param value: datetime with or without timezone, if don't contains timezone 70 | it will managed as it is UTC 71 | :return: Seconds since the Epoch 72 | """ 73 | if not isinstance(value, datetime): # pragma: no cover 74 | raise TypeError('a datetime is required') 75 | return int(value.timestamp()) 76 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -e . 2 | tox ~= 3.25 3 | mypy 4 | pytest ~= 6.0 5 | pytest-cov 6 | pytest-flake8 7 | freezegun 8 | types-freezegun 9 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = W503 E731 3 | 4 | [tool:pytest] 5 | addopts = --flake8 --cov jwt --cov-report term 6 | 7 | [coverage:report] 8 | show_missing = True 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2017 Gehirn Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | import os 19 | 20 | from setuptools import ( 21 | setup, 22 | find_packages, 23 | ) 24 | 25 | here = os.path.dirname(__file__) 26 | requires = [ 27 | 'cryptography >= 3.1, != 3.4.0', 28 | ] 29 | 30 | 31 | with open(os.path.join(here, './README.rst'), 'r') as fh: 32 | long_description = fh.read() 33 | 34 | setup( 35 | name='jwt', 36 | version='1.3.1', 37 | 38 | description='JSON Web Token library for Python 3.', 39 | long_description=long_description, 40 | url='https://github.com/GehirnInc/python-jwt', 41 | 42 | author='Kohei YOSHIDA', 43 | author_email='kohei.yoshida@gehirn.co.jp', 44 | 45 | classifiers=[ 46 | "Development Status :: 5 - Production/Stable", 47 | "Intended Audience :: Developers", 48 | "License :: OSI Approved :: Apache Software License", 49 | "Operating System :: OS Independent", 50 | "Programming Language :: Python", 51 | "Programming Language :: Python :: 3.6", 52 | "Programming Language :: Python :: 3.7", 53 | "Programming Language :: Python :: 3.8", 54 | "Topic :: Internet :: WWW/HTTP", 55 | "Topic :: Security", 56 | "Topic :: Software Development :: Libraries :: Python Modules", 57 | ], 58 | 59 | packages=find_packages(exclude=('jwt.tests', )), 60 | package_data={"jwt": ["py.typed"]}, 61 | 62 | install_requires=requires, 63 | python_requires='>= 3.6', 64 | ) 65 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{36,37,38,39} 3 | 4 | [testenv] 5 | deps = pytest ~= 6.0 6 | pytest-cov 7 | pytest-flake8 8 | freezegun 9 | commands = py.test --verbose --capture=tee-sys jwt 10 | --------------------------------------------------------------------------------