Skip to content

Commit

Permalink
Add new (signed) APIv1 login handler with tests.
Browse files Browse the repository at this point in the history
KeyserSosa committed Jun 8, 2016
1 parent 77acb16 commit 7cb0085
Showing 9 changed files with 290 additions and 9 deletions.
2 changes: 2 additions & 0 deletions r2/r2/config/routing.py
Original file line number Diff line number Diff line change
@@ -444,6 +444,8 @@ def make_map(config):
requirements=dict(action="scopes"))
mc("/api/v1/user/:username/trophies",
controller="apiv1user", action="usertrophies")
mc("/api/v1/:action", controller="apiv1login",
requirements=dict(action="register|login"))
mc("/api/v1/:action", controller="apiv1user")
# Same controller/action as /prefs/friends
mc("/api/v1/me/:where", controller="userlistlisting",
3 changes: 2 additions & 1 deletion r2/r2/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -80,14 +80,15 @@ def load_controllers():
from oembed import OEmbedController
from policies import PoliciesController
from web import WebLogController

from wiki import WikiController
from wiki import WikiApiController

from api import ApiController
from api import ApiminimalController
from api_docs import ApidocsController
from apiv1.user import APIv1UserController
from apiv1.login import APIv1LoginController
from apiv1.gold import APIv1GoldController
from apiv1.scopes import APIv1ScopesController
from multi import MultiApiController
88 changes: 88 additions & 0 deletions r2/r2/controllers/apiv1/login.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# The contents of this file are subject to the Common Public Attribution
# License Version 1.0. (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
# License Version 1.1, but Sections 14 and 15 have been added to cover use of
# software over a computer network and provide for limited attribution for the
# Original Developer. In addition, Exhibit A has been modified to be consistent
# with Exhibit B.
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
# the specific language governing rights and limitations under the License.
#
# The Original Code is reddit.
#
# The Original Developer is the Initial Developer. The Initial Developer of
# the Original Code is reddit Inc.
#
# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit
# Inc. All Rights Reserved.
###############################################################################
from pylons import tmpl_context as c

from r2.controllers.reddit_base import RedditController, generate_modhash
from r2.controllers.login import handle_login, handle_register
from r2.lib.csrf import csrf_exempt
from r2.lib.validator import (
json_validate,
ValidEmail,
VPasswordChange,
VRatelimit,
VSigned,
VThrottledLogin,
VUname,
)


class APIv1LoginController(RedditController):

def pre(self):
super(APIv1LoginController, self).pre()
c.extension = "json"

@csrf_exempt
@json_validate(
VRatelimit(rate_ip=True, prefix="rate_register_"),
signature=VSigned(),
name=VUname(['user']),
email=ValidEmail("email"),
password=VPasswordChange(['passwd', 'passwd2']),
)
def POST_register(self, responder, name, email, password, **kwargs):
kwargs.update(dict(
controller=self,
form=responder("noop"),
responder=responder,
name=name,
email=email,
password=password,
))
return handle_register(**kwargs)

@csrf_exempt
@json_validate(
signature=VSigned(),
user=VThrottledLogin(['user', 'passwd']),
)
def POST_login(self, responder, user, **kwargs):
kwargs.update(dict(
controller=self,
form=responder("noop"),
responder=responder,
user=user,
))
return handle_login(**kwargs)

def _login(self, responder, user, rem=None):
"""Login the user.
AJAX login handler, used by both login and register to set the
user cookie and send back a redirect.
"""
c.user = user
c.user_is_loggedin = True
self.login(user, rem=rem)

responder._send_data(modhash=generate_modhash())
responder._send_data(cookie=user.make_cookie())
7 changes: 4 additions & 3 deletions r2/r2/controllers/apiv1/user.py
Original file line number Diff line number Diff line change
@@ -19,8 +19,9 @@
# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit
# Inc. All Rights Reserved.
###############################################################################
from pylons import request, response
from pylons import response
from pylons import tmpl_context as c

from r2.controllers.api_docs import api_doc, api_section
from r2.controllers.oauth2 import require_oauth2_scope
from r2.controllers.reddit_base import OAuth2OnlyController
@@ -38,8 +39,8 @@
VFriendOfMine,
VLength,
VList,
VValidatedJSON,
VUser,
VValidatedJSON,
)
from r2.models import Account, Trophy
import r2.lib.errors as errors
@@ -59,7 +60,7 @@ class APIv1UserController(OAuth2OnlyController):
)
@api_doc(api_section.account)
def GET_me(self):
"""Returns the identity of the user currently authenticated via OAuth."""
"Returns the identity of the user currently authenticated via OAuth."
resp = IdentityJsonTemplate().data(c.oauth_user)
return self.api_wrapper(resp)

