diff --git a/gluon/tools.py b/gluon/tools.py index 7ef74ca0..f0a09850 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -34,13 +34,12 @@ import email.utils import random import hmac import hashlib -import json from email import MIMEBase, MIMEMultipart, MIMEText, Encoders, Header, message_from_string, Charset from gluon.serializers import json_parser from gluon.contenttype import contenttype from gluon.storage import Storage, StorageList, Settings, Messages -from gluon.utils import web2py_uuid +from gluon.utils import web2py_uuid, compare from gluon.fileutils import read_file, check_credentials from gluon import * from gluon.contrib.autolinks import expand_one @@ -53,17 +52,6 @@ import gluon.serializers as serializers Table = DAL.Table Field = DAL.Field -try: - # try stdlib (Python 2.6) - import json as json_parser -except ImportError: - try: - # try external module - import simplejson as json_parser - except: - # fallback to pure-Python module - import gluon.contrib.simplejson as json_parser - __all__ = ['Mail', 'Auth', 'Recaptcha', 'Recaptcha2', 'Crud', 'Service', 'Wiki', 'PluginManager', 'fetch', 'geocode', 'reverse_geocode', 'prettydate'] @@ -265,7 +253,7 @@ class Mail(object): MIMEBase.MIMEBase.__init__(self, *content_type.split('/', 1)) self.set_payload(payload) self['Content-Disposition'] = 'attachment; filename="%s"' % filename - if not content_id is None: + if content_id is not None: self['Content-Id'] = '<%s>' % content_id.encode(encoding) Encoders.encode_base64(self) @@ -1143,8 +1131,7 @@ def addrow(form, a, b, c, style, _id, position=-1): class AuthJWT(object): """ - If left externally, this needs the usual "singleton" approach. - Given I (we) don't know if to include in auth yet, let's stick to basics. + Experimental! Args: - secret_key: the secret. Without salting, an attacker knowing this can impersonate @@ -1175,7 +1162,9 @@ class AuthJWT(object): return payload - before_authorization: can be a callable that takes the deserialized token (a dict) as input. Gets called right after signature verification but before the actual - authorization takes place. You can raise with HTTP a proper error message + authorization takes place. It may be use to cast + the extra auth_user fields to their actual types. + You can raise with HTTP a proper error message Example: def mybefore_authorization(tokend): if not tokend['my_name_is'] == 'bond,james bond': @@ -1192,8 +1181,8 @@ class AuthJWT(object): def login_and_take_token(): return myjwt.jwt_token_manager() - A call then to /app/controller/login_and_take_token/auth with username and password returns the token - A call to /app/controller/login_and_take_token/refresh with the original token returns the refreshed token + A call then to /app/controller/login_and_take_token with username and password returns the token + A call to /app/controller/login_and_take_token with the original token returns the refreshed token To protect a function with JWT @@ -1204,7 +1193,7 @@ class AuthJWT(object): """ - def __init__(self, + def __init__(self, auth, secret_key, algorithm='HS256', @@ -1256,7 +1245,7 @@ class AuthJWT(object): @staticmethod def jwt_b64e(string): if isinstance(string, unicode): - string = string.encode('uft-8', 'strict') + string = string.encode('utf-8', 'strict') return base64.urlsafe_b64encode(string).strip(b'=') @staticmethod @@ -1279,7 +1268,7 @@ class AuthJWT(object): if isinstance(secret, unicode): secret = secret.encode('ascii', 'ignore') b64h = self.cached_b64h - b64p = self.jwt_b64e(json_parser.dumps(payload)) + b64p = self.jwt_b64e(serializers.json(payload)) jbody = b64h + '.' + b64p mauth = hmac.new(key=secret, msg=jbody, digestmod=self.digestmod) jsign = self.jwt_b64e(mauth.digest()) @@ -1287,7 +1276,7 @@ class AuthJWT(object): def verify_signature(self, body, signature, secret): mauth = hmac.new(key=secret, msg=body, digestmod=self.digestmod) - return hmac.compare_digest(self.jwt_b64e(mauth.digest()), signature) + return compare(self.jwt_b64e(mauth.digest()), signature) def load_token(self, token): if isinstance(token, unicode): @@ -1298,7 +1287,7 @@ class AuthJWT(object): # header not the same raise HTTP(400, u'Invalid JWT Header') secret = self.secret_key - tokend = json_parser.loads(self.jwt_b64d(b64b)) + tokend = serializers.loads_json(self.jwt_b64d(b64b)) if self.salt: if callable(self.salt): secret = "%s$%s" % (secret, self.salt(tokend)) @@ -1324,7 +1313,10 @@ class AuthJWT(object): We (mis)use the heavy default auth mechanism to avoid any further computation, while sticking to a somewhat-stable Auth API. """ - now = time.mktime(datetime.datetime.utcnow().timetuple()) + ## is the following safe or should we use + ## calendar.timegm(datetime.datetime.utcnow().timetuple()) + ## result seem to be the same (seconds since epoch, in UTC) + now = time.mktime(datetime.datetime.now().timetuple()) expires = now + self.expiration payload = dict( hmac_key=session_auth['hmac_key'], @@ -1336,7 +1328,7 @@ class AuthJWT(object): return payload def refresh_token(self, orig_payload): - now = time.mktime(datetime.datetime.utcnow().timetuple()) + now = time.mktime(datetime.datetime.now().timetuple()) if self.verify_expiration: orig_exp = orig_payload['exp'] if orig_exp + self.leeway < now: @@ -1346,7 +1338,7 @@ class AuthJWT(object): if orig_iat + self.refresh_expiration_delta < now: # refreshed too long ago raise HTTP(400, u'Token issued too long ago') - expires = now + self.refresh_expiration_delta + expires = now + self.expiration orig_payload.update( orig_iat=orig_iat, iat=now, @@ -1372,14 +1364,17 @@ class AuthJWT(object): def api_auth(): return myjwt.jwt_token_manager() - Then, a call to /app/c/api_auth/auth with username and password - returns a token, while /app/c/api_auth/refresh with the current token + Then, a call to /app/c/api_auth with username and password + returns a token, while /app/c/api_auth with the current token issues another token """ request = current.request response = current.response session = current.session # forget and unlock response + session.forget(response) + valid_user = None + ret = None if request.vars.token: if not self.allow_refresh: raise HTTP(403, u'Refreshing token is not allowed') @@ -1387,24 +1382,23 @@ class AuthJWT(object): tokend = self.load_token(token) # verification can fail here refreshed = self.refresh_token(tokend) - ret = {'token':self.generate_token(refreshed)} + ret = {'token': self.generate_token(refreshed)} elif self.user_param in request.vars and self.pass_param in request.vars: - session.forget(response) username = request.vars[self.user_param] password = request.vars[self.pass_param] valid_user = self.auth.login_bare(username, password) - if valid_user: - payload = self.serialize_auth_session(current.session.auth) - self.alter_payload(payload) - ret = {'token':self.generate_token(payload)} - else: - raise HTTP( - 401, u'Not Authorized', - **{'WWW-Authenticate': u'JWT realm="%s"' % self.realm}) else: - raise HTTP(400, u'Must pass token for refresh or username and password for login') - response.headers['content-type'] = 'application/json' - return json.dumps(ret) + valid_user = self.auth.user + if valid_user: + payload = self.serialize_auth_session(current.session.auth) + self.alter_payload(payload) + ret = {'token': self.generate_token(payload)} + elif ret is None: + raise HTTP( + 401, u'Not Authorized - need to be logged in, to pass a token for refresh or username and password for login', + **{'WWW-Authenticate': u'JWT realm="%s"' % self.realm}) + response.headers['Content-Type'] = 'application/json' + return serializers.json(ret) def inject_token(self, tokend): """ @@ -1846,7 +1840,6 @@ class Auth(object): self.define_signature() else: self.signature = None - self.jwt_handler = jwt and AuthJWT(self, **jwt) def get_vars_next(self): @@ -1931,7 +1924,7 @@ class Auth(object): elif args(1) == self.settings.cas_actions['proxyvalidate']: return self.cas_validate(version=2, proxy=True) elif args(1) == self.settings.cas_actions['logout']: - return self.logout(next=request.vars.service or DEFAULT) + return self.logout(next=request.vars.service or DEFAULT) else: raise HTTP(404) @@ -2956,7 +2949,7 @@ class Auth(object): user = table_user(**{username: entered_username}) if user: # user in db, check if registration pending or disabled - temp_user = user + temp_user = user if (temp_user.registration_key or '').startswith('pending'): response.flash = self.messages.registration_pending return form @@ -3759,7 +3752,7 @@ class Auth(object): except Exception: session.flash = self.messages.invalid_reset_password redirect(next, client_side=self.settings.client_side) - + key = user.registration_key if key in ('pending', 'disabled', 'blocked') or (key or '').startswith('pending'): session.flash = self.messages.registration_pending @@ -3858,7 +3851,7 @@ class Auth(object): onvalidation=onvalidation, hideerror=self.settings.hideerror): user = table_user(**{userfield:form.vars.get(userfield)}) - key = user.registration_key + key = user.registration_key if not user: session.flash = self.messages['invalid_%s' % userfield] redirect(self.url(args=request.args), @@ -4045,18 +4038,18 @@ class Auth(object): def jwt(self): """ To use JWT authentication: - 1) instantiate auth with + 1) instantiate auth with:: auth = Auth(db, jwt = {'secret_key':'secret'}) - where 'secret' is your own secret string. + where 'secret' is your own secret string. - 2) Secorate functions that require login but should accept the JWT token credentials: + 2) Decorate functions that require login but should accept the JWT token credentials:: @auth.allows_jwt() @auth.requires_login() def myapi(): return 'hello %s' % auth.user.email - + Notice jwt is allowed but not required. if user is logged in, myapi is accessible. 3) Use it! @@ -4074,7 +4067,7 @@ class Auth(object): Authorization: Bearer - Any additional attributes in the jwt argument of Auth() below: + Any additional attributes in the jwt argument of Auth() below:: auth = Auth(db, jwt = {...}) @@ -4083,9 +4076,9 @@ class Auth(object): if not self.jwt_handler: raise HTTP(400, "Not authorized") else: - current.response.headers['content-type'] = 'application/json' - raise HTTP(200, self.jwt_handler.jwt_token_manager()) - + rtn = self.jwt_handler.jwt_token_manager() + raise HTTP(200, rtn, cookies=None, **current.response.headers) + def is_impersonating(self): return self.is_logged_in() and 'impersonator' in current.session.auth @@ -4188,7 +4181,7 @@ class Auth(object): if not self.jwt_handler: raise HTTP(400, "Not authorized") else: - return self.jwt_handler.allows_jwt() + return self.jwt_handler.allows_jwt(otherwise=otherwise) def requires(self, condition, requires_login=True, otherwise=None): """ @@ -4201,7 +4194,6 @@ class Auth(object): basic_allowed, basic_accepted, user = self.basic() user = user or self.user - login_required = requires_login if callable(login_required): login_required = login_required() @@ -4210,7 +4202,7 @@ class Auth(object): if not user: if current.request.ajax: raise HTTP(401, self.messages.ajax_failed_authentication) - elif not otherwise is None: + elif otherwise is not None: if callable(otherwise): return otherwise() redirect(otherwise) @@ -4248,7 +4240,7 @@ class Auth(object): return self.requires(True, otherwise=otherwise) def requires_login_or_token(self, otherwise=None): - if self.settings.enable_tokens == True: + if self.settings.enable_tokens is True: user = None request = current.request token = request.env.http_web2py_user_token or request.vars._token diff --git a/gluon/utils.py b/gluon/utils.py index 4df0cbc3..9e0f6679 100644 --- a/gluon/utils.py +++ b/gluon/utils.py @@ -64,6 +64,10 @@ else: except (ImportError, ValueError): HAVE_PBKDF2 = False +HAVE_COMPARE_DIGEST = False +if hasattr(hmac, 'compare_digest'): + HAVE_COMPARE_DIGEST = True + logger = logging.getLogger("web2py") @@ -77,6 +81,8 @@ def AES_new(key, IV=None): def compare(a, b): """ Compares two strings and not vulnerable to timing attacks """ + if HAVE_COMPARE_DIGEST: + return hmac.compare_digest(a, b) if len(a) != len(b): return False result = 0 @@ -143,6 +149,7 @@ DIGEST_ALG_BY_SIZE = { 512 / 4: 'sha512', } + def get_callable_argspec(fn): if inspect.isfunction(fn) or inspect.ismethod(fn): inspectable = fn @@ -154,6 +161,7 @@ def get_callable_argspec(fn): inspectable = fn return inspect.getargspec(inspectable) + def pad(s, n=32, padchar=' '): return s + (32 - len(s) % 32) * padchar @@ -172,7 +180,7 @@ def secure_dumps(data, encryption_key, hash_key=None, compression_level=None): def secure_loads(data, encryption_key, hash_key=None, compression_level=None): - if not ':' in data: + if ':' not in data: return None if not hash_key: hash_key = sha1(encryption_key).hexdigest()