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 diff --git a/example/app.py b/example/app.py index da4c30d..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): @@ -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'), @@ -19,25 +19,31 @@ 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 +app = Flask(__name__) +app.debug = True +app.config['SECRET_KEY'] = 'super-secret' + +jwt = JWT(app) + + +@jwt.identity_handler 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, authenticate, 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() +@jwt.jwt_required() def protected(): - return '%s' % current_identity + return '{}'.format(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..617e303 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), @@ -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(): @@ -116,14 +115,10 @@ 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): - raise JWTError('Bad Request', 'Invalid credentials') - - identity = _jwt.authentication_callback(username, password) + try: + identity = _jwt.authentication_callback(**data) + except TypeError: + raise JWTError('Bad Request', 'Invalid credentials arguments') if identity: access_token = _jwt.jwt_encode_callback(identity) @@ -158,25 +153,53 @@ 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 """ + + if 0 < len(args): + realm = args[0] + elif 'realm' in kwargs: + realm = kwargs['realm'] + else: + realm = current_app.config['JWT_DEFAULT_REALM'] + + if 1 < len(args): + roles = args[1] + elif 'roles' in kwargs: + roles = kwargs['roles'] + 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="%s"' % realm}) + if soft: + _jwt.current_identity = None + _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) 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') @@ -203,13 +226,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): @@ -220,10 +239,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(): @@ -244,10 +263,26 @@ 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) + 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) @@ -257,10 +292,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)