From 04f793a82e9d90fdb851a071719ff159fd4ab991 Mon Sep 17 00:00:00 2001 From: JHDiekhoff Date: Wed, 5 Jun 2013 13:19:19 +0200 Subject: [PATCH 1/3] In case the loginstyle is set to alternate and the user clicks choses option "I have no password" the user is now able to login via email instead of requesting a new password. routing.py - added path to the "emaillogin" action in the user controller user.py - added perform_email_login method to the user controller.If the adhocracy.login_style is set to alternate it sends an email with an login link to the user - Added emaillogin method to catch EmailLoginRepozeWho errors Authentication.py - Changed the Path action path to perform_email_login - Added Email EmailLoginRepozeWho created emaillogin.py - Added EmailLoginRepozeWho controller created login_email.html - Informs the user that he got a new email --- src/adhocracy/config/routing.py | 2 + src/adhocracy/controllers/user.py | 30 +++++- src/adhocracy/lib/auth/authentication.py | 5 +- src/adhocracy/lib/auth/emaillogin.py | 97 +++++++++++++++++++ src/adhocracy/templates/user/login_email.html | 17 ++++ 5 files changed, 148 insertions(+), 3 deletions(-) create mode 100644 src/adhocracy/lib/auth/emaillogin.py create mode 100644 src/adhocracy/templates/user/login_email.html diff --git a/src/adhocracy/config/routing.py b/src/adhocracy/config/routing.py index 10d0537fd..b69eb8012 100644 --- a/src/adhocracy/config/routing.py +++ b/src/adhocracy/config/routing.py @@ -39,6 +39,8 @@ def make_map(config): action='dashboard_pages') map.connect('/welcome/{id}/{token}', controller='user', action='welcome') + map.connect('/emaillogin/{id}/{token}', controller='user', + action='emaillogin') map.resource('user', 'user', member={'votes': 'GET', 'delegations': 'GET', 'votes': 'GET', diff --git a/src/adhocracy/controllers/user.py b/src/adhocracy/controllers/user.py index 45071ee2d..f7975de2f 100644 --- a/src/adhocracy/controllers/user.py +++ b/src/adhocracy/controllers/user.py @@ -9,7 +9,7 @@ from pylons.decorators import validate from pylons.i18n import _ from babel import Locale - +from adhocracy.lib.session.session import get_secret from webob.exc import HTTPFound from repoze.who.api import get_api @@ -23,6 +23,7 @@ from adhocracy.lib.auth.csrf import RequireInternalRequest, token_id from adhocracy.lib.auth.welcome import (welcome_enabled, can_welcome, welcome_url) +from adhocracy.lib.auth.emaillogin import (create_token, email_url) from adhocracy.lib.base import BaseController from adhocracy.lib.instance import RequireInstance import adhocracy.lib.mail as libmail @@ -562,6 +563,33 @@ def nopassword(self): _("Sorry, registration has been disabled by administrator."), category='error', code=403) + @RequireInternalRequest(methods=['POST']) + @validate(schema=NoPasswordForm(), post_only=True) + def perform_email_login(self): + assert config.get('adhocracy.login_style') == 'alternate' + if config.get('adhocracy.login_style') == 'alternate' and self.form_result.get('have_password') == 'false': + user = model.User.find_by_email(self.form_result.get('login')) + if user: + login_code = create_token(user.email, config) + url = email_url(user, login_code) + body = ( + _("you have requested to authenticate you as a user via email address." + "to proceed, please open the " + "link below in your browser:") + + "\n\n " + url + "\n") + libmail.to_user(user, + _("Login for %s") % h.site.name(), + body) + return render("/user/login_email.html") + return self.nopassword() + + def emaillogin(self, id, token): + # Intercepted by EmailLoginRepozeWho, only errors go in here + h.flash(_('The login request was not valid, please try again'), + 'error') + return redirect(h.base_url('/login')) + + def dashboard(self, id): '''Render a personalized dashboard for users''' diff --git a/src/adhocracy/lib/auth/authentication.py b/src/adhocracy/lib/auth/authentication.py index 3ccf24fea..6cee72d28 100644 --- a/src/adhocracy/lib/auth/authentication.py +++ b/src/adhocracy/lib/auth/authentication.py @@ -10,6 +10,7 @@ import adhocracy.model as model from . import welcome +from . import emaillogin from authorization import InstanceGroupSourceAdapter from instance_auth_tkt import InstanceAuthTktCookiePlugin @@ -92,7 +93,7 @@ def identify(self, environ): request = Request(environ, charset=self.charset) form = dict(request.POST) if form.get('have_password') == 'false': - environ['PATH_INFO'] = '/user/nopassword' + environ['PATH_INFO'] = '/user/perform_email_login' login = form.get('login') environ['_adhocracy_nopassword_user'] = self._get_user(login) return None @@ -150,7 +151,7 @@ def setup_auth(app, config): mdproviders = [('sql_user_md', sql_user_md)] welcome.setup_auth(config, identifiers, authenticators) - + emaillogin.setup_auth(config, identifiers, authenticators) log_stream = None #log_stream = sys.stdout diff --git a/src/adhocracy/lib/auth/emaillogin.py b/src/adhocracy/lib/auth/emaillogin.py new file mode 100644 index 000000000..9f3f26833 --- /dev/null +++ b/src/adhocracy/lib/auth/emaillogin.py @@ -0,0 +1,97 @@ +""" Logs in user via email """ + +import re +import adhocracy.model as model +from adhocracy.lib.auth.authorization import has +from paste.deploy.converters import asbool +import pylons +from repoze.who.interfaces import IAuthenticator, IIdentifier +from webob.exc import HTTPFound +from zope.interface import implements +import time +import hashlib +import base64 +from adhocracy.lib.session.session import get_secret + + +def email_url(user, code): + from adhocracy.lib.helpers import base_url + return base_url("/emaillogin/%s/%s" % (user.user_name, code), + absolute=True) + + +def create_token(email, config): + secret = get_secret(config) + timestamp = str(int(time.time())) + message = timestamp + "_" + create_hash(email, timestamp, config) + return base64.urlsafe_b64encode(message.encode("utf-8")) + + +def create_hash(email, time, config): + secret = get_secret(config) + value = secret + email + time + return hashlib.sha256(value).hexdigest() + + +def validate_token(user_token, email, config): + try: + decoded = base64.urlsafe_b64decode(user_token) + user_time = decoded.split('_')[0] + user_hash = decoded.split('_')[1] + time_dif = int(time.time()) - int(user_time) + except (TypeError, ValueError): + return False + correct_value = create_hash(email, user_time, config) + if (user_hash == correct_value) and (time_dif < 3600): + return True + return False + + +class EmailLoginRepozeWho(object): + implements(IAuthenticator, IIdentifier) + + def __init__(self, config, rememberer_name, prefix='/emaillogin/'): + self.config = config + self.rememberer_name = rememberer_name + self.url_rex = re.compile(r'^' + re.escape(prefix) + + r'(?P[^/]+)/(?P[^/]+)$') + + def identify(self, environ): + path_info = environ['PATH_INFO'] + m = self.url_rex.match(path_info) + if not m: + return None + u = model.User.find(m.group('id')) + if not u: + return None + + if not validate_token(m.group('code'), u.email, self.config): + return None + + from adhocracy.lib.helpers import base_url + root_url = base_url('/', instance=None, config=self.config) + environ['repoze.who.application'] = HTTPFound(location=root_url) + return { + 'repoze.who.plugins.emaillogin.userid': u.user_name, + } + + def forget(self, environ, identity): + rememberer = environ['repoze.who.plugins'][self.rememberer_name] + return rememberer.forget(environ, identity) + + def remember(self, environ, identity): + rememberer = environ['repoze.who.plugins'][self.rememberer_name] + return rememberer.remember(environ, identity) + + def authenticate(self, environ, identity): + userid = identity.get('repoze.who.plugins.emaillogin.userid') + if userid is None: + return None + identity['repoze.who.userid'] = userid + return userid + + +def setup_auth(config, idenitifiers, authenticators): + email_rwho = EmailLoginRepozeWho(config, 'auth_tkt') + idenitifiers.append(('emaillogin', email_rwho)) + authenticators.append(('emaillogin', email_rwho)) diff --git a/src/adhocracy/templates/user/login_email.html b/src/adhocracy/templates/user/login_email.html new file mode 100644 index 000000000..10bebdbe0 --- /dev/null +++ b/src/adhocracy/templates/user/login_email.html @@ -0,0 +1,17 @@ +<%inherit file="/template.html" /> +<%def name="title()">${_("Email login request sent")} + +<%block name="headline"> +

${_("Email login request sent")}

+ + +<%block name="main_content"> + +
+
+ ${_("You will be sent an link to your email address. that, when opened will authenticate you automatically.")} +
+
+ From a1c350d64a4ac402bd29f65202ec35aaf182f44b Mon Sep 17 00:00:00 2001 From: JHDiekhoff Date: Tue, 30 Jul 2013 12:38:28 +0200 Subject: [PATCH 2/3] Deleted print --- src/adhocracy/lib/crypto.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/adhocracy/lib/crypto.py b/src/adhocracy/lib/crypto.py index 1d6f796f8..90a8a30d4 100644 --- a/src/adhocracy/lib/crypto.py +++ b/src/adhocracy/lib/crypto.py @@ -29,11 +29,11 @@ def get_secret(config=config, key=None): 'beaker.session.secret', 'adhocracy.auth.secret', ] - if key is not None: search_keys.insert(0, key) for k in search_keys: if config.get(k): + assert config[k] != 'autogenerated' res = config.get(k) if not isinstance(res, bytes): res = res.encode('ascii') @@ -63,7 +63,6 @@ def verify(signed, secret=None, salt=b''): assert isinstance(secret, bytes) assert isinstance(signed, bytes) assert isinstance(salt, bytes) - print signed.partition(b'!') signature, _, val = signed.partition(b'!') correct_signature = _sign(val, secret, salt) From 128ada0b88071b4f28488eebd8e63e4ab54a9dc3 Mon Sep 17 00:00:00 2001 From: JHDiekhoff Date: Mon, 5 Aug 2013 17:09:30 +0200 Subject: [PATCH 3/3] Removed config parameter, deleted changes to crypto.py --- src/adhocracy/controllers/user.py | 2 +- src/adhocracy/lib/auth/emaillogin.py | 4 ++-- src/adhocracy/lib/crypto.py | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/adhocracy/controllers/user.py b/src/adhocracy/controllers/user.py index b7e545ea7..bb3ab2588 100644 --- a/src/adhocracy/controllers/user.py +++ b/src/adhocracy/controllers/user.py @@ -890,7 +890,7 @@ def perform_email_login(self): if config.get('adhocracy.login_style') == 'alternate' and self.form_result.get('have_password') == 'false': user = model.User.find_by_email(self.form_result.get('login')) if user: - login_code = create_token(user.email, config) + login_code = create_token(user.email) url = email_url(user, login_code) body = ( _("you have requested to authenticate you as a user via email address." diff --git a/src/adhocracy/lib/auth/emaillogin.py b/src/adhocracy/lib/auth/emaillogin.py index da5b14651..6fedd8f89 100644 --- a/src/adhocracy/lib/auth/emaillogin.py +++ b/src/adhocracy/lib/auth/emaillogin.py @@ -19,8 +19,8 @@ def email_url(user, code): absolute=True) -def create_token(email, config): - secret = get_secret(config) +def create_token(email): + secret = get_secret() timestamp = str(int(time.time())) message = sign((email + "_" + timestamp).encode("utf-8"),secret) return base64.urlsafe_b64encode(message.encode("utf-8")) diff --git a/src/adhocracy/lib/crypto.py b/src/adhocracy/lib/crypto.py index 90a8a30d4..8324625d9 100644 --- a/src/adhocracy/lib/crypto.py +++ b/src/adhocracy/lib/crypto.py @@ -34,7 +34,7 @@ def get_secret(config=config, key=None): for k in search_keys: if config.get(k): assert config[k] != 'autogenerated' - res = config.get(k) + res = config[k] if not isinstance(res, bytes): res = res.encode('ascii') return res @@ -63,10 +63,10 @@ def verify(signed, secret=None, salt=b''): assert isinstance(secret, bytes) assert isinstance(signed, bytes) assert isinstance(salt, bytes) + signature, _, val = signed.partition(b'!') correct_signature = _sign(val, secret, salt) - if compare_digest(signature, correct_signature): return val else: - raise ValueError(salt.decode('ascii') + u' MAC verification failed') + raise ValueError(salt.decode('ascii') + u' MAC verification failed') \ No newline at end of file