Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

No longer requires authentication via Username & Password #5

Open
wants to merge 9 commits into
base: fork
Choose a base branch
from
9 changes: 6 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,17 @@ pip-log.txt
.coverage
.tox

#Translations
# Translations
*.mo


#Virtualenv
# Virtualenv
env/

#Editor temporaries
# Editor temporaries
*~

*.db

# PyCharm
.idea
32 changes: 19 additions & 13 deletions example/app.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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'),
Expand All @@ -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')
95 changes: 65 additions & 30 deletions flask_jwt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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)

Expand All @@ -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():
Expand All @@ -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)
Expand Down Expand Up @@ -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')
Expand All @@ -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):
Expand All @@ -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():
Expand All @@ -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)
Expand All @@ -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)
Expand Down