summary refs log tree commit diff
path: root/synapse/api
diff options
context:
space:
mode:
authorDaniel Wagner-Hall <daniel@matrix.org>2015-08-26 13:22:23 +0100
committerDaniel Wagner-Hall <daniel@matrix.org>2015-08-26 13:22:23 +0100
commit6a4b650d8ad3e6c095020cac3861e430d643d53d (patch)
tree87fa9682940fbecd34104763b2a6ba0c35143b60 /synapse/api
parentMerge pull request #251 from matrix-org/removeadmin (diff)
downloadsynapse-6a4b650d8ad3e6c095020cac3861e430d643d53d.tar.xz
Attempt to validate macaroons
A couple of weird caveats:
 * If we can't validate your macaroon, we fall back to checking that
   your access token is in the DB, and ignoring the failure
 * Even if we can validate your macaroon, we still have to hit the DB to
   get the access token ID, which we pretend is a device ID all over the
   codebase.

This mostly adds the interesting code, and points out the two pieces we
need to delete (and necessary conditions) in order to fix the above
caveats.
Diffstat (limited to 'synapse/api')
-rw-r--r--synapse/api/auth.py104
1 files changed, 95 insertions, 9 deletions
diff --git a/synapse/api/auth.py b/synapse/api/auth.py
index 65ee1452ce..f8ea1e2c69 100644
--- a/synapse/api/auth.py
+++ b/synapse/api/auth.py
@@ -23,6 +23,7 @@ from synapse.util.logutils import log_function
 from synapse.types import UserID
 
 import logging
+import pymacaroons
 
 logger = logging.getLogger(__name__)
 
@@ -40,6 +41,12 @@ class Auth(object):
         self.store = hs.get_datastore()
         self.state = hs.get_state_handler()
         self.TOKEN_NOT_FOUND_HTTP_STATUS = 401
+        self._KNOWN_CAVEAT_PREFIXES = set([
+            "gen = ",
+            "type = ",
+            "time < ",
+            "user_id = ",
+        ])
 
     def check(self, event, auth_events):
         """ Checks if this event is correctly authed.
@@ -359,8 +366,8 @@ class Auth(object):
             except KeyError:
                 pass  # normal users won't have the user_id query parameter set.
 
-            user_info = yield self.get_user_by_access_token(access_token)
-            user = user_info["user"]
+            user_info = yield self._get_user_by_access_token(access_token)
+            user_id = user_info["user_id"]
             token_id = user_info["token_id"]
 
             ip_addr = self.hs.get_ip_from_request(request)
@@ -368,17 +375,17 @@ class Auth(object):
                 "User-Agent",
                 default=[""]
             )[0]
-            if user and access_token and ip_addr:
+            if user_id and access_token and ip_addr:
                 self.store.insert_client_ip(
-                    user=user,
+                    user=user_id,
                     access_token=access_token,
                     ip=ip_addr,
                     user_agent=user_agent
                 )
 
-            request.authenticated_entity = user.to_string()
+            request.authenticated_entity = user_id.to_string()
 
-            defer.returnValue((user, token_id,))
+            defer.returnValue((user_id, token_id,))
         except KeyError:
             raise AuthError(
                 self.TOKEN_NOT_FOUND_HTTP_STATUS, "Missing access token.",
@@ -386,7 +393,7 @@ class Auth(object):
             )
 
     @defer.inlineCallbacks
-    def get_user_by_access_token(self, token):
+    def _get_user_by_access_token(self, token):
         """ Get a registered user's ID.
 
         Args:
@@ -396,6 +403,86 @@ class Auth(object):
         Raises:
             AuthError if no user by that token exists or the token is invalid.
         """
+        try:
+            ret = yield self._get_user_from_macaroon(token)
+        except AuthError:
+            # TODO(daniel): Remove this fallback when all existing access tokens
+            # have been re-issued as macaroons.
+            ret = yield self._look_up_user_by_access_token(token)
+        defer.returnValue(ret)
+
+    @defer.inlineCallbacks
+    def _get_user_from_macaroon(self, macaroon_str):
+        try:
+            macaroon = pymacaroons.Macaroon.deserialize(macaroon_str)
+            self._validate_macaroon(macaroon)
+
+            user_prefix = "user_id = "
+            for caveat in macaroon.caveats:
+                if caveat.caveat_id.startswith(user_prefix):
+                    user_id = UserID.from_string(caveat.caveat_id[len(user_prefix):])
+                    # This codepath exists so that we can actually return a
+                    # token ID, because we use token IDs in place of device
+                    # identifiers throughout the codebase.
+                    # TODO(daniel): Remove this fallback when device IDs are
+                    # properly implemented.
+                    ret = yield self._look_up_user_by_access_token(macaroon_str)
+                    if ret["user_id"] != user_id:
+                        logger.error(
+                            "Macaroon user (%s) != DB user (%s)",
+                            user_id,
+                            ret["user_id"]
+                        )
+                        raise AuthError(
+                            self.TOKEN_NOT_FOUND_HTTP_STATUS,
+                            "User mismatch in macaroon",
+                            errcode=Codes.UNKNOWN_TOKEN
+                        )
+                    defer.returnValue(ret)
+            raise AuthError(
+                self.TOKEN_NOT_FOUND_HTTP_STATUS, "No user caveat in macaroon",
+                errcode=Codes.UNKNOWN_TOKEN
+            )
+        except (pymacaroons.exceptions.MacaroonException, TypeError, ValueError):
+            raise AuthError(
+                self.TOKEN_NOT_FOUND_HTTP_STATUS, "Invalid macaroon passed.",
+                errcode=Codes.UNKNOWN_TOKEN
+            )
+
+    def _validate_macaroon(self, macaroon):
+        v = pymacaroons.Verifier()
+        v.satisfy_exact("gen = 1")
+        v.satisfy_exact("type = access")
+        v.satisfy_general(lambda c: c.startswith("user_id = "))
+        v.satisfy_general(self._verify_expiry)
+        v.verify(macaroon, self.hs.config.macaroon_secret_key)
+
+        v = pymacaroons.Verifier()
+        v.satisfy_general(self._verify_recognizes_caveats)
+        v.verify(macaroon, self.hs.config.macaroon_secret_key)
+
+    def _verify_expiry(self, caveat):
+        prefix = "time < "
+        if not caveat.startswith(prefix):
+            return False
+        # TODO(daniel): Enable expiry check when clients actually know how to
+        # refresh tokens. (And remember to enable the tests)
+        return True
+        expiry = int(caveat[len(prefix):])
+        now = self.hs.get_clock().time_msec()
+        return now < expiry
+
+    def _verify_recognizes_caveats(self, caveat):
+        first_space = caveat.find(" ")
+        if first_space < 0:
+            return False
+        second_space = caveat.find(" ", first_space + 1)
+        if second_space < 0:
+            return False
+        return caveat[:second_space + 1] in self._KNOWN_CAVEAT_PREFIXES
+
+    @defer.inlineCallbacks
+    def _look_up_user_by_access_token(self, token):
         ret = yield self.store.get_user_by_access_token(token)
         if not ret:
             raise AuthError(
@@ -403,10 +490,9 @@ class Auth(object):
                 errcode=Codes.UNKNOWN_TOKEN
             )
         user_info = {
-            "user": UserID.from_string(ret.get("name")),
+            "user_id": UserID.from_string(ret.get("name")),
             "token_id": ret.get("token_id", None),
         }
-
         defer.returnValue(user_info)
 
     @defer.inlineCallbacks