forked from MirrorHub/synapse
Merge pull request #256 from matrix-org/auth
Attempt to validate macaroons
This commit is contained in:
commit
728d07c8c1
7 changed files with 243 additions and 19 deletions
|
@ -23,6 +23,7 @@ from synapse.util.logutils import log_function
|
|||
from synapse.types import UserID, EventID
|
||||
|
||||
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.
|
||||
|
@ -367,7 +374,7 @@ 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_info = yield self._get_user_by_access_token(access_token)
|
||||
user = user_info["user"]
|
||||
token_id = user_info["token_id"]
|
||||
|
||||
|
@ -394,7 +401,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:
|
||||
|
@ -404,6 +411,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 = 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"] != user:
|
||||
logger.error(
|
||||
"Macaroon user (%s) != DB user (%s)",
|
||||
user,
|
||||
ret["user"]
|
||||
)
|
||||
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(
|
||||
|
@ -414,7 +501,6 @@ class Auth(object):
|
|||
"user": UserID.from_string(ret.get("name")),
|
||||
"token_id": ret.get("token_id", None),
|
||||
}
|
||||
|
||||
defer.returnValue(user_info)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
|
|
|
@ -19,17 +19,21 @@ from mock import Mock
|
|||
|
||||
from synapse.api.auth import Auth
|
||||
from synapse.api.errors import AuthError
|
||||
from synapse.types import UserID
|
||||
from tests.utils import setup_test_homeserver
|
||||
|
||||
import pymacaroons
|
||||
|
||||
|
||||
class AuthTestCase(unittest.TestCase):
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def setUp(self):
|
||||
self.state_handler = Mock()
|
||||
self.store = Mock()
|
||||
|
||||
self.hs = Mock()
|
||||
self.hs = yield setup_test_homeserver(handlers=None)
|
||||
self.hs.get_datastore = Mock(return_value=self.store)
|
||||
self.hs.get_state_handler = Mock(return_value=self.state_handler)
|
||||
self.auth = Auth(self.hs)
|
||||
|
||||
self.test_user = "@foo:bar"
|
||||
|
@ -133,3 +137,140 @@ class AuthTestCase(unittest.TestCase):
|
|||
request.requestHeaders.getRawHeaders = Mock(return_value=[""])
|
||||
d = self.auth.get_user_by_req(request)
|
||||
self.failureResultOf(d, AuthError)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_get_user_from_macaroon(self):
|
||||
# TODO(danielwh): Remove this mock when we remove the
|
||||
# get_user_by_access_token fallback.
|
||||
self.store.get_user_by_access_token = Mock(
|
||||
return_value={"name": "@baldrick:matrix.org"}
|
||||
)
|
||||
|
||||
user_id = "@baldrick:matrix.org"
|
||||
macaroon = pymacaroons.Macaroon(
|
||||
location=self.hs.config.server_name,
|
||||
identifier="key",
|
||||
key=self.hs.config.macaroon_secret_key)
|
||||
macaroon.add_first_party_caveat("gen = 1")
|
||||
macaroon.add_first_party_caveat("type = access")
|
||||
macaroon.add_first_party_caveat("user_id = %s" % (user_id,))
|
||||
user_info = yield self.auth._get_user_from_macaroon(macaroon.serialize())
|
||||
user = user_info["user"]
|
||||
self.assertEqual(UserID.from_string(user_id), user)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_get_user_from_macaroon_user_db_mismatch(self):
|
||||
self.store.get_user_by_access_token = Mock(
|
||||
return_value={"name": "@percy:matrix.org"}
|
||||
)
|
||||
|
||||
user = "@baldrick:matrix.org"
|
||||
macaroon = pymacaroons.Macaroon(
|
||||
location=self.hs.config.server_name,
|
||||
identifier="key",
|
||||
key=self.hs.config.macaroon_secret_key)
|
||||
macaroon.add_first_party_caveat("gen = 1")
|
||||
macaroon.add_first_party_caveat("type = access")
|
||||
macaroon.add_first_party_caveat("user_id = %s" % (user,))
|
||||
with self.assertRaises(AuthError) as cm:
|
||||
yield self.auth._get_user_from_macaroon(macaroon.serialize())
|
||||
self.assertEqual(401, cm.exception.code)
|
||||
self.assertIn("User mismatch", cm.exception.msg)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_get_user_from_macaroon_missing_caveat(self):
|
||||
# TODO(danielwh): Remove this mock when we remove the
|
||||
# get_user_by_access_token fallback.
|
||||
self.store.get_user_by_access_token = Mock(
|
||||
return_value={"name": "@baldrick:matrix.org"}
|
||||
)
|
||||
|
||||
macaroon = pymacaroons.Macaroon(
|
||||
location=self.hs.config.server_name,
|
||||
identifier="key",
|
||||
key=self.hs.config.macaroon_secret_key)
|
||||
macaroon.add_first_party_caveat("gen = 1")
|
||||
macaroon.add_first_party_caveat("type = access")
|
||||
|
||||
with self.assertRaises(AuthError) as cm:
|
||||
yield self.auth._get_user_from_macaroon(macaroon.serialize())
|
||||
self.assertEqual(401, cm.exception.code)
|
||||
self.assertIn("No user caveat", cm.exception.msg)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_get_user_from_macaroon_wrong_key(self):
|
||||
# TODO(danielwh): Remove this mock when we remove the
|
||||
# get_user_by_access_token fallback.
|
||||
self.store.get_user_by_access_token = Mock(
|
||||
return_value={"name": "@baldrick:matrix.org"}
|
||||
)
|
||||
|
||||
user = "@baldrick:matrix.org"
|
||||
macaroon = pymacaroons.Macaroon(
|
||||
location=self.hs.config.server_name,
|
||||
identifier="key",
|
||||
key=self.hs.config.macaroon_secret_key + "wrong")
|
||||
macaroon.add_first_party_caveat("gen = 1")
|
||||
macaroon.add_first_party_caveat("type = access")
|
||||
macaroon.add_first_party_caveat("user_id = %s" % (user,))
|
||||
|
||||
with self.assertRaises(AuthError) as cm:
|
||||
yield self.auth._get_user_from_macaroon(macaroon.serialize())
|
||||
self.assertEqual(401, cm.exception.code)
|
||||
self.assertIn("Invalid macaroon", cm.exception.msg)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_get_user_from_macaroon_unknown_caveat(self):
|
||||
# TODO(danielwh): Remove this mock when we remove the
|
||||
# get_user_by_access_token fallback.
|
||||
self.store.get_user_by_access_token = Mock(
|
||||
return_value={"name": "@baldrick:matrix.org"}
|
||||
)
|
||||
|
||||
user = "@baldrick:matrix.org"
|
||||
macaroon = pymacaroons.Macaroon(
|
||||
location=self.hs.config.server_name,
|
||||
identifier="key",
|
||||
key=self.hs.config.macaroon_secret_key)
|
||||
macaroon.add_first_party_caveat("gen = 1")
|
||||
macaroon.add_first_party_caveat("type = access")
|
||||
macaroon.add_first_party_caveat("user_id = %s" % (user,))
|
||||
macaroon.add_first_party_caveat("cunning > fox")
|
||||
|
||||
with self.assertRaises(AuthError) as cm:
|
||||
yield self.auth._get_user_from_macaroon(macaroon.serialize())
|
||||
self.assertEqual(401, cm.exception.code)
|
||||
self.assertIn("Invalid macaroon", cm.exception.msg)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_get_user_from_macaroon_expired(self):
|
||||
# TODO(danielwh): Remove this mock when we remove the
|
||||
# get_user_by_access_token fallback.
|
||||
self.store.get_user_by_access_token = Mock(
|
||||
return_value={"name": "@baldrick:matrix.org"}
|
||||
)
|
||||
|
||||
self.store.get_user_by_access_token = Mock(
|
||||
return_value={"name": "@baldrick:matrix.org"}
|
||||
)
|
||||
|
||||
user = "@baldrick:matrix.org"
|
||||
macaroon = pymacaroons.Macaroon(
|
||||
location=self.hs.config.server_name,
|
||||
identifier="key",
|
||||
key=self.hs.config.macaroon_secret_key)
|
||||
macaroon.add_first_party_caveat("gen = 1")
|
||||
macaroon.add_first_party_caveat("type = access")
|
||||
macaroon.add_first_party_caveat("user_id = %s" % (user,))
|
||||
macaroon.add_first_party_caveat("time < 1") # ms
|
||||
|
||||
self.hs.clock.now = 5000 # seconds
|
||||
|
||||
yield self.auth._get_user_from_macaroon(macaroon.serialize())
|
||||
# TODO(daniel): Turn on the check that we validate expiration, when we
|
||||
# validate expiration (and remove the above line, which will start
|
||||
# throwing).
|
||||
# with self.assertRaises(AuthError) as cm:
|
||||
# yield self.auth._get_user_from_macaroon(macaroon.serialize())
|
||||
# self.assertEqual(401, cm.exception.code)
|
||||
# self.assertIn("Invalid macaroon", cm.exception.msg)
|
||||
|
|
|
@ -76,7 +76,7 @@ class PresenceStateTestCase(unittest.TestCase):
|
|||
"token_id": 1,
|
||||
}
|
||||
|
||||
hs.get_v1auth().get_user_by_access_token = _get_user_by_access_token
|
||||
hs.get_v1auth()._get_user_by_access_token = _get_user_by_access_token
|
||||
|
||||
room_member_handler = hs.handlers.room_member_handler = Mock(
|
||||
spec=[
|
||||
|
@ -169,7 +169,7 @@ class PresenceListTestCase(unittest.TestCase):
|
|||
]
|
||||
)
|
||||
|
||||
hs.get_v1auth().get_user_by_access_token = _get_user_by_access_token
|
||||
hs.get_v1auth()._get_user_by_access_token = _get_user_by_access_token
|
||||
|
||||
presence.register_servlets(hs, self.mock_resource)
|
||||
|
||||
|
|
|
@ -59,7 +59,7 @@ class RoomPermissionsTestCase(RestTestCase):
|
|||
"user": UserID.from_string(self.auth_user_id),
|
||||
"token_id": 1,
|
||||
}
|
||||
hs.get_v1auth().get_user_by_access_token = _get_user_by_access_token
|
||||
hs.get_v1auth()._get_user_by_access_token = _get_user_by_access_token
|
||||
|
||||
def _insert_client_ip(*args, **kwargs):
|
||||
return defer.succeed(None)
|
||||
|
@ -444,7 +444,7 @@ class RoomsMemberListTestCase(RestTestCase):
|
|||
"user": UserID.from_string(self.auth_user_id),
|
||||
"token_id": 1,
|
||||
}
|
||||
hs.get_v1auth().get_user_by_access_token = _get_user_by_access_token
|
||||
hs.get_v1auth()._get_user_by_access_token = _get_user_by_access_token
|
||||
|
||||
def _insert_client_ip(*args, **kwargs):
|
||||
return defer.succeed(None)
|
||||
|
@ -522,7 +522,7 @@ class RoomsCreateTestCase(RestTestCase):
|
|||
"user": UserID.from_string(self.auth_user_id),
|
||||
"token_id": 1,
|
||||
}
|
||||
hs.get_v1auth().get_user_by_access_token = _get_user_by_access_token
|
||||
hs.get_v1auth()._get_user_by_access_token = _get_user_by_access_token
|
||||
|
||||
def _insert_client_ip(*args, **kwargs):
|
||||
return defer.succeed(None)
|
||||
|
@ -614,7 +614,7 @@ class RoomTopicTestCase(RestTestCase):
|
|||
"token_id": 1,
|
||||
}
|
||||
|
||||
hs.get_v1auth().get_user_by_access_token = _get_user_by_access_token
|
||||
hs.get_v1auth()._get_user_by_access_token = _get_user_by_access_token
|
||||
|
||||
def _insert_client_ip(*args, **kwargs):
|
||||
return defer.succeed(None)
|
||||
|
@ -718,7 +718,7 @@ class RoomMemberStateTestCase(RestTestCase):
|
|||
"user": UserID.from_string(self.auth_user_id),
|
||||
"token_id": 1,
|
||||
}
|
||||
hs.get_v1auth().get_user_by_access_token = _get_user_by_access_token
|
||||
hs.get_v1auth()._get_user_by_access_token = _get_user_by_access_token
|
||||
|
||||
def _insert_client_ip(*args, **kwargs):
|
||||
return defer.succeed(None)
|
||||
|
@ -843,7 +843,7 @@ class RoomMessagesTestCase(RestTestCase):
|
|||
"user": UserID.from_string(self.auth_user_id),
|
||||
"token_id": 1,
|
||||
}
|
||||
hs.get_v1auth().get_user_by_access_token = _get_user_by_access_token
|
||||
hs.get_v1auth()._get_user_by_access_token = _get_user_by_access_token
|
||||
|
||||
def _insert_client_ip(*args, **kwargs):
|
||||
return defer.succeed(None)
|
||||
|
@ -938,7 +938,7 @@ class RoomInitialSyncTestCase(RestTestCase):
|
|||
"user": UserID.from_string(self.auth_user_id),
|
||||
"token_id": 1,
|
||||
}
|
||||
hs.get_v1auth().get_user_by_access_token = _get_user_by_access_token
|
||||
hs.get_v1auth()._get_user_by_access_token = _get_user_by_access_token
|
||||
|
||||
def _insert_client_ip(*args, **kwargs):
|
||||
return defer.succeed(None)
|
||||
|
|
|
@ -67,7 +67,7 @@ class RoomTypingTestCase(RestTestCase):
|
|||
"token_id": 1,
|
||||
}
|
||||
|
||||
hs.get_v1auth().get_user_by_access_token = _get_user_by_access_token
|
||||
hs.get_v1auth()._get_user_by_access_token = _get_user_by_access_token
|
||||
|
||||
def _insert_client_ip(*args, **kwargs):
|
||||
return defer.succeed(None)
|
||||
|
|
|
@ -37,9 +37,6 @@ class RestTestCase(unittest.TestCase):
|
|||
self.mock_resource = None
|
||||
self.auth_user_id = None
|
||||
|
||||
def mock_get_user_by_access_token(self, token=None):
|
||||
return self.auth_user_id
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def create_room_as(self, room_creator, is_public=True, tok=None):
|
||||
temp_id = self.auth_user_id
|
||||
|
|
|
@ -48,7 +48,7 @@ class V2AlphaRestTestCase(unittest.TestCase):
|
|||
"user": UserID.from_string(self.USER_ID),
|
||||
"token_id": 1,
|
||||
}
|
||||
hs.get_auth().get_user_by_access_token = _get_user_by_access_token
|
||||
hs.get_auth()._get_user_by_access_token = _get_user_by_access_token
|
||||
|
||||
for r in self.TO_REGISTER:
|
||||
r.register_servlets(hs, self.mock_resource)
|
||||
|
|
Loading…
Reference in a new issue