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:
parent
e189c32658
commit
8042bb863e
7 changed files with 143 additions and 91 deletions
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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 -%}
|
||||
|
|
|
|||
42
make_post_sell/templates/verification-challenge.j2
Normal file
42
make_post_sell/templates/verification-challenge.j2
Normal 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 -%}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue