Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Change the format of access tokens away from macaroons #5588

Merged
merged 15 commits into from
May 12, 2021
20 changes: 17 additions & 3 deletions synapse/handlers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import time
import unicodedata
import urllib.parse
from binascii import crc32
from typing import (
TYPE_CHECKING,
Any,
Expand All @@ -34,6 +35,7 @@
import attr
import bcrypt
import pymacaroons
import unpaddedbase64

from twisted.web.server import Request

Expand Down Expand Up @@ -66,6 +68,7 @@
from synapse.util.async_helpers import maybe_awaitable
from synapse.util.macaroons import get_value_from_macaroon, satisfy_expiry
from synapse.util.msisdn import phone_number_to_msisdn
from synapse.util.stringutils import base62_encode
from synapse.util.threepids import canonicalise_email

if TYPE_CHECKING:
Expand Down Expand Up @@ -808,18 +811,20 @@ async def get_access_token_for_user_id(
logger.info(
"Logging in user %s as %s%s", user_id, puppets_user_id, fmt_expiry
)
target_user_id_obj = UserID.from_string(puppets_user_id)
else:
logger.info(
"Logging in user %s on device %s%s", user_id, device_id, fmt_expiry
)
target_user_id_obj = UserID.from_string(user_id)

if (
not is_appservice_ghost
or self.hs.config.appservice.track_appservice_user_ips
):
await self.auth.check_auth_blocking(user_id)

access_token = self.generate_access_token()
access_token = self.generate_access_token(target_user_id_obj)
await self.store.add_access_token_to_user(
user_id=user_id,
token=access_token,
Expand Down Expand Up @@ -1192,9 +1197,18 @@ async def _check_local_password(self, user_id: str, password: str) -> Optional[s
return None
return user_id

def generate_access_token(self):
def generate_access_token(self, for_user: UserID) -> str:
"""Generates an opaque string, for use as an access token"""
return stringutils.random_string(20)

# we use the following format for access tokens:
# syt_<base64 local part>_<random string>_<base62 crc check>

b64local = unpaddedbase64.encode_base64(for_user.localpart.encode("utf-8"))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a thought occurs. The use of base64 here means that access tokens will have to be correctly url-encoded when used in a query param.

maybe we should use a url-safe base64 encoding.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, hmm. Good point.

I had a quick look at haproxy support and it looks like b64dec only handles standard padded base64 (2.4 will have support for padded url safe base64 decoding). Don't that is a huge concern though, since I think conversion is relatively easy with simple string replacement?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

of course, we could just say that only people with spec-compliant mxids are allowed to use matrix, so it doesn't need encoding at all 😈

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love it :D

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this needs to particularly influence us; we should be deprecating and removing GET based access token use, as it's just a bad idea for http access logs outside of synapse, perhaps the effort of ensuring your access tokens are urlencoded correctly will push developers towards doing the right thing.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's fair. it's really not that hard to do it right, even from a shell command.

random_string = stringutils.random_string(20)
base = f"syt_{b64local}_{random_string}"

crc = base62_encode(crc32(base.encode("ascii")), minwidth=6)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we using base62 for the crc? Is that just how its typically done?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wfm

return f"{base}_{crc}"

async def validate_short_term_login_token(
self, login_token: str
Expand Down
20 changes: 20 additions & 0 deletions synapse/util/stringutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,3 +220,23 @@ def strtobool(val: str) -> bool:
return False
else:
raise ValueError("invalid truth value %r" % (val,))


_BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"


def base62_encode(num: int, minwidth: int = 1) -> str:
"""Encode a number using base62

Args:
num: number to be encoded
minwidth: width to pad to, if the number is small
"""
res = ""
while num:
num, rem = divmod(num, 62)
res = _BASE62[rem] + res

# pad to minimum width
pad = "0" * (minwidth - len(res))
return pad + res
4 changes: 2 additions & 2 deletions tests/handlers/test_register.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,7 @@ def check_registration_for_spam(
user_id = self.get_success(self.handler.register_user(localpart="user"))

# Get an access token.
token = self.hs.get_auth_handler().generate_access_token()
token = "testtok"
self.get_success(
self.store.add_access_token_to_user(
user_id=user_id, token=token, device_id=None, valid_until_ms=None
Expand Down Expand Up @@ -577,7 +577,7 @@ async def get_or_create_user(

user = UserID(localpart, self.hs.hostname)
user_id = user.to_string()
token = self.hs.get_auth_handler().generate_access_token()
token = self.hs.get_auth_handler().generate_access_token(user)

if need_register:
await self.handler.register_with_store(
Expand Down
8 changes: 7 additions & 1 deletion tests/util/test_stringutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# limitations under the License.

from synapse.api.errors import SynapseError
from synapse.util.stringutils import assert_valid_client_secret
from synapse.util.stringutils import assert_valid_client_secret, base62_encode

from .. import unittest

Expand Down Expand Up @@ -45,3 +45,9 @@ def test_client_secret_regex(self):
for client_secret in bad:
with self.assertRaises(SynapseError):
assert_valid_client_secret(client_secret)

def test_base62_encode(self):
self.assertEqual("0", base62_encode(0))
self.assertEqual("10", base62_encode(62))
self.assertEqual("1c", base62_encode(100))
self.assertEqual("001c", base62_encode(100, minwidth=4))