Repository: matrix-org/synapse

Issue:
CAS validation issue with synapse
<!-- 

**IF YOU HAVE SUPPORT QUESTIONS ABOUT RUNNING OR CONFIGURING YOUR OWN HOME SERVER**: 
You will likely get better support more quickly if you ask in ** #matrix:matrix.org ** ;)


This is a bug report template. By following the instructions below and
filling out the sections with your information, you will help the us to get all
the necessary data to fix your issue.

You can also preview your report before submitting it. You may remove sections
that aren't relevant to your particular case.

Text between <!-- and --​> marks will be invisible in the report.

-->

Hello,

### Description

It seems there is a problem with how CAS validation is handled in [login.py#L420](https://github.com/matrix-org/synapse/blob/632baf799ea876d2346e934dce90d6a24cb92e37/synapse/rest/client/v1/login.py#L420).

### Steps to reproduce

- Install and configure CAS 5.1.x (I didn't try with earlier ones)
- Install matrix-synapse
- Configure matrix-synapse to use CAS to connect with `service_url: https://matrix.domain`
- Try to login!

You'll see it doesn't work and if I'm not mistaken it is because in [login.py#L397](https://github.com/matrix-org/synapse/blob/632baf799ea876d2346e934dce90d6a24cb92e37/synapse/rest/client/v1/login.py#L397) we ask for a ticket with a service in the form of `https://matrix.domain/_matrix/client/api/v1/login/cas/ticket?redirectUrl=***`.
But when we validate the ticket, we only send `cas_service_url` which lacks the `/_matrix/client/api/v1/login/cas/ticket?redirectUrl=***` part so CAS fails the validation saying the ticket was used for another service than the one it was created for.

I don't know if earlier versions were more lenient on validation, but I got it to work by replacing line 420 by this line:
```python
"service": self.cas_service_url + request.uri[:request.uri.find("&ticket")]
```

But I'm sure there's a better way to do it...

I believe #2404 may be related to this problem.

### Version information

- **Homeserver**: local homeserver

If not matrix.org:
- **Version**: matrix-synapse 0.24.1-1    
- **Install method**: Debian repository
- **Platform**: VM on Debian9


Discussion:
same issue
The problem still exists in matrix-synapse-py3 0.99.2+stretch1

[matrix-synapse-0.99.2-CAS-fix.diff.gz](https://github.com/matrix-org/synapse/files/2949876/matrix-synapse-0.99.2-CAS-fix.diff.gz)

The service URL given to CAS is not supposed to change as long as synapse configuration stays the same, so we can have CasRedirectServlet store the url somewhere and later get it back in CasTicketServlet

This ticket is s old. I came across the same problem. So how are others using cas. It seems impossible to me, or can there be a less strict validation on the cas side?

we should implement the patch though! thanks @mijutu 
I had the same problem. I can confirm the patch works.
// EDIT:

After we implemented JSON in our CAS Server, it seems to be working BUT we had to disable the service url validation in our case to get this working. So this Bug is still there.

Why is no one fixing it or applying the provided workaround/fix?
The old patch didn't apply, so I made a new one.

[matrix-synapse-1.5.1-CAS-fix.diff.gz](https://github.com/matrix-org/synapse/files/3819383/matrix-synapse-1.5.1-CAS-fix.diff.gz)

so somebody opened a pull request with @mijutu's patch at https://github.com/matrix-org/synapse/pull/5044; however I had questions about it which were never answered so we had to close the PR.

@mijutu: perhaps you could open a PR so that we can work on landing your changes?
We had a pull request and there richvdh commented that the solution I used was wrong. I think he is correct. If two CAS logins happen at the same time, then users might end up at a wrong url.

Probability of two simultaneous CAS logins is small because CAS is used only at the initial login. Browsers remember access tokens and CAS is rarely used.

Homeserver owners might be able to use my patch for months or years without having issues, but if it's wrong, then it's wrong and shouldn't be merged.
[matrix-synapse-1.7.2-CAS-fix.diff.gz](https://github.com/matrix-org/synapse/files/4012323/matrix-synapse-1.7.2-CAS-fix.diff.gz)

Here is a better fix for the problem. CasTicketServlet generates the service parameter from synapse config and redirectUrl parameter of the request.
@mijutu please could you open a PR with your patch so that we can read and discuss it?

Resolving patch:
diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py
--- a/synapse/rest/client/v1/login.py
+++ b/synapse/rest/client/v1/login.py
@@ -73,6 +73,14 @@ def login_id_thirdparty_from_phone(identifier):
     return {"type": "m.id.thirdparty", "medium": "msisdn", "address": msisdn}
 
 
+def build_service_param(cas_service_url, client_redirect_url):
+    return "%s%s?redirectUrl=%s" % (
+        cas_service_url,
+        "/_matrix/client/r0/login/cas/ticket",
+        urllib.parse.quote(client_redirect_url, safe=""),
+    )
+
+
 class LoginRestServlet(RestServlet):
     PATTERNS = client_patterns("/login$", v1=True)
     CAS_TYPE = "m.login.cas"
@@ -428,18 +436,15 @@ def get_sso_url(self, client_redirect_url):
 class CasRedirectServlet(BaseSSORedirectServlet):
     def __init__(self, hs):
         super(CasRedirectServlet, self).__init__()
-        self.cas_server_url = hs.config.cas_server_url.encode("ascii")
-        self.cas_service_url = hs.config.cas_service_url.encode("ascii")
+        self.cas_server_url = hs.config.cas_server_url
+        self.cas_service_url = hs.config.cas_service_url
 
     def get_sso_url(self, client_redirect_url):
-        client_redirect_url_param = urllib.parse.urlencode(
-            {b"redirectUrl": client_redirect_url}
-        ).encode("ascii")
-        hs_redirect_url = self.cas_service_url + b"/_matrix/client/r0/login/cas/ticket"
-        service_param = urllib.parse.urlencode(
-            {b"service": b"%s?%s" % (hs_redirect_url, client_redirect_url_param)}
-        ).encode("ascii")
-        return b"%s/login?%s" % (self.cas_server_url, service_param)
+        args = urllib.parse.urlencode(
+            {"service": build_service_param(self.cas_service_url, client_redirect_url)}
+        )
+
+        return "%s/login?%s" % (self.cas_server_url, args)
 
 
 class CasTicketServlet(RestServlet):
@@ -459,7 +464,7 @@ async def on_GET(self, request):
         uri = self.cas_server_url + "/proxyValidate"
         args = {
             "ticket": parse_string(request, "ticket", required=True),
-            "service": self.cas_service_url,
+            "service": build_service_param(self.cas_service_url, client_redirect_url),
         }
         try:
             body = await self._http_client.get_raw(uri, args)


Test patch: