From 2e5a241777ef0bb0d76420d39bf3be41e16e042a Mon Sep 17 00:00:00 2001 From: Cédric Bonhomme Date: Thu, 18 Feb 2016 08:59:13 +0100 Subject: New management of the token for the account confirmation. --- src/bootstrap.py | 5 +++++ src/conf.py | 1 + src/conf/conf.cfg-sample | 1 + src/manager.py | 2 +- src/tests/fixtures.py | 2 +- src/web/controllers/user.py | 8 -------- src/web/forms.py | 9 ++++----- src/web/lib/user_utils.py | 23 +++++++++++++++++++++++ src/web/models/__init__.py | 2 +- src/web/models/user.py | 3 +-- src/web/notifications.py | 6 ++++-- src/web/templates/admin/dashboard.html | 4 ++-- src/web/views/admin.py | 11 +++++------ src/web/views/api/common.py | 3 +-- src/web/views/user.py | 12 +++++++----- 15 files changed, 57 insertions(+), 35 deletions(-) create mode 100644 src/web/lib/user_utils.py (limited to 'src') diff --git a/src/bootstrap.py b/src/bootstrap.py index 25528ef5..f1624111 100644 --- a/src/bootstrap.py +++ b/src/bootstrap.py @@ -44,6 +44,11 @@ application.config['SECRET_KEY'] = getattr(conf, 'WEBSERVER_SECRET', None) if not application.config['SECRET_KEY']: application.config['SECRET_KEY'] = os.urandom(12) +application.config['SECURITY_PASSWORD_SALT'] = getattr(conf, + 'SECURITY_PASSWORD_SALT', None) +if not application.config['SECURITY_PASSWORD_SALT']: + application.config['SECURITY_PASSWORD_SALT'] = os.urandom(12) + application.config['RECAPTCHA_USE_SSL'] = True application.config['RECAPTCHA_PUBLIC_KEY'] = conf.RECAPTCHA_PUBLIC_KEY application.config['RECAPTCHA_PRIVATE_KEY'] = conf.RECAPTCHA_PRIVATE_KEY diff --git a/src/conf.py b/src/conf.py index d65bb516..5c96fea8 100644 --- a/src/conf.py +++ b/src/conf.py @@ -77,6 +77,7 @@ ADMIN_EMAIL = config.get('misc', 'admin_email') RECAPTCHA_PUBLIC_KEY = config.get('misc', 'recaptcha_public_key') RECAPTCHA_PRIVATE_KEY = config.get('misc', 'recaptcha_private_key') +SECURITY_PASSWORD_SALT = config.get('misc', 'security_password_salt') LOG_PATH = os.path.abspath(config.get('misc', 'log_path')) NB_WORKER = config.getint('misc', 'nb_worker') API_LOGIN = config.get('crawler', 'api_login') diff --git a/src/conf/conf.cfg-sample b/src/conf/conf.cfg-sample index cc37a4a2..286ccbe5 100644 --- a/src/conf/conf.cfg-sample +++ b/src/conf/conf.cfg-sample @@ -9,6 +9,7 @@ platform_url = http://127.0.0.1:5000/ admin_email = recaptcha_public_key = recaptcha_private_key = +security_password_salt = a secret to confirm user account log_path = ./src/web/var/jarr.log nb_worker = 5 log_level = info diff --git a/src/manager.py b/src/manager.py index 781d742b..e64263f2 100755 --- a/src/manager.py +++ b/src/manager.py @@ -64,7 +64,7 @@ def fetch_asyncio(user_id, feed_id): loop = asyncio.get_event_loop() for user in users: - if user.activation_key == "": + if user.enabled: print("Fetching articles for " + user.nickname) g.user = user classic_crawler.retrieve_feed(loop, g.user, feed_id) diff --git a/src/tests/fixtures.py b/src/tests/fixtures.py index 99f46c37..16a9cb81 100644 --- a/src/tests/fixtures.py +++ b/src/tests/fixtures.py @@ -4,7 +4,7 @@ from web.models import db_create, db_empty, User, Article, Feed def populate_db(db): role_admin, role_user = db_create(db) user1, user2 = [User(nickname=name, email="%s@test.te" % name, - pwdhash=name, roles=[role_user], activation_key="") + pwdhash=name, roles=[role_user], enabled=True) for name in ["user1", "user2"]] db.session.add(user1) db.session.add(user2) diff --git a/src/web/controllers/user.py b/src/web/controllers/user.py index d8bf1fa1..ae169b05 100644 --- a/src/web/controllers/user.py +++ b/src/web/controllers/user.py @@ -9,14 +9,6 @@ class UserController(AbstractController): _db_cls = User _user_id_key = 'id' - def unset_activation_key(self, obj_id): - self.update({'id': obj_id}, {'activation_key': ""}) - - def set_activation_key(self, obj_id): - key = str(random.getrandbits(256)).encode("utf-8") - key = hashlib.sha512(key).hexdigest()[:86] - self.update({'id': obj_id}, {'activation_key': key}) - def _handle_password(self, attrs): if attrs.get('password'): attrs['pwdhash'] = generate_password_hash(attrs.pop('password')) diff --git a/src/web/forms.py b/src/web/forms.py index 172f31a8..b17d2f7a 100644 --- a/src/web/forms.py +++ b/src/web/forms.py @@ -99,10 +99,9 @@ class SigninForm(RedirectForm): return False user = User.query.filter(User.email == self.email.data).first() - if user and user.check_password(self.password.data) \ - and user.activation_key == "": + if user and user.check_password(self.password.data) and user.enabled: return True - elif user and user.activation_key != "": + elif user and not user.enabled: flash(lazy_gettext('Account not confirmed'), 'danger') return False else: @@ -207,9 +206,9 @@ class RecoverPasswordForm(Form): return False user = User.query.filter(User.email == self.email.data).first() - if user and user.activation_key == "": + if user and user.enabled: return True - elif user and user.activation_key != "": + elif user and not user.enabled: flash(lazy_gettext('Account not confirmed.'), 'danger') return False else: diff --git a/src/web/lib/user_utils.py b/src/web/lib/user_utils.py new file mode 100644 index 00000000..78468379 --- /dev/null +++ b/src/web/lib/user_utils.py @@ -0,0 +1,23 @@ + + +from itsdangerous import URLSafeTimedSerializer + +from bootstrap import application + + +def generate_confirmation_token(email): + serializer = URLSafeTimedSerializer(app.config['SECRET_KEY']) + return serializer.dumps(email, salt=app.config['SECURITY_PASSWORD_SALT']) + + +def confirm_token(token, expiration=3600): + serializer = URLSafeTimedSerializer(app.config['SECRET_KEY']) + try: + email = serializer.loads( + token, + salt=app.config['SECURITY_PASSWORD_SALT'], + max_age=expiration + ) + except: + return False + return email diff --git a/src/web/models/__init__.py b/src/web/models/__init__.py index e6615ab4..d9489dbb 100644 --- a/src/web/models/__init__.py +++ b/src/web/models/__init__.py @@ -96,7 +96,7 @@ def db_create(db): "root@jarr.localhost"), pwdhash=generate_password_hash( os.environ.get("ADMIN_PASSWORD", "password")), - activation_key="") + enabled=True) user1.roles.extend([role_admin, role_user]) db.session.add(user1) diff --git a/src/web/models/user.py b/src/web/models/user.py index cdbfb457..1a276f7e 100644 --- a/src/web/models/user.py +++ b/src/web/models/user.py @@ -45,8 +45,7 @@ class User(db.Model, UserMixin): email = db.Column(db.String(254), index=True, unique=True) pwdhash = db.Column(db.String()) roles = db.relationship('Role', backref='user', lazy='dynamic') - activation_key = db.Column(db.String(128), default=hashlib.sha512( - str(random.getrandbits(256)).encode("utf-8")).hexdigest()[:86]) + enabled = db.Column(db.Boolean(), default=False) date_created = db.Column(db.DateTime(), default=datetime.now) last_seen = db.Column(db.DateTime(), default=datetime.now) feeds = db.relationship('Feed', backref='subscriber', lazy='dynamic', diff --git a/src/web/notifications.py b/src/web/notifications.py index c0d4fb1c..309da2a3 100644 --- a/src/web/notifications.py +++ b/src/web/notifications.py @@ -21,6 +21,7 @@ import conf from web import emails +from web.lib.user_utils import generate_confirmation_token def information_message(subject, plaintext): @@ -30,7 +31,7 @@ def information_message(subject, plaintext): from web.models import User users = User.query.all() # Only send email for activated accounts. - user_emails = [user.email for user in users if user.activation_key == ""] + user_emails = [user.email for user in users if user.enabled] # Postmark has a limit of twenty recipients per message in total. for i in xrange(0, len(user_emails), 19): emails.send(to=conf.NOTIFICATION_EMAIL, @@ -41,8 +42,9 @@ def new_account_notification(user): """ Account creation notification. """ + token = generate_confirmation_token(user.email) plaintext = """Hello,\n\nYour account has been created. Click on the following link to confirm it:\n%s\n\nSee you,""" % \ - (conf.PLATFORM_URL + 'user/confirm_account/' + user.activation_key) + (conf.PLATFORM_URL + 'user/confirm_account/' + token) emails.send(to=user.email, bcc=conf.NOTIFICATION_EMAIL, subject="[jarr] Account creation", plaintext=plaintext) diff --git a/src/web/templates/admin/dashboard.html b/src/web/templates/admin/dashboard.html index 22e82349..57b20bb5 100644 --- a/src/web/templates/admin/dashboard.html +++ b/src/web/templates/admin/dashboard.html @@ -18,7 +18,7 @@ {% for user in users|sort(attribute="last_seen")|reverse %} - + {{ loop.index }} {{ user.nickname }}{% if user.id == current_user.id %} (It's you!){% endif %} {{ user.email }} @@ -28,7 +28,7 @@ {% if user.id != current_user.id %} - {% if user.activation_key == "" %} + {% if user.enabled %} {% else %} diff --git a/src/web/views/admin.py b/src/web/views/admin.py index 30758f63..832c134d 100644 --- a/src/web/views/admin.py +++ b/src/web/views/admin.py @@ -83,13 +83,13 @@ def process_user_form(user_id=None): flash(gettext('User %(nick)s successfully updated', nick=user.nickname), 'success') else: - # Create a new user + # Create a new user (by the admin) user = user_contr.create(nickname=form.nickname.data, email=form.email.data, password=form.password.data, roles=[role_user], refresh_rate=form.refresh_rate.data, - activation_key="") + enabled=True) flash(gettext('User %(nick)s successfully created', nick=user.nickname), 'success') return redirect(url_for('admin.user_form', user_id=user.id)) @@ -144,12 +144,11 @@ def toggle_user(user_id=None): flash(gettext('This user does not exist.'), 'danger') return redirect(url_for('admin.dashboard')) - if user.activation_key != "": - + if not user.enabled: # Send the confirmation email try: notifications.new_account_activation(user) - user_contr.unset_activation_key(user.id) + user_contr.update({'id': user.id}, {'enabled': True}) message = gettext('Account of the user %(nick)s successfully ' 'activated.', nick=user.nickname) except Exception as error: @@ -158,7 +157,7 @@ def toggle_user(user_id=None): return redirect(url_for('admin.dashboard')) else: - user_contr.set_activation_key(user.id) + user_contr.update({'id': user.id}, {'enabled': False}) message = gettext('Account of the user %(nick)s successfully disabled', nick=user.nickname) flash(message, 'success') diff --git a/src/web/views/api/common.py b/src/web/views/api/common.py index 3476cad9..c155a254 100644 --- a/src/web/views/api/common.py +++ b/src/web/views/api/common.py @@ -54,8 +54,7 @@ def authenticate(func): if auth is not None: user = User.query.filter( User.nickname == auth.username).first() - if user and user.check_password(auth.password) \ - and user.activation_key == "": + if user and user.check_password(auth.password) and user.enabled: g.user = user logged_in = True if logged_in: diff --git a/src/web/views/user.py b/src/web/views/user.py index 754d3b9a..0f9fe612 100644 --- a/src/web/views/user.py +++ b/src/web/views/user.py @@ -7,6 +7,7 @@ from flask.ext.login import login_required import conf from web import utils, notifications +from web.lib.user_utils import confirm_token from web.controllers import (UserController, FeedController, ArticleController) from web.forms import ProfileForm, RecoverPasswordForm @@ -102,16 +103,17 @@ def delete_account(): return redirect(url_for('login')) -@user_bp.route('/confirm_account/', methods=['GET']) -def confirm_account(activation_key=None): +@user_bp.route('/confirm_account/', methods=['GET']) +def confirm_account(token=None): """ Confirm the account of a user. """ user_contr = UserController() - if activation_key != "": - user = user_contr.read(activation_key=activation_key).first() + if token != "": + email = confirm_token(token, expiration=3600) + user = user_contr.read(email=email).first() if user is not None: - user_contr.update({'id': user.id}, {'activation_key': ''}) + user_contr.update({'id': user.id}, {'enabled': True}) flash(gettext('Your account has been confirmed.'), 'success') else: flash(gettext('Impossible to confirm this account.'), 'danger') -- cgit