Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/18174.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support device IDs that can't be represented in a scope when delegating auth to Matrix Authentication Service 0.15.0+.
49 changes: 32 additions & 17 deletions synapse/api/auth/msc3861_delegated.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,9 @@ async def _introspect_token(self, token: str) -> IntrospectionToken:
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": str(self._http_client.user_agent, "utf-8"),
"Accept": "application/json",
# Tell MAS that we support reading the device ID as an explicit
# value, not encoded in the scope. This is supported by MAS 0.15+
"X-MAS-Supports-Device-Id": "1",
}

args = {"token": token, "token_type_hint": "access_token"}
Expand Down Expand Up @@ -409,29 +412,41 @@ async def get_user_by_access_token(
else:
user_id = UserID.from_string(user_id_str)

# Find device_ids in scope
# We only allow a single device_id in the scope, so we find them all in the
# scope list, and raise if there are more than one. The OIDC server should be
# the one enforcing valid scopes, so we raise a 500 if we find an invalid scope.
device_ids = [
tok[len(SCOPE_MATRIX_DEVICE_PREFIX) :]
for tok in scope
if tok.startswith(SCOPE_MATRIX_DEVICE_PREFIX)
]

if len(device_ids) > 1:
raise AuthError(
500,
"Multiple device IDs in scope",
)
# MAS 0.15+ will give us the device ID as an explicit value for compatibility sessions
# If present, we get it from here, if not we get it in thee scope
device_id = introspection_result.get("device_id")
if device_id is not None:
# We got the device ID explicitly, just sanity check that it's a string
if not isinstance(device_id, str):
raise AuthError(
500,
"Invalid device ID in introspection result",
)
else:
# Find device_ids in scope
# We only allow a single device_id in the scope, so we find them all in the
# scope list, and raise if there are more than one. The OIDC server should be
# the one enforcing valid scopes, so we raise a 500 if we find an invalid scope.
device_ids = [
tok[len(SCOPE_MATRIX_DEVICE_PREFIX) :]
for tok in scope
if tok.startswith(SCOPE_MATRIX_DEVICE_PREFIX)
]

if len(device_ids) > 1:
raise AuthError(
500,
"Multiple device IDs in scope",
)

device_id = device_ids[0] if device_ids else None

device_id = device_ids[0] if device_ids else None
if device_id is not None:
# Sanity check the device_id
if len(device_id) > 255 or len(device_id) < 1:
raise AuthError(
500,
"Invalid device ID in scope",
"Invalid device ID in introspection result",
)

# Create the device on the fly if it does not exist
Expand Down
38 changes: 38 additions & 0 deletions tests/handlers/test_oauth_delegation.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,44 @@ def test_active_user_with_device(self) -> None:
)
self.assertEqual(requester.device_id, DEVICE)

def test_active_user_with_device_explicit_device_id(self) -> None:
"""The handler should return a requester with normal user rights and a device ID, given explicitly, as supported by MAS 0.15+"""

self.http_client.request = AsyncMock(
return_value=FakeResponse.json(
code=200,
payload={
"active": True,
"sub": SUBJECT,
"scope": " ".join([MATRIX_USER_SCOPE]),
"device_id": DEVICE,
"username": USERNAME,
},
)
)
request = Mock(args={})
request.args[b"access_token"] = [b"mockAccessToken"]
request.requestHeaders.getRawHeaders = mock_getRawHeaders()
requester = self.get_success(self.auth.get_user_by_req(request))
self.http_client.get_json.assert_called_once_with(WELL_KNOWN)
self.http_client.request.assert_called_once_with(
method="POST", uri=INTROSPECTION_ENDPOINT, data=ANY, headers=ANY
)
# It should have called with the 'X-MAS-Supports-Device-Id: 1' header
self.assertEqual(
self.http_client.request.call_args[1]["headers"].getRawHeaders(
b"X-MAS-Supports-Device-Id",
),
[b"1"],
)
self._assertParams()
self.assertEqual(requester.user.to_string(), "@%s:%s" % (USERNAME, SERVER_NAME))
self.assertEqual(requester.is_guest, False)
self.assertEqual(
get_awaitable_result(self.auth.is_server_admin(requester)), False
)
self.assertEqual(requester.device_id, DEVICE)

def test_multiple_devices(self) -> None:
"""The handler should raise an error if multiple devices are found in the scope."""

Expand Down
Loading