mirror of https://github.com/watcha-fr/synapse
commit
327ca883ec
@ -0,0 +1,31 @@ |
||||
Captcha can be enabled for this home server. This file explains how to do that. |
||||
The captcha mechanism used is Google's ReCaptcha. This requires API keys from Google. |
||||
|
||||
Getting keys |
||||
------------ |
||||
Requires a public/private key pair from: |
||||
|
||||
https://developers.google.com/recaptcha/ |
||||
|
||||
|
||||
Setting ReCaptcha Keys |
||||
---------------------- |
||||
The keys are a config option on the home server config. If they are not |
||||
visible, you can generate them via --generate-config. Set the following value: |
||||
|
||||
recaptcha_public_key: YOUR_PUBLIC_KEY |
||||
recaptcha_private_key: YOUR_PRIVATE_KEY |
||||
|
||||
In addition, you MUST enable captchas via: |
||||
|
||||
enable_registration_captcha: true |
||||
|
||||
Configuring IP used for auth |
||||
---------------------------- |
||||
The ReCaptcha API requires that the IP address of the user who solved the |
||||
captcha is sent. If the client is connecting through a proxy or load balancer, |
||||
it may be required to use the X-Forwarded-For (XFF) header instead of the origin |
||||
IP address. This can be configured as an option on the home server like so: |
||||
|
||||
captcha_ip_origin_is_x_forwarded: true |
||||
|
@ -1,42 +0,0 @@ |
||||
# -*- coding: utf-8 -*- |
||||
# Copyright 2014, 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 ._base import Config |
||||
|
||||
|
||||
class EmailConfig(Config): |
||||
|
||||
def __init__(self, args): |
||||
super(EmailConfig, self).__init__(args) |
||||
self.email_from_address = args.email_from_address |
||||
self.email_smtp_server = args.email_smtp_server |
||||
|
||||
@classmethod |
||||
def add_arguments(cls, parser): |
||||
super(EmailConfig, cls).add_arguments(parser) |
||||
email_group = parser.add_argument_group("email") |
||||
email_group.add_argument( |
||||
"--email-from-address", |
||||
default="FROM@EXAMPLE.COM", |
||||
help="The address to send emails from (e.g. for password resets)." |
||||
) |
||||
email_group.add_argument( |
||||
"--email-smtp-server", |
||||
default="", |
||||
help=( |
||||
"The SMTP server to send emails from (e.g. for password" |
||||
" resets)." |
||||
) |
||||
) |
@ -0,0 +1,277 @@ |
||||
# -*- coding: utf-8 -*- |
||||
# Copyright 2014, 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 ._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 synapse.util.async import run_on_reactor |
||||
|
||||
from twisted.web.client import PartialDownloadError |
||||
|
||||
import logging |
||||
import bcrypt |
||||
import simplejson |
||||
|
||||
import synapse.util.stringutils as stringutils |
||||
|
||||
|
||||
logger = logging.getLogger(__name__) |
||||
|
||||
|
||||
class AuthHandler(BaseHandler): |
||||
|
||||
def __init__(self, hs): |
||||
super(AuthHandler, self).__init__(hs) |
||||
self.checkers = { |
||||
LoginType.PASSWORD: self._check_password_auth, |
||||
LoginType.RECAPTCHA: self._check_recaptcha, |
||||
LoginType.EMAIL_IDENTITY: self._check_email_identity, |
||||
LoginType.DUMMY: self._check_dummy_auth, |
||||
} |
||||
self.sessions = {} |
||||
|
||||
@defer.inlineCallbacks |
||||
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. |
||||
|
||||
Args: |
||||
flows: list of list of stages |
||||
authdict: The dictionary from the client root level, not the |
||||
'auth' key: this method prompts for auth if none is sent. |
||||
Returns: |
||||
A tuple of authed, dict, dict where authed is true if the client |
||||
has successfully completed an auth flow. If it is true, the first |
||||
dict contains the authenticated credentials of each stage. |
||||
|
||||
If authed is false, the first dictionary is the server response to |
||||
the login request and should be passed back to the client. |
||||
|
||||
In either case, the second dict contains the parameters for this |
||||
request (which may have been given only in a previous call). |
||||
""" |
||||
|
||||
authdict = None |
||||
sid = None |
||||
if clientdict and 'auth' in clientdict: |
||||
authdict = clientdict['auth'] |
||||
del clientdict['auth'] |
||||
if 'session' in authdict: |
||||
sid = authdict['session'] |
||||
sess = self._get_session_info(sid) |
||||
|
||||
if len(clientdict) > 0: |
||||
# This was designed to allow the client to omit the parameters |
||||
# and just supply the session in subsequent calls so it split |
||||
# auth between devices by just sharing the session, (eg. so you |
||||
# could continue registration from your phone having clicked the |
||||
# email auth link on there). It's probably too open to abuse |
||||
# because it lets unauthenticated clients store arbitrary objects |
||||
# on a home server. |
||||
# sess['clientdict'] = clientdict |
||||
# self._save_session(sess) |
||||
pass |
||||
elif 'clientdict' in sess: |
||||
clientdict = sess['clientdict'] |
||||
|
||||
if not authdict: |
||||
defer.returnValue( |
||||
(False, self._auth_dict_for_flows(flows, sess), clientdict) |
||||
) |
||||
|
||||
if 'creds' not in sess: |
||||
sess['creds'] = {} |
||||
creds = sess['creds'] |
||||
|
||||
# check auth type currently being presented |
||||
if 'type' in authdict: |
||||
if authdict['type'] not in self.checkers: |
||||
raise LoginError(400, "", Codes.UNRECOGNIZED) |
||||
result = yield self.checkers[authdict['type']](authdict, clientip) |
||||
if result: |
||||
creds[authdict['type']] = result |
||||
self._save_session(sess) |
||||
|
||||
for f in flows: |
||||
if len(set(f) - set(creds.keys())) == 0: |
||||
logger.info("Auth completed with creds: %r", creds) |
||||
self._remove_session(sess) |
||||
defer.returnValue((True, creds, clientdict)) |
||||
|
||||
ret = self._auth_dict_for_flows(flows, sess) |
||||
ret['completed'] = creds.keys() |
||||
defer.returnValue((False, ret, clientdict)) |
||||
|
||||
@defer.inlineCallbacks |
||||
def add_oob_auth(self, stagetype, authdict, clientip): |
||||
""" |
||||
Adds the result of out-of-band authentication into an existing auth |
||||
session. Currently used for adding the result of fallback auth. |
||||
""" |
||||
if stagetype not in self.checkers: |
||||
raise LoginError(400, "", Codes.MISSING_PARAM) |
||||
if 'session' not in authdict: |
||||
raise LoginError(400, "", Codes.MISSING_PARAM) |
||||
|
||||
sess = self._get_session_info( |
||||
authdict['session'] |
||||
) |
||||
if 'creds' not in sess: |
||||
sess['creds'] = {} |
||||
creds = sess['creds'] |
||||
|
||||
result = yield self.checkers[stagetype](authdict, clientip) |
||||
if result: |
||||
creds[stagetype] = result |
||||
self._save_session(sess) |
||||
defer.returnValue(True) |
||||
defer.returnValue(False) |
||||
|
||||
@defer.inlineCallbacks |
||||
def _check_password_auth(self, authdict, _): |
||||
if "user" not in authdict or "password" not in authdict: |
||||
raise LoginError(400, "", Codes.MISSING_PARAM) |
||||
|
||||
user = authdict["user"] |
||||
password = authdict["password"] |
||||
if not user.startswith('@'): |
||||
user = UserID.create(user, self.hs.hostname).to_string() |
||||
|
||||
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(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(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 |
||||
) |
||||
|
||||
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) |
||||
|
||||
@defer.inlineCallbacks |
||||
def _check_email_identity(self, authdict, _): |
||||
yield run_on_reactor() |
||||
|
||||
if 'threepid_creds' not in authdict: |
||||
raise LoginError(400, "Missing threepid_creds", Codes.MISSING_PARAM) |
||||
|
||||
threepid_creds = authdict['threepid_creds'] |
||||
identity_handler = self.hs.get_handlers().identity_handler |
||||
|
||||
logger.info("Getting validated threepid. threepidcreds: %r" % (threepid_creds,)) |
||||
threepid = yield identity_handler.threepid_from_creds(threepid_creds) |
||||
|
||||
if not threepid: |
||||
raise LoginError(401, "", errcode=Codes.UNAUTHORIZED) |
||||
|
||||
threepid['threepid_creds'] = authdict['threepid_creds'] |
||||
|
||||
defer.returnValue(threepid) |
||||
|
||||
@defer.inlineCallbacks |
||||
def _check_dummy_auth(self, authdict, _): |
||||
yield run_on_reactor() |
||||
defer.returnValue(True) |
||||
|
||||
def _get_params_recaptcha(self): |
||||
return {"public_key": self.hs.config.recaptcha_public_key} |
||||
|
||||
def _auth_dict_for_flows(self, flows, session): |
||||
public_flows = [] |
||||
for f in flows: |
||||
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 { |
||||
"session": session['id'], |
||||
"flows": [{"stages": f} for f in public_flows], |
||||
"params": params |
||||
} |
||||
|
||||
def _get_session_info(self, session_id): |
||||
if session_id not in self.sessions: |
||||
session_id = None |
||||
|
||||
if not session_id: |
||||
# create a new session |
||||
while session_id is None or session_id in self.sessions: |
||||
session_id = stringutils.random_string(24) |
||||
self.sessions[session_id] = { |
||||
"id": session_id, |
||||
} |
||||
|
||||
return self.sessions[session_id] |
||||
|
||||
def _save_session(self, session): |
||||
# TODO: Persistent storage |
||||
logger.debug("Saving session %s", session) |
||||
self.sessions[session["id"]] = session |
||||
|
||||
def _remove_session(self, session): |
||||
logger.debug("Removing session %s", session) |
||||
del self.sessions[session["id"]] |
@ -0,0 +1,88 @@ |
||||
# -*- 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. |
||||
|
||||
"""Utilities for interacting with Identity Servers""" |
||||
from twisted.internet import defer |
||||
|
||||
from synapse.api.errors import ( |
||||
CodeMessageException |
||||
) |
||||
from ._base import BaseHandler |
||||
from synapse.http.client import SimpleHttpClient |
||||
from synapse.util.async import run_on_reactor |
||||
|
||||
import json |
||||
import logging |
||||
|
||||
logger = logging.getLogger(__name__) |
||||
|
||||
|
||||
class IdentityHandler(BaseHandler): |
||||
|
||||
def __init__(self, hs): |
||||
super(IdentityHandler, self).__init__(hs) |
||||
|
||||
@defer.inlineCallbacks |
||||
def threepid_from_creds(self, creds): |
||||
yield run_on_reactor() |
||||
|
||||
# TODO: get this from the homeserver rather than creating a new one for |
||||
# each request |
||||
http_client = SimpleHttpClient(self.hs) |
||||
# XXX: make this configurable! |
||||
# trustedIdServers = ['matrix.org', 'localhost:8090'] |
||||
trustedIdServers = ['matrix.org'] |
||||
if not creds['id_server'] in trustedIdServers: |
||||
logger.warn('%s is not a trusted ID server: rejecting 3pid ' + |
||||
'credentials', creds['id_server']) |
||||
defer.returnValue(None) |
||||
|
||||
data = {} |
||||
try: |
||||
data = yield http_client.get_json( |
||||
"https://%s%s" % ( |
||||
creds['id_server'], |
||||
"/_matrix/identity/api/v1/3pid/getValidated3pid" |
||||
), |
||||
{'sid': creds['sid'], 'client_secret': creds['client_secret']} |
||||
) |
||||
except CodeMessageException as e: |
||||
data = json.loads(e.msg) |
||||
|
||||
if 'medium' in data: |
||||
defer.returnValue(data) |
||||
defer.returnValue(None) |
||||
|
||||
@defer.inlineCallbacks |
||||
def bind_threepid(self, creds, mxid): |
||||
yield run_on_reactor() |
||||
logger.debug("binding threepid %r to %s", creds, mxid) |
||||
http_client = SimpleHttpClient(self.hs) |
||||
data = None |
||||
try: |
||||
data = yield http_client.post_urlencoded_get_json( |
||||
"https://%s%s" % ( |
||||
creds['id_server'], "/_matrix/identity/api/v1/3pid/bind" |
||||
), |
||||
{ |
||||
'sid': creds['sid'], |
||||
'client_secret': creds['client_secret'], |
||||
'mxid': mxid, |
||||
} |
||||
) |
||||
logger.debug("bound threepid %r to %s", creds, mxid) |
||||
except CodeMessageException as e: |
||||
data = json.loads(e.msg) |
||||
defer.returnValue(data) |
@ -0,0 +1,159 @@ |
||||
# -*- 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 synapse.util.async import run_on_reactor |
||||
|
||||
from ._base import client_v2_pattern, parse_json_dict_from_request |
||||
|
||||
import logging |
||||
|
||||
|
||||
logger = logging.getLogger(__name__) |
||||
|
||||
|
||||
class PasswordRestServlet(RestServlet): |
||||
PATTERN = client_v2_pattern("/account/password") |
||||
|
||||
def __init__(self, hs): |
||||
super(PasswordRestServlet, self).__init__() |
||||
self.hs = hs |
||||
self.auth = hs.get_auth() |
||||
self.auth_handler = hs.get_handlers().auth_handler |
||||
self.login_handler = hs.get_handlers().login_handler |
||||
|
||||
@defer.inlineCallbacks |
||||
def on_POST(self, request): |
||||
yield run_on_reactor() |
||||
|
||||
body = parse_json_dict_from_request(request) |
||||
|
||||
authed, result, params = yield self.auth_handler.check_auth([ |
||||
[LoginType.PASSWORD], |
||||
[LoginType.EMAIL_IDENTITY] |
||||
], body) |
||||
|
||||
if not authed: |
||||
defer.returnValue((401, result)) |
||||
|
||||
user_id = None |
||||
|
||||
if LoginType.PASSWORD in result: |
||||
# if using password, they should also be logged in |
||||
auth_user, client = yield self.auth.get_user_by_req(request) |
||||
if auth_user.to_string() != result[LoginType.PASSWORD]: |
||||
raise LoginError(400, "", Codes.UNKNOWN) |
||||
user_id = auth_user.to_string() |
||||
elif LoginType.EMAIL_IDENTITY in result: |
||||
threepid = result[LoginType.EMAIL_IDENTITY] |
||||
if 'medium' not in threepid or 'address' not in threepid: |
||||
raise SynapseError(500, "Malformed threepid") |
||||
# if using email, we must know about the email they're authing with! |
||||
threepid_user = yield self.hs.get_datastore().get_user_by_threepid( |
||||
threepid['medium'], threepid['address'] |
||||
) |
||||
if not threepid_user: |
||||
raise SynapseError(404, "Email address not found", Codes.NOT_FOUND) |
||||
user_id = threepid_user |
||||
else: |
||||
logger.error("Auth succeeded but no known type!", result.keys()) |
||||
raise SynapseError(500, "", Codes.UNKNOWN) |
||||
|
||||
if 'new_password' not in params: |
||||
raise SynapseError(400, "", Codes.MISSING_PARAM) |
||||
new_password = params['new_password'] |
||||
|
||||
yield self.login_handler.set_password( |
||||
user_id, new_password, None |
||||
) |
||||
|
||||
defer.returnValue((200, {})) |
||||
|
||||
def on_OPTIONS(self, _): |
||||
return 200, {} |
||||
|
||||
|
||||
class ThreepidRestServlet(RestServlet): |
||||
PATTERN = client_v2_pattern("/account/3pid") |
||||
|
||||
def __init__(self, hs): |
||||
super(ThreepidRestServlet, self).__init__() |
||||
self.hs = hs |
||||
self.login_handler = hs.get_handlers().login_handler |
||||
self.identity_handler = hs.get_handlers().identity_handler |
||||
self.auth = hs.get_auth() |
||||
|
||||
@defer.inlineCallbacks |
||||
def on_GET(self, request): |
||||
yield run_on_reactor() |
||||
|
||||
auth_user, _ = yield self.auth.get_user_by_req(request) |
||||
|
||||
threepids = yield self.hs.get_datastore().user_get_threepids( |
||||
auth_user.to_string() |
||||
) |
||||
|
||||
defer.returnValue((200, {'threepids': threepids})) |
||||
|
||||
@defer.inlineCallbacks |
||||
def on_POST(self, request): |
||||
yield run_on_reactor() |
||||
|
||||
body = parse_json_dict_from_request(request) |
||||
|
||||
if 'threePidCreds' not in body: |
||||
raise SynapseError(400, "Missing param", Codes.MISSING_PARAM) |
||||
threePidCreds = body['threePidCreds'] |
||||
|
||||
auth_user, client = yield self.auth.get_user_by_req(request) |
||||
|
||||
threepid = yield self.identity_handler.threepid_from_creds(threePidCreds) |
||||
|
||||
if not threepid: |
||||
raise SynapseError( |
||||
400, "Failed to auth 3pid", Codes.THREEPID_AUTH_FAILED |
||||
) |
||||
|
||||
for reqd in ['medium', 'address', 'validated_at']: |
||||
if reqd not in threepid: |
||||
logger.warn("Couldn't add 3pid: invalid response from ID sevrer") |
||||
raise SynapseError(500, "Invalid response from ID Server") |
||||
|
||||
yield self.login_handler.add_threepid( |
||||
auth_user.to_string(), |
||||
threepid['medium'], |
||||
threepid['address'], |
||||
threepid['validated_at'], |
||||
) |
||||
|
||||
if 'bind' in body and body['bind']: |
||||
logger.debug( |
||||
"Binding emails %s to %s", |
||||
threepid, auth_user.to_string() |
||||
) |
||||
yield self.identity_handler.bind_threepid( |
||||
threePidCreds, auth_user.to_string() |
||||
) |
||||
|
||||
defer.returnValue((200, {})) |
||||
|
||||
|
||||
def register_servlets(hs, http_server): |
||||
PasswordRestServlet(hs).register(http_server) |
||||
ThreepidRestServlet(hs).register(http_server) |
@ -0,0 +1,190 @@ |
||||
# -*- 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 SynapseError |
||||
from synapse.api.urls import CLIENT_V2_ALPHA_PREFIX |
||||
from synapse.http.servlet import RestServlet |
||||
|
||||
from ._base import client_v2_pattern |
||||
|
||||
import logging |
||||
|
||||
|
||||
logger = logging.getLogger(__name__) |
||||
|
||||
RECAPTCHA_TEMPLATE = """ |
||||
<html> |
||||
<head> |
||||
<title>Authentication</title> |
||||
<meta name='viewport' content='width=device-width, initial-scale=1, |
||||
user-scalable=no, minimum-scale=1.0, maximum-scale=1.0'> |
||||
<script src="https://www.google.com/recaptcha/api.js" |
||||
async defer></script> |
||||
<script src="//code.jquery.com/jquery-1.11.2.min.js"></script> |
||||
<link rel="stylesheet" href="/_matrix/static/client/register/style.css"> |
||||
<script> |
||||
function captchaDone() { |
||||
$('#registrationForm').submit(); |
||||
} |
||||
</script> |
||||
</head> |
||||
<body> |
||||
<form id="registrationForm" method="post" action="%(myurl)s"> |
||||
<div> |
||||
<p> |
||||
Hello! We need to prevent computer programs and other automated |
||||
things from creating accounts on this server. |
||||
</p> |
||||
<p> |
||||
Please verify that you're not a robot. |
||||
</p> |
||||
<input type="hidden" name="session" value="%(session)s" /> |
||||
<div class="g-recaptcha" |
||||
data-sitekey="%(sitekey)s" |
||||
data-callback="captchaDone"> |
||||
</div> |
||||
<noscript> |
||||
<input type="submit" value="All Done" /> |
||||
</noscript> |
||||
</div> |
||||
</div> |
||||
</form> |
||||
</body> |
||||
</html> |
||||
""" |
||||
|
||||
SUCCESS_TEMPLATE = """ |
||||
<html> |
||||
<head> |
||||
<title>Success!</title> |
||||
<meta name='viewport' content='width=device-width, initial-scale=1, |
||||
user-scalable=no, minimum-scale=1.0, maximum-scale=1.0'> |
||||
<link rel="stylesheet" href="/_matrix/static/client/register/style.css"> |
||||
<script> |
||||
if (window.onAuthDone != undefined) { |
||||
window.onAuthDone(); |
||||
} |
||||
</script> |
||||
</head> |
||||
<body> |
||||
<div> |
||||
<p>Thank you</p> |
||||
<p>You may now close this window and return to the application</p> |
||||
</div> |
||||
</body> |
||||
</html> |
||||
""" |
||||
|
||||
|
||||
class AuthRestServlet(RestServlet): |
||||
""" |
||||
Handles Client / Server API authentication in any situations where it |
||||
cannot be handled in the normal flow (with requests to the same endpoint). |
||||
Current use is for web fallback auth. |
||||
""" |
||||
PATTERN = client_v2_pattern("/auth/(?P<stagetype>[\w\.]*)/fallback/web") |
||||
|
||||
def __init__(self, hs): |
||||
super(AuthRestServlet, 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_GET(self, request, stagetype): |
||||
yield |
||||
if stagetype == LoginType.RECAPTCHA: |
||||
if ('session' not in request.args or |
||||
len(request.args['session']) == 0): |
||||
raise SynapseError(400, "No session supplied") |
||||
|
||||
session = request.args["session"][0] |
||||
|
||||
html = RECAPTCHA_TEMPLATE % { |
||||
'session': session, |
||||
'myurl': "%s/auth/%s/fallback/web" % ( |
||||
CLIENT_V2_ALPHA_PREFIX, LoginType.RECAPTCHA |
||||
), |
||||
'sitekey': self.hs.config.recaptcha_public_key, |
||||
} |
||||
html_bytes = html.encode("utf8") |
||||
request.setResponseCode(200) |
||||
request.setHeader(b"Content-Type", b"text/html; charset=utf-8") |
||||
request.setHeader(b"Server", self.hs.version_string) |
||||
request.setHeader(b"Content-Length", b"%d" % (len(html_bytes),)) |
||||
|
||||
request.write(html_bytes) |
||||
request.finish() |
||||
defer.returnValue(None) |
||||
else: |
||||
raise SynapseError(404, "Unknown auth stage type") |
||||
|
||||
@defer.inlineCallbacks |
||||
def on_POST(self, request, stagetype): |
||||
yield |
||||
if stagetype == "m.login.recaptcha": |
||||
if ('g-recaptcha-response' not in request.args or |
||||
len(request.args['g-recaptcha-response'])) == 0: |
||||
raise SynapseError(400, "No captcha response supplied") |
||||
if ('session' not in request.args or |
||||
len(request.args['session'])) == 0: |
||||
raise SynapseError(400, "No session supplied") |
||||
|
||||
session = request.args['session'][0] |
||||
|
||||
authdict = { |
||||
'response': request.args['g-recaptcha-response'][0], |
||||
'session': session, |
||||
} |
||||
|
||||
success = yield self.auth_handler.add_oob_auth( |
||||
LoginType.RECAPTCHA, |
||||
authdict, |
||||
self.hs.get_ip_from_request(request) |
||||
) |
||||
|
||||
if success: |
||||
html = SUCCESS_TEMPLATE |
||||
else: |
||||
html = RECAPTCHA_TEMPLATE % { |
||||
'session': session, |
||||
'myurl': "%s/auth/%s/fallback/web" % ( |
||||
CLIENT_V2_ALPHA_PREFIX, LoginType.RECAPTCHA |
||||
), |
||||
'sitekey': self.hs.config.recaptcha_public_key, |
||||
} |
||||
html_bytes = html.encode("utf8") |
||||
request.setResponseCode(200) |
||||
request.setHeader(b"Content-Type", b"text/html; charset=utf-8") |
||||
request.setHeader(b"Server", self.hs.version_string) |
||||
request.setHeader(b"Content-Length", b"%d" % (len(html_bytes),)) |
||||
|
||||
request.write(html_bytes) |
||||
request.finish() |
||||
|
||||
defer.returnValue(None) |
||||
else: |
||||
raise SynapseError(404, "Unknown auth stage type") |
||||
|
||||
def on_OPTIONS(self, _): |
||||
return 200, {} |
||||
|
||||
|
||||
def register_servlets(hs, http_server): |
||||
AuthRestServlet(hs).register(http_server) |
@ -0,0 +1,183 @@ |
||||
# -*- 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 SynapseError, Codes |
||||
from synapse.http.servlet import RestServlet |
||||
|
||||
from ._base import client_v2_pattern, parse_request_allow_empty |
||||
|
||||
import logging |
||||
import hmac |
||||
from hashlib import sha1 |
||||
from synapse.util.async import run_on_reactor |
||||
|
||||
|
||||
# We ought to be using hmac.compare_digest() but on older pythons it doesn't |
||||
# exist. It's a _really minor_ security flaw to use plain string comparison |
||||
# because the timing attack is so obscured by all the other code here it's |
||||
# unlikely to make much difference |
||||
if hasattr(hmac, "compare_digest"): |
||||
compare_digest = hmac.compare_digest |
||||
else: |
||||
compare_digest = lambda a, b: a == b |
||||
|
||||
|
||||
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 |
||||
self.identity_handler = hs.get_handlers().identity_handler |
||||
self.login_handler = hs.get_handlers().login_handler |
||||
|
||||
@defer.inlineCallbacks |
||||
def on_POST(self, request): |
||||
yield run_on_reactor() |
||||
|
||||
body = parse_request_allow_empty(request) |
||||
if 'password' not in body: |
||||
raise SynapseError(400, "", Codes.MISSING_PARAM) |
||||
|
||||
if 'username' in body: |
||||
desired_username = body['username'] |
||||
yield self.registration_handler.check_username(desired_username) |
||||
|
||||
is_using_shared_secret = False |
||||
is_application_server = False |
||||
|
||||
service = None |
||||
if 'access_token' in request.args: |
||||
service = yield self.auth.get_appservice_by_req(request) |
||||
|
||||
if self.hs.config.enable_registration_captcha: |
||||
flows = [ |
||||
[LoginType.RECAPTCHA], |
||||
[LoginType.EMAIL_IDENTITY, LoginType.RECAPTCHA] |
||||
] |
||||
else: |
||||
flows = [ |
||||
[LoginType.DUMMY], |
||||
[LoginType.EMAIL_IDENTITY] |
||||
] |
||||
|
||||
if service: |
||||
is_application_server = True |
||||
elif 'mac' in body: |
||||
# Check registration-specific shared secret auth |
||||
if 'username' not in body: |
||||
raise SynapseError(400, "", Codes.MISSING_PARAM) |
||||
self._check_shared_secret_auth( |
||||
body['username'], body['mac'] |
||||
) |
||||
is_using_shared_secret = True |
||||
else: |
||||
authed, result, params = yield self.auth_handler.check_auth( |
||||
flows, body, self.hs.get_ip_from_request(request) |
||||
) |
||||
|
||||
if not authed: |
||||
defer.returnValue((401, 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 'password' not in params: |
||||
raise SynapseError(400, "", Codes.MISSING_PARAM) |
||||
desired_username = params['username'] if 'username' in params else None |
||||
new_password = params['password'] |
||||
|
||||
(user_id, token) = yield self.registration_handler.register( |
||||
localpart=desired_username, |
||||
password=new_password |
||||
) |
||||
|
||||
if LoginType.EMAIL_IDENTITY in result: |
||||
threepid = result[LoginType.EMAIL_IDENTITY] |
||||
|
||||
for reqd in ['medium', 'address', 'validated_at']: |
||||
if reqd not in threepid: |
||||
logger.info("Can't add incomplete 3pid") |
||||
else: |
||||
yield self.login_handler.add_threepid( |
||||
user_id, |
||||
threepid['medium'], |
||||
threepid['address'], |
||||
threepid['validated_at'], |
||||
) |
||||
|
||||
if 'bind_email' in params and params['bind_email']: |
||||
logger.info("bind_email specified: binding") |
||||
|
||||
emailThreepid = result[LoginType.EMAIL_IDENTITY] |
||||
threepid_creds = emailThreepid['threepid_creds'] |
||||
logger.debug("Binding emails %s to %s" % ( |
||||
emailThreepid, user_id |
||||
)) |
||||
yield self.identity_handler.bind_threepid(threepid_creds, user_id) |
||||
else: |
||||
logger.info("bind_email not specified: not binding email") |
||||
|
||||
result = { |
||||
"user_id": user_id, |
||||
"access_token": token, |
||||
"home_server": self.hs.hostname, |
||||
} |
||||
|
||||
defer.returnValue((200, result)) |
||||
|
||||
def on_OPTIONS(self, _): |
||||
return 200, {} |
||||
|
||||
def _check_shared_secret_auth(self, username, mac): |
||||
if not self.hs.config.registration_shared_secret: |
||||
raise SynapseError(400, "Shared secret registration is not enabled") |
||||
|
||||
user = username.encode("utf-8") |
||||
|
||||
# str() because otherwise hmac complains that 'unicode' does not |
||||
# have the buffer interface |
||||
got_mac = str(mac) |
||||
|
||||
want_mac = hmac.new( |
||||
key=self.hs.config.registration_shared_secret, |
||||
msg=user, |
||||
digestmod=sha1, |
||||
).hexdigest() |
||||
|
||||
if compare_digest(want_mac, got_mac): |
||||
return True |
||||
else: |
||||
raise SynapseError( |
||||
403, "HMAC incorrect", |
||||
) |
||||
|
||||
|
||||
def register_servlets(hs, http_server): |
||||
RegisterRestServlet(hs).register(http_server) |
@ -0,0 +1,25 @@ |
||||
-- Drop, copy & recreate pushers table to change unique key |
||||
-- Also add access_token column at the same time |
||||
CREATE TABLE IF NOT EXISTS pushers2 ( |
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, |
||||
user_name TEXT NOT NULL, |
||||
access_token INTEGER DEFAULT NULL, |
||||
profile_tag varchar(32) NOT NULL, |
||||
kind varchar(8) NOT NULL, |
||||
app_id varchar(64) NOT NULL, |
||||
app_display_name varchar(64) NOT NULL, |
||||
device_display_name varchar(128) NOT NULL, |
||||
pushkey blob NOT NULL, |
||||
ts BIGINT NOT NULL, |
||||
lang varchar(8), |
||||
data blob, |
||||
last_token TEXT, |
||||
last_success BIGINT, |
||||
failing_since BIGINT, |
||||
FOREIGN KEY(user_name) REFERENCES users(name), |
||||
UNIQUE (app_id, pushkey, user_name) |
||||
); |
||||
INSERT INTO pushers2 (id, user_name, profile_tag, kind, app_id, app_display_name, device_display_name, pushkey, ts, lang, data, last_token, last_success, failing_since) |
||||
SELECT id, user_name, profile_tag, kind, app_id, app_display_name, device_display_name, pushkey, ts, lang, data, last_token, last_success, failing_since FROM pushers; |
||||
DROP TABLE pushers; |
||||
ALTER TABLE pushers2 RENAME TO pushers; |
Loading…
Reference in new issue