diff options
Diffstat (limited to 'wqflask')
-rw-r--r-- | wqflask/wqflask/model.py | 4 | ||||
-rw-r--r-- | wqflask/wqflask/send_mail.py | 45 | ||||
-rw-r--r-- | wqflask/wqflask/templates/email/verification.txt | 8 | ||||
-rw-r--r-- | wqflask/wqflask/user_manager.py | 123 | ||||
-rw-r--r-- | wqflask/wqflask/views.py | 5 |
5 files changed, 173 insertions, 12 deletions
diff --git a/wqflask/wqflask/model.py b/wqflask/wqflask/model.py index 1f7297a2..a3cd63a5 100644 --- a/wqflask/wqflask/model.py +++ b/wqflask/wqflask/model.py @@ -10,7 +10,7 @@ from flask.ext.sqlalchemy import SQLAlchemy from wqflask import app -from sqlalchemy import Column, Integer, String, Table, ForeignKey, Unicode, Boolean, DateTime +from sqlalchemy import Column, Integer, String, Table, ForeignKey, Unicode, Boolean, DateTime, Text from sqlalchemy.orm import relationship, backref from wqflask.database import Base @@ -60,7 +60,7 @@ class User(Base): email_address = Column(Unicode(50), unique=True, nullable=False) # Todo: Turn on strict mode for Mysql - password = Column(Unicode(500), nullable=False) + password = Column(Text, nullable=False) full_name = Column(Unicode(50)) organization = Column(Unicode(50)) diff --git a/wqflask/wqflask/send_mail.py b/wqflask/wqflask/send_mail.py new file mode 100644 index 00000000..be51ad0d --- /dev/null +++ b/wqflask/wqflask/send_mail.py @@ -0,0 +1,45 @@ +from __future__ import absolute_import, division, print_function + +import datetime + +import simplejson as json + +from redis import StrictRedis +Redis = StrictRedis() + +import mailer + +def timestamp(): + ts = datetime.datetime.utcnow() + return ts.isoformat() + +def main(): + while True: + print("Waiting for message to show up in queue...") + msg = Redis.blpop("mail_queue") + + # Queue name is the first element, we want the second, which is the actual message + msg = msg[1] + + print("\nGot a msg in queue at {}: {}".format(timestamp(), msg)) + # Todo: Truncate mail_processed when it gets to long + Redis.rpush("mail_processed", msg) + process_message(msg) + + +def process_message(msg): + msg = json.loads(msg) + + message = mailer.Message() + message.From = msg['From'] + message.To = msg['To'] + message.Subject = msg['Subject'] + message.Body = msg['Body'] + + sender = mailer.Mailer('localhost') + sender.send(message) + print("Sent message at {}: {}\n".format(timestamp(), msg)) + + +if __name__ == '__main__': + main()
\ No newline at end of file diff --git a/wqflask/wqflask/templates/email/verification.txt b/wqflask/wqflask/templates/email/verification.txt new file mode 100644 index 00000000..29229c68 --- /dev/null +++ b/wqflask/wqflask/templates/email/verification.txt @@ -0,0 +1,8 @@ +Thank you for signing up for GeneNetwork. + +We need to verify your email address. + +To do that please click the following link, or cut and paste it into your browser window: + +{{ url_for_hmac("verify", code = verification_code, _external=True )}} + diff --git a/wqflask/wqflask/user_manager.py b/wqflask/wqflask/user_manager.py index bafc1c3e..159a0ffc 100644 --- a/wqflask/wqflask/user_manager.py +++ b/wqflask/wqflask/user_manager.py @@ -12,8 +12,23 @@ import hashlib import datetime import time +import uuid +import hashlib +import hmac + import simplejson as json +from redis import StrictRedis +Redis = StrictRedis() + + +from flask import Flask, g, render_template, url_for + +from wqflask import app + + +from pprint import pformat as pf + from wqflask import pbkdf2 from wqflask.database import db_session @@ -22,9 +37,7 @@ from wqflask import model from utility import Bunch -from flask import Flask, g -from pprint import pformat as pf from base.data_set import create_datasets_list @@ -89,21 +102,25 @@ class RegisterUser(object): self.set_password(password) - new_user = model.User(**self.user.__dict__) - db_session.add(new_user) + self.new_user = model.User(**self.user.__dict__) + db_session.add(self.new_user) db_session.commit() + self.send_email_verification() + def set_password(self, password): pwfields = Bunch() - algo_string = "sha256" - algorithm = getattr(hashlib, algo_string) - pwfields.algorithm = "pbkdf2-" + algo_string + + pwfields.algorithm = "pbkdf2" + pwfields.hashfunc = "sha256" + hashfunc = getattr(hashlib, pwfields.hashfunc) + pwfields.salt = os.urandom(32) # https://forums.lastpass.com/viewtopic.php?t=84104 pwfields.iterations = 100000 - pwfields.keylength = 24 + pwfields.keylength = 32 pwfields.created_ts = datetime.datetime.utcnow().isoformat() # One more check on password length @@ -111,16 +128,102 @@ class RegisterUser(object): print("pwfields:", vars(pwfields)) print("locals:", locals()) + + # On our computer it takes around 1.4 seconds start_time = time.time() - pwfields.password = pbkdf2.pbkdf2_hex(password, pwfields.salt, pwfields.iterations, pwfields.keylength, algorithm) - print("Creating password took:", time.time() - start_time) + pwfields.password = pbkdf2.pbkdf2_hex(password, pwfields.salt, pwfields.iterations, pwfields.keylength, hashfunc) + pwfields.encrypt_time = round(time.time() - start_time, 3) + + print("Creating password took:", pwfields.encrypt_time) + self.user.password = json.dumps(pwfields.__dict__, sort_keys=True, # See http://stackoverflow.com/a/12312896 encoding="latin-1" ) + def send_email_verification(self): + verification_code = str(uuid.uuid4()) + key = "verification_code:" + verification_code + + data = json.dumps(dict(the_id=self.new_user.the_id, + timestamp=datetime.datetime.utcnow().isoformat()) + ) + + Redis.set(key, data) + two_days = 60 * 60 * 24 * 2 + Redis.expire(key, two_days) + to = self.user.email_address + subject = "GeneNetwork email verification" + body = render_template("email/verification.txt", + verification_code = verification_code) + send_email(to, subject, body) + + +def verify_email(request): + print("in verify_email request.url is:", request.url) + verify_url_hmac(request.url) + verification_code = request.args['code'] + data = Redis.get("verification_code:" + verification_code) + data = json.loads(data) + print("data is:", data) + + + +################################# Sign and unsign ##################################### +def url_for_hmac(endpoint, **values): + """Like url_for but adds an hmac at the end to insure the url hasn't been tampered with""" + + url = url_for(endpoint, **values) + + hm = actual_hmac_creation(url) + if '?' in url: + combiner = "&" + else: + combiner = "?" + return url + combiner + "hm=" + hm + +def verify_url_hmac(url): + """Pass in a url that was created with url_hmac and this assures it hasn't been tampered with""" + print("url passed in to verify is:", url) + # Verify parts are correct at the end - we expect to see &hm= or ?hm= followed by an hmac + assert url[-23:-20] == "hm=", "Unexpected url (stage 1)" + assert url[-24] in ["?", "&"], "Unexpected url (stage 2)" + hmac = url[-20:] + url = url[:-24] # Url without any of the hmac stuff + + #print("before urlsplit, url is:", url) + #url = divide_up_url(url)[1] + #print("after urlsplit, url is:", url) + + hm = actual_hmac_creation(url) + + assert hm == hmac, "Unexpected url (stage 3)" + +def actual_hmac_creation(url): + """Helper function to create the actual hmac""" + + secret = app.config['SECRET_HMAC_CODE'] + + hmaced = hmac.new(secret, url, hashlib.sha1) + hm = hmaced.hexdigest() + # "Conventional wisdom is that you don't lose much in terms of security if you throw away up to half of the output." + # http://www.w3.org/QA/2009/07/hmac_truncation_in_xml_signatu.html + hm = hm[:20] + return hm + +app.jinja_env.globals.update(url_for_hmac=url_for_hmac) + +####################################################################################### + +def send_email(to, subject, body): + msg = json.dumps(dict(From="no-reply@genenetwork.org", + To=to, + Subject=subject, + Body=body)) + Redis.rpush("mail_queue", msg) + #def combined_salt(user_salt): # """Combine the master salt with the user salt...we use two seperate salts so that if the database is compromised, the diff --git a/wqflask/wqflask/views.py b/wqflask/wqflask/views.py index 82d28eab..b552e160 100644 --- a/wqflask/wqflask/views.py +++ b/wqflask/wqflask/views.py @@ -292,6 +292,11 @@ def manage_groups(): template_vars = user_manager.GroupsManager(request.args) return render_template("admin/group_manager.html", **template_vars.__dict__) +@app.route("/manage/verify") +def verify(): + user_manager.verify_email(request) + return "foo" + @app.route("/n/register", methods=('GET', 'POST')) def new_register(): |