New registration for C/S API v2. Only ReCAPTCHA working currently.

This commit is contained in:
David Baker 2015-03-30 18:13:10 +01:00
parent 6f4f7e4e22
commit 59bf16eddc
8 changed files with 192 additions and 16 deletions

View File

@ -62,6 +62,8 @@ class LoginType(object):
APPLICATION_SERVICE = u"m.login.application_service"
SHARED_SECRET = u"org.matrix.login.shared_secret"
HIDDEN_TYPES = [APPLICATION_SERVICE, SHARED_SECRET]
class EventTypes(object):
Member = "m.room.member"

View File

@ -20,6 +20,7 @@ class CaptchaConfig(Config):
def __init__(self, args):
super(CaptchaConfig, self).__init__(args)
self.recaptcha_private_key = args.recaptcha_private_key
self.recaptcha_public_key = args.recaptcha_public_key
self.enable_registration_captcha = args.enable_registration_captcha
self.captcha_ip_origin_is_x_forwarded = (
args.captcha_ip_origin_is_x_forwarded
@ -30,9 +31,13 @@ class CaptchaConfig(Config):
def add_arguments(cls, parser):
super(CaptchaConfig, cls).add_arguments(parser)
group = parser.add_argument_group("recaptcha")
group.add_argument(
"--recaptcha-public-key", type=str, default="YOUR_PUBLIC_KEY",
help="This Home Server's ReCAPTCHA public key."
)
group.add_argument(
"--recaptcha-private-key", type=str, default="YOUR_PRIVATE_KEY",
help="The matching private key for the web client's public key."
help="This Home Server's ReCAPTCHA private key."
)
group.add_argument(
"--enable-registration-captcha", type=bool, default=False,

View File

@ -19,9 +19,12 @@ from ._base import BaseHandler
from synapse.api.constants import LoginType
from synapse.types import UserID
from synapse.api.errors import LoginError, Codes
from synapse.http.client import SimpleHttpClient
from twisted.web.client import PartialDownloadError
import logging
import bcrypt
import simplejson
logger = logging.getLogger(__name__)
@ -33,7 +36,7 @@ class AuthHandler(BaseHandler):
super(AuthHandler, self).__init__(hs)
@defer.inlineCallbacks
def check_auth(self, flows, clientdict):
def check_auth(self, flows, clientdict, clientip=None):
"""
Takes a dictionary sent by the client in the login / registration
protocol and handles the login flow.
@ -50,11 +53,12 @@ class AuthHandler(BaseHandler):
login request and should be passed back to the client.
"""
types = {
LoginType.PASSWORD: self.check_password_auth
LoginType.PASSWORD: self.check_password_auth,
LoginType.RECAPTCHA: self.check_recaptcha,
}
if 'auth' not in clientdict:
defer.returnValue((False, auth_dict_for_flows(flows)))
if not clientdict or 'auth' not in clientdict:
defer.returnValue((False, self.auth_dict_for_flows(flows)))
authdict = clientdict['auth']
@ -67,7 +71,7 @@ class AuthHandler(BaseHandler):
raise LoginError(400, "", Codes.MISSING_PARAM)
if authdict['type'] not in types:
raise LoginError(400, "", Codes.UNRECOGNIZED)
result = yield types[authdict['type']](authdict)
result = yield types[authdict['type']](authdict, clientip)
if result:
creds[authdict['type']] = result
@ -76,12 +80,12 @@ class AuthHandler(BaseHandler):
logger.info("Auth completed with creds: %r", creds)
defer.returnValue((True, creds))
ret = auth_dict_for_flows(flows)
ret = self.auth_dict_for_flows(flows)
ret['completed'] = creds.keys()
defer.returnValue((False, ret))
@defer.inlineCallbacks
def check_password_auth(self, authdict):
def check_password_auth(self, authdict, _):
if "user" not in authdict or "password" not in authdict:
raise LoginError(400, "", Codes.MISSING_PARAM)
@ -93,17 +97,77 @@ class AuthHandler(BaseHandler):
user_info = yield self.store.get_user_by_id(user_id=user)
if not user_info:
logger.warn("Attempted to login as %s but they do not exist", user)
raise LoginError(403, "", errcode=Codes.FORBIDDEN)
raise LoginError(401, "", errcode=Codes.UNAUTHORIZED)
stored_hash = user_info[0]["password_hash"]
if bcrypt.checkpw(password, stored_hash):
defer.returnValue(user)
else:
logger.warn("Failed password login for user %s", user)
raise LoginError(403, "", errcode=Codes.FORBIDDEN)
raise LoginError(401, "", errcode=Codes.UNAUTHORIZED)
@defer.inlineCallbacks
def check_recaptcha(self, authdict, clientip):
try:
user_response = authdict["response"]
except KeyError:
# Client tried to provide captcha but didn't give the parameter:
# bad request.
raise LoginError(
400, "Captcha response is required",
errcode=Codes.CAPTCHA_NEEDED
)
def auth_dict_for_flows(flows):
return {
"flows": {"stages": f for f in flows}
logger.info(
"Submitting recaptcha response %s with remoteip %s",
user_response, clientip
)
# TODO: get this from the homeserver rather than creating a new one for
# each request
try:
client = SimpleHttpClient(self.hs)
data = yield client.post_urlencoded_get_json(
"https://www.google.com/recaptcha/api/siteverify",
args={
'secret': self.hs.config.recaptcha_private_key,
'response': user_response,
'remoteip': clientip,
}
)
except PartialDownloadError as pde:
# Twisted is silly
data = pde.response
resp_body = simplejson.loads(data)
if 'success' in resp_body and resp_body['success']:
defer.returnValue(True)
raise LoginError(401, "", errcode=Codes.UNAUTHORIZED)
def get_params_recaptcha(self):
return {"public_key": self.hs.config.recaptcha_public_key}
def auth_dict_for_flows(self, flows):
public_flows = []
for f in flows:
hidden = False
for stagetype in f:
if stagetype in LoginType.HIDDEN_TYPES:
hidden = True
if not hidden:
public_flows.append(f)
get_params = {
LoginType.RECAPTCHA: self.get_params_recaptcha,
}
params = {}
for f in public_flows:
for stage in f:
if stage in get_params and stage not in params:
params[stage] = get_params[stage]()
return {
"flows": [{"stages": f} for f in public_flows],
"params": params
}

View File

@ -157,7 +157,11 @@ class RegistrationHandler(BaseHandler):
@defer.inlineCallbacks
def check_recaptcha(self, ip, private_key, challenge, response):
"""Checks a recaptcha is correct."""
"""
Checks a recaptcha is correct.
Used only by c/s api v1
"""
captcha_response = yield self._validate_captcha(
ip,
@ -282,6 +286,8 @@ class RegistrationHandler(BaseHandler):
def _validate_captcha(self, ip_addr, private_key, challenge, response):
"""Validates the captcha provided.
Used only by c/s api v1
Returns:
dict: Containing 'valid'(bool) and 'error_url'(str) if invalid.
@ -299,6 +305,9 @@ class RegistrationHandler(BaseHandler):
@defer.inlineCallbacks
def _submit_captcha(self, ip_addr, private_key, challenge, response):
"""
Used only by c/s api v1
"""
# TODO: get this from the homeserver rather than creating a new one for
# each request
client = CaptchaServerHttpClient(self.hs)

View File

@ -200,6 +200,8 @@ class CaptchaServerHttpClient(SimpleHttpClient):
"""
Separate HTTP client for talking to google's captcha servers
Only slightly special because accepts partial download responses
used only by c/s api v1
"""
@defer.inlineCallbacks

View File

@ -16,7 +16,8 @@
from . import (
sync,
filter,
password
password,
register
)
from synapse.http.server import JsonResource
@ -34,3 +35,4 @@ class ClientV2AlphaRestResource(JsonResource):
sync.register_servlets(hs, client_resource)
filter.register_servlets(hs, client_resource)
password.register_servlets(hs, client_resource)
register.register_servlets(hs, client_resource)

View File

@ -40,6 +40,12 @@ def client_v2_pattern(path_regex):
return re.compile("^" + CLIENT_V2_ALPHA_PREFIX + path_regex)
def parse_request_allow_empty(request):
content = request.content.read()
if content == None or content == '':
return None
return simplejson.loads(content)
def parse_json_dict_from_request(request):
try:
content = simplejson.loads(request.content.read())

View File

@ -0,0 +1,86 @@
# -*- coding: utf-8 -*-
# Copyright 2015 OpenMarket Ltd
#
# Licensed under the Apache License, Version 2.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://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from twisted.internet import defer
from synapse.api.constants import LoginType
from synapse.api.errors import LoginError, SynapseError, Codes
from synapse.http.servlet import RestServlet
from ._base import client_v2_pattern, parse_request_allow_empty
import logging
logger = logging.getLogger(__name__)
class RegisterRestServlet(RestServlet):
PATTERN = client_v2_pattern("/register")
def __init__(self, hs):
super(RegisterRestServlet, self).__init__()
self.hs = hs
self.auth = hs.get_auth()
self.auth_handler = hs.get_handlers().auth_handler
self.registration_handler = hs.get_handlers().registration_handler
@defer.inlineCallbacks
def on_POST(self, request):
body = parse_request_allow_empty(request)
authed, result = yield self.auth_handler.check_auth([
[LoginType.RECAPTCHA],
[LoginType.EMAIL_IDENTITY, LoginType.RECAPTCHA],
[LoginType.APPLICATION_SERVICE]
], body)
if not authed:
defer.returnValue((401, result))
is_application_server = LoginType.APPLICATION_SERVICE in result
is_using_shared_secret = LoginType.SHARED_SECRET in result
can_register = (
not self.hs.config.disable_registration
or is_application_server
or is_using_shared_secret
)
if not can_register:
raise SynapseError(403, "Registration has been disabled")
if 'username' not in body or 'password' not in body:
raise SynapseError(400, "", Codes.MISSING_PARAM)
desired_username = body['username']
new_password = body['password']
(user_id, token) = yield self.registration_handler.register(
localpart=desired_username,
password=new_password
)
result = {
"user_id": user_id,
"access_token": token,
"home_server": self.hs.hostname,
}
defer.returnValue((200, result))
def on_OPTIONS(self, _):
return 200, {}
def register_servlets(hs, http_server):
RegisterRestServlet(hs).register(http_server)