diff --git a/r2/r2/controllers/api.py b/r2/r2/controllers/api.py index af62e9df0..751c31016 100755 --- a/r2/r2/controllers/api.py +++ b/r2/r2/controllers/api.py @@ -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()) diff --git a/r2/r2/controllers/front.py b/r2/r2/controllers/front.py index b081212c5..b922d487e 100755 --- a/r2/r2/controllers/front.py +++ b/r2/r2/controllers/front.py @@ -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() diff --git a/r2/r2/controllers/validator/validator.py b/r2/r2/controllers/validator/validator.py index 8fc299cc8..5421b799c 100644 --- a/r2/r2/controllers/validator/validator.py +++ b/r2/r2/controllers/validator/validator.py @@ -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): diff --git a/r2/r2/lib/emailer.py b/r2/r2/lib/emailer.py index 68bcd7e1c..663427e25 100644 --- a/r2/r2/lib/emailer.py +++ b/r2/r2/lib/emailer.py @@ -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'), diff --git a/r2/r2/models/token.py b/r2/r2/models/token.py index b4ad50183..3eb6bc1de 100644 --- a/r2/r2/models/token.py +++ b/r2/r2/models/token.py @@ -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)