Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.
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/8320.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add `uk.half-shot.msc2778.login.application_service` login type to allow appservices to login.
49 changes: 39 additions & 10 deletions synapse/rest/client/v1/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

from synapse.api.errors import Codes, LoginError, SynapseError
from synapse.api.ratelimiting import Ratelimiter
from synapse.appservice import ApplicationService
from synapse.handlers.auth import (
convert_client_dict_legacy_fields_to_identifier,
login_id_phone_to_thirdparty,
Expand All @@ -44,6 +45,7 @@ class LoginRestServlet(RestServlet):
TOKEN_TYPE = "m.login.token"
JWT_TYPE = "org.matrix.login.jwt"
JWT_TYPE_DEPRECATED = "m.login.jwt"
APPSERVICE_TYPE = "uk.half-shot.msc2778.login.application_service"
Comment thread
Half-Shot marked this conversation as resolved.

def __init__(self, hs):
super(LoginRestServlet, self).__init__()
Expand All @@ -61,6 +63,8 @@ def __init__(self, hs):
self.cas_enabled = hs.config.cas_enabled
self.oidc_enabled = hs.config.oidc_enabled

self.auth = hs.get_auth()

self.auth_handler = self.hs.get_auth_handler()
self.registration_handler = hs.get_registration_handler()
self.handlers = hs.get_handlers()
Expand Down Expand Up @@ -107,6 +111,8 @@ def on_GET(self, request: SynapseRequest):
({"type": t} for t in self.auth_handler.get_supported_login_types())
)

flows.append({"type": LoginRestServlet.APPSERVICE_TYPE})

return 200, {"flows": flows}

def on_OPTIONS(self, request: SynapseRequest):
Expand All @@ -116,8 +122,12 @@ async def on_POST(self, request: SynapseRequest):
self._address_ratelimiter.ratelimit(request.getClientIP())

login_submission = parse_json_object_from_request(request)

