From 8042bb863ee748c18613e83f89ff73158d0a786a Mon Sep 17 00:00:00 2001 From: Russell Ballestrini Date: Thu, 9 Jun 2022 10:56:49 -0400 Subject: [PATCH] rough edges - verification challenge codes working modified: lib/mail.py modified: lib/mail_messages.py modified: models/user.py modified: routes.py modified: templates/join-or-log-in.j2 new file: templates/verification-challenge.j2 modified: views/authentication.py --- make_post_sell/lib/mail.py | 30 ++--- make_post_sell/lib/mail_messages.py | 19 +-- make_post_sell/models/user.py | 16 ++- make_post_sell/routes.py | 1 + make_post_sell/templates/join-or-log-in.j2 | 14 +-- .../templates/verification-challenge.j2 | 42 +++++++ make_post_sell/views/authentication.py | 112 +++++++++++------- 7 files changed, 143 insertions(+), 91 deletions(-) create mode 100644 make_post_sell/templates/verification-challenge.j2 diff --git a/make_post_sell/lib/mail.py b/make_post_sell/lib/mail.py index 891a9ff..7ac42d4 100644 --- a/make_post_sell/lib/mail.py +++ b/make_post_sell/lib/mail.py @@ -20,36 +20,22 @@ from make_post_sell.lib.mail_messages import ( ) -def send_otp_email(request, to_email, raw_otp, return_to): +def send_verification_digits_to_email(request, to_email, raw_digits): """ - Send email with OTP (one time password) link. + Send email with raw_digits a user may pass to verify & authenticate. request the request (of the successful log in attempt) to_email - the email address to send the OTP link + the email address to send the raw_digits - raw_otp - the raw (unencrypted) one time password - - return_to - the URI to return the user on successful authentication + raw_digits: + the raw (unencrypted) digits the user may use to verify & authenticate. """ - - query_params = [ - "email={}".format(quote_plus(to_email)), - "raw-otp={}".format(raw_otp), - ] - - if return_to: - query_params.append("return-to={}".format(return_to)) - - link = "{0}/join-or-log-in?{1}".format(request.app_url, "&".join(query_params)) - - subject = "Your log in link" - message_text = WELCOME_1_TEXT.format(link) - message_html = WELCOME_1_HTML.format(subject, link) + subject = "Verification Code" + message_text = WELCOME_1_TEXT.format(raw_digits) + message_html = WELCOME_1_HTML.format(subject, raw_digits) send_pyramid_email(request, to_email, subject, message_text, message_html) diff --git a/make_post_sell/lib/mail_messages.py b/make_post_sell/lib/mail_messages.py index 69fd649..0a07acf 100644 --- a/make_post_sell/lib/mail_messages.py +++ b/make_post_sell/lib/mail_messages.py @@ -1,9 +1,9 @@ WELCOME_1_TEXT = """ Hey there! -Here is the log in link that you requested. +Here is the verification code you requested. -You may paste this link into your browser to log in: +Type code into the challenge input field: \n{0}\n @@ -19,23 +19,14 @@ WELCOME_1_HTML = """

Hey there!

- Here is the log in link that you requested: + Here is the verification code you requested.

- Click here to log in! + Type code into the challenge input field:

-

- - You may paste this link into your browser to log in: - -
-
- - {1} - -

+

{1}

