Skip to content

Commit 7562d88

Browse files
authored
Change the format of access tokens away from macaroons (matrix-org#5588)
1 parent affaffb commit 7562d88

File tree

9 files changed

+78
-103
lines changed

9 files changed

+78
-103
lines changed

changelog.d/5588.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Reduce the length of Synapse's access tokens.

scripts-dev/dump_macaroon.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#!/usr/bin/env python2
1+
#!/usr/bin/env python
22

33
import sys
44

synapse/handlers/auth.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import time
1818
import unicodedata
1919
import urllib.parse
20+
from binascii import crc32
2021
from typing import (
2122
TYPE_CHECKING,
2223
Any,
@@ -34,6 +35,7 @@
3435
import attr
3536
import bcrypt
3637
import pymacaroons
38+
import unpaddedbase64
3739

3840
from twisted.web.server import Request
3941

@@ -66,6 +68,7 @@
6668
from synapse.util.async_helpers import maybe_awaitable
6769
from synapse.util.macaroons import get_value_from_macaroon, satisfy_expiry
6870
from synapse.util.msisdn import phone_number_to_msisdn
71+
from synapse.util.stringutils import base62_encode
6972
from synapse.util.threepids import canonicalise_email
7073

7174
if TYPE_CHECKING:
@@ -808,18 +811,20 @@ async def get_access_token_for_user_id(
808811
logger.info(
809812
"Logging in user %s as %s%s", user_id, puppets_user_id, fmt_expiry
810813
)
814+
target_user_id_obj = UserID.from_string(puppets_user_id)
811815
else:
812816
logger.info(
813817
"Logging in user %s on device %s%s", user_id, device_id, fmt_expiry
814818
)
819+
target_user_id_obj = UserID.from_string(user_id)
815820

816821
if (
817822
not is_appservice_ghost
818823
or self.hs.config.appservice.track_appservice_user_ips
819824
):
820825
await self.auth.check_auth_blocking(user_id)
821826

822-
access_token = self.macaroon_gen.generate_access_token(user_id)
827+
access_token = self.generate_access_token(target_user_id_obj)
823828
await self.store.add_access_token_to_user(
824829
user_id=user_id,
825830
token=access_token,
@@ -1192,6 +1197,19 @@ async def _check_local_password(self, user_id: str, password: str) -> Optional[s
11921197
return None
11931198
return user_id
11941199

1200+
def generate_access_token(self, for_user: UserID) -> str:
1201+
"""Generates an opaque string, for use as an access token"""
1202+
1203+
# we use the following format for access tokens:
1204+
# syt_<base64 local part>_<random string>_<base62 crc check>
1205+
1206+
b64local = unpaddedbase64.encode_base64(for_user.localpart.encode("utf-8"))
1207+
random_string = stringutils.random_string(20)
1208+
base = f"syt_{b64local}_{random_string}"
1209+
1210+
crc = base62_encode(crc32(base.encode("ascii")), minwidth=6)
1211+
return f"{base}_{crc}"
1212+
11951213
async def validate_short_term_login_token(
11961214
self, login_token: str
11971215
) -> LoginTokenAttributes:
@@ -1585,19 +1603,15 @@ class MacaroonGenerator:
15851603

15861604
hs = attr.ib()
15871605

1588-
def generate_access_token(
1589-
self, user_id: str, extra_caveats: Optional[List[str]] = None
1590-
) -> str:
1591-
extra_caveats = extra_caveats or []
1606+
def generate_guest_access_token(self, user_id: str) -> str:
15921607
macaroon = self._generate_base_macaroon(user_id)
15931608
macaroon.add_first_party_caveat("type = access")
15941609
# Include a nonce, to make sure that each login gets a different
15951610
# access token.
15961611
macaroon.add_first_party_caveat(
15971612
"nonce = %s" % (stringutils.random_string_with_symbols(16),)
15981613
)
1599-
for caveat in extra_caveats:
1600-
macaroon.add_first_party_caveat(caveat)
1614+
macaroon.add_first_party_caveat("guest = true")
16011615
return macaroon.serialize()
16021616

16031617
def generate_short_term_login_token(

synapse/handlers/register.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -722,9 +722,7 @@ class and RegisterDeviceReplicationServlet.
722722
)
723723
if is_guest:
724724
assert valid_until_ms is None
725-
access_token = self.macaroon_gen.generate_access_token(
726-
user_id, ["guest = true"]
727-
)
725+
access_token = self.macaroon_gen.generate_guest_access_token(user_id)
728726
else:
729727
access_token = await self._auth_handler.get_access_token_for_user_id(
730728
user_id,

synapse/util/stringutils.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,3 +220,23 @@ def strtobool(val: str) -> bool:
220220
return False
221221
else:
222222
raise ValueError("invalid truth value %r" % (val,))
223+
224+
225+
_BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
226+
227+
228+
def base62_encode(num: int, minwidth: int = 1) -> str:
229+
"""Encode a number using base62
230+
231+
Args:
232+
num: number to be encoded
233+
minwidth: width to pad to, if the number is small
234+
"""
235+
res = ""
236+
while num:
237+
num, rem = divmod(num, 62)
238+
res = _BASE62[rem] + res
239+
240+
# pad to minimum width
241+
pad = "0" * (minwidth - len(res))
242+
return pad + res

tests/api/test_auth.py

Lines changed: 0 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,11 @@
2121
from synapse.api.errors import (
2222
AuthError,
2323
Codes,
24-
InvalidClientCredentialsError,
2524
InvalidClientTokenError,
2625
MissingClientTokenError,
2726
ResourceLimitError,
2827
)
2928
from synapse.storage.databases.main.registration import TokenLookupResult
30-
from synapse.types import UserID
3129

3230
from tests import unittest
3331
from tests.test_utils import simple_async_mock
@@ -253,67 +251,6 @@ def test_get_guest_user_from_macaroon(self):
253251
self.assertTrue(user_info.is_guest)
254252
self.store.get_user_by_id.assert_called_with(user_id)
255253

256-
def test_cannot_use_regular_token_as_guest(self):
257-
USER_ID = "@percy:matrix.org"
258-
self.store.add_access_token_to_user = simple_async_mock(None)
259-
self.store.get_device = simple_async_mock(None)
260-
261-
token = self.get_success(
262-
self.hs.get_auth_handler().get_access_token_for_user_id(
263-
USER_ID, "DEVICE", valid_until_ms=None
264-
)
265-
)
266-
self.store.add_access_token_to_user.assert_called_with(
267-
user_id=USER_ID,
268-
token=token,
269-
device_id="DEVICE",
270-
valid_until_ms=None,
271-
puppets_user_id=None,
272-
)
273-
274-
async def get_user(tok):
275-
if token != tok:
276-
return None
277-
return TokenLookupResult(
278-
user_id=USER_ID,
279-
is_guest=False,
280-
token_id=1234,
281-
device_id="DEVICE",
282-
)
283-
284-
self.store.get_user_by_access_token = get_user
285-
self.store.get_user_by_id = simple_async_mock({"is_guest": False})
286-
287-
# check the token works
288-
request = Mock(args={})
289-
request.args[b"access_token"] = [token.encode("ascii")]
290-
request.requestHeaders.getRawHeaders = mock_getRawHeaders()
291-
requester = self.get_success(
292-
self.auth.get_user_by_req(request, allow_guest=True)
293-
)
294-
self.assertEqual(UserID.from_string(USER_ID), requester.user)
295-
self.assertFalse(requester.is_guest)
296-
297-
# add an is_guest caveat
298-
mac = pymacaroons.Macaroon.deserialize(token)
299-
mac.add_first_party_caveat("guest = true")
300-
guest_tok = mac.serialize()
301-
302-
# the token should *not* work now
303-
request = Mock(args={})
304-
request.args[b"access_token"] = [guest_tok.encode("ascii")]
305-
request.requestHeaders.getRawHeaders = mock_getRawHeaders()
306-
307-
cm = self.get_failure(
308-
self.auth.get_user_by_req(request, allow_guest=True),
309-
InvalidClientCredentialsError,
310-
)
311-
312-
self.assertEqual(401, cm.value.code)
313-
self.assertEqual("Guest access token used for regular user", cm.value.msg)
314-
315-
self.store.get_user_by_id.assert_called_with(USER_ID)
316-
317254
def test_blocking_mau(self):
318255
self.auth_blocking._limit_usage_by_mau = False
319256
self.auth_blocking._max_mau_value = 50

tests/handlers/test_auth.py

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,17 @@
1616
import pymacaroons
1717

1818
from synapse.api.errors import AuthError, ResourceLimitError
19+
from synapse.rest import admin
1920

2021
from tests import unittest
2122
from tests.test_utils import make_awaitable
2223

2324

2425
class AuthTestCase(unittest.HomeserverTestCase):
26+
servlets = [
27+
admin.register_servlets,
28+
]
29+
2530
def prepare(self, reactor, clock, hs):
2631
self.auth_handler = hs.get_auth_handler()
2732
self.macaroon_generator = hs.get_macaroon_generator()
@@ -35,16 +40,10 @@ def prepare(self, reactor, clock, hs):
3540
self.small_number_of_users = 1
3641
self.large_number_of_users = 100
3742

38-
def test_token_is_a_macaroon(self):
39-
token = self.macaroon_generator.generate_access_token("some_user")
40-
# Check that we can parse the thing with pymacaroons
41-
macaroon = pymacaroons.Macaroon.deserialize(token)
42-
# The most basic of sanity checks
43-
if "some_user" not in macaroon.inspect():
44-
self.fail("some_user was not in %s" % macaroon.inspect())
43+
self.user1 = self.register_user("a_user", "pass")
4544

4645
def test_macaroon_caveats(self):
47-
token = self.macaroon_generator.generate_access_token("a_user")
46+
token = self.macaroon_generator.generate_guest_access_token("a_user")
4847
macaroon = pymacaroons.Macaroon.deserialize(token)
4948

5049
def verify_gen(caveat):
@@ -59,19 +58,23 @@ def verify_type(caveat):
5958
def verify_nonce(caveat):
6059
return caveat.startswith("nonce =")
6160

61+
def verify_guest(caveat):
62+
return caveat == "guest = true"
63+
6264
v = pymacaroons.Verifier()
6365
v.satisfy_general(verify_gen)
6466
v.satisfy_general(verify_user)
6567
v.satisfy_general(verify_type)
6668
v.satisfy_general(verify_nonce)
69+
v.satisfy_general(verify_guest)
6770
v.verify(macaroon, self.hs.config.macaroon_secret_key)
6871

6972
def test_short_term_login_token_gives_user_id(self):
7073
token = self.macaroon_generator.generate_short_term_login_token(
71-
"a_user", "", 5000
74+
self.user1, "", 5000
7275
)
7376
res = self.get_success(self.auth_handler.validate_short_term_login_token(token))
74-
self.assertEqual("a_user", res.user_id)
77+
self.assertEqual(self.user1, res.user_id)
7578
self.assertEqual("", res.auth_provider_id)
7679

7780
# when we advance the clock, the token should be rejected
@@ -83,22 +86,22 @@ def test_short_term_login_token_gives_user_id(self):
8386

8487
def test_short_term_login_token_gives_auth_provider(self):
8588
token = self.macaroon_generator.generate_short_term_login_token(
86-
"a_user", auth_provider_id="my_idp"
89+
self.user1, auth_provider_id="my_idp"
8790
)
8891
res = self.get_success(self.auth_handler.validate_short_term_login_token(token))
89-
self.assertEqual("a_user", res.user_id)
92+
self.assertEqual(self.user1, res.user_id)
9093
self.assertEqual("my_idp", res.auth_provider_id)
9194

9295
def test_short_term_login_token_cannot_replace_user_id(self):
9396
token = self.macaroon_generator.generate_short_term_login_token(
94-
"a_user", "", 5000
97+
self.user1, "", 5000
9598
)
9699
macaroon = pymacaroons.Macaroon.deserialize(token)
97100

98101
res = self.get_success(
99102
self.auth_handler.validate_short_term_login_token(macaroon.serialize())
100103
)
101-
self.assertEqual("a_user", res.user_id)
104+
self.assertEqual(self.user1, res.user_id)
102105

103106
# add another "user_id" caveat, which might allow us to override the
104107
# user_id.
@@ -114,7 +117,7 @@ def test_mau_limits_disabled(self):
114117
# Ensure does not throw exception
115118
self.get_success(
116119
self.auth_handler.get_access_token_for_user_id(
117-
"user_a", device_id=None, valid_until_ms=None
120+
self.user1, device_id=None, valid_until_ms=None
118121
)
119122
)
120123

@@ -132,7 +135,7 @@ def test_mau_limits_exceeded_large(self):
132135

133136
self.get_failure(
134137
self.auth_handler.get_access_token_for_user_id(
135-
"user_a", device_id=None, valid_until_ms=None
138+
self.user1, device_id=None, valid_until_ms=None
136139
),
137140
ResourceLimitError,
138141
)
@@ -160,7 +163,7 @@ def test_mau_limits_parity(self):
160163
# If not in monthly active cohort
161164
self.get_failure(
162165
self.auth_handler.get_access_token_for_user_id(
163-
"user_a", device_id=None, valid_until_ms=None
166+
self.user1, device_id=None, valid_until_ms=None
164167
),
165168
ResourceLimitError,
166169
)
@@ -177,7 +180,7 @@ def test_mau_limits_parity(self):
177180
)
178181
self.get_success(
179182
self.auth_handler.get_access_token_for_user_id(
180-
"user_a", device_id=None, valid_until_ms=None
183+
self.user1, device_id=None, valid_until_ms=None
181184
)
182185
)
183186
self.get_success(
@@ -195,7 +198,7 @@ def test_mau_limits_not_exceeded(self):
195198
# Ensure does not raise exception
196199
self.get_success(
197200
self.auth_handler.get_access_token_for_user_id(
198-
"user_a", device_id=None, valid_until_ms=None
201+
self.user1, device_id=None, valid_until_ms=None
199202
)
200203
)
201204

@@ -210,6 +213,6 @@ def test_mau_limits_not_exceeded(self):
210213

211214
def _get_macaroon(self):
212215
token = self.macaroon_generator.generate_short_term_login_token(
213-
"user_a", "", 5000
216+
self.user1, "", 5000
214217
)
215218
return pymacaroons.Macaroon.deserialize(token)

tests/handlers/test_register.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,6 @@ def prepare(self, reactor, clock, hs):
4848
self.mock_distributor = Mock()
4949
self.mock_distributor.declare("registered_user")
5050
self.mock_captcha_client = Mock()
51-
self.macaroon_generator = Mock(
52-
generate_access_token=Mock(return_value="secret")
53-
)
54-
self.hs.get_macaroon_generator = Mock(return_value=self.macaroon_generator)
5551
self.handler = self.hs.get_registration_handler()
5652
self.store = self.hs.get_datastore()
5753
self.lots_of_users = 100
@@ -67,8 +63,8 @@ def test_user_is_created_and_logged_in_if_doesnt_exist(self):
6763
self.get_or_create_user(requester, frank.localpart, "Frankie")
6864
)
6965
self.assertEquals(result_user_id, user_id)
70-
self.assertTrue(result_token is not None)
71-
self.assertEquals(result_token, "secret")
66+
self.assertIsInstance(result_token, str)
67+
self.assertGreater(len(result_token), 20)
7268

7369
def test_if_user_exists(self):
7470
store = self.hs.get_datastore()
@@ -500,7 +496,7 @@ def check_registration_for_spam(
500496
user_id = self.get_success(self.handler.register_user(localpart="user"))
501497

502498
# Get an access token.
503-
token = self.macaroon_generator.generate_access_token(user_id)
499+
token = "testtok"
504500
self.get_success(
505501
self.store.add_access_token_to_user(
506502
user_id=user_id, token=token, device_id=None, valid_until_ms=None
@@ -577,7 +573,7 @@ async def get_or_create_user(
577573

578574
user = UserID(localpart, self.hs.hostname)
579575
user_id = user.to_string()
580-
token = self.macaroon_generator.generate_access_token(user_id)
576+
token = self.hs.get_auth_handler().generate_access_token(user)
581577

582578
if need_register:
583579
await self.handler.register_with_store(

0 commit comments

Comments
 (0)