diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index ff69c83cde..612654a517 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -18,7 +18,7 @@ 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.api.errors import SynapseError, LoginError, Codes from synapse.util.async import run_on_reactor from twisted.web.client import PartialDownloadError @@ -33,6 +33,8 @@ import synapse.util.stringutils as stringutils logger = logging.getLogger(__name__) +MACAROON_TYPE_LOGIN_TOKEN = "st_login" + class AuthHandler(BaseHandler): @@ -46,6 +48,22 @@ class AuthHandler(BaseHandler): } self.sessions = {} + self._nonces = {} + + self.clock.looping_call(self._prune_nonce, 60 * 1000) + + def _prune_nonce(self): + now = self.clock.time_msec() + self._nonces = { + user_id: { + nonce: nonce_dict + for nonce, nonce_dict in user_dict.items() + if nonce_dict.get("expiry", 0) < now - 60 * 1000 + } + for user_id, user_dict in self._nonces.items() + if user_dict + } + @defer.inlineCallbacks def check_auth(self, flows, clientdict, clientip): """ @@ -301,15 +319,16 @@ class AuthHandler(BaseHandler): defer.returnValue((user_id, access_token, refresh_token)) @defer.inlineCallbacks - def do_short_term_token_login(self, token, user_id): + def do_short_term_token_login(self, token, user_id, client_nonce): macaroon_exact_caveats = [ "gen = 1", - "type = st_login", + "type = %s" % (MACAROON_TYPE_LOGIN_TOKEN,), "user_id = %s" % (user_id,) ] macaroon_general_caveats = [ - self._verify_macaroon_expiry + self._verify_macaroon_expiry, + lambda c: self._verify_nonce(c, user_id, client_nonce) ] try: @@ -338,7 +357,8 @@ class AuthHandler(BaseHandler): } defer.returnValue(result) - except (pymacaroons.exceptions.MacaroonException, TypeError, ValueError): + except (pymacaroons.exceptions.MacaroonException, TypeError, ValueError) as e: + logger.info("Invalid token: %s", e.message) raise LoginError(403, "Invalid token", errcode=Codes.FORBIDDEN) def _verify_macaroon_expiry(self, caveat): @@ -350,12 +370,41 @@ class AuthHandler(BaseHandler): now = self.hs.get_clock().time_msec() return now < expiry - def make_short_term_token(self, user_id): + def _verify_nonce(self, caveat, user_id, client_nonce): + prefix = "nonce = " + if not caveat.startswith(prefix): + return False + + user_dict = self._nonces.get(user_id, {}) + + nonce = caveat[len(prefix):] + does_match = ( + nonce in user_dict + and user_dict[nonce].get("client_nonce", None) in (None, client_nonce) + ) + + if does_match: + user_dict[nonce] = client_nonce + + return does_match + + def make_short_term_token(self, user_id, nonce): + user_nonces = self._nonces.setdefault(user_id, {}) + if user_nonces.get(nonce, {}).get("client_nonce", None) is not None: + raise SynapseError(400, "nonce already used") + macaroon = self._generate_base_macaroon(user_id) - macaroon.add_first_party_caveat("type = st_login") + macaroon.add_first_party_caveat("type = %s" % (MACAROON_TYPE_LOGIN_TOKEN,)) now = self.hs.get_clock().time_msec() expiry = now + (60 * 1000) macaroon.add_first_party_caveat("time < %d" % (expiry,)) + macaroon.add_first_party_caveat("nonce = %s" % (nonce,)) + + user_nonces[nonce] = { + "client_nonce": None, + "expiry": expiry, + } + return macaroon.serialize() @defer.inlineCallbacks diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py index 92c05d61e4..d0d6f6f6e8 100644 --- a/synapse/rest/client/v1/login.py +++ b/synapse/rest/client/v1/login.py @@ -77,7 +77,10 @@ class LoginRestServlet(ClientV1RestServlet): auth_handler = self.handlers.auth_handler token = login_submission["token"] user_id = login_submission["user"] - result = yield auth_handler.do_short_term_token_login(token, user_id) + client_nonce = login_submission["nonce"] + result = yield auth_handler.do_short_term_token_login( + token, user_id, client_nonce + ) defer.returnValue((200, result)) else: raise SynapseError(400, "Bad login type.") @@ -112,51 +115,6 @@ class LoginRestServlet(ClientV1RestServlet): defer.returnValue((200, result)) - @defer.inlineCallbacks - def do_short_term_token_login(self, login_submission): - token = login_submission["token"] - user_id = login_submission["user"] - - macaroon_exact_caveats = [ - "gen = 1", - "type = st_login", - "user_id = %s" % (user_id,) - ] - - macaroon_general_caveats = [ - self._verify_macaroon_expiry - ] - - try: - macaroon = pymacaroons.Macaroon.deserialize(token) - - v = pymacaroons.Verifier() - for exact_caveat in macaroon_exact_caveats: - v.satisfy_exact(exact_caveat) - - for general_caveat in macaroon_general_caveats: - v.satisfy_general(general_caveat) - - verified = v.verify(macaroon, self.hs.config.macaroon_secret_key) - if not verified: - raise SynapseError(400, "Invalid token.") - - auth_handler = self.handlers.auth_handler - user_id, access_token, refresh_token = yield auth_handler.issue_tokens( - user_id=user_id, - ) - - result = { - "user_id": user_id, # may have changed - "access_token": access_token, - "refresh_token": refresh_token, - "home_server": self.hs.hostname, - } - - defer.returnValue(result) - except (pymacaroons.exceptions.MacaroonException, TypeError, ValueError): - raise SynapseError(400, "Invalid token.") - def _verify_macaroon_expiry(self, caveat): prefix = "time < " if not caveat.startswith(prefix): diff --git a/synapse/rest/media/v1/login_qr_resource.py b/synapse/rest/media/v1/login_qr_resource.py index 55708b2852..b4d7040181 100644 --- a/synapse/rest/media/v1/login_qr_resource.py +++ b/synapse/rest/media/v1/login_qr_resource.py @@ -17,6 +17,7 @@ from twisted.web.server import NOT_DONE_YET from twisted.internet import defer, threads from synapse.api.errors import CodeMessageException +from synapse.util.stringutils import random_string import simplejson import logging @@ -46,7 +47,16 @@ class LoginQRResource(Resource): def _async_render_GET(self, request): try: auth_user, _ = yield self.auth.get_user_by_req(request) - image = yield self.make_short_term_qr_code(auth_user.to_string()) + + nonce = request.path.split("/")[-1] + + if not nonce: + nonce = random_string(10) + + image = yield self.make_short_term_qr_code( + auth_user.to_string(), nonce + ) + request.setHeader(b"Content-Type", b"image/png") image.save(request) @@ -54,16 +64,18 @@ class LoginQRResource(Resource): except CodeMessageException as e: logger.info("Returning: %s", e) request.setResponseCode(e.code) + request.write("%s: %s" % (e.code, e.message)) request.finish() except Exception: logger.exception("Exception while generating token") request.setResponseCode(500) + request.write("Internal server error") request.finish() @defer.inlineCallbacks - def make_short_term_qr_code(self, user_id): + def make_short_term_qr_code(self, user_id, nonce): h = self.handlers.auth_handler - token = h.make_short_term_token(user_id) + token = h.make_short_term_token(user_id, nonce) x509_certificate_bytes = crypto.dump_certificate( crypto.FILETYPE_ASN1,