From 82767549e8cc06dad61b8bf336144dce5bb30c7b Mon Sep 17 00:00:00 2001 From: "Aaron N. Brock" Date: Thu, 24 Aug 2017 22:13:24 -0400 Subject: [PATCH 1/9] Added PyCharm data to .gitignore --- .gitignore | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index b0c8e37..81647f3 100644 --- a/.gitignore +++ b/.gitignore @@ -21,14 +21,17 @@ pip-log.txt .coverage .tox -#Translations +# Translations *.mo -#Virtualenv +# Virtualenv env/ -#Editor temporaries +# Editor temporaries *~ *.db + +# PyCharm +.idea From 6bf42381c4041eec5790456ef907e0dc51a4e5e7 Mon Sep 17 00:00:00 2001 From: "Aaron N. Brock" Date: Fri, 25 Aug 2017 00:04:01 -0400 Subject: [PATCH 2/9] Removed the need for Username/Password. --- example/app.py | 15 +++++++++------ flask_jwt/__init__.py | 10 +++------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/example/app.py b/example/app.py index da4c30d..cafb2e3 100644 --- a/example/app.py +++ b/example/app.py @@ -19,10 +19,6 @@ def __str__(self): username_table = {u.username: u for u in users} userid_table = {u.id: u for u in users} -def authenticate(username, password): - user = username_table.get(username, None) - if user and safe_str_cmp(user.password.encode('utf-8'), password.encode('utf-8')): - return user def identity(payload): user_id = payload['identity'] @@ -32,7 +28,14 @@ def identity(payload): app.debug = True app.config['SECRET_KEY'] = 'super-secret' -jwt = JWT(app, authenticate, identity) +jwt = JWT(app, identity_handler=identity) + +@jwt.authentication_handler +def authenticate(username, password, **kwargs): + user = username_table.get(username, None) + if user and safe_str_cmp(user.password.encode('utf-8'), password.encode('utf-8')): + return user + @app.route('/protected') @jwt_required() @@ -40,4 +43,4 @@ def protected(): return '%s' % current_identity if __name__ == '__main__': - app.run() + app.run(host='localhost') diff --git a/flask_jwt/__init__.py b/flask_jwt/__init__.py index a80b506..ae13b7c 100644 --- a/flask_jwt/__init__.py +++ b/flask_jwt/__init__.py @@ -116,15 +116,11 @@ def _default_auth_request_handler(): if not isinstance(data, dict): # Strings/arrays, or non-JSON mimetype raise JWTError('Bad Request', 'Credentials must supplied in JSON') - username = data.get(current_app.config.get('JWT_AUTH_USERNAME_KEY')) - password = data.get(current_app.config.get('JWT_AUTH_PASSWORD_KEY')) - criterion = [username, password, len(data) == 2] - - if not all(criterion): + try: + identity = _jwt.authentication_callback(**data) + except TypeError: raise JWTError('Bad Request', 'Invalid credentials') - identity = _jwt.authentication_callback(username, password) - if identity: access_token = _jwt.jwt_encode_callback(identity) return _jwt.auth_response_callback(access_token, identity) From 554d5b14a532b0da4e848b9fb121ede9dd6c2818 Mon Sep 17 00:00:00 2001 From: "Aaron N. Brock" Date: Fri, 25 Aug 2017 00:22:39 -0400 Subject: [PATCH 3/9] Allowed authentication_handler to be a @annotation. --- example/app.py | 13 ++++++++----- flask_jwt/__init__.py | 10 +++++----- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/example/app.py b/example/app.py index cafb2e3..bb5849e 100644 --- a/example/app.py +++ b/example/app.py @@ -20,15 +20,18 @@ def __str__(self): userid_table = {u.id: u for u in users} -def identity(payload): - user_id = payload['identity'] - return userid_table.get(user_id, None) - app = Flask(__name__) app.debug = True app.config['SECRET_KEY'] = 'super-secret' -jwt = JWT(app, identity_handler=identity) +jwt = JWT(app) + + +@jwt.identity_handler +def identity(payload): + user_id = payload['identity'] + return userid_table.get(user_id, None) + @jwt.authentication_handler def authenticate(username, password, **kwargs): diff --git a/flask_jwt/__init__.py b/flask_jwt/__init__.py index ae13b7c..1a6cd29 100644 --- a/flask_jwt/__init__.py +++ b/flask_jwt/__init__.py @@ -119,7 +119,7 @@ def _default_auth_request_handler(): try: identity = _jwt.authentication_callback(**data) except TypeError: - raise JWTError('Bad Request', 'Invalid credentials') + raise JWTError('Bad Request', 'Invalid credentials arguments') if identity: access_token = _jwt.jwt_encode_callback(identity) @@ -253,10 +253,10 @@ def init_app(self, app): endpoint = app.config.get('JWT_AUTH_ENDPOINT', None) if auth_url_rule and endpoint: - if self.auth_request_callback == _default_auth_request_handler: - assert self.authentication_callback is not None, ( - 'an authentication_handler function must be defined when using the built in ' - 'authentication resource') + # if self.auth_request_callback == _default_auth_request_handler: + # assert self.authentication_callback is not None, ( + # 'an authentication_handler function must be defined when using the built in ' + # 'authentication resource') auth_url_options = app.config.get('JWT_AUTH_URL_OPTIONS', {'methods': ['POST']}) auth_url_options.setdefault('view_func', self.auth_request_callback) From 745109c4aa3daef68c2a20166757b46929f9a8b5 Mon Sep 17 00:00:00 2001 From: "Aaron N. Brock" Date: Fri, 25 Aug 2017 01:25:05 -0400 Subject: [PATCH 4/9] Updated % to .format() --- example/app.py | 4 ++-- flask_jwt/__init__.py | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/example/app.py b/example/app.py index bb5849e..8e75d82 100644 --- a/example/app.py +++ b/example/app.py @@ -9,7 +9,7 @@ def __init__(self, id, username, password): self.password = password def __str__(self): - return "User(id='%s')" % self.id + return "User(id='{}')".format(self.id) users = [ User(1, 'user1', 'abcxyz'), @@ -43,7 +43,7 @@ def authenticate(username, password, **kwargs): @app.route('/protected') @jwt_required() def protected(): - return '%s' % current_identity + return '{}'.format(current_identity) if __name__ == '__main__': app.run(host='localhost') diff --git a/flask_jwt/__init__.py b/flask_jwt/__init__.py index 1a6cd29..9645f91 100644 --- a/flask_jwt/__init__.py +++ b/flask_jwt/__init__.py @@ -62,7 +62,7 @@ def _default_jwt_encode_handler(identity): missing_claims = list(set(required_claims) - set(payload.keys())) if missing_claims: - raise RuntimeError('Payload is missing required claims: %s' % ', '.join(missing_claims)) + raise RuntimeError('Payload is missing required claims: {}'.format(', '.join(missing_claims))) headers = _jwt.jwt_headers_callback(identity) @@ -88,8 +88,7 @@ def _default_jwt_decode_handler(token): for claim in ['exp', 'nbf', 'iat'] }) - return jwt.decode(token, secret, options=options, algorithms=[algorithm], leeway=leeway, - audience=audience) + return jwt.decode(token, secret, options=options, algorithms=[algorithm], leeway=leeway, audience=audience) def _default_request_handler(): @@ -165,7 +164,7 @@ def _jwt_required(realm, roles): if token is None: raise JWTError('Authorization Required', 'Request does not contain an access token', - headers={'WWW-Authenticate': 'JWT realm="%s"' % realm}) + headers={'WWW-Authenticate': 'JWT realm="{}"'.format(realm)}) try: payload = _jwt.jwt_decode_callback(token) @@ -216,10 +215,10 @@ def __init__(self, error, description, status_code=401, headers=None): self.headers = headers def __repr__(self): - return 'JWTError: %s' % self.error + return 'JWTError: {}'.format(self.error) def __str__(self): - return '%s. %s' % (self.error, self.description) + return '{}. {}'.format(self.error, self.description) def encode_token(): From b317e6bdae80a0e2984a1a975379afe766c76d1b Mon Sep 17 00:00:00 2001 From: "Aaron N. Brock" Date: Mon, 9 Oct 2017 22:37:21 -0400 Subject: [PATCH 5/9] Allowed jwt_required to be an annotation. --- example/app.py | 4 ++-- flask_jwt/__init__.py | 41 +++++++++++++++++++++++++++++++---------- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/example/app.py b/example/app.py index 8e75d82..18e869f 100644 --- a/example/app.py +++ b/example/app.py @@ -1,5 +1,5 @@ from flask import Flask -from flask_jwt import JWT, jwt_required, current_identity +from flask_jwt import JWT, current_identity from werkzeug.security import safe_str_cmp class User(object): @@ -41,7 +41,7 @@ def authenticate(username, password, **kwargs): @app.route('/protected') -@jwt_required() +@jwt.jwt_required() def protected(): return '{}'.format(current_identity) diff --git a/flask_jwt/__init__.py b/flask_jwt/__init__.py index 9645f91..6ebebe2 100644 --- a/flask_jwt/__init__.py +++ b/flask_jwt/__init__.py @@ -27,8 +27,8 @@ 'JWT_DEFAULT_REALM': 'Login Required', 'JWT_AUTH_URL_RULE': '/auth', 'JWT_AUTH_ENDPOINT': 'jwt', - 'JWT_AUTH_USERNAME_KEY': 'username', - 'JWT_AUTH_PASSWORD_KEY': 'password', + # 'JWT_AUTH_USERNAME_KEY': 'username', + # 'JWT_AUTH_PASSWORD_KEY': 'password', 'JWT_ALGORITHM': 'HS256', 'JWT_ROLE': 'role', 'JWT_LEEWAY': timedelta(seconds=10), @@ -153,13 +153,23 @@ def _force_iterable(input): return input -def _jwt_required(realm, roles): +def _default_jwt_required_handler(*args, **kwargs): """Does the actual work of verifying the JWT data in the current request. This is done automatically for you by `jwt_required()` but you could call it manually. Doing so would be useful in the context of optional JWT access in your APIs. :param realm: an optional realm """ + try: + realm = args[0] or kwargs['realm'] + except IndexError: + realm = current_app.config['JWT_DEFAULT_REALM'] + + try: + roles = args[1] or kwargs['roles'] + except IndexError: + roles = None + token = _jwt.request_callback() if token is None: @@ -198,13 +208,9 @@ def jwt_required(realm=None, roles=None): :param roles: an optional list of roles allowed, the role is pick in JWT_ROLE field of identity """ - def wrapper(fn): - @wraps(fn) - def decorator(*args, **kwargs): - _jwt_required(realm or current_app.config['JWT_DEFAULT_REALM'], roles) - return fn(*args, **kwargs) - return decorator - return wrapper + warnings.warn("jwt_required is deprecated. The recommended approach is " + "to use jwt.jwt_required instead", DeprecationWarning, stacklevel=2) + return _jwt.jwt_required(realm, roles) class JWTError(Exception): @@ -240,9 +246,24 @@ def __init__(self, app=None, authentication_handler=None, identity_handler=None) self.jwt_error_callback = _default_jwt_error_handler self.request_callback = _default_request_handler + self.jwt_required_callback = _default_jwt_required_handler + if app is not None: self.init_app(app) + def jwt_required(self, *args, **kwargs): + def wrapper(fn): + @wraps(fn) + def decorator(*fnargs, **fnkwargs): + self.jwt_required_callback(*args, **kwargs) + return fn(*fnargs, **fnkwargs) + return decorator + return wrapper + + def jwt_required_handler(self, callback): + self.jwt_required_callback = callback + return callback + def init_app(self, app): for k, v in CONFIG_DEFAULTS.items(): app.config.setdefault(k, v) From b9fadd541ad0cb84cca8bf8357885872c9374627 Mon Sep 17 00:00:00 2001 From: "Aaron N. Brock" Date: Mon, 9 Oct 2017 23:35:46 -0400 Subject: [PATCH 6/9] Fixed _default_jwt_required_handler --- flask_jwt/__init__.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/flask_jwt/__init__.py b/flask_jwt/__init__.py index 6ebebe2..ed33b37 100644 --- a/flask_jwt/__init__.py +++ b/flask_jwt/__init__.py @@ -160,14 +160,19 @@ def _default_jwt_required_handler(*args, **kwargs): :param realm: an optional realm """ - try: - realm = args[0] or kwargs['realm'] - except IndexError: + + if 0 <= len(args): + realm = args[0] + elif 'roles' in kwargs: + realm = kwargs['realm'] + else: realm = current_app.config['JWT_DEFAULT_REALM'] - try: - roles = args[1] or kwargs['roles'] - except IndexError: + if 1 <= len(args): + roles = args[1] + elif 'roles' in kwargs: + roles = kwargs['roles'] + else: roles = None token = _jwt.request_callback() From 211cda02981dd2ccdbb47cfeafa4dbe0c2632e0e Mon Sep 17 00:00:00 2001 From: "Aaron N. Brock" Date: Tue, 10 Oct 2017 00:35:36 -0400 Subject: [PATCH 7/9] Fixed my last commit :| --- flask_jwt/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flask_jwt/__init__.py b/flask_jwt/__init__.py index ed33b37..546d102 100644 --- a/flask_jwt/__init__.py +++ b/flask_jwt/__init__.py @@ -161,14 +161,14 @@ def _default_jwt_required_handler(*args, **kwargs): :param realm: an optional realm """ - if 0 <= len(args): + if 0 < len(args): realm = args[0] - elif 'roles' in kwargs: + elif 'realm' in kwargs: realm = kwargs['realm'] else: realm = current_app.config['JWT_DEFAULT_REALM'] - if 1 <= len(args): + if 1 < len(args): roles = args[1] elif 'roles' in kwargs: roles = kwargs['roles'] From f4575bdc558b35f4caf031e5e0e7f90d0147f7f2 Mon Sep 17 00:00:00 2001 From: "Aaron N. Brock" Date: Tue, 10 Oct 2017 00:37:24 -0400 Subject: [PATCH 8/9] Added support for "soft" --- flask_jwt/__init__.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/flask_jwt/__init__.py b/flask_jwt/__init__.py index 546d102..7d95d83 100644 --- a/flask_jwt/__init__.py +++ b/flask_jwt/__init__.py @@ -175,11 +175,22 @@ def _default_jwt_required_handler(*args, **kwargs): else: roles = None + if 2 < len(args): + soft = args[1] + elif 'soft' in kwargs: + soft = kwargs['soft'] + else: + soft = False + token = _jwt.request_callback() if token is None: - raise JWTError('Authorization Required', 'Request does not contain an access token', - headers={'WWW-Authenticate': 'JWT realm="{}"'.format(realm)}) + if soft: + _request_ctx_stack.top.current_identity = identity = None + return + else: + raise JWTError('Authorization Required', 'Request does not contain an access token', + headers={'WWW-Authenticate': 'JWT realm="{}"'.format(realm)}) try: payload = _jwt.jwt_decode_callback(token) From 2d330342c7d8ac545d5aca70ea41af6d4cbecd4f Mon Sep 17 00:00:00 2001 From: "Aaron N. Brock" Date: Tue, 10 Oct 2017 00:42:19 -0400 Subject: [PATCH 9/9] Added jwt.current_identity --- flask_jwt/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/flask_jwt/__init__.py b/flask_jwt/__init__.py index 7d95d83..617e303 100644 --- a/flask_jwt/__init__.py +++ b/flask_jwt/__init__.py @@ -186,6 +186,7 @@ def _default_jwt_required_handler(*args, **kwargs): if token is None: if soft: + _jwt.current_identity = None _request_ctx_stack.top.current_identity = identity = None return else: @@ -197,7 +198,8 @@ def _default_jwt_required_handler(*args, **kwargs): except jwt.InvalidTokenError as e: raise JWTError('Invalid token', str(e)) - _request_ctx_stack.top.current_identity = identity = _jwt.identity_callback(payload) + _jwt.current_identity = _jwt.identity_callback(payload) + _request_ctx_stack.top.current_identity = identity = _jwt.current_identity if identity is None: raise JWTError('Invalid JWT', 'User does not exist') @@ -261,9 +263,10 @@ def __init__(self, app=None, authentication_handler=None, identity_handler=None) self.jwt_payload_callback = _default_jwt_payload_handler self.jwt_error_callback = _default_jwt_error_handler self.request_callback = _default_request_handler - self.jwt_required_callback = _default_jwt_required_handler + self.current_identity = None + if app is not None: self.init_app(app)