summary refs log tree commit diff
diff options
context:
space:
mode:
authorErik Johnston <erik@matrix.org>2015-09-25 11:18:15 +0100
committerErik Johnston <erik@matrix.org>2015-09-25 11:18:15 +0100
commit936cdac6aacaf84f48a6d51529495d9476b035f4 (patch)
tree0cb7dcb02363ba42a1d6020417247543bf8fb99d
parentMerge pull request #289 from matrix-org/markjh/fix_sql (diff)
downloadsynapse-936cdac6aacaf84f48a6d51529495d9476b035f4.tar.xz
Add support for logging in via token. Also add QR code to server up token.
-rw-r--r--synapse/config/server.py8
-rw-r--r--synapse/handlers/auth.py63
-rw-r--r--synapse/rest/client/v1/login.py68
-rw-r--r--synapse/rest/media/v1/login_qr_resource.py95
-rw-r--r--synapse/rest/media/v1/media_repository.py2
5 files changed, 235 insertions, 1 deletions
diff --git a/synapse/config/server.py b/synapse/config/server.py
index 4d12d49857..0b1b2fbc9d 100644
--- a/synapse/config/server.py
+++ b/synapse/config/server.py
@@ -117,6 +117,11 @@ class ServerConfig(Config):
 
         self.content_addr = content_addr
 
+        client_addr = config.get("client_addr")
+        if not client_addr:
+            client_addr = content_addr
+        self.client_addr = client_addr
+
     def default_config(self, server_name, **kwargs):
         if ":" in server_name:
             bind_port = int(server_name.split(":")[1])
@@ -140,6 +145,9 @@ class ServerConfig(Config):
         # Whether to serve a web client from the HTTP/HTTPS root resource.
         web_client: True
 
+        # URL clients can use to talk to the server.
+        client_addr: "https://%(server_name)s:%(bind_port)s"
+
         # Set the soft limit on the number of file descriptors synapse can use
         # Zero is used to indicate synapse should set the soft limit to the
         # hard limit.
diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py
index 793b3fcd8b..ff69c83cde 100644
--- a/synapse/handlers/auth.py
+++ b/synapse/handlers/auth.py
@@ -290,12 +290,75 @@ class AuthHandler(BaseHandler):
         user_id, password_hash = yield self._find_user_id_and_pwd_hash(user_id)
         self._check_password(user_id, password, password_hash)
 
+        res = yield self._issue_tokens(user_id)
+        defer.returnValue(res)
+
+    @defer.inlineCallbacks
+    def _issue_tokens(self, user_id):
         logger.info("Logging in user %s", user_id)
         access_token = yield self.issue_access_token(user_id)
         refresh_token = yield self.issue_refresh_token(user_id)
         defer.returnValue((user_id, access_token, refresh_token))
 
     @defer.inlineCallbacks
