about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--wqflask/wqflask/model.py4
-rw-r--r--wqflask/wqflask/send_mail.py45
-rw-r--r--wqflask/wqflask/templates/email/verification.txt8
-rw-r--r--wqflask/wqflask/user_manager.py123
-rw-r--r--wqflask/wqflask/views.py5
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():