diff options
author | Mark Adams <mark@markadams.me> | 2015-10-24 20:07:47 -0500 |
---|---|---|
committer | Mark Adams <mark@markadams.me> | 2015-10-24 20:07:47 -0500 |
commit | 9c551e754d3edf1d500296b172e85904dc593006 (patch) | |
tree | ce391ef2789a9b631d8db4cf77ae2d560d8f8574 | |
parent | d505613e11ebcf7288d5d6768a0279d420625b37 (diff) | |
parent | f69a4c0c594ff1460bd469b76031937a328dab42 (diff) | |
download | pyjwt-9c551e754d3edf1d500296b172e85904dc593006.tar.gz |
Merge pull request #186 from jpadilla/add-docs
Make documentation awesome and add support for "Read the Docs"
-rw-r--r-- | README.md | 349 | ||||
-rw-r--r-- | docs/Makefile | 192 | ||||
-rw-r--r-- | docs/algorithms.rst | 56 | ||||
-rw-r--r-- | docs/api.rst | 54 | ||||
-rw-r--r-- | docs/conf.py | 297 | ||||
-rw-r--r-- | docs/faq.rst | 25 | ||||
-rw-r--r-- | docs/index.rst | 43 | ||||
-rw-r--r-- | docs/installation.rst | 53 | ||||
-rw-r--r-- | docs/usage.rst | 189 | ||||
-rw-r--r-- | setup.cfg | 1 |
10 files changed, 917 insertions, 342 deletions
@@ -4,8 +4,9 @@ [![appveyor-status-image]][appveyor] [![pypi-version-image]][pypi] [![coveralls-status-image]][coveralls] +[![docs-status-image]][docs] -A Python implementation of [JSON Web Token draft 32][jwt-spec]. +A Python implementation of [RFC 7519][jwt-spec]. Original implementation was written by [@progrium][progrium]. ## Installing @@ -14,114 +15,15 @@ Original implementation was written by [@progrium][progrium]. $ pip install PyJWT ``` -**A Note on Dependencies**: - -RSA and ECDSA signatures depend on the recommended `cryptography` package (0.8+). If you plan on -using any of those algorithms, you'll need to install it as well. - -``` -$ pip install cryptography -``` - -If your system doesn't allow installing `cryptography` like on Google App Engine, you can install `PyCrypto` for RSA signatures and `ecdsa` for ECDSA signatures. - ## Usage ```python >>> import jwt >>> encoded = jwt.encode({'some': 'payload'}, 'secret', algorithm='HS256') 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZg' -``` - -Additional headers may also be specified. - -```python ->>> jwt.encode({'some': 'payload'}, 'secret', algorithm='HS256', headers={'kid': '230498151c214b788dd97f22b85410a5'}) -'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjIzMDQ5ODE1MWMyMTRiNzg4ZGQ5N2YyMmI4NTQxMGE1In0.eyJzb21lIjoicGF5bG9hZCJ9.DogbDGmMHgA_bU05TAB-R6geQ2nMU2BRM-LnYEtefwg' -``` - -Note the resulting JWT will not be encrypted, but verifiable with a secret key. - -```python ->>> jwt.decode(encoded, 'secret', algorithms=['HS256']) -{u'some': u'payload'} -``` - -If the secret is wrong, it will raise a `jwt.DecodeError` telling you as such. -You can still get the payload by setting the `verify` argument to `False`. - -```python ->>> jwt.decode(encoded, verify=False) -{u'some': u'payload'} -``` - -## Validation -Exceptions can be raised during `decode()` for other errors besides an -invalid signature (e.g. for invalid issuer or audience (see below). All -exceptions that signify that the token is invalid extend from the base -`InvalidTokenError` exception class, so applications can use this approach to -catch any issues relating to invalid tokens: - -```python -try: - payload = jwt.decode(encoded) -except jwt.InvalidTokenError: - pass # do something sensible here, e.g. return HTTP 403 status code -``` - -### Skipping Claim Verification -You may also override claim verification via the `options` dictionary. The -default options are: - -```python -options = { - 'verify_signature': True, - 'verify_exp': True, - 'verify_nbf': True, - 'verify_iat': True, - 'verify_aud': True - 'require_exp': False, - 'require_iat': False, - 'require_nbf': False -} -``` - -You can skip validation of individual claims by passing an `options` dictionary -with the "verify_<claim_name>" key set to `False` when you call `jwt.decode()`. -For example, if you want to verify the signature of a JWT that has already -expired, you could do so by setting `verify_exp` to `False`. - -```python ->>> options = { ->>> 'verify_exp': False, ->>> } - ->>> encoded = '...' # JWT with an expired exp claim ->>> jwt.decode(encoded, 'secret', options=options) -{u'some': u'payload'} -``` - -**NOTE**: *Changing the default behavior is done at your own risk, and almost -certainly will make your application less secure. Doing so should only be done -with a very clear understanding of what you are doing.* - -### Requiring Optional Claims -In addition to skipping certain validations, you may also specify that certain -optional claims are required by setting the appropriate `require_<claim_name>` -option to True. If the claim is not present, PyJWT will raise a -`jwt.exceptions.MissingRequiredClaimError`. - -For instance, the following code would require that the token has a 'exp' -claim and raise an error if it is not present: -```python ->>> options = { ->>> 'require_exp': True ->>> } - ->>> encoded = '...' # JWT without an exp claim ->>> jwt.decode(encoded, 'secret', options=options) -jwt.exceptions.MissingRequiredClaimError: Token is missing the "exp" claim +>>> jwt.decode(encoded, 'secret'. algorithms=['HS256']) +{'some': 'payload'} ``` ## Tests @@ -132,244 +34,6 @@ You can run tests from the project root after cloning with: $ python setup.py test ``` -## Algorithms - -The JWT spec supports several algorithms for cryptographic signing. This library -currently supports: - -* HS256 - HMAC using SHA-256 hash algorithm (default) -* HS384 - HMAC using SHA-384 hash algorithm -* HS512 - HMAC using SHA-512 hash algorithm -* ES256 - ECDSA signature algorithm using SHA-256 hash algorithm -* ES384 - ECDSA signature algorithm using SHA-384 hash algorithm -* ES512 - ECDSA signature algorithm using SHA-512 hash algorithm -* RS256 - RSASSA-PKCS1-v1_5 signature algorithm using SHA-256 hash algorithm -* RS384 - RSASSA-PKCS1-v1_5 signature algorithm using SHA-384 hash algorithm -* RS512 - RSASSA-PKCS1-v1_5 signature algorithm using SHA-512 hash algorithm -* PS256 - RSASSA-PSS signature using SHA-256 and MGF1 padding with SHA-256 -* PS384 - RSASSA-PSS signature using SHA-384 and MGF1 padding with SHA-384 -* PS512 - RSASSA-PSS signature using SHA-512 and MGF1 padding with SHA-512 - -### Encoding -You can specify which algorithm you would like to use to sign the JWT -by using the `algorithm` parameter: - -```python ->>> encoded = jwt.encode({'some': 'payload'}, 'secret', algorithm='HS512') -'eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.WTzLzFO079PduJiFIyzrOah54YaM8qoxH9fLMQoQhKtw3_fMGjImIOokijDkXVbyfBqhMo2GCNu4w9v7UXvnpA' -``` - -### Decoding -When decoding, you can specify which algorithms you would like to permit -when validating the JWT by using the `algorithms` parameter which takes a list -of allowed algorithms: - -```python ->>> jwt.decode(encoded, 'secret', algorithms=['HS512', 'HS256']) -{u'some': u'payload'} -``` - -In the above case, if the JWT has any value for its alg header other than -HS512 or HS256, the claim will be rejected with an `InvalidAlgorithmError`. - -### Asymmetric (Public-key) Algorithms -Usage of RSA (RS\*) and EC (EC\*) algorithms require a basic understanding -of how public-key cryptography is used with regards to digital signatures. -If you are unfamiliar, you may want to read -[this article](http://en.wikipedia.org/wiki/Public-key_cryptography). - -When using the RSASSA-PKCS1-v1_5 algorithms, the `key` argument in both -`jwt.encode()` and `jwt.decode()` (`"secret"` in the examples) is expected to -be either an RSA public or private key in PEM or SSH format. The type of key -(private or public) depends on whether you are signing or verifying. - -When using the ECDSA algorithms, the `key` argument is expected to -be an Elliptic Curve public or private key in PEM format. The type of key -(private or public) depends on whether you are signing or verifying. - - -## Support of registered claim names - -JSON Web Token defines some registered claim names and defines how they should -be used. PyJWT supports these registered claim names: - - - "exp" (Expiration Time) Claim - - "nbf" (Not Before Time) Claim - - "iss" (Issuer) Claim - - "aud" (Audience) Claim - - "iat" (Issued At) Claim - -### Expiration Time Claim - -From [the JWT spec][jwt-spec-reg-claims]: - -> The "exp" (expiration time) claim identifies the expiration time on -> or after which the JWT MUST NOT be accepted for processing. The -> processing of the "exp" claim requires that the current date/time -> MUST be before the expiration date/time listed in the "exp" claim. -> Implementers MAY provide for some small leeway, usually no more than -> a few minutes, to account for clock skew. Its value MUST be a number -> containing a NumericDate value. Use of this claim is OPTIONAL. - -You can pass the expiration time as a UTC UNIX timestamp (an int) or as a -datetime, which will be converted into an int. For example: - -```python -jwt.encode({'exp': 1371720939}, 'secret') - -jwt.encode({'exp': datetime.utcnow()}, 'secret') -``` - -Expiration time is automatically verified in `jwt.decode()` and raises -`jwt.ExpiredSignatureError` if the expiration time is in the past: - -```python -import jwt - -try: - jwt.decode('JWT_STRING', 'secret') -except jwt.ExpiredSignatureError: - # Signature has expired -``` - -Expiration time will be compared to the current UTC time (as given by -`timegm(datetime.utcnow().utctimetuple())`), so be sure to use a UTC timestamp -or datetime in encoding. - -You can turn off expiration time verification with the `verify_exp` parameter in the options argument. - -PyJWT also supports the leeway part of the expiration time definition, which -means you can validate a expiration time which is in the past but not very far. -For example, if you have a JWT payload with a expiration time set to 30 seconds -after creation but you know that sometimes you will process it after 30 seconds, -you can set a leeway of 10 seconds in order to have some margin: - -```python -import datetime -import time -import jwt - -jwt_payload = jwt.encode({ - 'exp': datetime.datetime.utcnow() + datetime.timedelta(seconds=30) -}, 'secret') - -time.sleep(32) - -# JWT payload is now expired -# But with some leeway, it will still validate -jwt.decode(jwt_payload, 'secret', leeway=10) -``` - -Instead of specifying the leeway as a number of seconds, a `datetime.timedelta` -instance can be used. The last line in the example above is equivalent to: - -```python -jwt.decode(jwt_payload, 'secret', leeway=datetime.timedelta(seconds=10)) -``` - - -### Not Before Time Claim - -> The "nbf" (not before) claim identifies the time before which the JWT -> MUST NOT be accepted for processing. The processing of the "nbf" -> claim requires that the current date/time MUST be after or equal to -> the not-before date/time listed in the "nbf" claim. Implementers MAY -> provide for some small leeway, usually no more than a few minutes, to -> account for clock skew. Its value MUST be a number containing a -> NumericDate value. Use of this claim is OPTIONAL. - -The `nbf` claim works similarly to the `exp` claim above. - -```python -jwt.encode({'nbf': 1371720939}, 'secret') - -jwt.encode({'nbf': datetime.utcnow()}, 'secret') -``` - -### Issuer Claim - -> The "iss" (issuer) claim identifies the principal that issued the -> JWT. The processing of this claim is generally application specific. -> The "iss" value is a case-sensitive string containing a StringOrURI -> value. Use of this claim is OPTIONAL. - -```python -import jwt - - -payload = { - 'some': 'payload', - 'iss': 'urn:foo' -} - -token = jwt.encode(payload, 'secret') -decoded = jwt.decode(token, 'secret', issuer='urn:foo') -``` - -If the issuer claim is incorrect, `jwt.InvalidIssuerError` will be raised. - - -### Audience Claim - -> The "aud" (audience) claim identifies the recipients that the JWT is -> intended for. Each principal intended to process the JWT MUST -> identify itself with a value in the audience claim. If the principal -> processing the claim does not identify itself with a value in the -> "aud" claim when this claim is present, then the JWT MUST be -> rejected. In the general case, the "aud" value is an array of case- -> sensitive strings, each containing a StringOrURI value. In the -> special case when the JWT has one audience, the "aud" value MAY be a -> single case-sensitive string containing a StringOrURI value. The -> interpretation of audience values is generally application specific. -> Use of this claim is OPTIONAL. - -```python -import jwt - - -payload = { - 'some': 'payload', - 'aud': 'urn:foo' -} - -token = jwt.encode(payload, 'secret') -decoded = jwt.decode(token, 'secret', audience='urn:foo') -``` - -If the audience claim is incorrect, `jwt.InvalidAudienceError` will be raised. - -### Issued At Claim - -> The iat (issued at) claim identifies the time at which the JWT was issued. -> This claim can be used to determine the age of the JWT. Its value MUST be a -> number containing a NumericDate value. Use of this claim is OPTIONAL. - -If the `iat` claim is in the future, an `jwt.InvalidIssuedAtError` exception -will be raised. - -```python -jwt.encode({'iat': 1371720939}, 'secret') - -jwt.encode({'iat': datetime.utcnow()}, 'secret') -``` - -## Frequently Asked Questions - -**How can I extract a public / private key from a x509 certificate?** - -The `load_pem_x509_certificate()` function from `cryptography` can be used to -extract the public or private keys from a x509 certificate in PEM format. - -```python -from cryptography.x509 import load_pem_x509_certificate -from cryptography.hazmat.backends import default_backend - -cert_str = "-----BEGIN CERTIFICATE-----MIIDETCCAfm..." -cert_obj = load_pem_x509_certificate(cert_str, default_backend()) -public_key = cert_obj.public_key() -private_key = cert_obj.private_key() -``` - [travis-status-image]: https://secure.travis-ci.org/jpadilla/pyjwt.svg?branch=master [travis]: http://travis-ci.org/jpadilla/pyjwt?branch=master [appveyor-status-image]: https://ci.appveyor.com/api/projects/status/h8nt70aqtwhht39t?svg=true @@ -378,6 +42,7 @@ private_key = cert_obj.private_key() [pypi]: https://pypi.python.org/pypi/pyjwt [coveralls-status-image]: https://coveralls.io/repos/jpadilla/pyjwt/badge.svg?branch=master [coveralls]: https://coveralls.io/r/jpadilla/pyjwt?branch=master -[jwt-spec]: https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-32 -[jwt-spec-reg-claims]: http://self-issued.info/docs/draft-jones-json-web-token-01.html#ReservedClaimName +[docs-status-image]: https://readthedocs.org/projects/pyjwt/badge/?version=latest +[docs]: http://pyjwt.readthedocs.org +[jwt-spec]: https://tools.ietf.org/html/rfc7519 [progrium]: https://github.com/progrium diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..40a745d --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,192 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# 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 clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext + +help: + @echo "Please use \`make <target>' where <target> 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 " 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)" + +clean: + rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +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." + +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/PyJWT.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PyJWT.qhc" + +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." + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/PyJWT" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PyJWT" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +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)." + +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." + +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." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +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)." + +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." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +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." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +coverage: + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage + @echo "Testing of coverage in the sources finished, look at the " \ + "results in $(BUILDDIR)/coverage/python.txt." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/algorithms.rst b/docs/algorithms.rst new file mode 100644 index 0000000..69ed575 --- /dev/null +++ b/docs/algorithms.rst @@ -0,0 +1,56 @@ +Digital Signature Algorithms +============================ + +The JWT specification supports several algorithms for cryptographic signing. +This library currently supports: + +* HS256 - HMAC using SHA-256 hash algorithm (default) +* HS384 - HMAC using SHA-384 hash algorithm +* HS512 - HMAC using SHA-512 hash algorithm +* ES256 - ECDSA signature algorithm using SHA-256 hash algorithm +* ES384 - ECDSA signature algorithm using SHA-384 hash algorithm +* ES512 - ECDSA signature algorithm using SHA-512 hash algorithm +* RS256 - RSASSA-PKCS1-v1_5 signature algorithm using SHA-256 hash algorithm +* RS384 - RSASSA-PKCS1-v1_5 signature algorithm using SHA-384 hash algorithm +* RS512 - RSASSA-PKCS1-v1_5 signature algorithm using SHA-512 hash algorithm +* PS256 - RSASSA-PSS signature using SHA-256 and MGF1 padding with SHA-256 +* PS384 - RSASSA-PSS signature using SHA-384 and MGF1 padding with SHA-384 +* PS512 - RSASSA-PSS signature using SHA-512 and MGF1 padding with SHA-512 + +Asymmetric (Public-key) Algorithms +---------------------------------- +Usage of RSA (RS\*) and EC (EC\*) algorithms require a basic understanding +of how public-key cryptography is used with regards to digital signatures. +If you are unfamiliar, you may want to read +[this article](http://en.wikipedia.org/wiki/Public-key_cryptography). + +When using the RSASSA-PKCS1-v1_5 algorithms, the `key` argument in both +``jwt.encode()`` and ``jwt.decode()`` (``"secret"`` in the examples) is expected to +be either an RSA public or private key in PEM or SSH format. The type of key +(private or public) depends on whether you are signing or verifying a token. + +When using the ECDSA algorithms, the ``key`` argument is expected to +be an Elliptic Curve public or private key in PEM format. The type of key +(private or public) depends on whether you are signing or verifying. + +Specifying an Algorithm +----------------------- +You can specify which algorithm you would like to use to sign the JWT +by using the `algorithm` parameter: + +.. code-block:: python + + >>> encoded = jwt.encode({'some': 'payload'}, 'secret', algorithm='HS512') + 'eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.WTzLzFO079PduJiFIyzrOah54YaM8qoxH9fLMQoQhKtw3_fMGjImIOokijDkXVbyfBqhMo2GCNu4w9v7UXvnpA' + +When decoding, you can also specify which algorithms you would like to permit +when validating the JWT by using the `algorithms` parameter which takes a list +of allowed algorithms: + +.. code-block:: python + + >>> jwt.decode(encoded, 'secret', algorithms=['HS512', 'HS256']) + {u'some': u'payload'} + +In the above case, if the JWT has any value for its alg header other than +HS512 or HS256, the claim will be rejected with an ``InvalidAlgorithmError``. diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000..4bb4d3b --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,54 @@ +API Reference +============= + +.. module:: jwt + +TODO: Document PyJWS / PyJWT classes + +Exceptions +---------- + +.. currentmodule:: jwt.exceptions + + +.. class:: InvalidTokenError + + Base exception when ``decode()`` fails on a token + +.. class:: DecodeError + + Raised when a token cannot be decoded because it failed validation + +.. class:: ExpiredSignatureError + + Raised when a token's ``exp`` claim indicates that it has expired + +.. class:: InvalidAudienceError + + Raised when a token's ``aud`` claim does not match one of the expected + audience values + +.. class:: InvalidIssuerError + + Raised when a token's ``iss`` claim does not match the expected issuer + +.. class:: InvalidIssuedAtError + + Raised when a token's ``iat`` claim is in the future + +.. class:: ImmatureSignatureError + + Raised when a token's ``nbf`` claim represents a time in the future + +.. class:: InvalidKeyError + + Raised when the specified key is not in the proper format + +.. class:: InvalidAlgorithmError + + Raised when the specified algorithm is not recognized by PyJWT + +.. class:: MissingRequiredClaimError + + Raised when a claim that is required to be present is not contained + in the claimset diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..ae183e4 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,297 @@ +# -*- coding: utf-8 -*- +# +# PyJWT documentation build configuration file, created by +# sphinx-quickstart on Thu Oct 22 18:11:10 2015. +# +# 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. + +import sys +import os +import shlex +import re + +# 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('.')) + +# -- 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 = [] + +# 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'PyJWT' +copyright = u'2015, José Padilla' +author = u'José Padilla' + +# 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. +# +# The short X.Y version. +def get_version(package): + """ + Return package version as listed in `__version__` in `init.py`. + """ + with open(os.path.join('..', package, '__init__.py'), 'rb') as init_py: + src = init_py.read().decode('utf-8') + return re.search("__version__ = ['\"]([^'\"]+)['\"]", src).group(1) + +version = get_version('jwt') +# The full version, including alpha/beta/rc tags. +release = 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. +exclude_patterns = ['_build'] + +# 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. +import sphinx_rtd_theme + +html_theme = "sphinx_rtd_theme" + +html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] + +# 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 = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# "<project> v<release> documentation". +#html_title = None + +# 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 (within the static path) to use as 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 '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# 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 <link> 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' +#html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# Now only 'ja' uses this config value +#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 = 'PyJWTdoc' + +# -- 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, 'PyJWT.tex', u'PyJWT Documentation', + u'José Padilla', '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 = [] + +# 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, 'pyjwt', u'PyJWT 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, 'PyJWT', u'PyJWT Documentation', + author, 'PyJWT', '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 diff --git a/docs/faq.rst b/docs/faq.rst new file mode 100644 index 0000000..59a6cb6 --- /dev/null +++ b/docs/faq.rst @@ -0,0 +1,25 @@ +Frequently Asked Questions +========================== + +How can I extract a public / private key from a x509 certificate? +----------------------------------------------------------------- + +The ``load_pem_x509_certificate()`` function from ``cryptography`` can be used to +extract the public or private keys from a x509 certificate in PEM format. + +.. code-block:: python + + from cryptography.x509 import load_pem_x509_certificate + from cryptography.hazmat.backends import default_backend + + cert_str = "-----BEGIN CERTIFICATE-----MIIDETCCAfm..." + cert_obj = load_pem_x509_certificate(cert_str, default_backend()) + public_key = cert_obj.public_key() + private_key = cert_obj.private_key() + +I'm using Google App Engine and can't install `cryptography`, what can I do? +---------------------------------------------------------------------------- + +Some platforms like Google App Engine don't allow you to install libraries +that require C extensions to be built (like `cryptography`). If you're deploying +to one of those environments, you should check out :ref:`legacy-deps` diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..782031a --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,43 @@ +Welcome to ``PyJWT`` +=========================== + +``PyJWT`` is a Python library which allows you to encode and decode JSON Web +Tokens (JWT). JWT is an open, industry-standard (`RFC 7519`_) for representing +claims securely between two parties. + +Installation +------------ +You can install ``pyjwt`` with ``pip``: + +.. code-block:: console + + $ pip install pyjwt + +See :doc:`Installation <installation>` for more information. + +Example Usage +------------ + +.. code-block:: python + + >>> import jwt + + >>> jwt.encode({'some': 'payload'}, 'secret', algorithm='HS256') + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZg' + + >>> jwt.decode('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZg', 'secret') + {'some': 'payload'} + + +See :doc:`Usage Examples <usage>` for more examples. + +.. toctree:: + :maxdepth: 2 + + installation + usage + faq + algorithms + api + +.. _`RFC 7519`: https://tools.ietf.org/html/rfc7519 diff --git a/docs/installation.rst b/docs/installation.rst new file mode 100644 index 0000000..b60e06c --- /dev/null +++ b/docs/installation.rst @@ -0,0 +1,53 @@ +Installation +============ + +You can install ``PyJWT`` with ``pip``: + +.. code-block:: console + + $ pip install pyjwt + +Cryptographic Dependencies (Optional) +------------------- + +If you are planning on encoding or decoding tokens using certain digital +signature algorithms (like RSA or ECDSA), you will need to install the +cryptography_ library. + +.. code-block:: console + + $ pip install cryptography + +.. _legacy-deps: + +Legacy Dependencies +------------------- + +Some environments, most notably Google App Engine, do not allow the installation +of Python packages that require compilation of C extensions and therefore +cannot install ``cryptography``. If you can install ``cryptography``, you +should disregard this section. + +If you are deploying an application to one of these environments, you may +need to use the legacy implementations of the digital signature algorithms: + +.. code-block:: console + + $ pip install pycrypto ecdsa + +Once you have installed ``pycrypto`` and ``ecdcsa``, you can tell PyJWT to use +the legacy implementations with ``jwt.register_algorithm()``. The following +example code shows how to configure PyJWT to use the legacy implementations +for RSA with SHA256 and EC with SHA256 signatures. + +.. code-block:: python + + import jwt + import jwt.contrib.algorithms.pycrypto import RSAAlgorithm + import jwt.contrib.algorithms.py_ecdsa import ECAlgorithm + + jwt.register_algorithm('RS256', RSAAlgorithm(RSAAlgorithm.SHA256)) + jwt.register_algorithm('ES256', ECAlgorithm(ECAlgorithm.SHA256)) + + +.. _`cryptography`: https://cryptography.io diff --git a/docs/usage.rst b/docs/usage.rst new file mode 100644 index 0000000..c2e286c --- /dev/null +++ b/docs/usage.rst @@ -0,0 +1,189 @@ +Usage Examples +============== + +Encoding & Decoding Tokens +--------------------------------- + +.. code-block:: python + + >>import jwt + >>encoded = jwt.encode({'some': 'payload'}, 'secret', algorithm='HS256') + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZg' + + +Specifying Additional Headers +--------------------------------- + +.. code-block:: python + + >>jwt.encode({'some': 'payload'}, 'secret', algorithm='HS256', headers={'kid': '230498151c214b788dd97f22b85410a5'}) + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjIzMDQ5ODE1MWMyMTRiNzg4ZGQ5N2YyMmI4NTQxMGE1In0.eyJzb21lIjoicGF5bG9hZCJ9.DogbDGmMHgA_bU05TAB-R6geQ2nMU2BRM-LnYEtefwg' + + +Reading the Claimset without Validation +----------------------------------------- + +If you wish to read the claimset of a JWT without performing validation of the +signature or any of the registered claim names, you can set the ``verify`` +parameter to ``False``. + +Note: It is generally ill-advised to use this functionality unless you +clearly understand what you are doing. Without digital signature information, +the integrity or authenticity of the claimset cannot be trusted. + +.. code-block:: python + + >>jwt.decode(encoded, verify=False) + {u'some': u'payload'} + +Registered Claim Names +---------------------- + +The JWT specificaftion defines some registered claim names and defines +how they should be used. PyJWT supports these registered claim names: + + - "exp" (Expiration Time) Claim + - "nbf" (Not Before Time) Claim + - "iss" (Issuer) Claim + - "aud" (Audience) Claim + - "iat" (Issued At) Claim + +Expiration Time Claim (exp) +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + The "exp" (expiration time) claim identifies the expiration time on + or after which the JWT MUST NOT be accepted for processing. The + processing of the "exp" claim requires that the current date/time + MUST be before the expiration date/time listed in the "exp" claim. + Implementers MAY provide for some small leeway, usually no more than + a few minutes, to account for clock skew. Its value MUST be a number + containing a NumericDate value. Use of this claim is OPTIONAL. + +You can pass the expiration time as a UTC UNIX timestamp (an int) or as a +datetime, which will be converted into an int. For example: + +.. code-block:: python + + jwt.encode({'exp': 1371720939}, 'secret') + jwt.encode({'exp': datetime.utcnow()}, 'secret') + +Expiration time is automatically verified in `jwt.decode()` and raises +`jwt.ExpiredSignatureError` if the expiration time is in the past: + +.. code-block:: python + + try: + jwt.decode('JWT_STRING', 'secret') + except jwt.ExpiredSignatureError: + # Signature has expired + +Expiration time will be compared to the current UTC time (as given by +`timegm(datetime.utcnow().utctimetuple())`), so be sure to use a UTC timestamp +or datetime in encoding. + +You can turn off expiration time verification with the `verify_exp` parameter in the options argument. + +PyJWT also supports the leeway part of the expiration time definition, which +means you can validate a expiration time which is in the past but not very far. +For example, if you have a JWT payload with a expiration time set to 30 seconds +after creation but you know that sometimes you will process it after 30 seconds, +you can set a leeway of 10 seconds in order to have some margin: + +.. code-block:: python + + jwt_payload = jwt.encode({ + 'exp': datetime.datetime.utcnow() + datetime.timedelta(seconds=30) + }, 'secret') + + time.sleep(32) + + # JWT payload is now expired + # But with some leeway, it will still validate + jwt.decode(jwt_payload, 'secret', leeway=10) + +Instead of specifying the leeway as a number of seconds, a `datetime.timedelta` +instance can be used. The last line in the example above is equivalent to: + +.. code-block:: python + + jwt.decode(jwt_payload, 'secret', leeway=datetime.timedelta(seconds=10)) + +Not Before Time Claim (nbf) +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + The "nbf" (not before) claim identifies the time before which the JWT + MUST NOT be accepted for processing. The processing of the "nbf" + claim requires that the current date/time MUST be after or equal to + the not-before date/time listed in the "nbf" claim. Implementers MAY + provide for some small leeway, usually no more than a few minutes, to + account for clock skew. Its value MUST be a number containing a + NumericDate value. Use of this claim is OPTIONAL. + +The `nbf` claim works similarly to the `exp` claim above. + +.. code-block:: python + + jwt.encode({'nbf': 1371720939}, 'secret') + jwt.encode({'nbf': datetime.utcnow()}, 'secret') + +Issuer Claim (iss) +~~~~~~~~~~~~~~~~~~ + + The "iss" (issuer) claim identifies the principal that issued the + JWT. The processing of this claim is generally application specific. + The "iss" value is a case-sensitive string containing a StringOrURI + value. Use of this claim is OPTIONAL. + +.. code-block:: python + + payload = { + 'some': 'payload', + 'iss': 'urn:foo' + } + + token = jwt.encode(payload, 'secret') + decoded = jwt.decode(token, 'secret', issuer='urn:foo') + +If the issuer claim is incorrect, `jwt.InvalidIssuerError` will be raised. + +Audience Claim (aud) +~~~~~~~~~~~~~~~~~~~~ + + The "aud" (audience) claim identifies the recipients that the JWT is + intended for. Each principal intended to process the JWT MUST + identify itself with a value in the audience claim. If the principal + processing the claim does not identify itself with a value in the + "aud" claim when this claim is present, then the JWT MUST be + rejected. In the general case, the "aud" value is an array of case- + sensitive strings, each containing a StringOrURI value. In the + special case when the JWT has one audience, the "aud" value MAY be a + single case-sensitive string containing a StringOrURI value. The + interpretation of audience values is generally application specific. + Use of this claim is OPTIONAL. + +.. code-block:: python + + payload = { + 'some': 'payload', + 'aud': 'urn:foo' + } + + token = jwt.encode(payload, 'secret') + decoded = jwt.decode(token, 'secret', audience='urn:foo') + +If the audience claim is incorrect, `jwt.InvalidAudienceError` will be raised. + +Issued At Claim (iat) +~~~~~~~~~~~~~~~~~~~~~ + + The iat (issued at) claim identifies the time at which the JWT was issued. + This claim can be used to determine the age of the JWT. Its value MUST be a + number containing a NumericDate value. Use of this claim is OPTIONAL. + +If the `iat` claim is in the future, an `jwt.InvalidIssuedAtError` exception +will be raised. + +.. code-block:: python + + jwt.encode({'iat': 1371720939}, 'secret') + jwt.encode({'iat': datetime.utcnow()}, 'secret') @@ -1,5 +1,6 @@ [flake8] max-line-length = 119 +exclude = docs/ [wheel] universal = 1 |