+    def do_short_term_token_login(self, token, user_id):
+        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 LoginError(403, "Invalid token", errcode=Codes.FORBIDDEN)
+
+            user_id, access_token, refresh_token = yield self._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 LoginError(403, "Invalid token", errcode=Codes.FORBIDDEN)
+
+    def _verify_macaroon_expiry(self, caveat):
+        prefix = "time < "
+        if not caveat.startswith(prefix):
+            return False
+
+        expiry = int(caveat[len(prefix):])
+        now = self.hs.get_clock().time_msec()
+        return now < expiry
+
+    def make_short_term_token(self, user_id):
+        macaroon = self._generate_base_macaroon(user_id)
+        macaroon.add_first_party_caveat("type = st_login")
+        now = self.hs.get_clock().time_msec()
+        expiry = now + (60 * 1000)
+        macaroon.add_first_party_caveat("time < %d" % (expiry,))
+        return macaroon.serialize()
+
+    @defer.inlineCallbacks
     def _find_user_id_and_pwd_hash(self, user_id):
         """Checks to see if a user with the given id exists. Will check case
         insensitively, but will throw if there are multiple inexact matches.
diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py
index e580f71964..146b9b510e 100644
--- a/synapse/rest/client/v1/login.py
+++ b/synapse/rest/client/v1/login.py
@@ -27,6 +27,8 @@ from saml2 import BINDING_HTTP_POST
 from saml2 import config
 from saml2.client import Saml2Client
 
+import pymacaroons
+
 
 logger = logging.getLogger(__name__)
 
@@ -35,6 +37,7 @@ class LoginRestServlet(ClientV1RestServlet):
     PATTERN = client_path_pattern("/login$")
     PASS_TYPE = "m.login.password"
     SAML2_TYPE = "m.login.saml2"
+    TOKEN_TYPE = "m.login.token"
 
     def __init__(self, hs):
         super(LoginRestServlet, self).__init__(hs)
@@ -42,7 +45,10 @@ class LoginRestServlet(ClientV1RestServlet):
         self.saml2_enabled = hs.config.saml2_enabled
 
     def on_GET(self, request):
-        flows = [{"type": LoginRestServlet.PASS_TYPE}]
+        flows = [
+            {"type": LoginRestServlet.PASS_TYPE},
+            {"type": LoginRestServlet.TOKEN_TYPE}
+        ]
         if self.saml2_enabled:
             flows.append({"type": LoginRestServlet.SAML2_TYPE})
         return (200, {"flows": flows})
@@ -67,6 +73,12 @@ class LoginRestServlet(ClientV1RestServlet):
                     "uri": "%s%s" % (self.idp_redirect_url, relay_state)
                 }
                 defer.returnValue((200, result))
+            elif login_submission["type"] == LoginRestServlet.TOKEN_TYPE:
+                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)
+                defer.returnValue(result)
             else:
                 raise SynapseError(400, "Bad login type.")
         except KeyError:
@@ -100,6 +112,60 @@ 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):
+            return False
+
+        expiry = int(caveat[len(prefix):])
+        now = self.hs.get_clock().time_msec()
+        return now < expiry
+
 
 class LoginFallbackRestServlet(ClientV1RestServlet):
     PATTERN = client_path_pattern("/login/fallback$")
diff --git a/synapse/rest/media/v1/login_qr_resource.py b/synapse/rest/media/v1/login_qr_resource.py
new file mode 100644
index 0000000000..55708b2852
--- /dev/null
+++ b/synapse/rest/media/v1/login_qr_resource.py
@@ -0,0 +1,95 @@
+# 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.web.resource import Resource
+from twisted.web.server import NOT_DONE_YET
+from twisted.internet import defer, threads
+
+from synapse.api.errors import CodeMessageException
+
+import simplejson
+import logging
+
+from unpaddedbase64 import encode_base64
+from hashlib import sha256
+from OpenSSL import crypto
+
+logger = logging.getLogger(__name__)
+
+
+class LoginQRResource(Resource):
+    isLeaf = True
+
+    def __init__(self, hs):
+        Resource.__init__(self)
+        self.hs = hs
+        self.auth = hs.get_auth()
+        self.handlers = hs.get_handlers()
+        self.config = hs.get_config()
+
+    def render_GET(self, request):
+        self._async_render_GET(request)
+        return NOT_DONE_YET
+
+    @defer.inlineCallbacks
+    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())
+            request.setHeader(b"Content-Type", b"image/png")
+
+            image.save(request)
+            request.finish()
+        except CodeMessageException as e:
+            logger.info("Returning: %s", e)
+            request.setResponseCode(e.code)
+            request.finish()
+        except Exception:
+            logger.exception("Exception while generating token")
+            request.setResponseCode(500)
+            request.finish()
+
+    @defer.inlineCallbacks
+    def make_short_term_qr_code(self, user_id):
+        h = self.handlers.auth_handler
+        token = h.make_short_term_token(user_id)
+
+        x509_certificate_bytes = crypto.dump_certificate(
+            crypto.FILETYPE_ASN1,
+            self.config.tls_certificate
+        )
+
+        sha256_fingerprint = sha256(x509_certificate_bytes).digest()
+
+        def gen():
+            import qrcode
+            qr = qrcode.QRCode(
+                version=1,
+                error_correction=qrcode.constants.ERROR_CORRECT_L,
+                box_size=5,
+            )
+            qr.add_data(simplejson.dumps({
+                "user_id": user_id,
+                "token": token,
+                "homeserver_url": self.config.client_addr,
+                "fingerprints": [{
+                    "hash_type": "SHA256",
+                    "bytes": encode_base64(sha256_fingerprint),
+                }],
+            }))
+            qr.make(fit=True)
+            return qr.make_image()
+
+        res = yield threads.deferToThread(gen)
+        defer.returnValue(res)
diff --git a/synapse/rest/media/v1/media_repository.py b/synapse/rest/media/v1/media_repository.py
index 9ca4d884dd..5dcd7b659c 100644
--- a/synapse/rest/media/v1/media_repository.py
+++ b/synapse/rest/media/v1/media_repository.py
@@ -17,6 +17,7 @@ from .upload_resource import UploadResource
 from .download_resource import DownloadResource
 from .thumbnail_resource import ThumbnailResource
 from .identicon_resource import IdenticonResource
+from .login_qr_resource import LoginQRResource
 from .filepath import MediaFilePaths
 
 from twisted.web.resource import Resource
@@ -78,3 +79,4 @@ class MediaRepositoryResource(Resource):
         self.putChild("download", DownloadResource(hs, filepaths))
         self.putChild("thumbnail", ThumbnailResource(hs, filepaths))
         self.putChild("identicon", IdenticonResource())
+        self.putChild("login_qr", LoginQRResource(hs))