test_auth.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2015 - 2016 OpenMarket Ltd
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import pymacaroons
  16. from mock import Mock
  17. from twisted.internet import defer
  18. import synapse.handlers.auth
  19. from synapse.api.auth import Auth
  20. from synapse.api.errors import AuthError
  21. from synapse.types import UserID
  22. from tests import unittest
  23. from tests.utils import setup_test_homeserver, mock_getRawHeaders
  24. class TestHandlers(object):
  25. def __init__(self, hs):
  26. self.auth_handler = synapse.handlers.auth.AuthHandler(hs)
  27. class AuthTestCase(unittest.TestCase):
  28. @defer.inlineCallbacks
  29. def setUp(self):
  30. self.state_handler = Mock()
  31. self.store = Mock()
  32. self.hs = yield setup_test_homeserver(handlers=None)
  33. self.hs.get_datastore = Mock(return_value=self.store)
  34. self.hs.handlers = TestHandlers(self.hs)
  35. self.auth = Auth(self.hs)
  36. self.test_user = "@foo:bar"
  37. self.test_token = "_test_token_"
  38. # this is overridden for the appservice tests
  39. self.store.get_app_service_by_token = Mock(return_value=None)
  40. @defer.inlineCallbacks
  41. def test_get_user_by_req_user_valid_token(self):
  42. user_info = {
  43. "name": self.test_user,
  44. "token_id": "ditto",
  45. "device_id": "device",
  46. }
  47. self.store.get_user_by_access_token = Mock(return_value=user_info)
  48. request = Mock(args={})
  49. request.args["access_token"] = [self.test_token]
  50. request.requestHeaders.getRawHeaders = mock_getRawHeaders()
  51. requester = yield self.auth.get_user_by_req(request)
  52. self.assertEquals(requester.user.to_string(), self.test_user)
  53. def test_get_user_by_req_user_bad_token(self):
  54. self.store.get_user_by_access_token = Mock(return_value=None)
  55. request = Mock(args={})
  56. request.args["access_token"] = [self.test_token]
  57. request.requestHeaders.getRawHeaders = mock_getRawHeaders()
  58. d = self.auth.get_user_by_req(request)
  59. self.failureResultOf(d, AuthError)
  60. def test_get_user_by_req_user_missing_token(self):
  61. user_info = {
  62. "name": self.test_user,
  63. "token_id": "ditto",
  64. }
  65. self.store.get_user_by_access_token = Mock(return_value=user_info)
  66. request = Mock(args={})
  67. request.requestHeaders.getRawHeaders = mock_getRawHeaders()
  68. d = self.auth.get_user_by_req(request)
  69. self.failureResultOf(d, AuthError)
  70. @defer.inlineCallbacks
  71. def test_get_user_by_req_appservice_valid_token(self):
  72. app_service = Mock(token="foobar", url="a_url", sender=self.test_user)
  73. self.store.get_app_service_by_token = Mock(return_value=app_service)
  74. self.store.get_user_by_access_token = Mock(return_value=None)
  75. request = Mock(args={})
  76. request.args["access_token"] = [self.test_token]
  77. request.requestHeaders.getRawHeaders = mock_getRawHeaders()
  78. requester = yield self.auth.get_user_by_req(request)
  79. self.assertEquals(requester.user.to_string(), self.test_user)
  80. def test_get_user_by_req_appservice_bad_token(self):
  81. self.store.get_app_service_by_token = Mock(return_value=None)
  82. self.store.get_user_by_access_token = Mock(return_value=None)
  83. request = Mock(args={})
  84. request.args["access_token"] = [self.test_token]
  85. request.requestHeaders.getRawHeaders = mock_getRawHeaders()
  86. d = self.auth.get_user_by_req(request)
  87. self.failureResultOf(d, AuthError)
  88. def test_get_user_by_req_appservice_missing_token(self):
  89. app_service = Mock(token="foobar", url="a_url", sender=self.test_user)
  90. self.store.get_app_service_by_token = Mock(return_value=app_service)
  91. self.store.get_user_by_access_token = Mock(return_value=None)
  92. request = Mock(args={})
  93. request.requestHeaders.getRawHeaders = mock_getRawHeaders()
  94. d = self.auth.get_user_by_req(request)
  95. self.failureResultOf(d, AuthError)
  96. @defer.inlineCallbacks
  97. def test_get_user_by_req_appservice_valid_token_valid_user_id(self):
  98. masquerading_user_id = "@doppelganger:matrix.org"
  99. app_service = Mock(token="foobar", url="a_url", sender=self.test_user)
  100. app_service.is_interested_in_user = Mock(return_value=True)
  101. self.store.get_app_service_by_token = Mock(return_value=app_service)
  102. self.store.get_user_by_access_token = Mock(return_value=None)
  103. request = Mock(args={})
  104. request.args["access_token"] = [self.test_token]
  105. request.args["user_id"] = [masquerading_user_id]
  106. request.requestHeaders.getRawHeaders = mock_getRawHeaders()
  107. requester = yield self.auth.get_user_by_req(request)
  108. self.assertEquals(requester.user.to_string(), masquerading_user_id)
  109. def test_get_user_by_req_appservice_valid_token_bad_user_id(self):
  110. masquerading_user_id = "@doppelganger:matrix.org"
  111. app_service = Mock(token="foobar", url="a_url", sender=self.test_user)
  112. app_service.is_interested_in_user = Mock(return_value=False)
  113. self.store.get_app_service_by_token = Mock(return_value=app_service)
  114. self.store.get_user_by_access_token = Mock(return_value=None)
  115. request = Mock(args={})
  116. request.args["access_token"] = [self.test_token]
  117. request.args["user_id"] = [masquerading_user_id]
  118. request.requestHeaders.getRawHeaders = mock_getRawHeaders()
  119. d = self.auth.get_user_by_req(request)
  120. self.failureResultOf(d, AuthError)
  121. @defer.inlineCallbacks
  122. def test_get_user_from_macaroon(self):
  123. # TODO(danielwh): Remove this mock when we remove the
  124. # get_user_by_access_token fallback.
  125. self.store.get_user_by_access_token = Mock(
  126. return_value={
  127. "name": "@baldrick:matrix.org",
  128. "device_id": "device",
  129. }
  130. )
  131. user_id = "@baldrick:matrix.org"
  132. macaroon = pymacaroons.Macaroon(
  133. location=self.hs.config.server_name,
  134. identifier="key",
  135. key=self.hs.config.macaroon_secret_key)
  136. macaroon.add_first_party_caveat("gen = 1")
  137. macaroon.add_first_party_caveat("type = access")
  138. macaroon.add_first_party_caveat("user_id = %s" % (user_id,))
  139. user_info = yield self.auth.get_user_by_access_token(macaroon.serialize())
  140. user = user_info["user"]
  141. self.assertEqual(UserID.from_string(user_id), user)
  142. # TODO: device_id should come from the macaroon, but currently comes
  143. # from the db.
  144. self.assertEqual(user_info["device_id"], "device")
  145. @defer.inlineCallbacks
  146. def test_get_guest_user_from_macaroon(self):
  147. self.store.get_user_by_id = Mock(return_value={
  148. "is_guest": True,
  149. })
  150. user_id = "@baldrick:matrix.org"
  151. macaroon = pymacaroons.Macaroon(
  152. location=self.hs.config.server_name,
  153. identifier="key",
  154. key=self.hs.config.macaroon_secret_key)
  155. macaroon.add_first_party_caveat("gen = 1")
  156. macaroon.add_first_party_caveat("type = access")
  157. macaroon.add_first_party_caveat("user_id = %s" % (user_id,))
  158. macaroon.add_first_party_caveat("guest = true")
  159. serialized = macaroon.serialize()
  160. user_info = yield self.auth.get_user_by_access_token(serialized)
  161. user = user_info["user"]
  162. is_guest = user_info["is_guest"]
  163. self.assertEqual(UserID.from_string(user_id), user)
  164. self.assertTrue(is_guest)
  165. self.store.get_user_by_id.assert_called_with(user_id)
  166. @defer.inlineCallbacks
  167. def test_get_user_from_macaroon_user_db_mismatch(self):
  168. self.store.get_user_by_access_token = Mock(
  169. return_value={"name": "@percy:matrix.org"}
  170. )
  171. user = "@baldrick:matrix.org"
  172. macaroon = pymacaroons.Macaroon(
  173. location=self.hs.config.server_name,
  174. identifier="key",
  175. key=self.hs.config.macaroon_secret_key)
  176. macaroon.add_first_party_caveat("gen = 1")
  177. macaroon.add_first_party_caveat("type = access")
  178. macaroon.add_first_party_caveat("user_id = %s" % (user,))
  179. with self.assertRaises(AuthError) as cm:
  180. yield self.auth.get_user_by_access_token(macaroon.serialize())
  181. self.assertEqual(401, cm.exception.code)
  182. self.assertIn("User mismatch", cm.exception.msg)
  183. @defer.inlineCallbacks
  184. def test_get_user_from_macaroon_missing_caveat(self):
  185. # TODO(danielwh): Remove this mock when we remove the
  186. # get_user_by_access_token fallback.
  187. self.store.get_user_by_access_token = Mock(
  188. return_value={"name": "@baldrick:matrix.org"}
  189. )
  190. macaroon = pymacaroons.Macaroon(
  191. location=self.hs.config.server_name,
  192. identifier="key",
  193. key=self.hs.config.macaroon_secret_key)
  194. macaroon.add_first_party_caveat("gen = 1")
  195. macaroon.add_first_party_caveat("type = access")
  196. with self.assertRaises(AuthError) as cm:
  197. yield self.auth.get_user_by_access_token(macaroon.serialize())
  198. self.assertEqual(401, cm.exception.code)
  199. self.assertIn("No user caveat", cm.exception.msg)
  200. @defer.inlineCallbacks
  201. def test_get_user_from_macaroon_wrong_key(self):
  202. # TODO(danielwh): Remove this mock when we remove the
  203. # get_user_by_access_token fallback.
  204. self.store.get_user_by_access_token = Mock(
  205. return_value={"name": "@baldrick:matrix.org"}
  206. )
  207. user = "@baldrick:matrix.org"
  208. macaroon = pymacaroons.Macaroon(
  209. location=self.hs.config.server_name,
  210. identifier="key",
  211. key=self.hs.config.macaroon_secret_key + "wrong")
  212. macaroon.add_first_party_caveat("gen = 1")
  213. macaroon.add_first_party_caveat("type = access")
  214. macaroon.add_first_party_caveat("user_id = %s" % (user,))
  215. with self.assertRaises(AuthError) as cm:
  216. yield self.auth.get_user_by_access_token(macaroon.serialize())
  217. self.assertEqual(401, cm.exception.code)
  218. self.assertIn("Invalid macaroon", cm.exception.msg)
  219. @defer.inlineCallbacks
  220. def test_get_user_from_macaroon_unknown_caveat(self):
  221. # TODO(danielwh): Remove this mock when we remove the
  222. # get_user_by_access_token fallback.
  223. self.store.get_user_by_access_token = Mock(
  224. return_value={"name": "@baldrick:matrix.org"}
  225. )
  226. user = "@baldrick:matrix.org"
  227. macaroon = pymacaroons.Macaroon(
  228. location=self.hs.config.server_name,
  229. identifier="key",
  230. key=self.hs.config.macaroon_secret_key)
  231. macaroon.add_first_party_caveat("gen = 1")
  232. macaroon.add_first_party_caveat("type = access")
  233. macaroon.add_first_party_caveat("user_id = %s" % (user,))
  234. macaroon.add_first_party_caveat("cunning > fox")
  235. with self.assertRaises(AuthError) as cm:
  236. yield self.auth.get_user_by_access_token(macaroon.serialize())
  237. self.assertEqual(401, cm.exception.code)
  238. self.assertIn("Invalid macaroon", cm.exception.msg)
  239. @defer.inlineCallbacks
  240. def test_get_user_from_macaroon_expired(self):
  241. # TODO(danielwh): Remove this mock when we remove the
  242. # get_user_by_access_token fallback.
  243. self.store.get_user_by_access_token = Mock(
  244. return_value={"name": "@baldrick:matrix.org"}
  245. )
  246. self.store.get_user_by_access_token = Mock(
  247. return_value={"name": "@baldrick:matrix.org"}
  248. )
  249. user = "@baldrick:matrix.org"
  250. macaroon = pymacaroons.Macaroon(
  251. location=self.hs.config.server_name,
  252. identifier="key",
  253. key=self.hs.config.macaroon_secret_key)
  254. macaroon.add_first_party_caveat("gen = 1")
  255. macaroon.add_first_party_caveat("type = access")
  256. macaroon.add_first_party_caveat("user_id = %s" % (user,))
  257. macaroon.add_first_party_caveat("time < -2000") # ms
  258. self.hs.clock.now = 5000 # seconds
  259. self.hs.config.expire_access_token = True
  260. # yield self.auth.get_user_by_access_token(macaroon.serialize())
  261. # TODO(daniel): Turn on the check that we validate expiration, when we
  262. # validate expiration (and remove the above line, which will start
  263. # throwing).
  264. with self.assertRaises(AuthError) as cm:
  265. yield self.auth.get_user_by_access_token(macaroon.serialize())
  266. self.assertEqual(401, cm.exception.code)
  267. self.assertIn("Invalid macaroon", cm.exception.msg)
  268. @defer.inlineCallbacks
  269. def test_get_user_from_macaroon_with_valid_duration(self):
  270. # TODO(danielwh): Remove this mock when we remove the
  271. # get_user_by_access_token fallback.
  272. self.store.get_user_by_access_token = Mock(
  273. return_value={"name": "@baldrick:matrix.org"}
  274. )
  275. self.store.get_user_by_access_token = Mock(
  276. return_value={"name": "@baldrick:matrix.org"}
  277. )
  278. user_id = "@baldrick:matrix.org"
  279. macaroon = pymacaroons.Macaroon(
  280. location=self.hs.config.server_name,
  281. identifier="key",
  282. key=self.hs.config.macaroon_secret_key)
  283. macaroon.add_first_party_caveat("gen = 1")
  284. macaroon.add_first_party_caveat("type = access")
  285. macaroon.add_first_party_caveat("user_id = %s" % (user_id,))
  286. macaroon.add_first_party_caveat("time < 900000000") # ms
  287. self.hs.clock.now = 5000 # seconds
  288. self.hs.config.expire_access_token = True
  289. user_info = yield self.auth.get_user_by_access_token(macaroon.serialize())
  290. user = user_info["user"]
  291. self.assertEqual(UserID.from_string(user_id), user)
  292. @defer.inlineCallbacks
  293. def test_cannot_use_regular_token_as_guest(self):
  294. USER_ID = "@percy:matrix.org"
  295. self.store.add_access_token_to_user = Mock()
  296. token = yield self.hs.handlers.auth_handler.issue_access_token(
  297. USER_ID, "DEVICE"
  298. )
  299. self.store.add_access_token_to_user.assert_called_with(
  300. USER_ID, token, "DEVICE"
  301. )
  302. def get_user(tok):
  303. if token != tok:
  304. return None
  305. return {
  306. "name": USER_ID,
  307. "is_guest": False,
  308. "token_id": 1234,
  309. "device_id": "DEVICE",
  310. }
  311. self.store.get_user_by_access_token = get_user
  312. self.store.get_user_by_id = Mock(return_value={
  313. "is_guest": False,
  314. })
  315. # check the token works
  316. request = Mock(args={})
  317. request.args["access_token"] = [token]
  318. request.requestHeaders.getRawHeaders = mock_getRawHeaders()
  319. requester = yield self.auth.get_user_by_req(request, allow_guest=True)
  320. self.assertEqual(UserID.from_string(USER_ID), requester.user)
  321. self.assertFalse(requester.is_guest)
  322. # add an is_guest caveat
  323. mac = pymacaroons.Macaroon.deserialize(token)
  324. mac.add_first_party_caveat("guest = true")
  325. guest_tok = mac.serialize()
  326. # the token should *not* work now
  327. request = Mock(args={})
  328. request.args["access_token"] = [guest_tok]
  329. request.requestHeaders.getRawHeaders = mock_getRawHeaders()
  330. with self.assertRaises(AuthError) as cm:
  331. yield self.auth.get_user_by_req(request, allow_guest=True)
  332. self.assertEqual(401, cm.exception.code)
  333. self.assertEqual("Guest access token used for regular user", cm.exception.msg)
  334. self.store.get_user_by_id.assert_called_with(USER_ID)