try:
if self.jwt_enabled and (
if login_submission["type"] == LoginRestServlet.APPSERVICE_TYPE:
appservice = self.auth.get_appservice_by_req(request)
result = await self._do_appservice_login(login_submission, appservice)
elif self.jwt_enabled and (
login_submission["type"] == LoginRestServlet.JWT_TYPE
or login_submission["type"] == LoginRestServlet.JWT_TYPE_DEPRECATED
):
Expand All @@ -134,6 +144,33 @@ async def on_POST(self, request: SynapseRequest):
result["well_known"] = well_known_data
return 200, result

def _get_qualified_user_id(self, identifier):
if identifier["type"] != "m.id.user":
raise SynapseError(400, "Unknown login identifier type")
if "user" not in identifier:
raise SynapseError(400, "User identifier is missing 'user' key")

if identifier["user"].startswith("@"):
return identifier["user"]
else:
return UserID(identifier["user"], self.hs.hostname).to_string()

async def _do_appservice_login(
self, login_submission: JsonDict, appservice: ApplicationService
):
logger.info(
"Got appservice login request with identifier: %r",
login_submission.get("identifier"),
)

identifier = convert_client_dict_legacy_fields_to_identifier(login_submission)
Comment thread
Half-Shot marked this conversation as resolved.
qualified_user_id = self._get_qualified_user_id(identifier)

if not appservice.is_interested_in_user(qualified_user_id):
raise LoginError(403, "Invalid access_token", errcode=Codes.FORBIDDEN)

return await self._complete_login(qualified_user_id, login_submission)

async def _do_other_login(self, login_submission: JsonDict) -> Dict[str, str]:
"""Handle non-token/saml/jwt logins

Expand Down Expand Up @@ -219,15 +256,7 @@ async def _do_other_login(self, login_submission: JsonDict) -> Dict[str, str]:

# by this point, the identifier should be an m.id.user: if it's anything
# else, we haven't understood it.
if identifier["type"] != "m.id.user":
raise SynapseError(400, "Unknown login identifier type")
if "user" not in identifier:
raise SynapseError(400, "User identifier is missing 'user' key")

if identifier["user"].startswith("@"):
qualified_user_id = identifier["user"]
else:
qualified_user_id = UserID(identifier["user"], self.hs.hostname).to_string()
qualified_user_id = self._get_qualified_user_id(identifier)

# Check if we've hit the failed ratelimit (but don't update it)
self._failed_attempts_ratelimiter.ratelimit(
Expand Down
134 changes: 133 additions & 1 deletion tests/rest/client/v1/test_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
import jwt

import synapse.rest.admin
from synapse.appservice import ApplicationService
from synapse.rest.client.v1 import login, logout
from synapse.rest.client.v2_alpha import devices
from synapse.rest.client.v2_alpha import devices, register
from synapse.rest.client.v2_alpha.account import WhoamiRestServlet

from tests import unittest
Expand Down Expand Up @@ -748,3 +749,134 @@ def test_login_jwt_invalid_signature(self):
channel.json_body["error"],
"JWT validation failed: Signature verification failed",
)


AS_USER = "as_user_alice"


class AppserviceLoginRestServletTestCase(unittest.HomeserverTestCase):
servlets = [
login.register_servlets,
register.register_servlets,
]

def register_as_user(self, username):
request, channel = self.make_request(
b"POST",
"/_matrix/client/r0/register?access_token=%s" % (self.service.token,),
{"username": username},
)
self.render(request)

def make_homeserver(self, reactor, clock):
self.hs = self.setup_test_homeserver()

self.service = ApplicationService(
id="unique_identifier",
token="some_token",
hostname="example.com",
sender="@asbot:example.com",
namespaces={
ApplicationService.NS_USERS: [
{"regex": r"@as_user.*", "exclusive": False}
],
ApplicationService.NS_ROOMS: [],
ApplicationService.NS_ALIASES: [],
},
)
self.another_service = ApplicationService(
id="another__identifier",
token="another_token",
hostname="example.com",
sender="@as2bot:example.com",
namespaces={
ApplicationService.NS_USERS: [
{"regex": r"@as2_user.*", "exclusive": False}
],
ApplicationService.NS_ROOMS: [],
ApplicationService.NS_ALIASES: [],
},
)

self.hs.get_datastore().services_cache.append(self.service)
self.hs.get_datastore().services_cache.append(self.another_service)
return self.hs

def test_login_appservice_user(self):
"""Test that an appservice user can use /login
"""
self.register_as_user(AS_USER)

params = {
"type": login.LoginRestServlet.APPSERVICE_TYPE,
"identifier": {"type": "m.id.user", "user": AS_USER},
}
request, channel = self.make_request(
b"POST", LOGIN_URL, params, access_token=self.service.token
)

self.render(request)
self.assertEquals(channel.result["code"], b"200", channel.result)

def test_login_appservice_user_bot(self):
"""Test that the appservice bot can use /login
"""
self.register_as_user(AS_USER)

params = {
"type": login.LoginRestServlet.APPSERVICE_TYPE,
"identifier": {"type": "m.id.user", "user": self.service.sender},
}
request, channel = self.make_request(
b"POST", LOGIN_URL, params, access_token=self.service.token
)

self.render(request)
self.assertEquals(channel.result["code"], b"200", channel.result)

def test_login_appservice_wrong_user(self):
"""Test that non-as users cannot login with the as token
"""
self.register_as_user(AS_USER)

params = {
"type": login.LoginRestServlet.APPSERVICE_TYPE,
"identifier": {"type": "m.id.user", "user": "fibble_wibble"},
}
request, channel = self.make_request(
b"POST", LOGIN_URL, params, access_token=self.service.token
)

self.render(request)
self.assertEquals(channel.result["code"], b"403", channel.result)

def test_login_appservice_wrong_as(self):
"""Test that as users cannot login with wrong as token
"""
self.register_as_user(AS_USER)

params = {
"type": login.LoginRestServlet.APPSERVICE_TYPE,
"identifier": {"type": "m.id.user", "user": AS_USER},
}
request, channel = self.make_request(
b"POST", LOGIN_URL, params, access_token=self.another_service.token
)

self.render(request)
self.assertEquals(channel.result["code"], b"403", channel.result)

def test_login_appservice_no_token(self):
"""Test that users must provide a token when using the appservice
login method
"""
self.register_as_user(AS_USER)

params = {
"type": login.LoginRestServlet.APPSERVICE_TYPE,
"identifier": {"type": "m.id.user", "user": AS_USER},
}
request, channel = self.make_request(b"POST", LOGIN_URL, params)

self.render(request)
self.assertEquals(channel.result["code"], b"401", channel.result)