Skip to content

Commit 757c480

Browse files
Merge branch 'master' into master
2 parents 7c91c2a + 5bd8b1e commit 757c480

23 files changed

+623
-100
lines changed

.readthedocs.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
version: 2
2+
3+
sphinx:
4+
builder: htmldir
5+
configuration: docs/conf.py
6+
7+
python:
8+
version: 3.7
9+
install:
10+
- requirements: requirements.txt

.travis.yml

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
language: python
22
matrix:
33
include:
4+
- python: 3.8
5+
env: TOXENV=py38
6+
- python: 3.7
7+
env: TOXENV=py37
48
- python: 3.6
5-
env: TOXENV=py
9+
env: TOXENV=py36
610
- python: 3.5
7-
env: TOXENV=py
8-
- python: 3.4
9-
env: TOXENV=py
11+
env: TOXENV=py35
1012
- python: 2.7
11-
env: TOXENV=py
13+
env: TOXENV=py27
1214
- python: pypy
13-
env: TOXENV=py
15+
env: TOXENV=pypy
1416
sudo: false
1517
install:
1618
- pip install -U pip

README.md

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@
33
[![Coverage Status](https://coveralls.io/repos/github/vimalloc/flask-jwt-extended/badge.svg?branch=master)](https://coveralls.io/github/vimalloc/flask-jwt-extended?branch=master)
44
[![PyPI version](https://badge.fury.io/py/Flask-JWT-Extended.svg)](https://badge.fury.io/py/Flask-JWT-Extended)
55
[![Documentation Status](https://readthedocs.org/projects/flask-jwt-extended/badge/)](http://flask-jwt-extended.readthedocs.io/en/latest/)
6-
[![Join the chat at https://gitter.im/flask-jwt-extended/Lobby](https://badges.gitter.im/flask-jwt-extended/Lobby.svg)](https://gitter.im/flask-jwt-extended/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
76

87
### Features
9-
108
Flask-JWT-Extended not only adds support for using JSON Web Tokens (JWT) to Flask for protecting views,
119
but also many helpful (and **optional**) features built in to make working with JSON Web Tokens
1210
easier. These include:
@@ -19,23 +17,20 @@ easier. These include:
1917
* Token revoking/blacklisting
2018
* Storing tokens in cookies and CSRF protection
2119

22-
2320
### Usage
24-
[View the documentation online](http://flask-jwt-extended.readthedocs.io/en/latest/)
25-
21+
[View the documentation online](https://flask-jwt-extended.readthedocs.io/en/stable/)
2622

2723
### Changelog
2824
You can view the changelog [here](https://github.com/vimalloc/flask-jwt-extended/releases).
2925
This project follows [semantic versioning](https://semver.org/).
3026

3127
### Chatting
32-
Come chat with the community or ask questions at https://gitter.im/flask-jwt-extended/Lobby.
33-
28+
Come chat with the community or ask questions at https://discord.gg/EJBsbFd
3429

3530
### Local Development
3631
We require 100% code coverage in our unit tests. You can run the tests locally
3732
with `tox` which will print out a code coverage report. Creating a pull request
38-
will run the tests against python 2.7, 3.4, 3.5, 3.6, and PyPy.
33+
will run the tests against python 2.7, 3.5, 3.6, 3.7, 3.8 and PyPy.
3934
```
4035
$ tox
4136
```

docs/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
3535
# ones.
3636
extensions = [
37+
'pallets_sphinx_themes',
3738
'sphinx.ext.autodoc',
3839
'sphinx.ext.intersphinx',
3940
'sphinx.ext.ifconfig',

docs/options.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,14 @@ These are only applicable if ``JWT_TOKEN_LOCATION`` is set to use cookies and
157157
Only applicable if ``JWT_CSRF_IN_COOKIES`` is ``True``
158158
``JWT_REFRESH_CSRF_COOKIE_PATH`` Path of the CSRF refresh cookie. Defaults to ``'/'``.
159159
Only applicable if ``JWT_CSRF_IN_COOKIES`` is ``True``
160+
``JWT_CSRF_CHECK_FORM`` When no CSRF token can be found in the header, check the form data. Defaults to
161+
``False``.
162+
``JWT_ACCESS_CSRF_FIELD_NAME`` Name of the form field that should contain the CSRF double submit value for access
163+
tokens when no header is present. Only applicable if ``JWT_CSRF_CHECK_FORM`` is
164+
``True``. Defaults to ``'csrf_token'``.
165+
``JWT_REFRESH_CSRF_FIELD_NAME`` Name of the form field that should contain the CSRF double submit value for refresh
166+
tokens when no header is present. Only applicable if ``JWT_CSRF_CHECK_FORM`` is
167+
``True``. Defaults to ``'csrf_token'``.
160168
================================= =========================================
161169

162170

docs/tokens_in_cookies.rst

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,39 @@ to the caller, like such:
7878
set_access_cookies(resp, access_token)
7979
set_refresh_cookies(resp, refresh_token)
8080
return resp, 200
81+
82+
83+
Typically JWT is used with API servers using JSON payloads, often via AJAX. However you may have an endpoint that
84+
receives POST requests directly from an HTML form. Without AJAX, you can't set the CSRF headers to pass your token to
85+
the server. In this scenario you can send the token in a hidden form field. To accomplish this, first configure JWT to
86+
check the form for CSRF tokens. Now it's not necessary to send the csrf in a separate cookie, you can render it
87+
directly into your HTML template:
88+
89+
90+
.. code-block:: python
91+
92+
app.config['JWT_CSRF_CHECK_FORM'] = True
93+
94+
...
95+
96+
@app.route('/protected', methods=['GET', 'POST'])
97+
@jwt_optional
98+
def protected():
99+
if request.method == "GET":
100+
return render_template(
101+
"form.html", csrf_token=(get_raw_jwt() or {}).get("csrf")
102+
)
103+
else:
104+
# handle POST request
105+
current_user = get_jwt_identity()
106+
107+
108+
In the HTML template, pass the token back to the server via a hidden input.
109+
110+
.. code-block:: html
111+
112+
<form method="POST">
113+
...
114+
<input name="csrf_token" type="hidden" value="{{ csrf_token }}">
115+
<button>Submit</button>
116+
</form>

flask_jwt_extended/__init__.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
from .jwt_manager import JWTManager
2-
from .view_decorators import (
3-
fresh_jwt_required, jwt_optional, jwt_refresh_token_required, jwt_required,
4-
verify_fresh_jwt_in_request, verify_jwt_in_request,
5-
verify_jwt_in_request_optional, verify_jwt_refresh_token_in_request
6-
)
72
from .utils import (
83
create_access_token, create_refresh_token, current_user, decode_token,
94
get_csrf_token, get_current_user, get_jti, get_jwt_claims, get_jwt_identity,
105
get_raw_jwt, set_access_cookies, set_refresh_cookies, unset_access_cookies,
11-
unset_jwt_cookies, unset_refresh_cookies
6+
unset_jwt_cookies, unset_refresh_cookies, get_unverified_jwt_headers,
7+
get_raw_jwt_header
8+
)
9+
from .view_decorators import (
10+
fresh_jwt_required, jwt_optional, jwt_refresh_token_required, jwt_required,
11+
verify_fresh_jwt_in_request, verify_jwt_in_request,
12+
verify_jwt_in_request_optional, verify_jwt_refresh_token_in_request
1213
)
1314

14-
__version__ = '3.19.0'
15+
__version__ = '3.24.1'

flask_jwt_extended/config.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,18 @@ def refresh_csrf_header_name(self):
184184
return self._get_depreciated_csrf_header_name() or \
185185
current_app.config['JWT_REFRESH_CSRF_HEADER_NAME']
186186

187+
@property
188+
def csrf_check_form(self):
189+
return current_app.config['JWT_CSRF_CHECK_FORM']
190+
191+
@property
192+
def access_csrf_field_name(self):
193+
return current_app.config['JWT_ACCESS_CSRF_FIELD_NAME']
194+
195+
@property
196+
def refresh_csrf_field_name(self):
197+
return current_app.config['JWT_REFRESH_CSRF_FIELD_NAME']
198+
187199
@property
188200
def access_expires(self):
189201
delta = current_app.config['JWT_ACCESS_TOKEN_EXPIRES']
@@ -316,6 +328,14 @@ def json_encoder(self):
316328
def audience(self):
317329
return current_app.config['JWT_DECODE_AUDIENCE']
318330

331+
@property
332+
def encode_issuer(self):
333+
return current_app.config['JWT_ENCODE_ISSUER']
334+
335+
@property
336+
def decode_issuer(self):
337+
return current_app.config['JWT_DECODE_ISSUER']
338+
319339
@property
320340
def leeway(self):
321341
return current_app.config['JWT_DECODE_LEEWAY']

flask_jwt_extended/default_callbacks.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,17 @@ def default_user_claims_callback(userdata):
2222
return {}
2323

2424

25+
def default_jwt_headers_callback(default_headers):
26+
"""
27+
By default header typically consists of two parts: the type of the token,
28+
which is JWT, and the signing algorithm being used, such as HMAC SHA256
29+
or RSA. But we don't set the default header here we set it as empty which
30+
further by default set while encoding the token
31+
:return: default we set None here
32+
"""
33+
return None
34+
35+
2536
def default_user_identity_callback(userdata):
2637
"""
2738
By default, we use the passed in object directly as the jwt identity.

flask_jwt_extended/jwt_manager.py

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import datetime
22
from warnings import warn
33

4-
from jwt import ExpiredSignatureError, InvalidTokenError, InvalidAudienceError
4+
from jwt import (
5+
ExpiredSignatureError, InvalidTokenError, InvalidAudienceError,
6+
InvalidIssuerError, DecodeError
7+
)
8+
59
try:
610
from flask import _app_ctx_stack as ctx_stack
711
except ImportError: # pragma: no cover
@@ -19,8 +23,8 @@
1923
default_unauthorized_callback, default_needs_fresh_token_callback,
2024
default_revoked_token_callback, default_user_loader_error_callback,
2125
default_claims_verification_callback, default_verify_claims_failed_callback,
22-
default_decode_key_callback, default_encode_key_callback
23-
)
26+
default_decode_key_callback, default_encode_key_callback,
27+
default_jwt_headers_callback)
2428
from flask_jwt_extended.tokens import (
2529
encode_refresh_token, encode_access_token
2630
)
@@ -61,6 +65,7 @@ def __init__(self, app=None):
6165
self._verify_claims_failed_callback = default_verify_claims_failed_callback
6266
self._decode_key_callback = default_decode_key_callback
6367
self._encode_key_callback = default_encode_key_callback
68+
self._jwt_additional_header_callback = default_jwt_headers_callback
6469

6570
# Register this extension with the flask app now (if it is provided)
6671
if app is not None:
@@ -101,7 +106,7 @@ def handle_expired_error(e):
101106
except TypeError:
102107
msg = (
103108
"jwt.expired_token_loader callback now takes the expired token "
104-
"as an additional paramter. Example: expired_callback(token)"
109+
"as an additional parameter. Example: expired_callback(token)"
105110
)
106111
warn(msg, DeprecationWarning)
107112
return self._expired_token_callback()
@@ -110,6 +115,10 @@ def handle_expired_error(e):
110115
def handle_invalid_header_error(e):
111116
return self._invalid_token_callback(str(e))
112117

118+
@app.errorhandler(DecodeError)
119+
def handle_invalid_header_error(e):
120+
return self._invalid_token_callback(str(e))
121+
113122
@app.errorhandler(InvalidTokenError)
114123
def handle_invalid_token_error(e):
115124
return self._invalid_token_callback(str(e))
@@ -126,6 +135,10 @@ def handle_wrong_token_error(e):
126135
def handle_invalid_audience_error(e):
127136
return self._invalid_token_callback(str(e))
128137

138+
@app.errorhandler(InvalidIssuerError)
139+
def handle_invalid_issuer_error(e):
140+
return self._invalid_token_callback(str(e))
141+
129142
@app.errorhandler(RevokedTokenError)
130143
def handle_revoked_token_error(e):
131144
return self._revoked_token_callback()
@@ -185,6 +198,9 @@ def _set_default_configuration_options(app):
185198
app.config.setdefault('JWT_REFRESH_CSRF_COOKIE_NAME', 'csrf_refresh_token')
186199
app.config.setdefault('JWT_ACCESS_CSRF_COOKIE_PATH', '/')
187200
app.config.setdefault('JWT_REFRESH_CSRF_COOKIE_PATH', '/')
201+
app.config.setdefault('JWT_CSRF_CHECK_FORM', False)
202+
app.config.setdefault('JWT_ACCESS_CSRF_FIELD_NAME', 'csrf_token')
203+
app.config.setdefault('JWT_REFRESH_CSRF_FIELD_NAME', 'csrf_token')
188204

189205
# How long an a token will live before they expire.
190206
app.config.setdefault('JWT_ACCESS_TOKEN_EXPIRES', datetime.timedelta(minutes=15))
@@ -214,6 +230,8 @@ def _set_default_configuration_options(app):
214230
app.config.setdefault('JWT_IDENTITY_CLAIM', 'identity')
215231
app.config.setdefault('JWT_USER_CLAIMS', 'user_claims')
216232
app.config.setdefault('JWT_DECODE_AUDIENCE', None)
233+
app.config.setdefault('JWT_ENCODE_ISSUER', None)
234+
app.config.setdefault('JWT_DECODE_ISSUER', None)
217235
app.config.setdefault('JWT_DECODE_LEEWAY', 0)
218236

219237
app.config.setdefault('JWT_CLAIMS_IN_REFRESH_TOKEN', False)
@@ -439,13 +457,33 @@ def encode_key_loader(self, callback):
439457
self._encode_key_callback = callback
440458
return callback
441459

442-
def _create_refresh_token(self, identity, expires_delta=None, user_claims=None):
460+
def additional_headers_loader(self, callback):
461+
"""
462+
This decorator sets the callback function for adding custom headers to an
463+
access token when :func:`~flask_jwt_extended.create_access_token` is
464+
called. By default, two headers will be added the type of the token, which is JWT,
465+
and the signing algorithm being used, such as HMAC SHA256 or RSA.
466+
467+
*HINT*: The callback function must be a function that takes **no** argument,
468+
which is the object passed into
469+
:func:`~flask_jwt_extended.create_access_token`, and returns the custom
470+
claims you want included in the access tokens. This returned claims
471+
must be *JSON serializable*.
472+
"""
473+
self._jwt_additional_header_callback = callback
474+
return callback
475+
476+
def _create_refresh_token(self, identity, expires_delta=None, user_claims=None,
477+
headers=None):
443478
if expires_delta is None:
444479
expires_delta = config.refresh_expires
445480

446481
if user_claims is None and config.user_claims_in_refresh_token:
447482
user_claims = self._user_claims_callback(identity)
448483

484+
if headers is None:
485+
headers = self._jwt_additional_header_callback(identity)
486+
449487
refresh_token = encode_refresh_token(
450488
identity=self._user_identity_callback(identity),
451489
secret=self._encode_key_callback(identity),
@@ -455,17 +493,22 @@ def _create_refresh_token(self, identity, expires_delta=None, user_claims=None):
455493
csrf=config.csrf_protect,
456494
identity_claim_key=config.identity_claim_key,
457495
user_claims_key=config.user_claims_key,
458-
json_encoder=config.json_encoder
496+
json_encoder=config.json_encoder,
497+
headers=headers
459498
)
460499
return refresh_token
461500

462-
def _create_access_token(self, identity, fresh=False, expires_delta=None, user_claims=None):
501+
def _create_access_token(self, identity, fresh=False, expires_delta=None,
502+
user_claims=None, headers=None):
463503
if expires_delta is None:
464504
expires_delta = config.access_expires
465505

466506
if user_claims is None:
467507
user_claims = self._user_claims_callback(identity)
468508

509+
if headers is None:
510+
headers = self._jwt_additional_header_callback(identity)
511+
469512
access_token = encode_access_token(
470513
identity=self._user_identity_callback(identity),
471514
secret=self._encode_key_callback(identity),
@@ -476,6 +519,8 @@ def _create_access_token(self, identity, fresh=False, expires_delta=None, user_c
476519
csrf=config.csrf_protect,
477520
identity_claim_key=config.identity_claim_key,
478521
user_claims_key=config.user_claims_key,
479-
json_encoder=config.json_encoder
522+
json_encoder=config.json_encoder,
523+
headers=headers,
524+
issuer=config.encode_issuer,
480525
)
481526
return access_token

0 commit comments

Comments
 (0)