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
This commit is contained in:
Russell Ballestrini 2022-06-09 10:56:49 -04:00
parent e189c32658
commit 8042bb863e
7 changed files with 143 additions and 91 deletions

View file

@ -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)

View file

@ -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 = """
<h2>Hey there!</h2>
<p>
Here is the log in link that you requested:
Here is the verification code you requested.
</p>
<p>
<a href="{1}" style="font-weight: bold;" target="_blank">Click here to log in!</a>
Type code into the challenge input field:
</p>
<p style="font-size: .8em;">
<span style="color: #aaaaaa;">
You may paste this link into your browser to log in:
</span>
<br>
<br>
<a href="{1}" style="color: #439fe0; font-weight: normal; text-decoration: none; word-break: break-word;" target="_blank">
{1}
</a>
</p>
<p><b>{1}</b></p>
</body>
</html>

View file

@ -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

View file

@ -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")

View file

@ -1,18 +1,13 @@
{% extends "base.j2" -%}
{% block content -%}
<section class="one-column">
<section class="log-in-form well">
<h2>Log in</h2>
<b>New and Existing Users</b>
<br>
<br>
<form method="post" action="/join-or-log-in" onsubmit="submit.disabled = true; return true;">
<input
name = "email2"
type = "email"
@ -20,6 +15,7 @@
class = "common-text-input"
value = ""
placeholder = "Your Email Address" />
<input
name = "email"
type = "email"
@ -30,7 +26,7 @@
placeholder = "Your Email Address"
required
autofocus />
<br/>
<br/>
@ -40,12 +36,8 @@
<input type="hidden" name="return-to" value="{{ return_to }}" >
{% endif %}
</form>
<br/>
<p class="whats-next"><b>What's next?</b> check your email to log in!</p>
</section>
</section>
{%- endblock -%}

View file

@ -0,0 +1,42 @@
{% extends "base.j2" -%}
{% block content -%}
<section class="one-column">
<section class="log-in-form well">
<h2>Verification Code</h2>
<b>Please enter code to verify & log in.<b>
<br>
<br>
<form method="post" action="/verification-challenge" onsubmit="submit.disabled = true; return true;">
<input
name = "email"
type = "hidden"
id = "email_input"
class = "common-text-input"
value = "{{ email }}"
placeholder = "Your Email Address" />
<input
name = "raw-otp"
type = "number"
id = "raw-otp"
class = "common-text-input"
tabindex = "1"
value = ""
placeholder = "6 digit challenge verification code"
required
autofocus />
<br/>
<br/>
<input type="submit" name="submit" value="verify code" />
{% if return_to %}
<input type="hidden" name="return-to" value="{{ return_to }}" >
{% endif %}
</form>
<br/>
</section>
</section>
{%- endblock -%}

View file

@ -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,
}