7 changes: 5 additions & 2 deletions r2/r2/controllers/login.py
Original file line number Diff line number Diff line change
@@ -35,14 +35,15 @@


def handle_login(
controller, form, responder, user, rem=None, **kwargs
controller, form, responder, user, rem=None, signature=None, **kwargs
):
def _event(error):
g.events.login_event(
'login_attempt',
error_msg=error,
user_name=request.urlvars.get('url_user'),
remember_me=rem,
signature=signature,
request=request,
context=c)

@@ -78,10 +79,11 @@ def _event(error):
controller._login(responder, user, rem)
_event(error=None)


def handle_register(
controller, form, responder, name, email,
password, rem=None, newsletter_subscribe=False,
sponsor=False, **kwargs
sponsor=False, signature=None, **kwargs
):

def _event(error):
@@ -92,6 +94,7 @@ def _event(error):
email=request.POST.get('email'),
remember_me=rem,
newsletter=newsletter_subscribe,
signature=signature,
request=request,
context=c)

6 changes: 5 additions & 1 deletion r2/r2/lib/eventcollector.py
Original file line number Diff line number Diff line change
@@ -692,7 +692,7 @@ def message_event(self, message, event_type="ss.send_message",
def login_event(self, action_name, error_msg,
user_name=None, email=None,
remember_me=None, newsletter=None, email_verified=None,
request=None, context=None):
signature=None, request=None, context=None):
"""Create a 'login' event for event-collector.
action_name: login_attempt, register_attempt, password_reset
@@ -724,6 +724,10 @@ def login_event(self, action_name, error_msg,
event.add('remember_me', remember_me)
event.add('newsletter', newsletter)
event.add('email_verified', email_verified)
if signature:
event.add("signed", True)
event.add("signature_platform", signature.platform)
event.add("signature_version", signature.version)

self.save_event(event)

20 changes: 19 additions & 1 deletion r2/r2/lib/validator/validator.py
Original file line number Diff line number Diff line change
@@ -53,6 +53,7 @@
from r2.lib.authorize import Address, CreditCard
from r2.lib.utils import constant_time_compare
from r2.lib.require import require, require_split, RequirementException
from r2.lib import signing

from r2.lib.errors import errors, RedditError, UserRequiredException
from r2.lib.errors import VerifiedUserRequiredException
@@ -833,7 +834,7 @@ def run(self, short_name):
self.set_error(errors.SR_RULE_DOESNT_EXIST)
else:
return rule


class VAccountByName(VRequired):
def __init__(self, param, error = errors.USER_DOESNT_EXIST, *a, **kw):
@@ -3238,3 +3239,20 @@ def param_docs(self):
'(`%s`)' % '`, `'.join(self.options)
),
}


class VSigned(Validator):
def run(self):
ua_signature = signing.valid_ua_signature(request)
if not ua_signature.valid:
g.stats.simple_event(
"signing.ua.invalid.%s" % ua_signature.error.code.lower())
abort(403, 'forbidden')

signature = signing.valid_post_signature(request)
if not signature.valid:
g.stats.simple_event(
"signing.body.invalid.%s" % signature.error.code.lower())
abort(403, 'forbidden')

return signature
Loading

0 comments on commit 7cb0085

Please sign in to comment.