mirror of
https://github.com/reddit-archive/reddit.git
synced 2026-01-24 14:27:58 -05:00
Move verification email and password reset tokens into Cassandra.
In memcached there is a chance of the keys being evicted before their time runs out. We can mitigate this by adding more memcaches, but that has other downsides (such as increased risk of failure).
This commit is contained in:
@@ -1938,25 +1938,37 @@ class ApiController(RedditController):
|
||||
form.set_html(".status", _("try again tomorrow"))
|
||||
|
||||
|
||||
@validatedForm(cache_evt = VHardCacheKey('email-reset', ('key',)),
|
||||
password = VPassword(['passwd', 'passwd2']))
|
||||
def POST_resetpassword(self, form, jquery, cache_evt, password):
|
||||
if form.has_errors('name', errors.EXPIRED):
|
||||
cache_evt.clear()
|
||||
@validatedForm(token=VOneTimeToken(PasswordResetToken, "key"),
|
||||
password=VPassword(["passwd", "passwd2"]))
|
||||
def POST_resetpassword(self, form, jquery, token, password):
|
||||
# was the token invalid or has it expired?
|
||||
if not token:
|
||||
form.redirect("/password?expired=true")
|
||||
return
|
||||
|
||||
# did they fill out the password form correctly?
|
||||
form.has_errors("passwd", errors.BAD_PASSWORD)
|
||||
form.has_errors("passwd2", errors.BAD_PASSWORD_MATCH)
|
||||
if form.has_error():
|
||||
return
|
||||
|
||||
# at this point, we should mark the token used since it's either
|
||||
# valid now or will never be valid again.
|
||||
token.consume()
|
||||
|
||||
# load up the user and check that things haven't changed
|
||||
user = Account._by_fullname(token.user_id)
|
||||
if not token.valid_for_user(user):
|
||||
form.redirect('/password?expired=true')
|
||||
elif form.has_errors('passwd', errors.BAD_PASSWORD):
|
||||
pass
|
||||
elif form.has_errors('passwd2', errors.BAD_PASSWORD_MATCH):
|
||||
pass
|
||||
elif cache_evt.user:
|
||||
# successfully entered user name and valid new password
|
||||
change_password(cache_evt.user, password)
|
||||
g.hardcache.delete("%s_%s" % (cache_evt.cache_prefix, cache_evt.key))
|
||||
print "%s did a password reset for %s via %s" % (
|
||||
request.ip, cache_evt.user.name, cache_evt.key)
|
||||
self._login(jquery, cache_evt.user)
|
||||
jquery.redirect('/')
|
||||
cache_evt.clear()
|
||||
return
|
||||
|
||||
# successfully entered user name and valid new password
|
||||
change_password(user, password)
|
||||
g.log.warning("%s did a password reset for %s via %s",
|
||||
request.ip, user.name, token._id)
|
||||
|
||||
self._login(jquery, user)
|
||||
jquery.redirect('/')
|
||||
|
||||
|
||||
@noresponse(VUser())
|
||||
|
||||
@@ -49,6 +49,7 @@ from errors import errors
|
||||
from listingcontroller import ListingController
|
||||
from api_docs import api_doc, api_section
|
||||
from pylons import c, request, request, Response
|
||||
from r2.models.token import EmailVerificationToken
|
||||
|
||||
import string
|
||||
import random as rand
|
||||
@@ -990,34 +991,36 @@ class FormsController(RedditController):
|
||||
return BoringPage(_("verify email"), content = content).render()
|
||||
|
||||
@validate(VUser(),
|
||||
cache_evt = VCacheKey('email_verify', ('key',)),
|
||||
key = nop('key'),
|
||||
dest = VDestination(default = "/prefs/update"))
|
||||
def GET_verify_email(self, cache_evt, key, dest):
|
||||
if c.user_is_loggedin and c.user.email_verified:
|
||||
cache_evt.clear()
|
||||
return self.redirect(dest)
|
||||
elif not (cache_evt.user and
|
||||
key == passhash(cache_evt.user.name, cache_evt.user.email)):
|
||||
content = PaneStack(
|
||||
[InfoBar(message = strings.email_verify_failed),
|
||||
PrefUpdate(email = True, verify = True,
|
||||
password = False)])
|
||||
return BoringPage(_("verify email"), content = content).render()
|
||||
elif c.user != cache_evt.user:
|
||||
# wrong user. Log them out and try again.
|
||||
token=VOneTimeToken(EmailVerificationToken, "key"),
|
||||
dest=VDestination(default="/prefs/update"))
|
||||
def GET_verify_email(self, token, dest):
|
||||
if token and token.user_id != c.user._fullname:
|
||||
# wrong user. log them out and try again.
|
||||
self.logout()
|
||||
return self.redirect(request.fullpath)
|
||||
else:
|
||||
cache_evt.clear()
|
||||
elif token and c.user.email_verified:
|
||||
# they've already verified. consume and ignore this token.
|
||||
token.consume()
|
||||
return self.redirect(dest)
|
||||
elif token and token.valid_for_user(c.user):
|
||||
# successful verification!
|
||||
token.consume()
|
||||
c.user.email_verified = True
|
||||
c.user._commit()
|
||||
Award.give_if_needed("verified_email", c.user)
|
||||
return self.redirect(dest)
|
||||
else:
|
||||
# failure. let 'em know.
|
||||
content = PaneStack(
|
||||
[InfoBar(message=strings.email_verify_failed),
|
||||
PrefUpdate(email=True,
|
||||
verify=True,
|
||||
password=False)])
|
||||
return BoringPage(_("verify email"), content=content).render()
|
||||
|
||||
@validate(cache_evt = VHardCacheKey('email-reset', ('key',)),
|
||||
key = nop('key'))
|
||||
def GET_resetpassword(self, cache_evt, key):
|
||||
@validate(token=VOneTimeToken(PasswordResetToken, "key"),
|
||||
key=nop("key"))
|
||||
def GET_resetpassword(self, token, key):
|
||||
"""page hit once a user has been sent a password reset email
|
||||
to verify their identity before allowing them to update their
|
||||
password."""
|
||||
@@ -1031,7 +1034,7 @@ class FormsController(RedditController):
|
||||
if not key and request.referer:
|
||||
referer_path = request.referer.split(g.domain)[-1]
|
||||
done = referer_path.startswith(request.fullpath)
|
||||
elif not getattr(cache_evt, "user", None):
|
||||
elif not token:
|
||||
return self.redirect("/password?expired=true")
|
||||
return BoringPage(_("reset password"),
|
||||
content=ResetPassword(key=key, done=done)).render()
|
||||
|
||||
@@ -1369,29 +1369,19 @@ class CachedUser(object):
|
||||
if self.key and self.cache_prefix:
|
||||
g.cache.delete(str(self.cache_prefix + "_" + self.key))
|
||||
|
||||
|
||||
class VCacheKey(Validator):
|
||||
def __init__(self, cache_prefix, param, *a, **kw):
|
||||
self.cache = g.cache
|
||||
self.cache_prefix = cache_prefix
|
||||
Validator.__init__(self, param, *a, **kw)
|
||||
class VOneTimeToken(Validator):
|
||||
def __init__(self, model, param, *args, **kwargs):
|
||||
self.model = model
|
||||
Validator.__init__(self, param, *args, **kwargs)
|
||||
|
||||
def run(self, key):
|
||||
c_user = CachedUser(self.cache_prefix, None, key)
|
||||
if key:
|
||||
uid = self.cache.get(str(self.cache_prefix + "_" + key))
|
||||
if uid:
|
||||
try:
|
||||
c_user.user = Account._byID(uid, data = True)
|
||||
except NotFound:
|
||||
return
|
||||
return c_user
|
||||
self.set_error(errors.EXPIRED)
|
||||
token = self.model.get_token(key)
|
||||
|
||||
class VHardCacheKey(VCacheKey):
|
||||
def __init__(self, cache_prefix, param, *a, **kw):
|
||||
VCacheKey.__init__(self, cache_prefix, param, *a, **kw)
|
||||
self.cache = g.hardcache
|
||||
if token:
|
||||
return token
|
||||
else:
|
||||
self.set_error(errors.EXPIRED)
|
||||
return None
|
||||
|
||||
class VOneOf(Validator):
|
||||
def __init__(self, param, options = (), *a, **kw):
|
||||
|
||||
@@ -27,6 +27,8 @@ from r2.lib.utils import timeago, query_string, randstr
|
||||
from r2.models import passhash, Email, DefaultSR, has_opted_out, Account, Award
|
||||
import os, random, datetime
|
||||
import traceback, sys, smtplib
|
||||
from r2.models.token import EmailVerificationToken, PasswordResetToken
|
||||
|
||||
|
||||
def _feedback_email(email, body, kind, name='', reply_to = ''):
|
||||
"""Function for handling feedback and ad_inq emails. Adds an
|
||||
@@ -64,14 +66,14 @@ def verify_email(user, dest):
|
||||
For verifying an email address
|
||||
"""
|
||||
from r2.lib.pages import VerifyEmail
|
||||
key = passhash(user.name, user.email)
|
||||
user.email_verified = False
|
||||
user._commit()
|
||||
Award.take_away("verified_email", user)
|
||||
emaillink = ('http://' + g.domain + '/verification/' + key
|
||||
|
||||
token = EmailVerificationToken._new(user)
|
||||
emaillink = ('http://' + g.domain + '/verification/' + token._id
|
||||
+ query_string(dict(dest=dest)))
|
||||
g.log.debug("Generated email verification link: " + emaillink)
|
||||
g.cache.set("email_verify_%s" %key, user._id, time=1800)
|
||||
|
||||
_system_email(user.email,
|
||||
VerifyEmail(user=user,
|
||||
@@ -94,10 +96,9 @@ def password_email(user):
|
||||
if g.cache.incr(reset_count_global) > 1000:
|
||||
raise ValueError("Somebody's beating the hell out of the password reset box")
|
||||
|
||||
key = passhash(randstr(64, reallyrandom = True), user.email)
|
||||
passlink = 'http://' + g.domain + '/resetpassword/' + key
|
||||
token = PasswordResetToken._new(user)
|
||||
passlink = 'http://' + g.domain + '/resetpassword/' + token._id
|
||||
g.log.info("Generated password reset link: " + passlink)
|
||||
g.hardcache.set("email-reset_%s" % key, user._id, time=3600 * 12)
|
||||
_system_email(user.email,
|
||||
PasswordReset(user=user,
|
||||
passlink=passlink).render(style='email'),
|
||||
|
||||
@@ -161,3 +161,35 @@ class OAuth2AccessToken(Token):
|
||||
return super(OAuth2AccessToken, cls)._new(
|
||||
user_id=user_id,
|
||||
scope=scope)
|
||||
|
||||
|
||||
class EmailVerificationToken(ConsumableToken):
|
||||
_use_db = True
|
||||
_connection_pool = "main"
|
||||
_ttl = 60 * 60 * 12
|
||||
token_size = 20
|
||||
|
||||
@classmethod
|
||||
def _new(cls, user):
|
||||
return super(EmailVerificationToken, cls)._new(user_id=user._fullname,
|
||||
email=user.email)
|
||||
|
||||
def valid_for_user(self, user):
|
||||
return self.email == user.email
|
||||
|
||||
|
||||
class PasswordResetToken(ConsumableToken):
|
||||
_use_db = True
|
||||
_connection_pool = "main"
|
||||
_ttl = 60 * 60 * 12
|
||||
token_size = 20
|
||||
|
||||
@classmethod
|
||||
def _new(cls, user):
|
||||
return super(PasswordResetToken, cls)._new(user_id=user._fullname,
|
||||
email_address=user.email,
|
||||
password=user.password)
|
||||
|
||||
def valid_for_user(self, user):
|
||||
return (self.email_address == user.email and
|
||||
self.password == user.password)
|
||||
|
||||
Reference in New Issue
Block a user