diff --git a/make_post_sell/models/user.py b/make_post_sell/models/user.py index 6728cc4..18a5816 100644 --- a/make_post_sell/models/user.py +++ b/make_post_sell/models/user.py @@ -47,6 +47,7 @@ class User(RBase, Base): name = Column(Unicode(64), unique=True, nullable=False) full_name = Column(Unicode(64)) email = Column(Unicode(64), unique=True, nullable=False) + #email_unverified = Column(Unicode(64)) password = Column(Unicode(64)) password_timestamp = Column(BigInteger) created_timestamp = Column(BigInteger, nullable=False) @@ -93,9 +94,12 @@ class User(RBase, Base): # don't password throttle new User objects. self.password_timestamp = 0 - def _generate_raw_password(self, size=32): + def _generate_raw_password(self): """Return a system generated password""" - return generate_password(size) + #return generate_password(size) + from random import choice + numbers = '0123456789' + return "".join([choice(numbers) for i in range(0,6)]) def new_password(self): """Generate and return raw password, store password hash into DB.""" @@ -113,13 +117,19 @@ class User(RBase, Base): """Accept plain-text raw password, create hash, compare with DB.""" stored_hash = self.password + # expire password after 15 minutes. + # 900000 milliseconds == 15 minutes + if self.password_timestamp_delta >= 900000: + return False + # bcrypt works with bytes so we encode to utf-8. new_hash = bcrypt.hashpw( password.encode("utf-8"), stored_hash.encode("utf-8"), ).decode("utf-8") - # log.info("new_hash={} stored_hash={}".format(new_hash, stored_hash)) + log.info("new_hash={} stored_hash={}".format(new_hash, stored_hash)) + print("blah") if new_hash == stored_hash: return True diff --git a/make_post_sell/routes.py b/make_post_sell/routes.py index 24add89..424cef2 100644 --- a/make_post_sell/routes.py +++ b/make_post_sell/routes.py @@ -9,6 +9,7 @@ def includeme(config): config.add_route("log-out", "/log-out") config.add_route("join-or-log-in", "/join-or-log-in") + config.add_route("verification-challenge", "/verification-challenge") # views with just a bunch of buttons to other pages. config.add_route("actions_view", "/actions/view") diff --git a/make_post_sell/templates/join-or-log-in.j2 b/make_post_sell/templates/join-or-log-in.j2 index 36bea3b..e025610 100644 --- a/make_post_sell/templates/join-or-log-in.j2 +++ b/make_post_sell/templates/join-or-log-in.j2 @@ -1,18 +1,13 @@ {% extends "base.j2" -%} - {% block content -%} -
- -
- {%- endblock -%} diff --git a/make_post_sell/templates/verification-challenge.j2 b/make_post_sell/templates/verification-challenge.j2 new file mode 100644 index 0000000..26ad92e --- /dev/null +++ b/make_post_sell/templates/verification-challenge.j2 @@ -0,0 +1,42 @@ +{% extends "base.j2" -%} +{% block content -%} +
+ +
+{%- endblock -%} diff --git a/make_post_sell/views/authentication.py b/make_post_sell/views/authentication.py index 8d3725c..08b59af 100644 --- a/make_post_sell/views/authentication.py +++ b/make_post_sell/views/authentication.py @@ -6,10 +6,14 @@ from pyramid.httpexceptions import HTTPFound from ..models.user import get_or_create_user_by_email -from ..lib.mail import send_otp_email +from ..lib.mail import send_verification_digits_to_email from . import get_referer_or_home +import logging + +log = logging.getLogger(__name__) + @view_config(route_name="log-out") def log_out(request): @@ -19,17 +23,13 @@ def log_out(request): return HTTPFound("/") -# disable CSRF checking for iframe embedded version of this view. -# If a client has 3rd party cookies disabled this security feature causes -# more trouble then it helps, essentially blocking unauthenticated users. -# disable CSRF checking for basic mode since we only set the -# CSRF token for logged in users. -@view_config(route_name="join-or-log-in", renderer="join-or-log-in.j2", require_csrf=False) +@view_config(route_name="join-or-log-in", renderer="join-or-log-in.j2") def join_or_log_in(request): """ This view handles user registration, verification, and log in. - It uses "password-less" authentication by sending OTP (one-time-password) - links to the user's email address. + It uses "password-less" authentication by sending 6 digit + OTP (one-time-password) tokens to email addresses to verify both the email + address & to authenticate the device displaying the challenge input field. """ _email_regex = re.compile("^[^@]+@[^@]+\.[^.@]+$") @@ -44,8 +44,8 @@ def join_or_log_in(request): if email and _email_regex.match(email) is None: # posted email does not pass regex, set it to None. - email = None - request.session.flash(("That email address is invalid." ,"error")) + email = "" + request.session.flash(("That email address is invalid.", "error")) if request.spam: return request.spam @@ -60,32 +60,6 @@ def join_or_log_in(request): # get or create a User object from the posted email. user = get_or_create_user_by_email(request.dbsession, email) - if raw_otp and user.check_password(raw_otp): - # success: the user was verified. - user.verified = True - name = "" - if user.full_name: - name = user.name - msg = ("Welcome {}".format(name), "success") - request.session["authenticated_user_id"] = str(user.id) - request.session.flash(msg) - - # merge any products in the session cart - # into user's main active cart. - user.active_cart.merge_in_cart(request.active_cart) - - # make session cart match user's active cart. - request.session["active_cart_id"] = str(user.active_cart.id) - - # delete old session only cart. - request.dbsession.delete(request.active_cart) - - # save user and user's active cart. - request.dbsession.add(user) - request.dbsession.add(user.active_cart) - request.dbsession.flush() - return HTTPFound(return_to) - if user.throttle_password(): msg = ( "We already sent a link to {}. Check email to log in.".format(user.email), @@ -98,10 +72,8 @@ def join_or_log_in(request): request.dbsession.add(user) request.dbsession.flush() - email_return_to = return_to - # email user the one-time-password and flash message. - send_otp_email(request, user.email, raw_otp, email_return_to) + send_verification_digits_to_email(request, user.email, raw_otp) msg = ( "We just sent a link to {}. Check email to log in.".format(user.email), @@ -110,9 +82,67 @@ def join_or_log_in(request): request.session.flash(msg) - return HTTPFound(return_to) + #return HTTPFound(return_to) + return HTTPFound("/verification-challenge?email={}&return-to={}".format(email, return_to)) return { "title": "join or log in", + } + + +@view_config(route_name="verification-challenge", renderer="verification-challenge.j2") +def verification_challenge(request): + + # get the return_to uri from posted parameters. + return_to = request.params.get("return-to", "") + + # get the raw OTP (one-time-password) from posted parameters. + raw_otp = request.params.get("raw-otp", "") + + # get the email from posted parameters. + email = request.params.get("email", "") + + user = None + if email: + # get or create a User object from the posted email. + user = get_or_create_user_by_email(request.dbsession, email) + + print("blah33") + print(raw_otp) + + if raw_otp and user.check_password(raw_otp): + # success: the user was verified. + user.verified = True + name = "" + if user.full_name: + name = user.name + msg = ("Welcome {}".format(name), "success") + request.session["authenticated_user_id"] = str(user.id) + request.session.flash(msg) + + # merge any products in the session cart + # into user's main active cart. + user.active_cart.merge_in_cart(request.active_cart) + + # make session cart match user's active cart. + request.session["active_cart_id"] = str(user.active_cart.id) + + # delete old session only cart. + request.dbsession.delete(request.active_cart) + + # save user and user's active cart. + request.dbsession.add(user) + request.dbsession.add(user.active_cart) + request.dbsession.flush() + return HTTPFound(return_to) + + else: + msg = ("Invalid Verification Code", "error") + request.session.flash(msg) + + return { + "title": "Please Enter Verification Code", + "email": email, + "raw_otp": raw_otp, "return_to": return_to, }